Completed
Push — master ( f67525...5cf1da )
by Florent
02:34
created

Encrypter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 11
rs 9.4286
cc 1
eloc 9
nc 1
nop 4
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2015 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\Behaviour\HasCompressionManager;
16
use Jose\Behaviour\HasJWAManager;
17
use Jose\Behaviour\HasJWTManager;
18
use Jose\Behaviour\HasKeyChecker;
19
use Jose\Behaviour\HasPayloadConverter;
20
use Jose\Compression\CompressionManagerInterface;
21
use Jose\Operation\ContentEncryptionInterface;
22
use Jose\Operation\DirectEncryptionInterface;
23
use Jose\Operation\KeyAgreementInterface;
24
use Jose\Operation\KeyAgreementWrappingInterface;
25
use Jose\Operation\KeyEncryptionInterface;
26
use Jose\Payload\PayloadConverterManagerInterface;
27
use Jose\Util\Converter;
28
29
/**
30
 */
31
class Encrypter implements EncrypterInterface
32
{
33
    use HasKeyChecker;
34
    use HasJWAManager;
35
    use HasJWTManager;
36
    use HasPayloadConverter;
37
    use HasCompressionManager;
38
39
    /**
40
     * Encrypter constructor.
41
     *
42
     * @param \Jose\JWTManagerInterface                      $jwt_manager
43
     * @param \Jose\JWAManagerInterface                      $jwa_manager
44
     * @param \Jose\Payload\PayloadConverterManagerInterface $payload_converter_manager
45
     * @param \Jose\Compression\CompressionManagerInterface  $compression_manager
46
     */
47
    public function __construct(
48
        JWTManagerInterface $jwt_manager,
49
        JWAManagerInterface $jwa_manager,
50
        PayloadConverterManagerInterface $payload_converter_manager,
51
        CompressionManagerInterface $compression_manager)
52
    {
53
        $this->setJWTManager($jwt_manager);
54
        $this->setJWAManager($jwa_manager);
55
        $this->setPayloadConverter($payload_converter_manager);
56
        $this->setCompressionManager($compression_manager);
57
    }
58
59
    protected function createCEK($size)
60
    {
61
        return $this->generateRandomString($size / 8);
62
    }
63
64
    protected function createIV($size)
65
    {
66
        return $this->generateRandomString($size / 8);
67
    }
68
69
    /**
70
     * @param int $length
71
     *
72
     * @return string
73
     */
74
    private function generateRandomString($length)
75
    {
76
        if (function_exists('random_bytes')) {
77
            return random_bytes($length);
78
        } else {
79
            return openssl_random_pseudo_bytes($length);
80
        }
81
    }
82
83
    /**
84
     * @param $input
85
     */
86
    private function checkInput(&$input)
87
    {
88
        if ($input instanceof JWTInterface) {
89
            return;
90
        }
91
92
        $header = [];
93
        $payload = $this->getPayloadConverter()->convertPayloadToString($header, $input);
94
95
        $jwt = $this->getJWTManager()->createJWT();
96
        $jwt = $jwt->withPayload($payload);
97
        $jwt = $jwt->withProtectedHeader($header);
98
        $input = $jwt;
99
    }
100
101
    /**
102
     * @param array|JWKInterface|JWKSetInterface|JWTInterface|string $input
103
     * @param array                                                  $instructions
104
     * @param array                                                  $shared_protected_header
105
     * @param array                                                  $shared_unprotected_header
106
     * @param string                                                 $serialization
107
     * @param null                                                   $aad
108
     *
109
     * @return string
110
     */
111
    public function encrypt($input, array $instructions, array $shared_protected_header = [], array $shared_unprotected_header = [], $serialization = JSONSerializationModes::JSON_COMPACT_SERIALIZATION, $aad = null)
112
    {
113
        $this->checkInput($input);
114
        $this->checkInstructions($instructions, $serialization);
115
116
        $protected_header = array_merge($input->getProtectedHeader(), $shared_protected_header);
117
        $unprotected_header = array_merge($input->getUnprotectedHeader(), $shared_unprotected_header);
118
119
        // We check if key management mode is OK
120
        $key_management_mode = $this->getKeyManagementMode($instructions, $protected_header, $unprotected_header);
121
122
        // We get the content encryption algorithm
123
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($instructions, $protected_header, $unprotected_header);
124
125
        // CEK
126
        $cek = $this->determineCEK($key_management_mode, $instructions, $protected_header, $unprotected_header, $content_encryption_algorithm->getCEKSize());
127
128
        $recipients = ['recipients' => []];
129
        foreach ($instructions as $instruction) {
130
            $recipients['recipients'][] = $this->computeRecipient($instruction, $protected_header, $unprotected_header, $cek, $content_encryption_algorithm->getCEKSize(), $serialization);
131
        }
132
133
        // We prepare the payload and compress it if required
134
        $payload = $input->getPayload();
135
        $compression_method = $this->findCompressionMethod($instructions, $protected_header, $unprotected_header);
136
        $this->compressPayload($payload, $compression_method);
137
138
        // We compute the initialization vector
139
        $iv = null;
140
        if (null !== $iv_size = $content_encryption_algorithm->getIVSize()) {
141
            $iv = $this->createIV($iv_size);
142
        }
143
144
        // JWT Shared protected header
145
        $jwt_shared_protected_header = Base64Url::encode(json_encode($protected_header));
146
147
        // We encrypt the payload and get the tag
148
        $tag = null;
149
        $ciphertext = $content_encryption_algorithm->encryptContent($payload, $cek, $iv, $aad, $jwt_shared_protected_header, $tag);
150
151
        // JWT Ciphertext
152
        $jwt_ciphertext = Base64Url::encode($ciphertext);
153
154
        // JWT AAD
155
        $jwt_aad = null === $aad ? null : Base64Url::encode($aad);
156
157
        // JWT Tag
158
        $jwt_tag = null === $tag ? null : Base64Url::encode($tag);
159
160
        // JWT IV
161
        $jwt_iv = null === $iv ? '' : Base64Url::encode($iv);
162
163
        $values = [
164
            'ciphertext'  => $jwt_ciphertext,
165
            'protected'   => $jwt_shared_protected_header,
166
            'unprotected' => $unprotected_header,
167
            'iv'          => $jwt_iv,
168
            'tag'         => $jwt_tag,
169
            'aad'         => $jwt_aad,
170
        ];
171
        foreach ($values as $key => $value) {
172
            if (!empty($value)) {
173
                $recipients[$key] = $value;
174
            }
175
        }
176
177
        $prepared = Converter::convert($recipients, $serialization);
178
179
        return is_array($prepared) ? current($prepared) : $prepared;
180
    }
181
182
    /**
183
     * @param \Jose\EncryptionInstructionInterface $instruction
184
     * @param                                      $protected_header
185
     * @param                                      $unprotected_header
186
     * @param string                               $cek
187
     * @param int                                  $cek_size
188
     * @param string                               $serialization
189
     *
190
     * @throws \Exception
191
     *
192
     * @return array
193
     */
194
    protected function computeRecipient(EncryptionInstructionInterface $instruction, &$protected_header, $unprotected_header, $cek, $cek_size, $serialization)
195
    {
196
        if (!$this->checkKeyUsage($instruction->getRecipientKey(), 'encryption')) {
197
            throw new \InvalidArgumentException('Key cannot be used to encrypt');
198
        }
199
200
        $recipient_header = $instruction->getRecipientUnprotectedHeader();
201
        $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
202
203
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
204
205
        if (!$this->checkKeyAlgorithm($instruction->getRecipientKey(), $key_encryption_algorithm->getAlgorithmName())) {
206
            throw new \InvalidArgumentException(sprintf('Key is only allowed for algorithm "%s".', $key_encryption_algorithm->getAlgorithmName()));
207
        }
208
209
        $jwt_cek = null;
210
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
211
            $jwt_cek = Base64Url::encode($key_encryption_algorithm->encryptKey($instruction->getRecipientKey(), $cek, $protected_header));
212
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
213
            if (null === $instruction->getSenderKey()) {
214
                throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
215
            }
216
            $additional_header_values = [];
217
            $jwt_cek = Base64Url::encode($key_encryption_algorithm->wrapAgreementKey($instruction->getSenderKey(), $instruction->getRecipientKey(), $cek, $cek_size, $complete_header, $additional_header_values));
218
            $this->updateHeader($additional_header_values, $protected_header, $recipient_header, $serialization);
219
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
220
            if (null === $instruction->getSenderKey()) {
221
                throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
222
            }
223
            $additional_header_values = [];
224
            $jwt_cek = Base64Url::encode($key_encryption_algorithm->getAgreementKey($cek_size, $instruction->getSenderKey(), $instruction->getRecipientKey(), $complete_header, $additional_header_values));
225
            $this->updateHeader($additional_header_values, $protected_header, $recipient_header, $serialization);
226
        }
227
228
        $result = [];
229
        if (null !== $jwt_cek) {
230
            $result['encrypted_key'] = $jwt_cek;
231
        }
232
        if (!empty($recipient_header)) {
233
            $result['header'] = $recipient_header;
234
        }
235
236
        return $result;
237
    }
238
239
    /**
240
     * @param array  $additional_header_values
241
     * @param array  $protected_header
242
     * @param array  $recipient_header
243
     * @param string $serialization
244
     */
245
    protected function updateHeader(array $additional_header_values, array &$protected_header, array &$recipient_header, $serialization)
246
    {
247
        if (JSONSerializationModes::JSON_COMPACT_SERIALIZATION === $serialization) {
248
            $protected_header = array_merge($protected_header, $additional_header_values);
249
        } else {
250
            $recipient_header = array_merge($recipient_header, $additional_header_values);
251
        }
252
    }
253
254
    /**
255
     * @param \Jose\EncryptionInstructionInterface[] $instructions
256
     * @param array                                  $protected_header
257
     * @param array                                  $unprotected_header
258
     *
259
     * @return string
260
     */
261
    protected function getKeyManagementMode(array $instructions, $protected_header, $unprotected_header)
262
    {
263
        $mode = null;
264
        foreach ($instructions as $instruction) {
265
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
266
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
267
268
            $temp = $this->getKeyManagementMode2($complete_header);
269
            if (null === $mode) {
270
                $mode = $temp;
271
            } else {
272
                if (!$this->areKeyManagementModeAuthorized($mode, $temp)) {
273
                    throw new \RuntimeException('Foreign key management mode forbidden.');
274
                }
275
            }
276
        }
277
278
        return $mode;
279
    }
280
281
    /**
282
     * @param string $mode1
283
     * @param string $mode2
284
     *
285
     * @return bool
286
     */
287
    protected function areKeyManagementModeAuthorized($mode1, $mode2)
288
    {
289
        if ($mode1 > $mode2) {
290
            $temp = $mode1;
291
            $mode1 = $mode2;
292
            $mode2 = $temp;
293
        }
294
        switch ($mode1.$mode2) {
295
            case 'encenc':
296
            case 'encwrap':
297
            case 'wrapwrap':
298
            case 'agreeagree':
299
            case 'dirdir':
300
                return true;
301
            default:
302
                return false;
303
        }
304
    }
305
306
    /**
307
     * @param array $complete_header
308
     *
309
     * @return string
310
     */
311
    protected function getKeyManagementMode2($complete_header)
312
    {
313
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
314
315
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
316
            return 'enc';
317
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
318
            return 'wrap';
319
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
320
            return 'agree';
321
        } elseif ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
322
            return 'dir';
323
        } else {
324
            throw new \RuntimeException('Unable to get key management mode.');
325
        }
326
    }
327
328
    /**
329
     * @param string                                 $key_management_mode
330
     * @param \Jose\EncryptionInstructionInterface[] $instructions
331
     * @param array                                  $protected_header
332
     * @param array                                  $unprotected_header
333
     * @param int                                    $cek_size
334
     *
335
     * @return string
336
     */
337
    protected function determineCEK($key_management_mode, array $instructions, $protected_header, $unprotected_header, $cek_size)
338
    {
339
        switch ($key_management_mode) {
340
            case 'enc':
341
            case 'wrap':
342
                return $this->createCEK($cek_size);
343
            case 'dir':
344
                return $this->getDirectKey($instructions, $protected_header, $unprotected_header);
345
            case 'agree':
346
                return $this->getAgreementKey($instructions, $protected_header, $unprotected_header, $cek_size);
347
            default:
348
                throw new \RuntimeException('Unable to get CEK (unsupported key management mode).');
349
        }
350
    }
351
352
    /**
353
     * @param \Jose\EncryptionInstructionInterface[] $instructions
354
     * @param array                                  $protected_header
355
     * @param array                                  $unprotected_header
356
     *
357
     * @return string
358
     */
359
    protected function getDirectKey(array $instructions, $protected_header, $unprotected_header)
360
    {
361
        $cek = null;
362
        foreach ($instructions as $instruction) {
363
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
364
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
365
366
            $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
367
            if (!$key_encryption_algorithm instanceof DirectEncryptionInterface) {
368
                throw new \RuntimeException('The key encryption algorithm is not an instance of DirectEncryptionInterface');
369
            }
370
371
            $temp = $key_encryption_algorithm->getCEK($instruction->getRecipientKey(), $complete_header);
372
            if (null === $cek) {
373
                $cek = $temp;
374
            } else {
375
                if ($cek !== $temp) {
376
                    throw new \RuntimeException('Foreign CEK forbidden using direct key.');
377
                }
378
            }
379
        }
380
381
        return $cek;
382
    }
383
384
    /**
385
     * @param \Jose\EncryptionInstructionInterface[] $instructions
386
     * @param array                                  $protected_header
387
     * @param array                                  $unprotected_header
388
     * @param int                                    $cek_size
389
     *
390
     * @return string
391
     */
392
    protected function getAgreementKey(array $instructions, $protected_header, $unprotected_header, $cek_size)
393
    {
394
        $cek = null;
395
        foreach ($instructions as $instruction) {
396
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
397
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
398
399
            $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
400
401
            if (!$key_encryption_algorithm instanceof KeyAgreementInterface) {
402
                throw new \RuntimeException('The key encryption algorithm is not an instance of KeyAgreementInterface');
403
            }
404
405
            if (null === $instruction->getSenderKey()) {
406
                throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
407
            }
408
            $additional_header_values = [];
409
            $temp = $key_encryption_algorithm->getAgreementKey($cek_size, $instruction->getSenderKey(), $instruction->getRecipientKey(), $complete_header, $additional_header_values);
410
            if (null === $cek) {
411
                $cek = $temp;
412
            } else {
413
                if ($cek !== $temp) {
414
                    throw new \RuntimeException('Foreign CEK forbidden using direct key agreement.');
415
                }
416
            }
417
        }
418
419
        return $cek;
420
    }
421
422
    /**
423
     * @param string $payload
424
     * @param string $method
425
     */
426
    protected function compressPayload(&$payload, $method = null)
427
    {
428
        if (null !== $method) {
429
            $compression_method = $this->getCompressionMethod($method);
430
            $payload = $compression_method->compress($payload);
431
            if (!is_string($payload)) {
432
                throw new \RuntimeException('Compression failed.');
433
            }
434
        }
435
    }
436
437
    /**
438
     * @param \Jose\EncryptionInstructionInterface[] $instructions
439
     * @param array                                  $protected_header
440
     * @param array                                  $unprotected_header
441
     *
442
     * @return string
443
     */
444
    protected function findCompressionMethod(array $instructions, $protected_header, $unprotected_header)
445
    {
446
        $method = null;
447
        $first = true;
448
        foreach ($instructions as $instruction) {
449
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
450
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
451
            if ($first) {
452
                if (array_key_exists('zip', $complete_header)) {
453
                    $method = $complete_header['zip'];
454
                }
455
                $first = null;
456
            } else {
457
                if (array_key_exists('zip', $complete_header) && $method !== $complete_header['zip']) {
458
                    throw new \RuntimeException('Foreign compression method forbidden');
459
                }
460
            }
461
        }
462
463
        return $method;
464
    }
465
466
    /**
467
     * @param string $method
468
     *
469
     * @return \Jose\Compression\CompressionInterface
470
     */
471
    protected function getCompressionMethod($method)
472
    {
473
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
474
        if (null === $compression_method) {
475
            throw new \RuntimeException(sprintf('Compression method "%s" not supported', $method));
476
        }
477
478
        return $compression_method;
479
    }
480
481
    /**
482
     * @param \Jose\EncryptionInstructionInterface[] $instructions
483
     * @param string                                 $serialization
484
     */
485
    protected function checkInstructions(array $instructions, $serialization)
486
    {
487
        if (empty($instructions)) {
488
            throw new \InvalidArgumentException('No instruction.');
489
        }
490
        if (count($instructions) > 1 && JSONSerializationModes::JSON_SERIALIZATION !== $serialization) {
491
            throw new \InvalidArgumentException('Only one instruction authorized when Compact or Flattened Serialization Overview is selected.');
492
        }
493
        foreach ($instructions as $instruction) {
494
            if (!$instruction instanceof EncryptionInstructionInterface) {
495
                throw new \InvalidArgumentException('Bad instruction. Must implement EncryptionInstructionInterface.');
496
            }
497
        }
498
    }
499
500
    /**
501
     * @param array $complete_header
502
     *
503
     * @return \Jose\Operation\DirectEncryptionInterface|\Jose\Operation\KeyEncryptionInterface|\Jose\Operation\KeyAgreementInterface|\Jose\Operation\KeyAgreementWrappingInterface
504
     */
505
    protected function getKeyEncryptionAlgorithm($complete_header)
506
    {
507
        if (!array_key_exists('alg', $complete_header)) {
508
            throw new \InvalidArgumentException('Parameter "alg" is missing.');
509
        }
510
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_header['alg']);
511
        foreach ([
512
                     '\Jose\Operation\DirectEncryptionInterface',
513
                     '\Jose\Operation\KeyEncryptionInterface',
514
                     '\Jose\Operation\KeyAgreementInterface',
515
                     '\Jose\Operation\KeyAgreementWrappingInterface',
516
                 ] as $class) {
517
            if ($key_encryption_algorithm instanceof $class) {
518
                return $key_encryption_algorithm;
519
            }
520
        }
521
        throw new \RuntimeException(sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_header['alg']));
522
    }
523
524
    /**
525
     * @param \Jose\EncryptionInstructionInterface[] $instructions
526
     * @param array                                  $protected_header
527
     * @param array                                  $unprotected_header
528
     *
529
     * @return \Jose\Operation\ContentEncryptionInterface
530
     */
531
    protected function getContentEncryptionAlgorithm(array $instructions, array $protected_header = [], array $unprotected_header = [])
532
    {
533
        $algorithm = null;
534
        foreach ($instructions as $instruction) {
535
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
536
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
537
            if (!array_key_exists('enc', $complete_header)) {
538
                throw new \InvalidArgumentException('Parameter "enc" is missing.');
539
            }
540
            if (null === $algorithm) {
541
                $algorithm = $complete_header['enc'];
542
            } else {
543
                if ($algorithm !== $complete_header['enc']) {
544
                    throw new \InvalidArgumentException('Foreign "enc" parameter forbidden.');
545
                }
546
            }
547
        }
548
549
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
550
        if (!$content_encryption_algorithm instanceof ContentEncryptionInterface) {
551
            throw new \RuntimeException(sprintf('The algorithm "%s" does not implement ContentEncryptionInterface.', $algorithm));
552
        }
553
554
        return $content_encryption_algorithm;
555
    }
556
}
557