Failed Conditions
Push — master ( 28d61e...98c33a )
by Florent
03:29
created

JWEBuilder::getCompressionMethod()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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