Completed
Push — master ( 7aad34...96e43e )
by Florent
02:35
created

getEncryptedKeyFroKeyEncryptionAlgorithm()   A

Complexity

Conditions 1
Paths 1

Size

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