Completed
Push — master ( 636cdd...8c476c )
by Florent
02:36
created

Encrypter::checkKeys()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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