Failed Conditions
Push — master ( 1dc91d...20ac4f )
by Florent
03:32
created

Encrypter::getContentEncryptionAlgorithm()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 23
rs 9.0856
cc 3
eloc 15
nc 3
nop 1
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
            return;
141
        }
142
        $complete_headers = array_merge(
143
            $jwe->getSharedProtectedHeaders(),
144
            $jwe->getSharedHeaders(),
145
            $recipient->getHeaders()
146
        );
147
148
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
149
150
        // We check keys (usage and algorithm if restrictions are set)
151
        $this->checkKeys(
152
            $key_encryption_algorithm,
153
            $content_encryption_algorithm,
154
            $recipient->getRecipientKey()
155
        );
156
157
        $encrypted_content_encryption_key = $this->getEncryptedKey(
158
            $complete_headers,
159
            $cek,
160
            $key_encryption_algorithm,
161
            $content_encryption_algorithm,
162
            $additional_headers,
163
            $recipient->getRecipientKey()
164
        );
165
166
        $recipient_headers = $recipient->getHeaders();
167
        if (!empty($additional_headers) && 1 !== $jwe->countRecipients()) {
168
            $recipient_headers = array_merge(
169
                $recipient_headers,
170
                $additional_headers
171
            );
172
            $additional_headers = [];
173
        }
174
175
        $recipient = Recipient::createRecipientFromLoadedJWE($recipient_headers, $encrypted_content_encryption_key);
176
    }
177
178
    /**
179
     * @param \Jose\Object\JWEInterface                           $jwe
180
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
181
     * @param string                                              $key_management_mode
182
     * @param array                                               $additional_headers
183
     *
184
     * @return string
185
     */
186
    private function determineCEK(JWEInterface $jwe,
187
                                  ContentEncryptionAlgorithmInterface $content_encryption_algorithm,
188
                                  $key_management_mode,
189
                                  array &$additional_headers
190
    ) {
191
        switch ($key_management_mode) {
192
            case KeyEncryptionInterface::MODE_ENCRYPT:
193
            case KeyEncryptionInterface::MODE_WRAP:
194
                return $this->createCEK($content_encryption_algorithm->getCEKSize());
195
            case KeyEncryptionInterface::MODE_AGREEMENT:
196
                Assertion::eq(1, $jwe->countRecipients(), 'Unable to encrypt for multiple recipients using key agreement algorithms.');
197
198
                $complete_headers = array_merge(
199
                    $jwe->getSharedProtectedHeaders(),
200
                    $jwe->getSharedHeaders(),
201
                    $jwe->getRecipient(0)->getHeaders()
202
                );
203
                $algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
204
205
                return $algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $content_encryption_algorithm->getAlgorithmName(), $jwe->getRecipient(0)->getRecipientKey(), $complete_headers, $additional_headers);
206
            case KeyEncryptionInterface::MODE_DIRECT:
207
                Assertion::eq(1, $jwe->countRecipients(), 'Unable to encrypt for multiple recipients using key agreement algorithms.');
208
209
                Assertion::eq($jwe->getRecipient(0)->getRecipientKey()->get('kty'), 'oct', 'Wrong key type.');
210
                Assertion::true($jwe->getRecipient(0)->getRecipientKey()->has('k'), 'The key parameter "k" is missing.');
211
212
                return Base64Url::decode($jwe->getRecipient(0)->getRecipientKey()->get('k'));
213
            default:
214
                throw new \InvalidArgumentException(sprintf('Unsupported key management mode "%s".', $key_management_mode));
215
        }
216
    }
217
218
    /**
219
     * @param \Jose\Object\JWEInterface $jwe
220
     *
221
     * @return string
222
     */
223
    private function getKeyManagementMode(JWEInterface $jwe)
224
    {
225
        $mode = null;
226
        $recipients = $jwe->getRecipients();
227
228
        foreach ($recipients as $recipient) {
229
            $complete_headers = array_merge(
230
                $jwe->getSharedProtectedHeaders(),
231
                $jwe->getSharedHeaders(),
232
                $recipient->getHeaders()
233
            );
234
            Assertion::keyExists($complete_headers, 'alg', 'Parameter "alg" is missing.');
235
236
            $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
237
            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']));
238
239
            if (null === $mode) {
240
                $mode = $key_encryption_algorithm->getKeyManagementMode();
241
            } else {
242
                Assertion::true(
243
                    $this->areKeyManagementModesCompatible($mode, $key_encryption_algorithm->getKeyManagementMode()),
244
                    'Foreign key management mode forbidden.'
245
                );
246
            }
247
        }
248
249
        return $mode;
250
    }
251
252
    /**
253
     * @param \Jose\Object\JWEInterface $jwe
254
     *
255
     * @return \Jose\Compression\CompressionInterface|null
256
     */
257
    private function getCompressionMethod(JWEInterface $jwe)
258
    {
259
        $method = null;
260
        $nb_recipients = $jwe->countRecipients();
261
262
        for ($i = 0; $i < $nb_recipients; $i++) {
263
            $complete_headers = array_merge(
264
                $jwe->getSharedProtectedHeaders(),
265
                $jwe->getSharedHeaders(),
266
                $jwe->getRecipient($i)->getHeaders()
267
            );
268
            if (array_key_exists('zip', $complete_headers)) {
269
                if (null === $method) {
270
                    if (0 === $i) {
271
                        $method = $complete_headers['zip'];
272
                    } else {
273
                        throw new \InvalidArgumentException('Inconsistent "zip" parameter.');
274
                    }
275
                } else {
276
                    Assertion::eq($method, $complete_headers['zip'], 'Inconsistent "zip" parameter.');
277
                }
278
            } else {
279
                Assertion::eq(null, $method, 'Inconsistent "zip" parameter.');
280
            }
281
        }
282
283
        if (null === $method) {
284
            return;
285
        }
286
287
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
288
        Assertion::isInstanceOf($compression_method, CompressionInterface::class, sprintf('Compression method "%s" not supported.', $method));
289
290
        return $compression_method;
291
    }
292
293
    /**
294
     * @param \Jose\Object\JWEInterface $jwe
295
     *
296
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
297
     */
298
    private function getContentEncryptionAlgorithm(JWEInterface $jwe)
299
    {
300
        $algorithm = null;
301
302
        foreach ($jwe->getRecipients() as $recipient) {
303
            $complete_headers = array_merge(
304
                $jwe->getSharedProtectedHeaders(),
305
                $jwe->getSharedHeaders(),
306
                $recipient->getHeaders()
307
            );
308
            Assertion::keyExists($complete_headers, 'enc', 'Parameter "enc" is missing.');
309
            if (null === $algorithm) {
310
                $algorithm = $complete_headers['enc'];
311
            } else {
312
                Assertion::eq($algorithm, $complete_headers['enc'], 'Foreign content encryption algorithms are not allowed.');
313
            }
314
        }
315
316
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
317
        Assertion::isInstanceOf($content_encryption_algorithm, ContentEncryptionAlgorithmInterface::class, sprintf('The content encryption algorithm "%s" is not supported or not a content encryption algorithm instance.', $algorithm));
318
319
        return $content_encryption_algorithm;
320
    }
321
322
    /**
323
     * @param \Jose\Object\JWEInterface                           $jwe
324
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
325
     * @param string                                              $cek
326
     * @param string                                              $iv
327
     * @param \Jose\Compression\CompressionInterface|null         $compression_method
328
     */
329
    private function encryptJWE(JWEInterface &$jwe,
330
                                ContentEncryptionAlgorithmInterface $content_encryption_algorithm,
331
                                $cek,
332
                                $iv,
333
                                CompressionInterface $compression_method = null
334
    ) {
335
        if (!empty($jwe->getSharedProtectedHeaders())) {
336
            $jwe = $jwe->withEncodedSharedProtectedHeaders(Base64Url::encode(json_encode($jwe->getSharedProtectedHeaders())));
337
        }
338
339
        // We encrypt the payload and get the tag
340
        $tag = null;
341
        $payload = $this->preparePayload($jwe->getPayload(), $compression_method);
342
343
        $ciphertext = $content_encryption_algorithm->encryptContent(
344
            $payload,
345
            $cek,
346
            $iv,
347
            null === $jwe->getAAD() ? null : Base64Url::encode($jwe->getAAD()),
348
            $jwe->getEncodedSharedProtectedHeaders(),
349
            $tag
350
        );
351
352
        $jwe = $jwe->withCiphertext($ciphertext);
353
        $jwe = $jwe->withIV($iv);
354
355
        // Tag
356
        if (null !== $tag) {
357
            $jwe = $jwe->withTag($tag);
358
        }
359
    }
360
361
    /**
362
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
363
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
364
     * @param \Jose\Object\JWKInterface                           $recipient_key
365
     */
366
    private function checkKeys(KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key)
367
    {
368
        $this->checkKeyUsage($recipient_key, 'encryption');
369
        if ('dir' !== $key_encryption_algorithm->getAlgorithmName()) {
370
            $this->checkKeyAlgorithm($recipient_key, $key_encryption_algorithm->getAlgorithmName());
371
        } else {
372
            $this->checkKeyAlgorithm($recipient_key, $content_encryption_algorithm->getAlgorithmName());
373
        }
374
    }
375
376
    /**
377
     * @param string $current
378
     * @param string $new
379
     *
380
     * @return bool
381
     */
382
    private function areKeyManagementModesCompatible($current, $new)
383
    {
384
        $agree = KeyEncryptionAlgorithmInterface::MODE_AGREEMENT;
385
        $dir = KeyEncryptionAlgorithmInterface::MODE_DIRECT;
386
        $enc = KeyEncryptionAlgorithmInterface::MODE_ENCRYPT;
387
        $wrap = KeyEncryptionAlgorithmInterface::MODE_WRAP;
388
389
        $supported_key_management_mode_combinations = [
390
            $enc.$enc     => true,
391
            $enc.$wrap    => true,
392
            $wrap.$enc    => true,
393
            $wrap.$wrap   => true,
394
            $agree.$agree => false,
395
            $agree.$dir   => false,
396
            $agree.$enc   => false,
397
            $agree.$wrap  => false,
398
            $dir.$agree   => false,
399
            $dir.$dir     => false,
400
            $dir.$enc     => false,
401
            $dir.$wrap    => false,
402
            $enc.$agree   => false,
403
            $enc.$dir     => false,
404
            $wrap.$agree  => false,
405
            $wrap.$dir    => false,
406
        ];
407
408
        if (array_key_exists($current.$new, $supported_key_management_mode_combinations)) {
409
            return $supported_key_management_mode_combinations[$current.$new];
410
        }
411
412
        return false;
413
    }
414
415
    /**
416
     * @param string                                      $payload
417
     * @param \Jose\Compression\CompressionInterface|null $compression_method
418
     *
419
     * @return string
420
     */
421
    private function preparePayload($payload, CompressionInterface $compression_method = null)
422
    {
423
        $prepared = is_string($payload) ? $payload : json_encode($payload);
424
425
        Assertion::notNull($prepared, 'The payload is empty or cannot encoded into JSON.');
426
427
        if (null === $compression_method) {
428
            return $prepared;
429
        }
430
        $compressed_payload = $compression_method->compress($prepared);
431
        Assertion::string($compressed_payload, 'Compression failed.');
432
433
        return $compressed_payload;
434
    }
435
436
    /**
437
     * @param array                                               $complete_headers
438
     * @param string                                              $cek
439
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
440
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
441
     * @param \Jose\Object\JWKInterface                           $recipient_key
442
     * @param array                                               $additional_headers
443
     *
444
     * @return string|null
445
     */
446
    private function getEncryptedKey(array $complete_headers, $cek, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key)
447
    {
448
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
449
            return $this->getEncryptedKeyFromKeyEncryptionAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key, $additional_headers);
450
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
451
            return $this->getEncryptedKeyFromKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key, $additional_headers);
452
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
453
            return $this->getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $content_encryption_algorithm, $additional_headers, $recipient_key);
454
        }
455
456
        // Using KeyAgreementInterface or DirectEncryptionInterface, the encrypted key is an empty string
457
    }
458
459
    /**
460
     * @param array                                                       $complete_headers
461
     * @param string                                                      $cek
462
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface $key_encryption_algorithm
463
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface         $content_encryption_algorithm
464
     * @param array                                                       $additional_headers
465
     * @param \Jose\Object\JWKInterface                                   $recipient_key
466
     *
467
     * @return string
468
     */
469
    private function getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm(array $complete_headers, $cek, KeyAgreementWrappingInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key)
470
    {
471
        $jwt_cek = $key_encryption_algorithm->wrapAgreementKey($recipient_key, $cek, $content_encryption_algorithm->getCEKSize(), $complete_headers, $additional_headers);
472
473
        return $jwt_cek;
474
    }
475
476
    /**
477
     * @param array                                                $complete_headers
478
     * @param string                                               $cek
479
     * @param \Jose\Algorithm\KeyEncryption\KeyEncryptionInterface $key_encryption_algorithm
480
     * @param \Jose\Object\JWKInterface                            $recipient_key
481
     * @param array                                                $additional_headers
482
     *
483
     * @return string
484
     */
485
    private function getEncryptedKeyFromKeyEncryptionAlgorithm(array $complete_headers, $cek, KeyEncryptionInterface $key_encryption_algorithm, JWKInterface $recipient_key, array &$additional_headers)
486
    {
487
        return $key_encryption_algorithm->encryptKey(
488
            $recipient_key,
489
            $cek,
490
            $complete_headers,
491
            $additional_headers
492
        );
493
    }
494
495
    /**
496
     * @param array                                              $complete_headers
497
     * @param string                                             $cek
498
     * @param \Jose\Algorithm\KeyEncryption\KeyWrappingInterface $key_encryption_algorithm
499
     * @param \Jose\Object\JWKInterface                          $recipient_key
500
     * @param array                                              $additional_headers
501
     *
502
     * @return string
503
     */
504
    private function getEncryptedKeyFromKeyWrappingAlgorithm(array $complete_headers, $cek, KeyWrappingInterface $key_encryption_algorithm, JWKInterface $recipient_key, &$additional_headers)
505
    {
506
        return $key_encryption_algorithm->wrapKey(
507
            $recipient_key,
508
            $cek,
509
            $complete_headers,
510
            $additional_headers
511
        );
512
    }
513
514
    /**
515
     * @param array $complete_headers
516
     *
517
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
518
     */
519
    private function findKeyEncryptionAlgorithm(array $complete_headers)
520
    {
521
        Assertion::keyExists($complete_headers, 'alg', 'Parameter "alg" is missing.');
522
523
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
524
        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']));
525
526
        return $key_encryption_algorithm;
527
    }
528
529
    /**
530
     * @param int $size
531
     *
532
     * @return string
533
     */
534
    private function createCEK($size)
535
    {
536
        return random_bytes($size / 8);
537
    }
538
539
    /**
540
     * @param int $size
541
     *
542
     * @return string
543
     */
544
    private function createIV($size)
545
    {
546
        return random_bytes($size / 8);
547
    }
548
}
549