Failed Conditions
Push — master ( d2ad41...5d048e )
by Florent
21:38 queued 15:06
created

JWEBuilder::createCEK()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Encryption;
15
16
use Base64Url\Base64Url;
17
use Jose\Component\Core\AlgorithmManager;
18
use Jose\Component\Core\Converter\JsonConverter;
19
use Jose\Component\Core\JWK;
20
use Jose\Component\Core\Util\KeyChecker;
21
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
22
use Jose\Component\Encryption\Algorithm\KeyEncryption\DirectEncryption;
23
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreement;
24
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreementWithKeyWrapping;
25
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyEncryption;
26
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrapping;
27
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
28
use Jose\Component\Encryption\Compression\CompressionMethod;
29
use Jose\Component\Encryption\Compression\CompressionMethodManager;
30
31
class JWEBuilder
32
{
33
    /**
34
     * @var JsonConverter
35
     */
36
    private $jsonConverter;
37
38
    /**
39
     * @var null|string
40
     */
41
    private $payload;
42
43
    /**
44
     * @var string|null
45
     */
46
    private $aad;
47
48
    /**
49
     * @var array
50
     */
51
    private $recipients = [];
52
53
    /**
54
     * @var AlgorithmManager
55
     */
56
    private $keyEncryptionAlgorithmManager;
57
58
    /**
59
     * @var AlgorithmManager
60
     */
61
    private $contentEncryptionAlgorithmManager;
62
63
    /**
64
     * @var CompressionMethodManager
65
     */
66
    private $compressionManager;
67
68
    /**
69
     * @var array
70
     */
71
    private $sharedProtectedHeader = [];
72
73
    /**
74
     * @var array
75
     */
76
    private $sharedHeader = [];
77
78
    /**
79
     * @var null|CompressionMethod
80
     */
81
    private $compressionMethod = null;
82
83
    /**
84
     * @var null|ContentEncryptionAlgorithm
85
     */
86
    private $contentEncryptionAlgorithm = null;
87
88
    /**
89
     * @var null|string
90
     */
91
    private $keyManagementMode = null;
92
93
    /**
94
     * JWEBuilder constructor.
95
     *
96
     * @param JsonConverter            $jsonConverter
97
     * @param AlgorithmManager         $keyEncryptionAlgorithmManager
98
     * @param AlgorithmManager         $contentEncryptionAlgorithmManager
99
     * @param CompressionMethodManager $compressionManager
100
     */
101
    public function __construct(JsonConverter $jsonConverter, AlgorithmManager $keyEncryptionAlgorithmManager, AlgorithmManager $contentEncryptionAlgorithmManager, CompressionMethodManager $compressionManager)
102
    {
103
        $this->jsonConverter = $jsonConverter;
104
        $this->keyEncryptionAlgorithmManager = $keyEncryptionAlgorithmManager;
105
        $this->contentEncryptionAlgorithmManager = $contentEncryptionAlgorithmManager;
106
        $this->compressionManager = $compressionManager;
107
    }
108
109
    /**
110
     * Reset the current data.
111
     *
112
     * @return JWEBuilder
113
     */
114
    public function create(): self
115
    {
116
        $this->payload = null;
117
        $this->aad = null;
118
        $this->recipients = [];
119
        $this->sharedProtectedHeader = [];
120
        $this->sharedHeader = [];
121
        $this->compressionMethod = null;
122
        $this->contentEncryptionAlgorithm = null;
123
        $this->keyManagementMode = null;
124
125
        return $this;
126
    }
127
128
    /**
129
     * Returns the key encryption algorithm manager.
130
     *
131
     * @return AlgorithmManager
132
     */
133
    public function getKeyEncryptionAlgorithmManager(): AlgorithmManager
134
    {
135
        return $this->keyEncryptionAlgorithmManager;
136
    }
137
138
    /**
139
     * Returns the content encryption algorithm manager.
140
     *
141
     * @return AlgorithmManager
142
     */
143
    public function getContentEncryptionAlgorithmManager(): AlgorithmManager
144
    {
145
        return $this->contentEncryptionAlgorithmManager;
146
    }
147
148
    /**
149
     * Returns the compression method manager.
150
     *
151
     * @return CompressionMethodManager
152
     */
153
    public function getCompressionMethodManager(): CompressionMethodManager
154
    {
155
        return $this->compressionManager;
156
    }
157
158
    /**
159
     * Set the payload of the JWE to build.
160
     *
161
     * @param mixed $payload
162
     *
163
     * @return JWEBuilder
164
     */
165
    public function withPayload($payload): self
166
    {
167
        $payload = is_string($payload) ? $payload : $this->jsonConverter->encode($payload);
168
        if (false === mb_detect_encoding($payload, 'UTF-8', true)) {
169
            throw new \InvalidArgumentException('The payload must be encoded in UTF-8');
170
        }
171
        $clone = clone $this;
172
        $clone->payload = $payload;
173
174
        return $clone;
175
    }
176
177
    /**
178
     * Set the Additional Authenticated Data of the JWE to build.
179
     *
180
     * @param string|null $aad
181
     *
182
     * @return JWEBuilder
183
     */
184
    public function withAAD(?string $aad): self
185
    {
186
        $clone = clone $this;
187
        $clone->aad = $aad;
188
189
        return $clone;
190
    }
191
192
    /**
193
     * Set the shared protected header of the JWE to build.
194
     *
195
     * @param array $sharedProtectedHeader
196
     *
197
     * @return JWEBuilder
198
     */
199
    public function withSharedProtectedHeader(array $sharedProtectedHeader): self
200
    {
201
        $this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $this->sharedHeader);
202
        foreach ($this->recipients as $recipient) {
203
            $this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $recipient->getHeader());
204
        }
205
        $clone = clone $this;
206
        $clone->sharedProtectedHeader = $sharedProtectedHeader;
207
208
        return $clone;
209
    }
210
211
    /**
212
     * Set the shared header of the JWE to build.
213
     *
214
     * @param array $sharedHeader
215
     *
216
     * @return JWEBuilder
217
     */
218
    public function withSharedHeader(array $sharedHeader): self
219
    {
220
        $this->checkDuplicatedHeaderParameters($this->sharedProtectedHeader, $sharedHeader);
221
        foreach ($this->recipients as $recipient) {
222
            $this->checkDuplicatedHeaderParameters($sharedHeader, $recipient->getHeader());
223
        }
224
        $clone = clone $this;
225
        $clone->sharedHeader = $sharedHeader;
226
227
        return $clone;
228
    }
229
230
    /**
231
     * Adds a recipient to the JWE to build.
232
     *
233
     * @param JWK   $recipientKey
234
     * @param array $recipientHeader
235
     *
236
     * @return JWEBuilder
237
     */
238
    public function addRecipient(JWK $recipientKey, array $recipientHeader = []): self
239
    {
240
        $this->checkDuplicatedHeaderParameters($this->sharedProtectedHeader, $recipientHeader);
241
        $this->checkDuplicatedHeaderParameters($this->sharedHeader, $recipientHeader);
242
        $clone = clone $this;
243
        $completeHeader = array_merge($clone->sharedHeader, $recipientHeader, $clone->sharedProtectedHeader);
244
        $clone->checkAndSetContentEncryptionAlgorithm($completeHeader);
245
        $keyEncryptionAlgorithm = $clone->getKeyEncryptionAlgorithm($completeHeader);
246
        if (null === $clone->keyManagementMode) {
247
            $clone->keyManagementMode = $keyEncryptionAlgorithm->getKeyManagementMode();
248
        } else {
249
            if (!$clone->areKeyManagementModesCompatible($clone->keyManagementMode, $keyEncryptionAlgorithm->getKeyManagementMode())) {
250
                throw new \InvalidArgumentException('Foreign key management mode forbidden.');
251
            }
252
        }
253
254
        $compressionMethod = $clone->getCompressionMethod($completeHeader);
255
        if (null !== $compressionMethod) {
256
            if (null === $clone->compressionMethod) {
257
                $clone->compressionMethod = $compressionMethod;
258
            } elseif ($clone->compressionMethod->name() !== $compressionMethod->name()) {
259
                throw new \InvalidArgumentException('Incompatible compression method.');
260
            }
261
        }
262
        if (null === $compressionMethod && null !== $clone->compressionMethod) {
263
            throw new \InvalidArgumentException('Inconsistent compression method.');
264
        }
265
        $clone->checkKey($keyEncryptionAlgorithm, $recipientKey);
266
        $clone->recipients[] = [
267
            'key'                      => $recipientKey,
268
            'header'                   => $recipientHeader,
269
            'key_encryption_algorithm' => $keyEncryptionAlgorithm,
270
        ];
271
272
        return $clone;
273
    }
274
275
    /**
276
     * Builds the JWE.
277
     *
278
     * @return JWE
279
     */
280
    public function build(): JWE
281
    {
282
        if (null === $this->payload) {
283
            throw new \LogicException('Payload not set.');
284
        }
285
        if (0 === count($this->recipients)) {
286
            throw new \LogicException('No recipient.');
287
        }
288
289
        $additionalHeader = [];
290
        $cek = $this->determineCEK($additionalHeader);
291
292
        $recipients = [];
293
        foreach ($this->recipients as $recipient) {
294
            $recipient = $this->processRecipient($recipient, $cek, $additionalHeader);
295
            $recipients[] = $recipient;
296
        }
297
298
        if (!empty($additionalHeader) && 1 === count($this->recipients)) {
299
            $sharedProtectedHeader = array_merge($additionalHeader, $this->sharedProtectedHeader);
300
        } else {
301
            $sharedProtectedHeader = $this->sharedProtectedHeader;
302
        }
303
        $encodedSharedProtectedHeader = empty($sharedProtectedHeader) ? '' : Base64Url::encode($this->jsonConverter->encode($sharedProtectedHeader));
304
305
        list($ciphertext, $iv, $tag) = $this->encryptJWE($cek, $encodedSharedProtectedHeader);
306
307
        return JWE::create($ciphertext, $iv, $tag, $this->aad, $this->sharedHeader, $sharedProtectedHeader, $encodedSharedProtectedHeader, $recipients);
308
    }
309
310
    /**
311
     * @param array $completeHeader
312
     */
313
    private function checkAndSetContentEncryptionAlgorithm(array $completeHeader): void
314
    {
315
        $contentEncryptionAlgorithm = $this->getContentEncryptionAlgorithm($completeHeader);
316
        if (null === $this->contentEncryptionAlgorithm) {
317
            $this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
318
        } elseif ($contentEncryptionAlgorithm->name() !== $this->contentEncryptionAlgorithm->name()) {
319
            throw new \InvalidArgumentException('Inconsistent content encryption algorithm');
320
        }
321
    }
322
323
    /**
324
     * @param array  $recipient
325
     * @param string $cek
326
     * @param array  $additionalHeader
327
     *
328
     * @return Recipient
329
     */
330
    private function processRecipient(array $recipient, string $cek, array &$additionalHeader): Recipient
331
    {
332
        $completeHeader = array_merge($this->sharedHeader, $recipient['header'], $this->sharedProtectedHeader);
333
        /** @var KeyEncryptionAlgorithm $keyEncryptionAlgorithm */
334
        $keyEncryptionAlgorithm = $recipient['key_encryption_algorithm'];
335
        $encryptedContentEncryptionKey = $this->getEncryptedKey($completeHeader, $cek, $keyEncryptionAlgorithm, $additionalHeader, $recipient['key']);
336
        $recipientHeader = $recipient['header'];
337
        if (!empty($additionalHeader) && 1 !== count($this->recipients)) {
338
            $recipientHeader = array_merge($recipientHeader, $additionalHeader);
339
            $additionalHeader = [];
340
        }
341
342
        return Recipient::create($recipientHeader, $encryptedContentEncryptionKey);
343
    }
344
345
    /**
346
     * @param string $cek
347
     * @param string $encodedSharedProtectedHeader
348
     *
349
     * @return array
350
     */
351
    private function encryptJWE(string $cek, string $encodedSharedProtectedHeader): array
352
    {
353
        $tag = null;
354
        $iv_size = $this->contentEncryptionAlgorithm->getIVSize();
355
        $iv = $this->createIV($iv_size);
356
        $payload = $this->preparePayload();
357
        $aad = $this->aad ? Base64Url::encode($this->aad) : null;
358
        $ciphertext = $this->contentEncryptionAlgorithm->encryptContent($payload, $cek, $iv, $aad, $encodedSharedProtectedHeader, $tag);
359
360
        return [$ciphertext, $iv, $tag];
361
    }
362
363
    /**
364
     * @return string
365
     */
366
    private function preparePayload(): ?string
367
    {
368
        $prepared = $this->payload;
369
370
        if (null === $this->compressionMethod) {
371
            return $prepared;
372
        }
373
        $compressedPayload = $this->compressionMethod->compress($prepared);
374
375
        return $compressedPayload;
376
    }
377
378
    /**
379
     * @param array                  $completeHeader
380
     * @param string                 $cek
381
     * @param KeyEncryptionAlgorithm $keyEncryptionAlgorithm
382
     * @param JWK                    $recipientKey
383
     * @param array                  $additionalHeader
384
     *
385
     * @return string|null
386
     */
387
    private function getEncryptedKey(array $completeHeader, string $cek, KeyEncryptionAlgorithm $keyEncryptionAlgorithm, array &$additionalHeader, JWK $recipientKey): ?string
388
    {
389
        if ($keyEncryptionAlgorithm instanceof KeyEncryption) {
390
            return $this->getEncryptedKeyFromKeyEncryptionAlgorithm($completeHeader, $cek, $keyEncryptionAlgorithm, $recipientKey, $additionalHeader);
391
        } elseif ($keyEncryptionAlgorithm instanceof KeyWrapping) {
392
            return $this->getEncryptedKeyFromKeyWrappingAlgorithm($completeHeader, $cek, $keyEncryptionAlgorithm, $recipientKey, $additionalHeader);
393
        } elseif ($keyEncryptionAlgorithm instanceof KeyAgreementWithKeyWrapping) {
394
            return $this->getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm($completeHeader, $cek, $keyEncryptionAlgorithm, $additionalHeader, $recipientKey);
395
        } elseif ($keyEncryptionAlgorithm instanceof KeyAgreement) {
396
            return null;
397
        } elseif ($keyEncryptionAlgorithm instanceof DirectEncryption) {
398
            return null;
399
        }
400
401
        throw new \InvalidArgumentException('Unsupported key encryption algorithm.');
402
    }
403
404
    /**
405
     * @param array                       $completeHeader
406
     * @param string                      $cek
407
     * @param KeyAgreementWithKeyWrapping $keyEncryptionAlgorithm
408
     * @param array                       $additionalHeader
409
     * @param JWK                         $recipientKey
410
     *
411
     * @return string
412
     */
413
    private function getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm(array $completeHeader, string $cek, KeyAgreementWithKeyWrapping $keyEncryptionAlgorithm, array &$additionalHeader, JWK $recipientKey): string
414
    {
415
        return $keyEncryptionAlgorithm->wrapAgreementKey($recipientKey, $cek, $this->contentEncryptionAlgorithm->getCEKSize(), $completeHeader, $additionalHeader);
416
    }
417
418
    /**
419
     * @param array         $completeHeader
420
     * @param string        $cek
421
     * @param KeyEncryption $keyEncryptionAlgorithm
422
     * @param JWK           $recipientKey
423
     * @param array         $additionalHeader
424
     *
425
     * @return string
426
     */
427
    private function getEncryptedKeyFromKeyEncryptionAlgorithm(array $completeHeader, string $cek, KeyEncryption $keyEncryptionAlgorithm, JWK $recipientKey, array &$additionalHeader): string
428
    {
429
        return $keyEncryptionAlgorithm->encryptKey($recipientKey, $cek, $completeHeader, $additionalHeader);
430
    }
431
432
    /**
433
     * @param array       $completeHeader
434
     * @param string      $cek
435
     * @param KeyWrapping $keyEncryptionAlgorithm
436
     * @param JWK         $recipientKey
437
     * @param array       $additionalHeader
438
     *
439
     * @return string
440
     */
441
    private function getEncryptedKeyFromKeyWrappingAlgorithm(array $completeHeader, string $cek, KeyWrapping $keyEncryptionAlgorithm, JWK $recipientKey, array &$additionalHeader): string
442
    {
443
        return $keyEncryptionAlgorithm->wrapKey($recipientKey, $cek, $completeHeader, $additionalHeader);
444
    }
445
446
    /**
447
     * @param KeyEncryptionAlgorithm $keyEncryptionAlgorithm
448
     * @param JWK                    $recipientKey
449
     */
450
    private function checkKey(KeyEncryptionAlgorithm $keyEncryptionAlgorithm, JWK $recipientKey)
451
    {
452
        KeyChecker::checkKeyUsage($recipientKey, 'encryption');
453
        if ('dir' !== $keyEncryptionAlgorithm->name()) {
454
            KeyChecker::checkKeyAlgorithm($recipientKey, $keyEncryptionAlgorithm->name());
455
        } else {
456
            KeyChecker::checkKeyAlgorithm($recipientKey, $this->contentEncryptionAlgorithm->name());
457
        }
458
    }
459
460
    /**
461
     * @param array $additionalHeader
462
     *
463
     * @return string
464
     */
465
    private function determineCEK(array &$additionalHeader): string
466
    {
467
        switch ($this->keyManagementMode) {
468
            case KeyEncryption::MODE_ENCRYPT:
469
            case KeyEncryption::MODE_WRAP:
470
                return $this->createCEK($this->contentEncryptionAlgorithm->getCEKSize());
471
            case KeyEncryption::MODE_AGREEMENT:
472
                if (1 !== count($this->recipients)) {
473
                    throw new \LogicException('Unable to encrypt for multiple recipients using key agreement algorithms.');
474
                }
475
                /** @var JWK $key */
476
                $key = $this->recipients[0]['key'];
477
                /** @var KeyAgreement $algorithm */
478
                $algorithm = $this->recipients[0]['key_encryption_algorithm'];
479
                $completeHeader = array_merge($this->sharedHeader, $this->recipients[0]['header'], $this->sharedProtectedHeader);
480
481
                return $algorithm->getAgreementKey($this->contentEncryptionAlgorithm->getCEKSize(), $this->contentEncryptionAlgorithm->name(), $key, $completeHeader, $additionalHeader);
482
            case KeyEncryption::MODE_DIRECT:
483
                if (1 !== count($this->recipients)) {
484
                    throw new \LogicException('Unable to encrypt for multiple recipients using key agreement algorithms.');
485
                }
486
                /** @var JWK $key */
487
                $key = $this->recipients[0]['key'];
488
                if ('oct' !== $key->get('kty')) {
489
                    throw new \RuntimeException('Wrong key type.');
490
                }
491
492
                return Base64Url::decode($key->get('k'));
493
            default:
494
                throw new \InvalidArgumentException(sprintf('Unsupported key management mode "%s".', $this->keyManagementMode));
495
        }
496
    }
497
498
    /**
499
     * @param array $completeHeader
500
     *
501
     * @return CompressionMethod|null
502
     */
503
    private function getCompressionMethod(array $completeHeader): ?CompressionMethod
504
    {
505
        if (!array_key_exists('zip', $completeHeader)) {
506
            return null;
507
        }
508
509
        return $this->compressionManager->get($completeHeader['zip']);
510
    }
511
512
    /**
513
     * @param string $current
514
     * @param string $new
515
     *
516
     * @return bool
517
     */
518
    private function areKeyManagementModesCompatible(string $current, string $new): bool
519
    {
520
        $agree = KeyEncryptionAlgorithm::MODE_AGREEMENT;
521
        $dir = KeyEncryptionAlgorithm::MODE_DIRECT;
522
        $enc = KeyEncryptionAlgorithm::MODE_ENCRYPT;
523
        $wrap = KeyEncryptionAlgorithm::MODE_WRAP;
524
        $supportedKeyManagementModeCombinations = [$enc.$enc => true, $enc.$wrap => true, $wrap.$enc => true, $wrap.$wrap => true, $agree.$agree => false, $agree.$dir => false, $agree.$enc => false, $agree.$wrap => false, $dir.$agree => false, $dir.$dir => false, $dir.$enc => false, $dir.$wrap => false, $enc.$agree => false, $enc.$dir => false, $wrap.$agree => false, $wrap.$dir => false];
525
526
        if (array_key_exists($current.$new, $supportedKeyManagementModeCombinations)) {
527
            return $supportedKeyManagementModeCombinations[$current.$new];
528
        }
529
530
        return false;
531
    }
532
533
    /**
534
     * @param int $size
535
     *
536
     * @return string
537
     */
538
    private function createCEK(int $size): string
539
    {
540
        return random_bytes($size / 8);
541
    }
542
543
    /**
544
     * @param int $size
545
     *
546
     * @return string
547
     */
548
    private function createIV(int $size): string
549
    {
550
        return random_bytes($size / 8);
551
    }
552
553
    /**
554
     * @param array $completeHeader
555
     *
556
     * @return KeyEncryptionAlgorithm
557
     */
558
    private function getKeyEncryptionAlgorithm(array $completeHeader): KeyEncryptionAlgorithm
559
    {
560
        if (!array_key_exists('alg', $completeHeader)) {
561
            throw new \InvalidArgumentException('Parameter "alg" is missing.');
562
        }
563
        $keyEncryptionAlgorithm = $this->keyEncryptionAlgorithmManager->get($completeHeader['alg']);
564
        if (!$keyEncryptionAlgorithm instanceof KeyEncryptionAlgorithm) {
565
            throw new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $completeHeader['alg']));
566
        }
567
568
        return $keyEncryptionAlgorithm;
569
    }
570
571
    /**
572
     * @param array $completeHeader
573
     *
574
     * @return ContentEncryptionAlgorithm
575
     */
576
    private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm
577
    {
578
        if (!array_key_exists('enc', $completeHeader)) {
579
            throw new \InvalidArgumentException('Parameter "enc" is missing.');
580
        }
581
        $contentEncryptionAlgorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']);
582
        if (!$contentEncryptionAlgorithm instanceof ContentEncryptionAlgorithm) {
583
            throw new \InvalidArgumentException(sprintf('The content encryption algorithm "%s" is not supported or not a content encryption algorithm instance.', $completeHeader['alg']));
584
        }
585
586
        return $contentEncryptionAlgorithm;
587
    }
588
589
    /**
590
     * @param array $header1
591
     * @param array $header2
592
     */
593
    private function checkDuplicatedHeaderParameters(array $header1, array $header2)
594
    {
595
        $inter = array_intersect_key($header1, $header2);
596
        if (!empty($inter)) {
597
            throw new \InvalidArgumentException(sprintf('The header contains duplicated entries: %s.', implode(', ', array_keys($inter))));
598
        }
599
    }
600
}
601