Algorithm::wrap()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

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