Completed
Push — v2.0.x ( 149802...dc4bfa )
by Florent
02:32
created

getEncryptedKeyFroKeyAgreementAlgorithm()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 6
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2016 Spomky-Labs
7
 *
8
 * This software may be modified and distributed under the terms
9
 * of the MIT license.  See the LICENSE file for details.
10
 */
11
12
namespace Jose;
13
14
use Base64Url\Base64Url;
15
use Jose\Algorithm\ContentEncryptionAlgorithmInterface;
16
use Jose\Algorithm\JWAManagerInterface;
17
use Jose\Algorithm\KeyEncryption\DirectEncryptionInterface;
18
use Jose\Algorithm\KeyEncryption\KeyAgreementInterface;
19
use Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface;
20
use Jose\Algorithm\KeyEncryption\KeyEncryptionInterface;
21
use Jose\Algorithm\KeyEncryption\KeyWrappingInterface;
22
use Jose\Algorithm\KeyEncryptionAlgorithmInterface;
23
use Jose\Behaviour\HasCompressionManager;
24
use Jose\Behaviour\HasJWAManager;
25
use Jose\Behaviour\HasKeyChecker;
26
use Jose\Compression\CompressionManagerInterface;
27
use Jose\Object\JWEInterface;
28
use Jose\Object\JWKInterface;
29
use Jose\Object\Recipient;
30
31
/**
32
 */
33
final class Encrypter implements EncrypterInterface
34
{
35
    use HasKeyChecker;
36
    use HasJWAManager;
37
    use HasCompressionManager;
38
39
    /**
40
     * Encrypter constructor.
41
     *
42
     * @param \Jose\Algorithm\JWAManagerInterface           $jwa_manager
43
     * @param \Jose\Compression\CompressionManagerInterface $compression_manager
44
     */
45
    public function __construct(
46
        JWAManagerInterface $jwa_manager,
47
        CompressionManagerInterface $compression_manager)
48
    {
49
        $this->setJWAManager($jwa_manager);
50
        $this->setCompressionManager($compression_manager);
51
    }
52
53
    /**
54
     * @param \Jose\Object\JWEInterface      $jwe
55
     * @param \Jose\Object\JWKInterface      $recipient_key
56
     * @param \Jose\Object\JWKInterface|null $sender_key
57
     * @param array                          $recipient_headers
58
     *
59
     * @return \Jose\Object\JWEInterface
60
     */
61
    public function addRecipient(JWEInterface $jwe, JWKInterface $recipient_key, JWKInterface $sender_key = null, array $recipient_headers = [])
62
    {
63
        $complete_headers = array_merge(
64
            $jwe->getSharedProtectedHeaders(),
65
            $jwe->getSharedHeaders(),
66
            $recipient_headers
67
        );
68
69
        // Key Encryption Algorithm
70
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
71
72
        // Content Encryption Algorithm
73
        $content_encryption_algorithm = $this->findContentEncryptionAlgorithm($complete_headers);
74
75
        // We check keys (usage and algorithm if restrictions are set)
76
        $this->checkKeys($key_encryption_algorithm, $recipient_key, $sender_key);
77
78
        if (null === $jwe->getCiphertext()) {
79
            // the content is not yet encrypted (no recipient)
80
81
            if (!empty($jwe->getSharedProtectedHeaders())) {
82
                $jwe = $jwe->withEncodedSharedProtectedHeaders(Base64Url::encode(json_encode($jwe->getSharedProtectedHeaders())));
83
            }
84
85
            // CEK
86
            $content_encryption_key = $this->getCEK(
87
                $complete_headers,
88
                $key_encryption_algorithm,
89
                $content_encryption_algorithm,
90
                $recipient_key,
91
                $sender_key
92
            );
93
            $jwe = $jwe->withContentEncryptionKey($content_encryption_key);
94
95
            // IV
96
            if (null !== $iv_size = $content_encryption_algorithm->getIVSize()) {
97
                $iv = $this->createIV($iv_size);
98
                $jwe = $jwe->withIV($iv);
99
            }
100
101
            // We encrypt the payload and get the tag
102
            $tag = null;
103
            $payload = $this->preparePayload($jwe->getPayload(), $complete_headers);
104
105
            $ciphertext = $content_encryption_algorithm->encryptContent(
106
                $payload,
107
                $content_encryption_key,
108
                $jwe->getIV(),
109
                $jwe->getAAD(),
110
                $jwe->getEncodedSharedProtectedHeaders(),
111
                $tag
112
            );
113
            $jwe = $jwe->withCiphertext($ciphertext);
114
115
            // Tag
116
            if (null !== $tag) {
117
                $jwe = $jwe->withTag($tag);
118
            }
119
120
            $recipient = $this->computeRecipient(
121
                $jwe,
122
                $key_encryption_algorithm,
123
                $content_encryption_algorithm,
124
                $complete_headers,
125
                $recipient_headers,
126
                $recipient_key,
127
                $sender_key
128
            );
129
130
            $jwe = $jwe->addRecipient($recipient);
131
        } else {
132
            if (0 === $jwe->countRecipients()) {
133
                throw new \InvalidArgumentException('Invalid JWE. The payload is encrypted but no recipient is available.');
134
            }
135
            if (null === $jwe->getContentEncryptionKey()) {
136
                throw new \InvalidArgumentException('Unable to add a recipient. The JWE must be decrypted first.');
137
            }
138
            $current_key_management_mode = $this->getCurrentKeyManagementMode($jwe);
139
140
            if (false === $this->areKeyManagementModesCompatible($current_key_management_mode, $key_encryption_algorithm->getKeyManagementMode())) {
141
                throw new \InvalidArgumentException('Foreign key management mode forbidden.');
142
            }
143
144
            $recipient = $this->computeRecipient(
145
                $jwe,
146
                $key_encryption_algorithm,
147
                $content_encryption_algorithm,
148
                $complete_headers,
149
                $recipient_headers,
150
                $recipient_key,
151
                $sender_key
152
            );
153
154
            $jwe = $jwe->addRecipient($recipient);
155
        }
156
157
        return $jwe;
158
    }
159
160
    /**
161
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface $algorithm
162
     * @param \Jose\Object\JWKInterface                       $recipient_key
163
     * @param \Jose\Object\JWKInterface|null                  $sender_key
164
     */
165
    private function checkKeys(KeyEncryptionAlgorithmInterface $algorithm, JWKInterface $recipient_key, JWKInterface $sender_key = null)
166
    {
167
        $this->checkKeyUsage($recipient_key, 'encryption');
168
        $this->checkKeyAlgorithm($recipient_key, $algorithm->getAlgorithmName());
169
        if ($sender_key instanceof JWKInterface) {
170
            $this->checkKeyUsage($sender_key, 'encryption');
171
            $this->checkKeyAlgorithm($sender_key, $algorithm->getAlgorithmName());
172
        }
173
    }
174
175
    /**
176
     * @param \Jose\Object\JWEInterface $jwe
177
     *
178
     * @return string
179
     */
180
    private function getCurrentKeyManagementMode(JWEInterface $jwe)
181
    {
182
        $complete_headers = array_merge(
183
            $jwe->getSharedProtectedHeaders(),
184
            $jwe->getSharedHeaders(),
185
            $jwe->getRecipient(0)->getHeaders()
186
        );
187
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
188
189
        return $key_encryption_algorithm->getKeyManagementMode();
190
    }
191
192
    /**
193
     * @param \Jose\Object\JWEInterface                           $jwe
194
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
195
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
196
     * @param array                                               $complete_headers
197
     * @param array                                               $recipient_headers
198
     * @param \Jose\Object\JWKInterface                           $recipient_key
199
     * @param \Jose\Object\JWKInterface|null                      $sender_key
200
     *
201
     * @return \Jose\Object\RecipientInterface
202
     */
203
    private function computeRecipient(JWEInterface $jwe, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array $complete_headers, array $recipient_headers, JWKInterface $recipient_key, JWKInterface $sender_key = null)
204
    {
205
        $recipient = new Recipient();
206
        $recipient = $recipient->withHeaders($recipient_headers);
207
208
        $additional_headers = [];
209
        $encrypted_content_encryption_key = $this->getEncryptedKey(
210
            $complete_headers,
211
            $jwe->getContentEncryptionKey(),
212
            $key_encryption_algorithm,
213
            $content_encryption_algorithm,
214
            $additional_headers,
215
            $recipient_key,
216
            $sender_key
217
        );
218
        if (!empty($additional_headers)) {
219
            foreach ($additional_headers as $key => $value) {
220
                $recipient = $recipient->withHeader($key, $value);
221
            }
222
        }
223
        if (null !== $encrypted_content_encryption_key) {
224
            $recipient = $recipient->withEncryptedKey($encrypted_content_encryption_key);
225
        }
226
227
        return $recipient;
228
    }
229
230
    /**
231
     * @param string $current
232
     * @param string $new
233
     *
234
     * @return bool
235
     */
236
    private function areKeyManagementModesCompatible($current, $new)
237
    {
238
        $agree = KeyEncryptionAlgorithmInterface::MODE_AGREEMENT;
239
        $dir = KeyEncryptionAlgorithmInterface::MODE_DIRECT;
240
        $enc = KeyEncryptionAlgorithmInterface::MODE_ENCRYPT;
241
        $wrap = KeyEncryptionAlgorithmInterface::MODE_WRAP;
242
243
        $supported_key_management_mode_combinations = [
244
            $agree.$enc => true,
245
            $agree.$wrap => true,
246
            $dir.$enc => true,
247
            $dir.$wrap => true,
248
            $enc.$enc => true,
249
            $enc.$wrap => true,
250
            $wrap.$enc => true,
251
            $wrap.$wrap => true,
252
            $agree.$agree => false,
253
            $agree.$dir => false,
254
            $dir.$agree => false,
255
            $dir.$dir => false,
256
            $enc.$agree => false,
257
            $enc.$dir => false,
258
            $wrap.$agree => false,
259
            $wrap.$dir => false,
260
        ];
261
262
        if (array_key_exists($current.$new, $supported_key_management_mode_combinations)) {
263
            return $supported_key_management_mode_combinations[$current.$new];
264
        }
265
266
        return false;
267
    }
268
269
    /**
270
     * @param string $payload
271
     * @param array  $complete_headers
272
     *
273
     * @return string
274
     */
275
    private function preparePayload($payload, array $complete_headers)
276
    {
277
        $prepared = is_string($payload) ? $payload : json_encode($payload);
278
279
        if (null === $prepared) {
280
            throw new \InvalidArgumentException('The payload is empty or cannot encoded into JSON.');
281
        }
282
        if (!array_key_exists('zip', $complete_headers)) {
283
            return $prepared;
284
        }
285
286
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($complete_headers['zip']);
287
        if (null === $compression_method) {
288
            throw new \RuntimeException(sprintf('Compression method "%s" not supported', $complete_headers['zip']));
289
        }
290
        $compressed_payload = $compression_method->compress($prepared);
291
        if (!is_string($compressed_payload)) {
292
            throw new \RuntimeException('Compression failed.');
293
        }
294
295
        return $compressed_payload;
296
    }
297
298
    /**
299
     * @param array                                               $complete_headers
300
     * @param string                                              $cek
301
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
302
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
303
     * @param \Jose\Object\JWKInterface                           $recipient_key
304
     * @param \Jose\Object\JWKInterface|null                      $sender_key
305
     * @param array                                               $additional_headers
306
     *
307
     * @return string|null
308
     */
309
    private function getEncryptedKey(array $complete_headers, $cek, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key, JWKInterface $sender_key = null)
310
    {
311
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
312
313
            return $this->getEncryptedKeyFroKeyEncryptionAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key);
314
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
315
316
            return $this->getEncryptedKeyFroKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key);
317
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
318
319
            return $this->getEncryptedKeyFroKeyAgreementAndKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $content_encryption_algorithm, $additional_headers, $recipient_key, $sender_key);
320
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
321
322
            return $this->getEncryptedKeyFroKeyAgreementAlgorithm($complete_headers, $key_encryption_algorithm, $content_encryption_algorithm, $additional_headers, $recipient_key, $sender_key);
323
        }
324
    }
325
326
    /**
327
     * @param array                                               $complete_headers
328
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementInterface $key_encryption_algorithm
329
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
330
     * @param array                                               $additional_headers
331
     * @param \Jose\Object\JWKInterface                           $recipient_key
332
     * @param \Jose\Object\JWKInterface|null                      $sender_key
333
     *
334
     * @return mixed
335
     */
336
    private function getEncryptedKeyFroKeyAgreementAlgorithm(array $complete_headers, KeyAgreementInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key, JWKInterface $sender_key = null)
337
    {
338
        if (!$sender_key instanceof JWKInterface) {
339
            throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
340
        }
341
        $jwt_cek = $key_encryption_algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $sender_key, $recipient_key, $complete_headers, $additional_headers);
342
343
        return $jwt_cek;
344
    }
345
346
    /**
347
     * @param array                                                       $complete_headers
348
     * @param string                                                      $cek
349
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface $key_encryption_algorithm
350
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface         $content_encryption_algorithm
351
     * @param array                                                       $additional_headers
352
     * @param \Jose\Object\JWKInterface                                   $recipient_key
353
     * @param \Jose\Object\JWKInterface|null                              $sender_key
354
     *
355
     * @return string
356
     */
357
    private function getEncryptedKeyFroKeyAgreementAndKeyWrappingAlgorithm(array $complete_headers, $cek, KeyAgreementWrappingInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key, JWKInterface $sender_key = null)
358
    {
359
        if (!$sender_key instanceof JWKInterface) {
360
            throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
361
        }
362
        $jwt_cek = $key_encryption_algorithm->wrapAgreementKey($sender_key, $recipient_key, $cek, $content_encryption_algorithm->getCEKSize(), $complete_headers, $additional_headers);
363
364
        return $jwt_cek;
365
    }
366
367
    /**
368
     * @param array                                                $complete_headers
369
     * @param string                                               $cek
370
     * @param \Jose\Algorithm\KeyEncryption\KeyEncryptionInterface $key_encryption_algorithm
371
     * @param \Jose\Object\JWKInterface                            $recipient_key
372
     *
373
     * @return string
374
     */
375
    private function getEncryptedKeyFroKeyEncryptionAlgorithm(array $complete_headers, $cek, KeyEncryptionInterface $key_encryption_algorithm, JWKInterface $recipient_key)
376
    {
377
        return $key_encryption_algorithm->encryptKey(
378
            $recipient_key,
379
            $cek,
380
            $complete_headers
381
        );
382
    }
383
384
    /**
385
     * @param array                                              $complete_headers
386
     * @param string                                             $cek
387
     * @param \Jose\Algorithm\KeyEncryption\KeyWrappingInterface $key_encryption_algorithm
388
     * @param \Jose\Object\JWKInterface                          $recipient_key
389
     *
390
     * @return string
391
     */
392
    private function getEncryptedKeyFroKeyWrappingAlgorithm(array $complete_headers, $cek, KeyWrappingInterface $key_encryption_algorithm, JWKInterface $recipient_key)
393
    {
394
        return $key_encryption_algorithm->wrapKey(
395
            $recipient_key,
396
            $cek,
397
            $complete_headers
398
        );
399
    }
400
401
    /**
402
     * @param array                                               $complete_headers
403
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
404
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
405
     * @param \Jose\Object\JWKInterface                           $recipient_key
406
     * @param \Jose\Object\JWKInterface|null                      $sender_key
407
     *
408
     * @return string
409
     */
410
    private function getCEK(array $complete_headers, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key, JWKInterface $sender_key = null)
411
    {
412
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
413
            return $this->createCEK($content_encryption_algorithm->getCEKSize());
414
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
415
            return $this->createCEK($content_encryption_algorithm->getCEKSize());
416
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
417
            return $this->calculateAgreementKey($complete_headers, $key_encryption_algorithm, $content_encryption_algorithm, $recipient_key, $sender_key);
418
        } elseif ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
419
            return $key_encryption_algorithm->getCEK($recipient_key);
420
        } else {
421
            throw new \RuntimeException('Unable to get key management mode.');
422
        }
423
    }
424
425
    /**
426
     * @param array $complete_headers
427
     *
428
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
429
     */
430
    private function findKeyEncryptionAlgorithm(array $complete_headers)
431
    {
432
        if (!array_key_exists('alg', $complete_headers)) {
433
            throw new \InvalidArgumentException('Parameter "alg" is missing.');
434
        }
435
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
436
        if ($key_encryption_algorithm instanceof KeyEncryptionAlgorithmInterface) {
437
            return $key_encryption_algorithm;
438
        }
439
        throw new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_headers['alg']));
440
    }
441
442
    /**
443
     * @param array $complete_headers
444
     *
445
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
446
     */
447
    private function findContentEncryptionAlgorithm(array $complete_headers)
448
    {
449
        if (!array_key_exists('enc', $complete_headers)) {
450
            throw new \InvalidArgumentException('Parameter "enc" is missing.');
451
        }
452
453
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['enc']);
454
        if (!$content_encryption_algorithm instanceof ContentEncryptionAlgorithmInterface) {
455
            throw new \RuntimeException(sprintf('The algorithm "%s" is not enabled or does not implement ContentEncryptionInterface.', $complete_headers['enc']));
456
        }
457
458
        return $content_encryption_algorithm;
459
    }
460
461
    /**
462
     * @param array                                               $complete_headers
463
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementInterface $key_encryption_algorithm
464
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
465
     * @param \Jose\Object\JWKInterface                           $recipient_key
466
     * @param \Jose\Object\JWKInterface|null                      $sender_key
467
     *
468
     * @return string
469
     */
470
    private function calculateAgreementKey(array $complete_headers, KeyAgreementInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key, JWKInterface $sender_key = null)
471
    {
472
        if (!$sender_key instanceof JWKInterface) {
473
            throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
474
        }
475
        $additional_header_values = [];
476
        $cek = $key_encryption_algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $sender_key, $recipient_key, $complete_headers, $additional_header_values);
477
478
        return $cek;
479
    }
480
481
    /**
482
     * @param int $size
483
     *
484
     * @return string
485
     */
486
    private function createCEK($size)
487
    {
488
        return $this->generateRandomString($size / 8);
489
    }
490
491
    /**
492
     * @param int $size
493
     *
494
     * @return string
495
     */
496
    private function createIV($size)
497
    {
498
        return $this->generateRandomString($size / 8);
499
    }
500
501
    /**
502
     * @param int $length
503
     *
504
     * @return string
505
     */
506
    private function generateRandomString($length)
507
    {
508
        if (function_exists('random_bytes')) {
509
            return random_bytes($length);
510
        } else {
511
            return openssl_random_pseudo_bytes($length);
512
        }
513
    }
514
}
515