Failed Conditions
Push — v7 ( 629225...6b9564 )
by Florent
02:02
created

JWEBuilder::processRecipient()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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