Completed
Push — master ( c7f4f9...c78e29 )
by Joni
02:31
created

Algorithm::_checkPaddedIntegrity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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