Completed
Push — master ( 82d3b6...287b9a )
by Joni
03:31
created

Algorithm::_padKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2
1
<?php
2
3
namespace AESKW;
4
5
6
/**
7
 * Base class for AES key wrap algorithms with varying key sizes.
8
 *
9
 * @link https://tools.ietf.org/html/rfc3394
10
 */
11
abstract class Algorithm implements AESKeyWrapAlgorithm
12
{
13
	/**
14
	 * Default initial value.
15
	 *
16
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.3.1
17
	 * @var string
18
	 */
19
	const DEFAULT_IV = "\xA6\xA6\xA6\xA6\xA6\xA6\xA6\xA6";
20
	
21
	/**
22
	 * High order bytes of the alternative initial value for padding.
23
	 *
24
	 * @link https://tools.ietf.org/html/rfc5649#section-3
25
	 * @var string
26
	 */
27
	const AIV_HI = "\xA6\x59\x59\xA6";
28
	
29
	/**
30
	 * Initial value.
31
	 *
32
	 * @var string $_iv
33
	 */
34
	protected $_iv;
35
	
36
	/**
37
	 * Get OpenSSL cipher method.
38
	 *
39
	 * @return string
40
	 */
41
	abstract protected function _cipherMethod();
42
	
43
	/**
44
	 * Get key encryption key size.
45
	 *
46
	 * @return int
47
	 */
48
	abstract protected function _keySize();
49
	
50
	/**
51
	 * Constructor
52
	 *
53
	 * @param string $iv Initial value
54
	 */
55 48
	public function __construct($iv = self::DEFAULT_IV) {
56 48
		if (strlen($iv) != 8) {
57 1
			throw new \UnexpectedValueException("IV size must be 64 bits.");
58
		}
59 47
		$this->_iv = $iv;
60 47
	}
61
	
62
	/**
63
	 * Wrap a key using given key encryption key.
64
	 *
65
	 * Key length must be at least 64 bits (8 octets) and a multiple
66
	 * of 64 bits (8 octets).
67
	 * Use <i>wrapPad</i> to wrap a key of arbitrary length.
68
	 *
69
	 * Key encryption key must have a size of underlying AES algorithm,
70
	 * ie. 128, 196 or 256 bits.
71
	 *
72
	 * @param string $key Key to wrap
73
	 * @param string $kek Key encryption key
74
	 * @throws \UnexpectedValueException If the key length is invalid
75
	 * @return string Ciphertext
76
	 */
77 13
	public function wrap($key, $kek) {
78 13
		$key_len = strlen($key);
79
		// rfc3394 dictates n to be at least 2
80 13
		if ($key_len < 16) {
81 2
			throw new \UnexpectedValueException(
82 2
				"Key length must be at least 16 octets.");
83
		}
84 11
		if (0 !== $key_len % 8) {
85 1
			throw new \UnexpectedValueException(
86 1
				"Key length must be a multiple of 64 bits.");
87
		}
88 10
		$this->_checkKEKSize($kek);
89
		// P = plaintext as 64 bit blocks
90 8
		$P = [];
91 8
		$i = 1;
92 8
		foreach (str_split($key, 8) as $val) {
93 8
			$P[$i++] = $val;
94 8
		}
95 8
		$C = $this->_wrapBlocks($P, $kek, $this->_iv);
96 8
		return implode("", $C);
97
	}
98
	
99
	/**
100
	 * Unwrap a key from a ciphertext using given key encryption key.
101
	 *
102
	 * @param string $ciphertext Ciphertext of the wrapped key
103
	 * @param string $kek Key encryption key
104
	 * @throws \UnexpectedValueException If the ciphertext is invalid
105
	 * @return string Unwrapped key
106
	 */
107 12
	public function unwrap($ciphertext, $kek) {
108 12
		if (0 !== strlen($ciphertext) % 8) {
109 1
			throw new \UnexpectedValueException(
110 1
				"Ciphertext length must be a multiple of 64 bits.");
111
		}
112 11
		$this->_checkKEKSize($kek);
113
		// C = ciphertext as 64 bit blocks with integrity check value prepended
114 10
		$C = str_split($ciphertext, 8);
115 10
		list($A, $R) = $this->_unwrapBlocks($C, $kek);
116
		// check integrity value
117 9
		if ($A != $this->_iv) {
118 1
			throw new \UnexpectedValueException("Integrity check failed.");
119
		}
120
		// output the plaintext
121 8
		$P = array_slice($R, 1, null, true);
122 8
		return implode("", $P);
123
	}
124
	
125
	/**
126
	 * Wrap a key of arbitrary length using given key encryption key.
127
	 *
128
	 * This variant of wrapping does not place any restriction on key size.
129
	 *
130
	 * Key encryption key has the same restrictions as with <i>wrap</i> method.
131
	 *
132
	 * @param string $key Key to wrap
133
	 * @param string $kek Key encryption key
134
	 * @throws \UnexpectedValueException If the key length is invalid
135
	 * @return string Ciphertext
136
	 */
137 19
	public function wrapPad($key, $kek) {
138 19
		if (!strlen($key)) {
139 1
			throw new \UnexpectedValueException(
140 1
				"Key must have at least one octet.");
141
		}
142 18
		$this->_checkKEKSize($kek);
143 17
		list($key, $aiv) = $this->_padKey($key);
144
		// If the padded key contains exactly eight octets,
145
		// let the ciphertext be:
146
		// C[0] | C[1] = ENC(K, A | P[1]).
147 17
		if (8 == strlen($key)) {
148 8
			return $this->_encrypt($kek, $aiv . $key);
149
		}
150
		// build plaintext blocks and apply normal wrapping with AIV as an
151
		// initial value
152 9
		$P = [];
153 9
		$i = 1;
154 9
		foreach (str_split($key, 8) as $val) {
155 9
			$P[$i++] = $val;
156 9
		}
157 9
		$C = $this->_wrapBlocks($P, $kek, $aiv);
158 9
		return implode("", $C);
159
	}
160
	
161
	/**
162
	 * Unwrap a key from a padded ciphertext using given key encryption key.
163
	 *
164
	 * This variant of unwrapping must be used if the key was wrapped using
165
	 * <i>wrapPad</i>.
166
	 *
167
	 * @param string $ciphertext Ciphertext of the wrapped and padded key
168
	 * @param string $kek Key encryption key
169
	 * @throws \UnexpectedValueException If the ciphertext is invalid
170
	 * @return string Unwrapped key
171
	 */
172 15
	public function unwrapPad($ciphertext, $kek) {
173 15
		if (0 !== strlen($ciphertext) % 8) {
174 1
			throw new \UnexpectedValueException(
175 1
				"Ciphertext length must be a multiple of 64 bits.");
176
		}
177 14
		$this->_checkKEKSize($kek);
178 13
		list($P, $A) = $this->_unwrapPaddedCiphertext($ciphertext, $kek);
179
		// check message integrity
180 13
		$this->_checkPaddedIntegrity($A);
181
		// verify padding
182 12
		$len = $this->_verifyPadding($P, $A);
183
		// remove padding and return unwrapped key
184 10
		return substr(implode("", $P), 0, $len);
185
	}
186
	
187
	/**
188
	 * Check KEK size.
189
	 *
190
	 * @param string $kek
191
	 * @throws \UnexpectedValueException
192
	 * @return self
193
	 */
194 38
	protected function _checkKEKSize($kek) {
195 38
		$len = $this->_keySize();
196 38
		if (strlen($kek) != $len) {
197 5
			throw new \UnexpectedValueException("KEK size must be $len bytes.");
198
		}
199 33
		return $this;
200
	}
201
	
202
	/**
203
	 * Apply Key Wrap to data blocks.
204
	 *
205
	 * Uses alternative version of the key wrap procedure described in the RFC.
206
	 *
207
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.1
208
	 * @param string[] $P Plaintext, n 64-bit values <code>{P1, P2, ...,
209
	 *        Pn}</code>
210
	 * @param string $kek Key encryption key
211
	 * @param string $iv Initial value
212
	 * @return string[] Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
213
	 *         Cn}</code>
214
	 */
215 17
	protected function _wrapBlocks(array $P, $kek, $iv) {
216 17
		$n = count($P);
217
		// Set A = IV
218 17
		$A = $iv;
219
		// For i = 1 to n
220
		//   R[i] = P[i]
221 17
		$R = $P;
222
		// For j = 0 to 5
223 17
		for ($j = 0; $j <= 5; ++$j) {
224
			// For i = 1 to n
225 17 View Code Duplication
			for ($i = 1; $i <= $n; ++$i) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
				// B = AES(K, A | R[i])
227 17
				$B = $this->_encrypt($kek, $A . $R[$i]);
228
				// A = MSB(64, B) ^ t where t = (n*j)+i
229 17
				$t = $n * $j + $i;
230 17
				$A = $this->_msb64($B) ^ $this->_uint64($t);
231
				// R[i] = LSB(64, B)
232 17
				$R[$i] = $this->_lsb64($B);
233 17
			}
234 17
		}
235
		// Set C[0] = A
236 17
		$C = [$A];
237
		// For i = 1 to n
238 17
		for ($i = 1; $i <= $n; ++$i) {
239
			// C[i] = R[i]
240 17
			$C[$i] = $R[$i];
241 17
		}
242 17
		return $C;
243
	}
244
	
245
	/**
246
	 * Unwrap the padded ciphertext producing plaintext and integrity value.
247
	 *
248
	 * @param string $ciphertext Ciphertext
249
	 * @param string $kek Encryption key
250
	 * @return array Tuple of plaintext <code>{P1, P2, ..., Pn}</code> and
251
	 *         integrity value <code>A</code>
252
	 */
253 13
	protected function _unwrapPaddedCiphertext($ciphertext, $kek) {
254
		// split to blocks
255 13
		$C = str_split($ciphertext, 8);
256 13
		$n = count($C) - 1;
257
		// if key consists of only one block, recover AIV and padded key as:
258
		// A | P[1] = DEC(K, C[0] | C[1])
259 13
		if ($n == 1) {
260 8
			$P = str_split($this->_decrypt($kek, $C[0] . $C[1]), 8);
261 8
			$A = $P[0];
262 8
			unset($P[0]);
263 8
		} else {
264
			// apply normal unwrapping
265 5
			list($A, $R) = $this->_unwrapBlocks($C, $kek);
266 5
			$P = array_slice($R, 1, null, true);
267
		}
268 13
		return [$P, $A];
269
	}
270
	
271
	/**
272
	 * Apply Key Unwrap to data blocks.
273
	 *
274
	 * Uses the index based version of key unwrap procedure
275
	 * described in the RFC.
276
	 *
277
	 * Does not compute step 3.
278
	 *
279
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.2
280
	 * @param string[] $C Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
281
	 *        Cn}</code>
282
	 * @param string $kek Key encryption key
283
	 * @throws \UnexpectedValueException
284
	 * @return array Tuple of integrity value <code>A</code> and register
285
	 *         <code>R</code>
286
	 */
287 15
	protected function _unwrapBlocks(array $C, $kek) {
288 15
		$n = count($C) - 1;
289 15
		if (!$n) {
290 1
			throw new \UnexpectedValueException("No blocks.");
291
		}
292
		// Set A = C[0]
293 14
		$A = $C[0];
294
		// For i = 1 to n
295
		//   R[i] = C[i]
296 14
		$R = $C;
297
		// For j = 5 to 0
298 14
		for ($j = 5; $j >= 0; --$j) {
299
			// For i = n to 1
300 14 View Code Duplication
			for ($i = $n; $i >= 1; --$i) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
301
				// B = AES-1(K, (A ^ t) | R[i]) where t = n*j+i
302 14
				$t = $n * $j + $i;
303 14
				$B = $this->_decrypt($kek, ($A ^ $this->_uint64($t)) . $R[$i]);
304
				// A = MSB(64, B)
305 14
				$A = $this->_msb64($B);
306
				// R[i] = LSB(64, B)
307 14
				$R[$i] = $this->_lsb64($B);
308 14
			}
309 14
		}
310 14
		return array($A, $R);
311
	}
312
	
313
	/**
314
	 * Pad a key with zeroes and compute alternative initial value.
315
	 *
316
	 * @param string $key Key
317
	 * @return array Tuple of padded key and AIV
318
	 */
319 17
	protected function _padKey($key) {
320 17
		$len = strlen($key);
321
		// append padding
322 17
		if (0 != $len % 8) {
323 12
			$key .= str_repeat("\0", 8 - $len % 8);
324 12
		}
325
		// compute AIV
326 17
		$mli = pack("N", $len);
327 17
		$aiv = self::AIV_HI . $mli;
328 17
		return [$key, $aiv];
329
	}
330
	
331
	/**
332
	 * Check that the integrity check value of the padded key is correct.
333
	 *
334
	 * @param string $A
335
	 * @throws \UnexpectedValueException
336
	 */
337 13
	protected function _checkPaddedIntegrity($A) {
338
		// check that MSB(32,A) = A65959A6
339 13
		if (substr($A, 0, 4) != self::AIV_HI) {
340 1
			throw new \UnexpectedValueException("Integrity check failed.");
341
		}
342 12
	}
343
	
344
	/**
345
	 * Verify that the padding of the plaintext is valid.
346
	 *
347
	 * @param array $P Plaintext, n 64-bit values <code>{P1, P2, ...,
348
	 *        Pn}</code>
349
	 * @param string $A Integrity check value
350
	 * @throws \UnexpectedValueException
351
	 * @return int Message length without padding
352
	 */
353 12
	protected function _verifyPadding(array $P, $A) {
354
		// extract mli
355 12
		$mli = substr($A, -4);
356 12
		$len = unpack("N1", $mli)[1];
357
		// check under and overflow
358 12
		$n = count($P);
359 12
		if (8 * ($n - 1) >= $len || $len > 8 * $n) {
360 1
			throw new \UnexpectedValueException("Invalid message length.");
361
		}
362
		// if key is padded
363 11
		$b = 8 - ($len % 8);
364 11
		if ($b < 8) {
365
			// last block (note that the first index in P is 1)
366 8
			$Pn = $P[$n];
367
			// check that padding consists of zeroes
368 8
			if (substr($Pn, -$b) != str_repeat("\0", $b)) {
369 1
				throw new \UnexpectedValueException("Invalid padding.");
370
			}
371 7
		}
372 10
		return $len;
373
	}
374
	
375
	/**
376
	 * Apply AES(K, W) operation (encrypt) to 64 bit block.
377
	 *
378
	 * @param string $kek
379
	 * @param string $block
380
	 * @throws \RuntimeException If encrypt fails
381
	 * @return string
382
	 */
383 26 View Code Duplication
	protected function _encrypt($kek, $block) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
384 26
		$str = openssl_encrypt($block, $this->_cipherMethod(), $kek, 
385 26
			OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
386 26
		if (false === $str) {
387 1
			throw new \RuntimeException(
388 1
				"openssl_encrypt() failed: " . $this->_getLastOpenSSLError());
389
		}
390 25
		return $str;
391
	}
392
	
393
	/**
394
	 * Apply AES-1(K, W) operation (decrypt) to 64 bit block.
395
	 *
396
	 * @param string $kek
397
	 * @param string $block
398
	 * @throws \RuntimeException If decrypt fails
399
	 * @return string
400
	 */
401 23 View Code Duplication
	protected function _decrypt($kek, $block) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
402 23
		$str = openssl_decrypt($block, $this->_cipherMethod(), $kek, 
403 23
			OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
404 23
		if (false === $str) {
405 1
			throw new \RuntimeException(
406 1
				"openssl_decrypt() failed: " . $this->_getLastOpenSSLError());
407
		}
408 22
		return $str;
409
	}
410
	
411
	/**
412
	 * Get the latest OpenSSL error message.
413
	 *
414
	 * @return string
415
	 */
416 2
	protected function _getLastOpenSSLError() {
417 2
		$msg = null;
418 2
		while (false !== ($err = openssl_error_string())) {
419 2
			$msg = $err;
420 2
		}
421 2
		return $msg;
422
	}
423
	
424
	/**
425
	 * Take 64 most significant bits from value.
426
	 *
427
	 * @param string $val
428
	 * @return string
429
	 */
430 20
	protected function _msb64($val) {
431 20
		return substr($val, 0, 8);
432
	}
433
	
434
	/**
435
	 * Take 64 least significant bits from value.
436
	 *
437
	 * @param string $val
438
	 * @return string
439
	 */
440 20
	protected function _lsb64($val) {
441 20
		return substr($val, -8);
442
	}
443
	
444
	/**
445
	 * Convert number to 64 bit unsigned integer octet string with
446
	 * most significant bit first.
447
	 *
448
	 * @param int $num
449
	 * @return string
450
	 */
451 20
	protected function _uint64($num) {
452
		// truncate on 32 bit hosts
453 20
		if (PHP_INT_SIZE < 8) {
454
			return "\0\0\0\0" . pack("N", $num);
455
		}
456 20
		return pack("J", $num);
457
	}
458
}
459