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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.