Failed Conditions
Push — v7 ( 722dd5...1e67af )
by Florent
03:17
created

JWEBuilder::build()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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