Completed
Push — master ( fe79cd...dd210f )
by Joni
02:39
created

Algorithm::_verifyPadding()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
ccs 13
cts 13
cp 1
rs 8.7624
cc 5
eloc 12
nc 4
nop 2
crap 5
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 13
		list($P, $A) = $this->_unwrapPaddedCiphertext($ciphertext, $kek);
187
		// check message integrity
188 13
		$this->_checkPaddedIntegrity($A);
189
		// verify padding
190 12
		$len = $this->_verifyPadding($P, $A);
191
		// remove padding and return unwrapped key
192 10
		return substr(implode("", $P), 0, $len);
193
	}
194
	
195
	/**
196
	 * Check KEK size.
197
	 *
198
	 * @param string $kek
199
	 * @throws \UnexpectedValueException
200
	 * @return self
201
	 */
202 38
	protected function _checkKEKSize($kek) {
203 38
		$len = $this->_keySize();
204 38
		if (strlen($kek) != $len) {
205 5
			throw new \UnexpectedValueException("KEK size must be $len bytes.");
206
		}
207 33
		return $this;
208
	}
209
	
210
	/**
211
	 * Apply Key Wrap to data blocks.
212
	 *
213
	 * Uses alternative version of the key wrap procedure described in the RFC.
214
	 *
215
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.1
216
	 * @param string[] $P Plaintext, n 64-bit values <code>{P1, P2, ...,
217
	 *        Pn}</code>
218
	 * @param string $kek Key encryption key
219
	 * @param string $iv Initial value
220
	 * @return string[] Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
221
	 *         Cn}</code>
222
	 */
223 17
	protected function _wrapBlocks(array $P, $kek, $iv) {
224 17
		$n = count($P);
225
		// Set A = IV
226 17
		$A = $iv;
227
		// For i = 1 to n
228
		//   R[i] = P[i]
229 17
		$R = $P;
230
		// For j = 0 to 5
231 17
		for ($j = 0; $j <= 5; ++$j) {
232
			// For i = 1 to n
233 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...
234
				// B = AES(K, A | R[i])
235 17
				$B = $this->_encrypt($kek, $A . $R[$i]);
236
				// A = MSB(64, B) ^ t where t = (n*j)+i
237 17
				$t = $n * $j + $i;
238 17
				$A = $this->_msb64($B) ^ $this->_uint64($t);
239
				// R[i] = LSB(64, B)
240 17
				$R[$i] = $this->_lsb64($B);
241 17
			}
242 17
		}
243
		// Set C[0] = A
244 17
		$C = [$A];
245
		// For i = 1 to n
246 17
		for ($i = 1; $i <= $n; ++$i) {
247
			// C[i] = R[i]
248 17
			$C[$i] = $R[$i];
249 17
		}
250 17
		return $C;
251
	}
252
	
253
	/**
254
	 * Unwrap the padded ciphertext producing plaintext and integrity value.
255
	 *
256
	 * @param string $ciphertext Ciphertext
257
	 * @param string $kek Encryption key
258
	 * @return array Tuple of plaintext <code>{P1, P2, ..., Pn}</code> and
259
	 *         integrity value <code>A</code>
260
	 */
261 13
	protected function _unwrapPaddedCiphertext($ciphertext, $kek) {
262
		// split to blocks
263 13
		$C = str_split($ciphertext, 8);
264 13
		$n = count($C) - 1;
265
		// if key consists of only one block, recover AIV and padded key as:
266
		// A | P[1] = DEC(K, C[0] | C[1])
267 13
		if ($n == 1) {
268 8
			$P = str_split($this->_decrypt($kek, $C[0] . $C[1]), 8);
269 8
			$A = $P[0];
270 8
			unset($P[0]);
271 8
		} else {
272
			// apply normal unwrapping
273 5
			list($A, $R) = $this->_unwrapBlocks($C, $kek);
274 5
			$P = array_slice($R, 1, null, true);
275
		}
276 13
		return [$P, $A];
277
	}
278
	
279
	/**
280
	 * Apply Key Unwrap to data blocks.
281
	 *
282
	 * Uses the index based version of key unwrap procedure
283
	 * described in the RFC.
284
	 *
285
	 * Does not compute step 3.
286
	 *
287
	 * @link https://tools.ietf.org/html/rfc3394#section-2.2.2
288
	 * @param string[] $C Ciphertext, (n+1) 64-bit values <code>{C0, C1, ...,
289
	 *        Cn}</code>
290
	 * @param string $kek Key encryption key
291
	 * @throws \UnexpectedValueException
292
	 * @return array Tuple of integrity value <code>A</code> and register
293
	 *         <code>R</code>
294
	 */
295 15
	protected function _unwrapBlocks(array $C, $kek) {
296 15
		$n = count($C) - 1;
297 15
		if (!$n) {
298 1
			throw new \UnexpectedValueException("No blocks.");
299
		}
300
		// Set A = C[0]
301 14
		$A = $C[0];
302
		// For i = 1 to n
303
		//   R[i] = C[i]
304 14
		$R = $C;
305
		// For j = 5 to 0
306 14
		for ($j = 5; $j >= 0; --$j) {
307
			// For i = n to 1
308 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...
309
				// B = AES-1(K, (A ^ t) | R[i]) where t = n*j+i
310 14
				$t = $n * $j + $i;
311 14
				$B = $this->_decrypt($kek, ($A ^ $this->_uint64($t)) . $R[$i]);
312
				// A = MSB(64, B)
313 14
				$A = $this->_msb64($B);
314
				// R[i] = LSB(64, B)
315 14
				$R[$i] = $this->_lsb64($B);
316 14
			}
317 14
		}
318 14
		return array($A, $R);
319
	}
320
	
321
	/**
322
	 * Check that the integrity check value of the padded key is correct.
323
	 *
324
	 * @param string $A
325
	 * @throws \UnexpectedValueException
326
	 */
327 13
	protected function _checkPaddedIntegrity($A) {
328
		// check that MSB(32,A) = A65959A6
329 13
		if (substr($A, 0, 4) != self::AIV_HI) {
330 1
			throw new \UnexpectedValueException("Integrity check failed.");
331
		}
332 12
	}
333
	
334
	/**
335
	 * Verify that the padding of the plaintext is valid.
336
	 *
337
	 * @param array $P Plaintext, n 64-bit values <code>{P1, P2, ...,
338
	 *        Pn}</code>
339
	 * @param string $A Integrity check value
340
	 * @throws \UnexpectedValueException
341
	 * @return int Message length without padding
342
	 */
343 12
	protected function _verifyPadding(array $P, $A) {
344
		// extract mli
345 12
		$mli = substr($A, -4);
346 12
		$len = unpack("N1", $mli)[1];
347
		// check under and overflow
348 12
		$n = count($P);
349 12
		if (8 * ($n - 1) >= $len || $len > 8 * $n) {
350 1
			throw new \UnexpectedValueException("Invalid message length.");
351
		}
352
		// if key is padded
353 11
		$b = 8 - ($len % 8);
354 11
		if ($b < 8) {
355
			// last block (note that the first index in P is 1)
356 8
			$Pn = $P[$n];
357
			// check that padding consists of zeroes
358 8
			if (substr($Pn, -$b) != str_repeat("\0", $b)) {
359 1
				throw new \UnexpectedValueException("Invalid padding.");
360
			}
361 7
		}
362 10
		return $len;
363
	}
364
	
365
	/**
366
	 * Apply AES(K, W) operation (encrypt) to 64 bit block.
367
	 *
368
	 * @param string $kek
369
	 * @param string $block
370
	 * @throws \RuntimeException If encrypt fails
371
	 * @return string
372
	 */
373 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...
374 26
		$str = openssl_encrypt($block, $this->_cipherMethod(), $kek, 
375 26
			OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
376 26
		if (false === $str) {
377 1
			throw new \RuntimeException(
378 1
				"openssl_encrypt() failed: " . $this->_getLastOpenSSLError());
379
		}
380 25
		return $str;
381
	}
382
	
383
	/**
384
	 * Apply AES-1(K, W) operation (decrypt) to 64 bit block.
385
	 *
386
	 * @param string $kek
387
	 * @param string $block
388
	 * @throws \RuntimeException If decrypt fails
389
	 * @return string
390
	 */
391 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...
392 23
		$str = openssl_decrypt($block, $this->_cipherMethod(), $kek, 
393 23
			OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
394 23
		if (false === $str) {
395 1
			throw new \RuntimeException(
396 1
				"openssl_decrypt() failed: " . $this->_getLastOpenSSLError());
397
		}
398 22
		return $str;
399
	}
400
	
401
	/**
402
	 * Get latest OpenSSL error message.
403
	 *
404
	 * @return string
405
	 */
406 2
	protected function _getLastOpenSSLError() {
407 2
		$msg = null;
408 2
		while (false !== ($err = openssl_error_string())) {
409 2
			$msg = $err;
410 2
		}
411 2
		return $msg;
412
	}
413
	
414
	/**
415
	 * Take 64 most significant bits from value.
416
	 *
417
	 * @param string $val
418
	 * @return string
419
	 */
420 20
	protected function _msb64($val) {
421 20
		return substr($val, 0, 8);
422
	}
423
	
424
	/**
425
	 * Take 64 least significant bits from value.
426
	 *
427
	 * @param string $val
428
	 * @return string
429
	 */
430 20
	protected function _lsb64($val) {
431 20
		return substr($val, -8);
432
	}
433
	
434
	/**
435
	 * Convert number to 64 bit unsigned integer octet string with
436
	 * most significant bit first.
437
	 *
438
	 * @param int $num
439
	 * @return string
440
	 */
441 20
	protected function _uint64($num) {
442
		// truncate on 32 bit hosts
443 20
		if (PHP_INT_SIZE < 8) {
444
			return "\0\0\0\0" . pack("N", $num);
445
		}
446 20
		return pack("J", $num);
447
	}
448
}
449