Failed Conditions
Push — v7 ( 3b619f...61eb8a )
by Florent
02:14
created

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