Completed
Push — master ( 8c5640...fe79cd )
by Joni
03:10
created

Algorithm::_unwrapPaddedBlocks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 10
cts 10
cp 1
rs 9.4285
cc 2
eloc 10
nc 2
nop 2
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 
12
	AESKeyWrapAlgorithm
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 1 found
Loading history...
13
{
14
	/**
15
	 * Default initial value.
16
	 *
17
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.3.1
18
	 * @var string
19
	 */
20
	const DEFAULT_IV = "\xA6\xA6\xA6\xA6\xA6\xA6\xA6\xA6";
21
	
22
	/**
23
	 * High order bytes of the alternative initial value for padding.
24
	 *
25
	 * @link https://tools.ietf.org/html/rfc5649#section-3
26
	 * @var string
27
	 */
28
	const AIV_HI = "\xA6\x59\x59\xA6";
29
	
30
	/**
31
	 * Initial value.
32
	 *
33
	 * @var string $_iv
34
	 */
35
	protected $_iv;
36
	
37
	/**
38
	 * Get OpenSSL cipher method.
39
	 *
40
	 * @return string
41
	 */
42
	abstract protected function _cipherMethod();
43
	
44
	/**
45
	 * Get key encryption key size.
46
	 *
47
	 * @return int
48
	 */
49
	abstract protected function _keySize();
50
	
51
	/**
52
	 * Constructor
53
	 *
54
	 * @param string $iv Initial value
55
	 */
56 48
	public function __construct($iv = self::DEFAULT_IV) {
57 48
		if (strlen($iv) != 8) {
58 1
			throw new \UnexpectedValueException("IV size must be 64 bits.");
59
		}
60 47
		$this->_iv = $iv;
61 47
	}
62
	
63
	/**
64
	 * Wrap a key using given key encryption key.
65
	 *
66
	 * Key length must be at least 64 bits (8 octets) and a multiple
67
	 * of 64 bits (8 octets).
68
	 * Use <i>wrapPad</i> to wrap a key of arbitrary length.
69
	 *
70
	 * Key encryption key must have a size of underlying AES algorithm,
71
	 * ie. 128, 196 or 256 bits.
72
	 *
73
	 * @param string $key Key to wrap
74
	 * @param string $kek Key encryption key
75
	 * @throws \UnexpectedValueException
76
	 * @return string Ciphertext
77
	 */
78 13
	public function wrap($key, $kek) {
79 13
		$key_len = strlen($key);
80
		// rfc3394 dictates n to be at least 2
81 13
		if ($key_len < 16) {
82 2
			throw new \UnexpectedValueException(
83 2
				"Key length must be at least 16 octets.");
84
		}
85 11
		if (0 !== $key_len % 8) {
86 1
			throw new \UnexpectedValueException(
87 1
				"Key length must be a multiple of 64 bits.");
88
		}
89 10
		$this->_checkKEKSize($kek);
90
		// P = plaintext as 64 bit blocks
91 8
		$P = [];
92 8
		$i = 1;
93 8
		foreach (str_split($key, 8) as $val) {
94 8
			$P[$i++] = $val;
95 8
		}
96 8
		$C = $this->_wrapBlocks($P, $kek, $this->_iv);
97 8
		return implode("", $C);
98
	}
99
	
100
	/**
101
	 * Unwrap a key from a ciphertext using given key encryption key.
102
	 *
103
	 * @param string $ciphertext Ciphertext of the wrapped key
104
	 * @param string $kek Key encryption key
105
	 * @throws \UnexpectedValueException
106
	 * @return string Unwrapped key
107
	 */
108 12
	public function unwrap($ciphertext, $kek) {
109 12
		if (0 !== strlen($ciphertext) % 8) {
110 1
			throw new \UnexpectedValueException(
111 1
				"Ciphertext length must be a multiple of 64 bits.");
112
		}
113 11
		$this->_checkKEKSize($kek);
114
		// C = ciphertext as 64 bit blocks with integrity check value prepended
115 10
		$C = str_split($ciphertext, 8);
116 10
		list($A, $R) = $this->_unwrapBlocks($C, $kek);
117
		// check integrity value
118 9
		if ($A != $this->_iv) {
119 1
			throw new \UnexpectedValueException("Integrity check failed.");
120
		}
121
		// output the plaintext
122 8
		$P = array_slice($R, 1, null, true);
123 8
		return implode("", $P);
124
	}
125
	
126
	/**
127
	 * Wrap a key of arbitrary length using given key encryption key.
128
	 *
129
	 * This variant of wrapping does not place any restriction on key size.
130
	 *
131
	 * Key encryption key has the same restrictions as with <i>wrap</i> method.
132
	 *
133
	 * @param string $key Key to wrap
134
	 * @param string $kek Key encryption key
135
	 * @return string Ciphertext
136
	 */
137 19
	public function wrapPad($key, $kek) {
138 19
		$len = strlen($key);
139 19
		if (!$len) {
140 1
			throw new \UnexpectedValueException(
141 1
				"Key must have at least one octet.");
142
		}
143 18
		$this->_checkKEKSize($kek);
144
		// append padding
145 17
		if (0 != $len % 8) {
146 12
			$key = str_pad($key, $len + (8 - $len % 8), "\0", STR_PAD_RIGHT);
147 12
		}
148
		// compute AIV
149 17
		$mli = pack("N", $len);
150 17
		$aiv = self::AIV_HI . $mli;
151
		// if key length was less than 8 octets (padded key contains
152
		// exactly 8 octets), let the ciphertext be:
153
		// C[0] | C[1] = ENC(K, A | P[1]).
154 17
		if ($len <= 8) {
155 8
			return $this->_encrypt($kek, $aiv . $key);
156
		}
157
		// build plaintext blocks and apply normal wrapping with AIV as an
158
		// initial value
159 9
		$P = [];
160 9
		$i = 1;
161 9
		foreach (str_split($key, 8) as $val) {
162 9
			$P[$i++] = $val;
163 9
		}
164 9
		$C = $this->_wrapBlocks($P, $kek, $aiv);
165 9
		return implode("", $C);
166
	}
167
	
168
	/**
169
	 * Unwrap a key from a padded ciphertext using given key encryption key.
170
	 *
171
	 * This variant of unwrapping must be used if the key was wrapped using
172
	 * <i>wrapPad</i>.
173
	 *
174
	 * @param string $ciphertext Ciphertext of the wrapped and padded key
175
	 * @param string $kek Key encryption key
176
	 * @throws \UnexpectedValueException
177
	 * @throws \RangeException
178
	 * @return string Unwrapped key
179
	 */
180 15
	public function unwrapPad($ciphertext, $kek) {
181 15
		if (0 !== strlen($ciphertext) % 8) {
182 1
			throw new \UnexpectedValueException(
183 1
				"Ciphertext length must be a multiple of 64 bits.");
184
		}
185 14
		$this->_checkKEKSize($kek);
186
		// split to blocks
187 13
		$C = str_split($ciphertext, 8);
188 13
		list($P, $A) = $this->_unwrapPaddedBlocks($C, $kek);
189
		// check that MSB(32,A) = A65959A6
190 13
		$iv = substr($A, 0, 4);
191 13
		if ($iv != self::AIV_HI) {
192 1
			throw new \UnexpectedValueException("Integrity check failed.");
193
		}
194
		// extract mli
195 12
		$mli = substr($A, -4);
196 12
		$len = unpack("N1", $mli)[1];
197
		// check under and overflow
198 12
		$n = count($P);
199 12
		if (8 * ($n - 1) >= $len || $len > 8 * $n) {
200 1
			throw new \UnexpectedValueException("Invalid message length.");
201
		}
202 11
		$output = implode("", $P);
203
		// if key is padded
204 11
		$b = 8 - ($len % 8);
205 11
		if ($b < 8) {
206
			// check that padding consists of zeroes
207 8
			if (substr($output, -$b) != str_repeat("\0", $b)) {
208 1
				throw new \UnexpectedValueException("Invalid padding.");
209
			}
210 7
		}
211
		// remove padding and return unwrapped key
212 10
		return substr($output, 0, $len);
213
	}
214
	
215
	/**
216
	 * Check KEK size.
217
	 *
218
	 * @param string $kek
219
	 * @throws \UnexpectedValueException
220
	 * @return self
221
	 */
222 38
	protected function _checkKEKSize($kek) {
223 38
		$len = $this->_keySize();
224 38
		if (strlen($kek) != $len) {
225 5
			throw new \UnexpectedValueException("KEK size must be $len bytes.");
226
		}
227 33
		return $this;
228
	}
229
	
230
	/**
231
	 * Apply Key Wrap to data blocks.
232
	 *
233
	 * Uses alternative version of the key wrap procedure described in the RFC.
234
	 *
235
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.1
236
	 * @param string[] $P Plaintext, n 64-bit values <code>{P1, P2, ...,
237
	 *        Pn}</code>
238
	 * @param string $kek Key encryption key
239
	 * @param string $iv Initial value
240
	 * @return string[] Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
241
	 *         Cn}</code>
242
	 */
243 17
	protected function _wrapBlocks(array $P, $kek, $iv) {
244 17
		$n = count($P);
245
		// Set A = IV
246 17
		$A = $iv;
247
		// For i = 1 to n
248
		//   R[i] = P[i]
249 17
		$R = $P;
250
		// For j = 0 to 5
251 17
		for ($j = 0; $j <= 5; ++$j) {
252
			// For i = 1 to n
253 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...
254
				// B = AES(K, A | R[i])
255 17
				$B = $this->_encrypt($kek, $A . $R[$i]);
256
				// A = MSB(64, B) ^ t where t = (n*j)+i
257 17
				$t = $n * $j + $i;
258 17
				$A = $this->_msb64($B) ^ $this->_uint64($t);
259
				// R[i] = LSB(64, B)
260 17
				$R[$i] = $this->_lsb64($B);
261 17
			}
262 17
		}
263
		// Set C[0] = A
264 17
		$C = [$A];
265
		// For i = 1 to n
266 17
		for ($i = 1; $i <= $n; ++$i) {
267
			// C[i] = R[i]
268 17
			$C[$i] = $R[$i];
269 17
		}
270 17
		return $C;
271
	}
272
	
273
	/**
274
	 * Unwrap the padded ciphertext producing plaintext and integrity value.
275
	 *
276
	 * @param string[] $C Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
277
	 *        Cn}</code>
278
	 * @param string $kek Encryption key
279
	 * @return array Tuple of plaintext <code>P</code> and integrity value
280
	 *         <code>A</code>
281
	 */
282 13
	protected function _unwrapPaddedBlocks(array $C, $kek) {
283 13
		$n = count($C) - 1;
284
		// if key consists of only one block, recover AIV and padded key as:
285
		// A | P[1] = DEC(K, C[0] | C[1])
286 13
		if ($n == 1) {
287 8
			$P = str_split($this->_decrypt($kek, $C[0] . $C[1]), 8);
288 8
			$A = $P[0];
289 8
			unset($P[0]);
290 8
		} else {
291
			// apply normal unwrapping
292 5
			list($A, $R) = $this->_unwrapBlocks($C, $kek);
293 5
			$P = array_slice($R, 1, null, true);
294
		}
295 13
		return [$P, $A];
296
	}
297
	
298
	/**
299
	 * Apply Key Unwrap to data blocks.
300
	 *
301
	 * Uses the index based version of key unwrap procedure
302
	 * described in the RFC.
303
	 *
304
	 * Does not compute step 3.
305
	 *
306
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.2
307
	 * @param string[] $C Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
308
	 *        Cn}</code>
309
	 * @param string $kek Key encryption key
310
	 * @throws \UnexpectedValueException
311
	 * @return array Tuple of integrity value <code>A</code> and register
312
	 *         <code>R</code>
313
	 */
314 15
	protected function _unwrapBlocks(array $C, $kek) {
315 15
		$n = count($C) - 1;
316 15
		if (!$n) {
317 1
			throw new \UnexpectedValueException("No blocks.");
318
		}
319
		// Set A = C[0]
320 14
		$A = $C[0];
321
		// For i = 1 to n
322
		//   R[i] = C[i]
323 14
		$R = $C;
324
		// For j = 5 to 0
325 14
		for ($j = 5; $j >= 0; --$j) {
326
			// For i = n to 1
327 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...
328
				// B = AES-1(K, (A ^ t) | R[i]) where t = n*j+i
329 14
				$t = $n * $j + $i;
330 14
				$B = $this->_decrypt($kek, ($A ^ $this->_uint64($t)) . $R[$i]);
331
				// A = MSB(64, B)
332 14
				$A = $this->_msb64($B);
333
				// R[i] = LSB(64, B)
334 14
				$R[$i] = $this->_lsb64($B);
335 14
			}
336 14
		}
337 14
		return array($A, $R);
338
	}
339
	
340
	/**
341
	 * Apply AES(K, W) operation (encrypt) to 64 bit block.
342
	 *
343
	 * @param string $kek
344
	 * @param string $block
345
	 * @throws \RuntimeException If encrypt fails
346
	 * @return string
347
	 */
348 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...
349 26
		$str = openssl_encrypt($block, $this->_cipherMethod(), $kek, 
350 26
			OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
351 26
		if (false === $str) {
352 1
			throw new \RuntimeException(
353 1
				"openssl_encrypt() failed: " . $this->_getLastOpenSSLError());
354
		}
355 25
		return $str;
356
	}
357
	
358
	/**
359
	 * Apply AES-1(K, W) operation (decrypt) to 64 bit block.
360
	 *
361
	 * @param string $kek
362
	 * @param string $block
363
	 * @throws \RuntimeException If decrypt fails
364
	 * @return string
365
	 */
366 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...
367 23
		$str = openssl_decrypt($block, $this->_cipherMethod(), $kek, 
368 23
			OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
369 23
		if (false === $str) {
370 1
			throw new \RuntimeException(
371 1
				"openssl_decrypt() failed: " . $this->_getLastOpenSSLError());
372
		}
373 22
		return $str;
374
	}
375
	
376
	/**
377
	 * Get latest OpenSSL error message.
378
	 *
379
	 * @return string
380
	 */
381 2
	protected function _getLastOpenSSLError() {
382 2
		$msg = null;
383 2
		while (false !== ($err = openssl_error_string())) {
384 2
			$msg = $err;
385 2
		}
386 2
		return $msg;
387
	}
388
	
389
	/**
390
	 * Take 64 most significant bits from value.
391
	 *
392
	 * @param string $val
393
	 * @return string
394
	 */
395 20
	protected function _msb64($val) {
396 20
		return substr($val, 0, 8);
397
	}
398
	
399
	/**
400
	 * Take 64 least significant bits from value.
401
	 *
402
	 * @param string $val
403
	 * @return string
404
	 */
405 20
	protected function _lsb64($val) {
406 20
		return substr($val, -8);
407
	}
408
	
409
	/**
410
	 * Convert number to 64 bit unsigned integer octet string with
411
	 * most significant bit first.
412
	 *
413
	 * @param int $num
414
	 * @return string
415
	 */
416 20
	protected function _uint64($num) {
417
		// truncate on 32 bit hosts
418 20
		if (PHP_INT_SIZE < 8) {
419
			return "\0\0\0\0" . pack("N", $num);
420
		}
421 20
		return pack("J", $num);
422
	}
423
}
424