Completed
Push — master ( 65e224...f25ceb )
by Florent
03:47
created

Encrypter::determineCEK()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 31
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 31
rs 8.439
cc 5
eloc 23
nc 5
nop 4
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2016 Spomky-Labs
7
 *
8
 * This software may be modified and distributed under the terms
9
 * of the MIT license.  See the LICENSE file for details.
10
 */
11
12
namespace Jose;
13
14
use Assert\Assertion;
15
use Base64Url\Base64Url;
16
use Jose\Algorithm\ContentEncryptionAlgorithmInterface;
17
use Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface;
18
use Jose\Algorithm\KeyEncryption\KeyEncryptionInterface;
19
use Jose\Algorithm\KeyEncryption\KeyWrappingInterface;
20
use Jose\Algorithm\KeyEncryptionAlgorithmInterface;
21
use Jose\Behaviour\CommonCipheringMethods;
22
use Jose\Behaviour\HasCompressionManager;
23
use Jose\Behaviour\HasJWAManager;
24
use Jose\Behaviour\HasKeyChecker;
25
use Jose\Compression\CompressionInterface;
26
use Jose\Factory\AlgorithmManagerFactory;
27
use Jose\Factory\CompressionManagerFactory;
28
use Jose\Object\JWEInterface;
29
use Jose\Object\JWKInterface;
30
use Jose\Object\Recipient;
31
use Jose\Object\RecipientInterface;
32
33
final class Encrypter implements EncrypterInterface
34
{
35
    use HasKeyChecker;
36
    use HasJWAManager;
37
    use HasCompressionManager;
38
    use CommonCipheringMethods;
39
40
    /**
41
     * {@inheritdoc}
42
     */
43
    public static function createEncrypter(array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF', 'ZLIB', 'GZ'])
44
    {
45
        $encrypter = new self($key_encryption_algorithms, $content_encryption_algorithms, $compression_methods);
46
47
        return $encrypter;
48
    }
49
50
    /**
51
     * Decrypter constructor.
52
     *
53
     * @param string[]|\Jose\Algorithm\KeyEncryptionAlgorithmInterface[]     $key_encryption_algorithms
54
     * @param string[]|\Jose\Algorithm\ContentEncryptionAlgorithmInterface[] $content_encryption_algorithms
55
     * @param string[]|\Jose\Compression\CompressionInterface[]              $compression_methods
56
     */
57
    public function __construct(
58
        array $key_encryption_algorithms,
59
        array $content_encryption_algorithms,
60
        array $compression_methods
61
    ) {
62
        $this->setKeyEncryptionAlgorithms($key_encryption_algorithms);
63
        $this->setContentEncryptionAlgorithms($content_encryption_algorithms);
64
        $this->setCompressionMethods($compression_methods);
65
        $this->setJWAManager(AlgorithmManagerFactory::createAlgorithmManager(array_merge(
66
            $key_encryption_algorithms,
67
            $content_encryption_algorithms
68
        )));
69
        $this->setCompressionManager(CompressionManagerFactory::createCompressionManager($compression_methods));
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function encrypt(JWEInterface &$jwe)
76
    {
77
        Assertion::false($jwe->isEncrypted(), 'The JWE is already encrypted.');
78
        Assertion::greaterThan($jwe->countRecipients(), 0, 'The JWE does not contain recipient.');
79
80
        // Content Encryption Algorithm
81
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($jwe);
82
83
        // Compression Method
84
        $compression_method = $this->getCompressionMethod($jwe);
85
86
        // Key Management Mode
87
        $key_management_mode = $this->getKeyManagementMode($jwe);
88
89
        // Additional Headers
90
        $additional_headers = [];
91
92
        // CEK
93
        $cek = $this->determineCEK(
94
            $jwe,
95
            $content_encryption_algorithm,
96
            $key_management_mode,
97
            $additional_headers
98
        );
99
100
        $nb_recipients = $jwe->countRecipients();
101
102
        for ($i = 0; $i < $nb_recipients; $i++) {
103
            $this->processRecipient(
104
                $jwe,
105
                $jwe->getRecipient($i),
106
                $cek,
107
                $content_encryption_algorithm,
108
                $additional_headers
109
            );
110
        }
111
112
        if (!empty($additional_headers) && 1 === $jwe->countRecipients()) {
113
            $jwe = $jwe->withSharedProtectedHeaders(array_merge(
114
                $jwe->getSharedProtectedHeaders(),
115
                $additional_headers
116
            ));
117
        }
118
119
        // IV
120
        $iv_size = $content_encryption_algorithm->getIVSize();
121
        $iv = $this->createIV($iv_size);
122
123
        $this->encryptJWE($jwe, $content_encryption_algorithm, $cek, $iv, $compression_method);
124
    }
125
126
    /**
127
     * @param \Jose\Object\JWEInterface                           $jwe
128
     * @param \Jose\Object\RecipientInterface                     $recipient
129
     * @param string                                              $cek
130
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
131
     * @param array                                               $additional_headers
132
     */
133
    private function processRecipient(JWEInterface $jwe,
134
                                      RecipientInterface &$recipient,
135
                                      $cek,
136
                                      ContentEncryptionAlgorithmInterface $content_encryption_algorithm,
137
                                      array &$additional_headers
138
    ) {
139
        if (null === $recipient->getRecipientKey()) {
140
141
            return;
142
        }
143
        $complete_headers = array_merge(
144
            $jwe->getSharedProtectedHeaders(),
145
            $jwe->getSharedHeaders(),
146
            $recipient->getHeaders()
147
        );
148
149
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
150
151
        // We check keys (usage and algorithm if restrictions are set)
152
        $this->checkKeys(
153
            $key_encryption_algorithm,
154
            $content_encryption_algorithm,
155
            $recipient->getRecipientKey()
156
        );
157
158
        $encrypted_content_encryption_key = $this->getEncryptedKey(
159
            $complete_headers,
160
            $cek,
161
            $key_encryption_algorithm,
162
            $content_encryption_algorithm,
163
            $additional_headers,
164
            $recipient->getRecipientKey()
165
        );
166
167
        $recipient_headers = $recipient->getHeaders();
168
        if (!empty($additional_headers) && 1 !== $jwe->countRecipients()) {
169
            $recipient_headers = array_merge(
170
                $recipient_headers,
171
                $additional_headers
172
            );
173
            $additional_headers = [];
174
        }
175
176
        $recipient = Recipient::createRecipientFromLoadedJWE($recipient_headers, $encrypted_content_encryption_key);
177
    }
178
179
    /**
180
     * @param \Jose\Object\JWEInterface                           $jwe
181
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
182
     * @param string                                              $key_management_mode
183
     * @param array                                               $additional_headers
184
     *
185
     * @return string
186
     */
187
    private function determineCEK(JWEInterface $jwe,
188
                                  ContentEncryptionAlgorithmInterface $content_encryption_algorithm,
189
                                  $key_management_mode,
190
                                  array &$additional_headers
191
    ) {
192
        switch ($key_management_mode) {
193
            case KeyEncryptionInterface::MODE_ENCRYPT:
194
            case KeyEncryptionInterface::MODE_WRAP:
195
                return $this->createCEK($content_encryption_algorithm->getCEKSize());
196
            case KeyEncryptionInterface::MODE_AGREEMENT:
197
                Assertion::eq(1, $jwe->countRecipients(), 'Unable to encrypt for multiple recipients using key agreement algorithms.');
198
199
                $complete_headers = array_merge(
200
                    $jwe->getSharedProtectedHeaders(),
201
                    $jwe->getSharedHeaders(),
202
                    $jwe->getRecipient(0)->getHeaders()
203
                );
204
                $algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
205
206
                return $algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $content_encryption_algorithm->getAlgorithmName(), $jwe->getRecipient(0)->getRecipientKey(), $complete_headers, $additional_headers);
207
            case KeyEncryptionInterface::MODE_DIRECT:
208
                Assertion::eq(1, $jwe->countRecipients(), 'Unable to encrypt for multiple recipients using key agreement algorithms.');
209
210
                Assertion::eq($jwe->getRecipient(0)->getRecipientKey()->get('kty'), 'oct', 'Wrong key type.');
211
                Assertion::true($jwe->getRecipient(0)->getRecipientKey()->has('k'), 'The key parameter "k" is missing.');
212
213
                return Base64Url::decode($jwe->getRecipient(0)->getRecipientKey()->get('k'));
214
            default:
215
                throw new \InvalidArgumentException(sprintf('Unsupported key management mode "%s".', $key_management_mode));
216
        }
217
    }
218
219
    /**
220
     * @param \Jose\Object\JWEInterface $jwe
221
     *
222
     * @return string
223
     */
224
    private function getKeyManagementMode(JWEInterface $jwe)
225
    {
226
        $mode = null;
227
        $recipients = $jwe->getRecipients();
228
229
        foreach ($recipients as $recipient) {
230
            $complete_headers = array_merge(
231
                $jwe->getSharedProtectedHeaders(),
232
                $jwe->getSharedHeaders(),
233
                $recipient->getHeaders()
234
            );
235
            Assertion::keyExists($complete_headers, 'alg', 'Parameter "alg" is missing.');
236
237
            $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
238
            Assertion::isInstanceOf($key_encryption_algorithm, KeyEncryptionAlgorithmInterface::class, sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_headers['alg']));
239
240
            if (null === $mode) {
241
                $mode = $key_encryption_algorithm->getKeyManagementMode();
242
            } else {
243
                Assertion::true(
244
                    $this->areKeyManagementModesCompatible($mode, $key_encryption_algorithm->getKeyManagementMode()),
245
                    'Foreign key management mode forbidden.'
246
                );
247
            }
248
        }
249
250
        return $mode;
251
    }
252
253
    /**
254
     * @param \Jose\Object\JWEInterface $jwe
255
     *
256
     * @return \Jose\Compression\CompressionInterface|null
257
     */
258
    private function getCompressionMethod(JWEInterface $jwe)
259
    {
260
        $method = null;
261
        $nb_recipients = $jwe->countRecipients();
262
263
        for ($i = 0; $i < $nb_recipients; $i++) {
264
            $complete_headers = array_merge(
265
                $jwe->getSharedProtectedHeaders(),
266
                $jwe->getSharedHeaders(),
267
                $jwe->getRecipient($i)->getHeaders()
268
            );
269
            if (array_key_exists('zip', $complete_headers)) {
270
                if (null === $method) {
271
                    if (0 === $i) {
272
                        $method = $complete_headers['zip'];
273
                    } else {
274
                        throw new \InvalidArgumentException('Inconsistent "zip" parameter.');
275
                    }
276
                } else {
277
                    Assertion::eq($method, $complete_headers['zip'], 'Inconsistent "zip" parameter.');
278
                }
279
            } else {
280
                Assertion::eq(null, $method, 'Inconsistent "zip" parameter.');
281
            }
282
        }
283
284
        if (null === $method) {
285
            return;
286
        }
287
288
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
289
        Assertion::isInstanceOf($compression_method, CompressionInterface::class, sprintf('Compression method "%s" not supported.', $method));
290
291
        return $compression_method;
292
    }
293
294
    /**
295
     * @param \Jose\Object\JWEInterface $jwe
296
     *
297
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
298
     */
299
    private function getContentEncryptionAlgorithm(JWEInterface $jwe)
300
    {
301
        $algorithm = null;
302
303
        foreach ($jwe->getRecipients() as $recipient) {
304
            $complete_headers = array_merge(
305
                $jwe->getSharedProtectedHeaders(),
306
                $jwe->getSharedHeaders(),
307
                $recipient->getHeaders()
308
            );
309
            Assertion::keyExists($complete_headers, 'enc', 'Parameter "enc" is missing.');
310
            if (null === $algorithm) {
311
                $algorithm = $complete_headers['enc'];
312
            } else {
313
                Assertion::eq($algorithm, $complete_headers['enc'], 'Foreign content encryption algorithms are not allowed.');
314
            }
315
        }
316
317
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
318
        Assertion::isInstanceOf($content_encryption_algorithm, ContentEncryptionAlgorithmInterface::class, sprintf('The content encryption algorithm "%s" is not supported or not a content encryption algorithm instance.', $algorithm));
319
320
        return $content_encryption_algorithm;
321
    }
322
323
    /**
324
     * @param \Jose\Object\JWEInterface                           $jwe
325
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
326
     * @param string                                              $cek
327
     * @param string                                              $iv
328
     * @param \Jose\Compression\CompressionInterface|null         $compression_method
329
     */
330
    private function encryptJWE(JWEInterface &$jwe,
331
                                ContentEncryptionAlgorithmInterface $content_encryption_algorithm,
332
                                $cek,
333
                                $iv,
334
                                CompressionInterface $compression_method = null
335
    ) {
336
        if (!empty($jwe->getSharedProtectedHeaders())) {
337
            $jwe = $jwe->withEncodedSharedProtectedHeaders(Base64Url::encode(json_encode($jwe->getSharedProtectedHeaders())));
338
        }
339
340
        // We encrypt the payload and get the tag
341
        $tag = null;
342
        $payload = $this->preparePayload($jwe->getPayload(), $compression_method);
343
344
        $ciphertext = $content_encryption_algorithm->encryptContent(
345
            $payload,
346
            $cek,
347
            $iv,
348
            null === $jwe->getAAD() ? null : Base64Url::encode($jwe->getAAD()),
349
            $jwe->getEncodedSharedProtectedHeaders(),
350
            $tag
351
        );
352
353
        $jwe = $jwe->withCiphertext($ciphertext);
354
        $jwe = $jwe->withIV($iv);
355
356
        // Tag
357
        if (null !== $tag) {
358
            $jwe = $jwe->withTag($tag);
359
        }
360
    }
361
362
    /**
363
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
364
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
365
     * @param \Jose\Object\JWKInterface                           $recipient_key
366
     */
367
    private function checkKeys(KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key)
368
    {
369
        $this->checkKeyUsage($recipient_key, 'encryption');
370
        if ('dir' !== $key_encryption_algorithm->getAlgorithmName()) {
371
            $this->checkKeyAlgorithm($recipient_key, $key_encryption_algorithm->getAlgorithmName());
372
        } else {
373
            $this->checkKeyAlgorithm($recipient_key, $content_encryption_algorithm->getAlgorithmName());
374
        }
375
    }
376
377
    /**
378
     * @param string $current
379
     * @param string $new
380
     *
381
     * @return bool
382
     */
383
    private function areKeyManagementModesCompatible($current, $new)
384
    {
385
        $agree = KeyEncryptionAlgorithmInterface::MODE_AGREEMENT;
386
        $dir = KeyEncryptionAlgorithmInterface::MODE_DIRECT;
387
        $enc = KeyEncryptionAlgorithmInterface::MODE_ENCRYPT;
388
        $wrap = KeyEncryptionAlgorithmInterface::MODE_WRAP;
389
390
        $supported_key_management_mode_combinations = [
391
            $enc.$enc     => true,
392
            $enc.$wrap    => true,
393
            $wrap.$enc    => true,
394
            $wrap.$wrap   => true,
395
            $agree.$agree => false,
396
            $agree.$dir   => false,
397
            $agree.$enc   => false,
398
            $agree.$wrap  => false,
399
            $dir.$agree   => false,
400
            $dir.$dir     => false,
401
            $dir.$enc     => false,
402
            $dir.$wrap    => false,
403
            $enc.$agree   => false,
404
            $enc.$dir     => false,
405
            $wrap.$agree  => false,
406
            $wrap.$dir    => false,
407
        ];
408
409
        if (array_key_exists($current.$new, $supported_key_management_mode_combinations)) {
410
            return $supported_key_management_mode_combinations[$current.$new];
411
        }
412
413
        return false;
414
    }
415
416
    /**
417
     * @param string                                      $payload
418
     * @param \Jose\Compression\CompressionInterface|null $compression_method
419
     *
420
     * @return string
421
     */
422
    private function preparePayload($payload, CompressionInterface $compression_method = null)
423
    {
424
        $prepared = is_string($payload) ? $payload : json_encode($payload);
425
426
        Assertion::notNull($prepared, 'The payload is empty or cannot encoded into JSON.');
427
428
        if (null === $compression_method) {
429
            return $prepared;
430
        }
431
        $compressed_payload = $compression_method->compress($prepared);
432
        Assertion::string($compressed_payload, 'Compression failed.');
433
434
        return $compressed_payload;
435
    }
436
437
    /**
438
     * @param array                                               $complete_headers
439
     * @param string                                              $cek
440
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
441
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
442
     * @param \Jose\Object\JWKInterface                           $recipient_key
443
     * @param array                                               $additional_headers
444
     *
445
     * @return string|null
446
     */
447
    private function getEncryptedKey(array $complete_headers, $cek, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key)
448
    {
449
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
450
            return $this->getEncryptedKeyFromKeyEncryptionAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key, $additional_headers);
451
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
452
            return $this->getEncryptedKeyFromKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key, $additional_headers);
453
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
454
            return $this->getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $content_encryption_algorithm, $additional_headers, $recipient_key);
455
        }
456
457
        // Using KeyAgreementInterface or DirectEncryptionInterface, the encrypted key is an empty string
458
    }
459
460
    /**
461
     * @param array                                                       $complete_headers
462
     * @param string                                                      $cek
463
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface $key_encryption_algorithm
464
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface         $content_encryption_algorithm
465
     * @param array                                                       $additional_headers
466
     * @param \Jose\Object\JWKInterface                                   $recipient_key
467
     *
468
     * @return string
469
     */
470
    private function getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm(array $complete_headers, $cek, KeyAgreementWrappingInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key)
471
    {
472
        $jwt_cek = $key_encryption_algorithm->wrapAgreementKey($recipient_key, $cek, $content_encryption_algorithm->getCEKSize(), $complete_headers, $additional_headers);
473
474
        return $jwt_cek;
475
    }
476
477
    /**
478
     * @param array                                                $complete_headers
479
     * @param string                                               $cek
480
     * @param \Jose\Algorithm\KeyEncryption\KeyEncryptionInterface $key_encryption_algorithm
481
     * @param \Jose\Object\JWKInterface                            $recipient_key
482
     * @param array                                                $additional_headers
483
     *
484
     * @return string
485
     */
486
    private function getEncryptedKeyFromKeyEncryptionAlgorithm(array $complete_headers, $cek, KeyEncryptionInterface $key_encryption_algorithm, JWKInterface $recipient_key, array &$additional_headers)
487
    {
488
        return $key_encryption_algorithm->encryptKey(
489
            $recipient_key,
490
            $cek,
491
            $complete_headers,
492
            $additional_headers
493
        );
494
    }
495
496
    /**
497
     * @param array                                              $complete_headers
498
     * @param string                                             $cek
499
     * @param \Jose\Algorithm\KeyEncryption\KeyWrappingInterface $key_encryption_algorithm
500
     * @param \Jose\Object\JWKInterface                          $recipient_key
501
     * @param array                                              $additional_headers
502
     *
503
     * @return string
504
     */
505
    private function getEncryptedKeyFromKeyWrappingAlgorithm(array $complete_headers, $cek, KeyWrappingInterface $key_encryption_algorithm, JWKInterface $recipient_key, &$additional_headers)
506
    {
507
        return $key_encryption_algorithm->wrapKey(
508
            $recipient_key,
509
            $cek,
510
            $complete_headers,
511
            $additional_headers
512
        );
513
    }
514
515
    /**
516
     * @param array $complete_headers
517
     *
518
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
519
     */
520
    private function findKeyEncryptionAlgorithm(array $complete_headers)
521
    {
522
        Assertion::keyExists($complete_headers, 'alg', 'Parameter "alg" is missing.');
523
524
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
525
        Assertion::isInstanceOf($key_encryption_algorithm, KeyEncryptionAlgorithmInterface::class, sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_headers['alg']));
526
527
        return $key_encryption_algorithm;
528
    }
529
530
    /**
531
     * @param int $size
532
     *
533
     * @return string
534
     */
535
    private function createCEK($size)
536
    {
537
        return random_bytes($size / 8);
538
    }
539
540
    /**
541
     * @param int $size
542
     *
543
     * @return string
544
     */
545
    private function createIV($size)
546
    {
547
        return random_bytes($size / 8);
548
    }
549
}
550