Completed
Push — master ( 5f00e3...074281 )
by Florent
02:25
created

Encrypter::checkInput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 14
rs 9.4286
cc 2
eloc 9
nc 2
nop 1
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\Algorithm\ContentEncryption\ContentEncryptionInterface;
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\Behaviour\HasCompressionManager;
22
use Jose\Behaviour\HasJWAManager;
23
use Jose\Behaviour\HasKeyChecker;
24
use Jose\Behaviour\HasPayloadConverter;
25
use Jose\Compression\CompressionManagerInterface;
26
use Jose\Object\EncryptionInstructionInterface;
27
use Jose\Payload\PayloadConverterManagerInterface;
28
use Jose\Util\Converter;
29
30
/**
31
 */
32
final class Encrypter implements EncrypterInterface
33
{
34
    use HasKeyChecker;
35
    use HasJWAManager;
36
    use HasPayloadConverter;
37
    use HasCompressionManager;
38
39
    /**
40
     * Encrypter constructor.
41
     *
42
     * @param \Jose\Algorithm\JWAManagerInterface            $jwa_manager
43
     * @param \Jose\Payload\PayloadConverterManagerInterface $payload_converter_manager
44
     * @param \Jose\Compression\CompressionManagerInterface  $compression_manager
45
     */
46
    public function __construct(
47
        JWAManagerInterface $jwa_manager,
48
        PayloadConverterManagerInterface $payload_converter_manager,
49
        CompressionManagerInterface $compression_manager)
50
    {
51
        $this->setJWAManager($jwa_manager);
52
        $this->setPayloadConverter($payload_converter_manager);
53
        $this->setCompressionManager($compression_manager);
54
    }
55
56
    /**
57
     * @param array|\Jose\Object\JWKInterface|\Jose\Object\JWKSetInterface|\Jose\Object\JWTInterface|string $input
58
     * @param array                                                                                         $instructions
59
     * @param array                                                                                         $shared_protected_header
60
     * @param array                                                                                         $shared_unprotected_header
61
     * @param string                                                                                        $serialization
62
     * @param null                                                                                          $aad
63
     *
64
     * @return string
65
     */
66
    public function encrypt($input, array $instructions, array $shared_protected_header = [], array $shared_unprotected_header = [], $serialization = JSONSerializationModes::JSON_COMPACT_SERIALIZATION, $aad = null)
67
    {
68
        $additional_header = [];
69
        $input = $this->getPayloadConverter()->convertPayloadToString($additional_header, $input);
70
        $this->checkInstructions($instructions, $serialization);
71
72
        $protected_header = array_merge($shared_protected_header, $additional_header);
73
74
        // We check if key management mode is OK
75
        $key_management_mode = $this->getKeyManagementMode($instructions, $protected_header, $shared_unprotected_header);
76
77
        // We get the content encryption algorithm
78
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($instructions, $protected_header, $shared_unprotected_header);
79
80
        // CEK
81
        $cek = $this->determineCEK($key_management_mode, $instructions, $protected_header, $shared_unprotected_header, $content_encryption_algorithm->getCEKSize());
82
83
        $recipients = ['recipients' => []];
84
        foreach ($instructions as $instruction) {
85
            $recipients['recipients'][] = $this->computeRecipient($instruction, $protected_header, $shared_unprotected_header, $cek, $content_encryption_algorithm->getCEKSize(), $serialization);
86
        }
87
88
        // We prepare the payload and compress it if required
89
        $compression_method = $this->findCompressionMethod($instructions, $protected_header, $shared_unprotected_header);
90
        $this->compressPayload($input, $compression_method);
91
92
        // We compute the initialization vector
93
        $iv = null;
94
        if (null !== $iv_size = $content_encryption_algorithm->getIVSize()) {
95
            $iv = $this->createIV($iv_size);
96
        }
97
98
        // JWT Shared protected header
99
        $jwt_shared_protected_header = Base64Url::encode(json_encode($protected_header));
100
101
        // We encrypt the payload and get the tag
102
        $tag = null;
103
        $ciphertext = $content_encryption_algorithm->encryptContent($input, $cek, $iv, $aad, $jwt_shared_protected_header, $tag);
104
105
        // JWT Ciphertext
106
        $jwt_ciphertext = Base64Url::encode($ciphertext);
107
108
        // JWT AAD
109
        $jwt_aad = null === $aad ? null : Base64Url::encode($aad);
110
111
        // JWT Tag
112
        $jwt_tag = null === $tag ? null : Base64Url::encode($tag);
113
114
        // JWT IV
115
        $jwt_iv = null === $iv ? '' : Base64Url::encode($iv);
116
117
        $values = [
118
            'ciphertext'  => $jwt_ciphertext,
119
            'protected'   => $jwt_shared_protected_header,
120
            'unprotected' => $shared_unprotected_header,
121
            'iv'          => $jwt_iv,
122
            'tag'         => $jwt_tag,
123
            'aad'         => $jwt_aad,
124
        ];
125
        foreach ($values as $key => $value) {
126
            if (!empty($value)) {
127
                $recipients[$key] = $value;
128
            }
129
        }
130
131
        $prepared = Converter::convert($recipients, $serialization);
132
133
        return is_array($prepared) ? current($prepared) : $prepared;
134
    }
135
136
    /**
137
     * @param \Jose\Object\EncryptionInstructionInterface $instruction
138
     * @param                                             $protected_header
139
     * @param                                             $unprotected_header
140
     * @param string                                      $cek
141
     * @param int                                         $cek_size
142
     * @param string                                      $serialization
143
     *
144
     * @throws \Exception
145
     *
146
     * @return array
147
     */
148
    private function computeRecipient(EncryptionInstructionInterface $instruction, &$protected_header, $unprotected_header, $cek, $cek_size, $serialization)
149
    {
150
        if (!$this->checkKeyUsage($instruction->getRecipientKey(), 'encryption')) {
151
            throw new \InvalidArgumentException('Key cannot be used to encrypt');
152
        }
153
154
        $recipient_header = $instruction->getRecipientUnprotectedHeader();
155
        $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
156
157
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
158
159
        if (!$this->checkKeyAlgorithm($instruction->getRecipientKey(), $key_encryption_algorithm->getAlgorithmName())) {
160
            throw new \InvalidArgumentException(sprintf('Key is only allowed for algorithm "%s".', $key_encryption_algorithm->getAlgorithmName()));
161
        }
162
163
        $jwt_cek = null;
164
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
165
            $jwt_cek = Base64Url::encode($key_encryption_algorithm->encryptKey($instruction->getRecipientKey(), $cek, $protected_header));
166
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
167
            if (null === $instruction->getSenderKey()) {
168
                throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
169
            }
170
            $additional_header_values = [];
171
            $jwt_cek = Base64Url::encode($key_encryption_algorithm->wrapAgreementKey($instruction->getSenderKey(), $instruction->getRecipientKey(), $cek, $cek_size, $complete_header, $additional_header_values));
172
            $this->updateHeader($additional_header_values, $protected_header, $recipient_header, $serialization);
173
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
174
            $additional_header_values = [];
175
            $jwt_cek = Base64Url::encode($key_encryption_algorithm->getAgreementKey($cek_size, $instruction->getSenderKey(), $instruction->getRecipientKey(), $complete_header, $additional_header_values));
0 ignored issues
show
Bug introduced by
It seems like $instruction->getSenderKey() can be null; however, getAgreementKey() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
176
            $this->updateHeader($additional_header_values, $protected_header, $recipient_header, $serialization);
177
        }
178
179
        $result = [];
180
        if (null !== $jwt_cek) {
181
            $result['encrypted_key'] = $jwt_cek;
182
        }
183
        if (!empty($recipient_header)) {
184
            $result['header'] = $recipient_header;
185
        }
186
187
        return $result;
188
    }
189
190
    /**
191
     * @param array  $additional_header_values
192
     * @param array  $protected_header
193
     * @param array  $recipient_header
194
     * @param string $serialization
195
     */
196
    private function updateHeader(array $additional_header_values, array &$protected_header, array &$recipient_header, $serialization)
197
    {
198
        if (JSONSerializationModes::JSON_COMPACT_SERIALIZATION === $serialization) {
199
            $protected_header = array_merge($protected_header, $additional_header_values);
200
        } else {
201
            $recipient_header = array_merge($recipient_header, $additional_header_values);
202
        }
203
    }
204
205
    /**
206
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
207
     * @param array                                         $protected_header
208
     * @param array                                         $unprotected_header
209
     *
210
     * @return string
211
     */
212
    private function getKeyManagementMode(array $instructions, $protected_header, $unprotected_header)
213
    {
214
        $mode = null;
215
        foreach ($instructions as $instruction) {
216
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
217
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
218
219
            $temp = $this->getKeyManagementMode2($complete_header);
220
            if (null === $mode) {
221
                $mode = $temp;
222
            } else {
223
                if (!$this->areKeyManagementModeAuthorized($mode, $temp)) {
224
                    throw new \RuntimeException('Foreign key management mode forbidden.');
225
                }
226
            }
227
        }
228
229
        return $mode;
230
    }
231
232
    /**
233
     * @param string $mode1
234
     * @param string $mode2
235
     *
236
     * @return bool
237
     */
238
    private function areKeyManagementModeAuthorized($mode1, $mode2)
239
    {
240
        if ($mode1 > $mode2) {
241
            $temp = $mode1;
242
            $mode1 = $mode2;
243
            $mode2 = $temp;
244
        }
245
        switch ($mode1.$mode2) {
246
            case 'encenc':
247
            case 'encwrap':
248
            case 'wrapwrap':
249
            case 'agreeagree':
250
            case 'dirdir':
251
                return true;
252
            default:
253
                return false;
254
        }
255
    }
256
257
    /**
258
     * @param array $complete_header
259
     *
260
     * @return string
261
     */
262
    private function getKeyManagementMode2($complete_header)
263
    {
264
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
265
266
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
267
            return 'enc';
268
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
269
            return 'wrap';
270
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
271
            return 'agree';
272
        } elseif ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
273
            return 'dir';
274
        } else {
275
            throw new \RuntimeException('Unable to get key management mode.');
276
        }
277
    }
278
279
    /**
280
     * @param string                                        $key_management_mode
281
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
282
     * @param array                                         $protected_header
283
     * @param array                                         $unprotected_header
284
     * @param int                                           $cek_size
285
     *
286
     * @return string
287
     */
288
    private function determineCEK($key_management_mode, array $instructions, $protected_header, $unprotected_header, $cek_size)
289
    {
290
        switch ($key_management_mode) {
291
            case 'enc':
292
            case 'wrap':
293
                return $this->createCEK($cek_size);
294
            case 'dir':
295
                return $this->getDirectKey($instructions, $protected_header, $unprotected_header);
296
            case 'agree':
297
                return $this->getAgreementKey($instructions, $protected_header, $unprotected_header, $cek_size);
298
            default:
299
                throw new \RuntimeException('Unable to get CEK (unsupported key management mode).');
300
        }
301
    }
302
303
    /**
304
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
305
     * @param array                                         $protected_header
306
     * @param array                                         $unprotected_header
307
     *
308
     * @return string
309
     */
310
    private function getDirectKey(array $instructions, $protected_header, $unprotected_header)
311
    {
312
        $cek = null;
313
        foreach ($instructions as $instruction) {
314
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
315
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
316
317
            $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
318
            if (!$key_encryption_algorithm instanceof DirectEncryptionInterface) {
319
                throw new \RuntimeException('The key encryption algorithm is not an instance of DirectEncryptionInterface');
320
            }
321
322
            $temp = $key_encryption_algorithm->getCEK($instruction->getRecipientKey(), $complete_header);
323
            if (null === $cek) {
324
                $cek = $temp;
325
            } else {
326
                if ($cek !== $temp) {
327
                    throw new \RuntimeException('Foreign CEK forbidden using direct key.');
328
                }
329
            }
330
        }
331
332
        return $cek;
333
    }
334
335
    /**
336
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
337
     * @param array                                         $protected_header
338
     * @param array                                         $unprotected_header
339
     * @param int                                           $cek_size
340
     *
341
     * @return string
342
     */
343
    private function getAgreementKey(array $instructions, $protected_header, $unprotected_header, $cek_size)
344
    {
345
        $cek = null;
346
        foreach ($instructions as $instruction) {
347
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
348
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
349
350
            $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header);
351
352
            if (!$key_encryption_algorithm instanceof KeyAgreementInterface) {
353
                throw new \RuntimeException('The key encryption algorithm is not an instance of KeyAgreementInterface');
354
            }
355
356
            if (null === $instruction->getSenderKey()) {
357
                throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
358
            }
359
            $additional_header_values = [];
360
            $temp = $key_encryption_algorithm->getAgreementKey($cek_size, $instruction->getSenderKey(), $instruction->getRecipientKey(), $complete_header, $additional_header_values);
361
            if (null === $cek) {
362
                $cek = $temp;
363
            } else {
364
                if ($cek !== $temp) {
365
                    throw new \RuntimeException('Foreign CEK forbidden using direct key agreement.');
366
                }
367
            }
368
        }
369
370
        return $cek;
371
    }
372
373
    /**
374
     * @param string $payload
375
     * @param string $method
376
     */
377
    private function compressPayload(&$payload, $method = null)
378
    {
379
        if (null !== $method) {
380
            $compression_method = $this->getCompressionMethod($method);
381
            $payload = $compression_method->compress($payload);
382
            if (!is_string($payload)) {
383
                throw new \RuntimeException('Compression failed.');
384
            }
385
        }
386
    }
387
388
    /**
389
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
390
     * @param array                                         $protected_header
391
     * @param array                                         $unprotected_header
392
     *
393
     * @return string
394
     */
395
    private function findCompressionMethod(array $instructions, $protected_header, $unprotected_header)
396
    {
397
        $method = null;
398
        $first = true;
399
        foreach ($instructions as $instruction) {
400
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
401
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
402
            if ($first) {
403
                if (array_key_exists('zip', $complete_header)) {
404
                    $method = $complete_header['zip'];
405
                }
406
                $first = null;
407
            } else {
408
                if (array_key_exists('zip', $complete_header) && $method !== $complete_header['zip']) {
409
                    throw new \RuntimeException('Foreign compression method forbidden');
410
                }
411
            }
412
        }
413
414
        return $method;
415
    }
416
417
    /**
418
     * @param string $method
419
     *
420
     * @return \Jose\Compression\CompressionInterface
421
     */
422
    private function getCompressionMethod($method)
423
    {
424
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
425
        if (null === $compression_method) {
426
            throw new \RuntimeException(sprintf('Compression method "%s" not supported', $method));
427
        }
428
429
        return $compression_method;
430
    }
431
432
    /**
433
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
434
     * @param string                                        $serialization
435
     */
436
    private function checkInstructions(array $instructions, $serialization)
437
    {
438
        if (empty($instructions)) {
439
            throw new \InvalidArgumentException('No instruction.');
440
        }
441
        if (count($instructions) > 1 && JSONSerializationModes::JSON_SERIALIZATION !== $serialization) {
442
            throw new \InvalidArgumentException('Only one instruction authorized when Compact or Flattened Serialization Overview is selected.');
443
        }
444
        foreach ($instructions as $instruction) {
445
            if (!$instruction instanceof EncryptionInstructionInterface) {
446
                throw new \InvalidArgumentException('Bad instruction. Must implement EncryptionInstructionInterface.');
447
            }
448
        }
449
    }
450
451
    /**
452
     * @param array $complete_header
453
     *
454
     * @return \Jose\Algorithm\KeyEncryption\DirectEncryptionInterface|\Jose\Algorithm\KeyEncryption\KeyEncryptionInterface|\Jose\Algorithm\KeyEncryption\KeyAgreementInterface|\Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface
455
     */
456
    private function getKeyEncryptionAlgorithm($complete_header)
457
    {
458
        if (!array_key_exists('alg', $complete_header)) {
459
            throw new \InvalidArgumentException('Parameter "alg" is missing.');
460
        }
461
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_header['alg']);
462
        foreach ([
463
                     '\Jose\Algorithm\KeyEncryption\DirectEncryptionInterface',
464
                     '\Jose\Algorithm\KeyEncryption\KeyEncryptionInterface',
465
                     '\Jose\Algorithm\KeyEncryption\KeyAgreementInterface',
466
                     '\Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface',
467
                 ] as $class) {
468
            if ($key_encryption_algorithm instanceof $class) {
469
                return $key_encryption_algorithm;
470
            }
471
        }
472
        throw new \RuntimeException(sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_header['alg']));
473
    }
474
475
    /**
476
     * @param \Jose\Object\EncryptionInstructionInterface[] $instructions
477
     * @param array                                         $protected_header
478
     * @param array                                         $unprotected_header
479
     *
480
     * @return \Jose\Algorithm\ContentEncryption\ContentEncryptionInterface
481
     */
482
    private function getContentEncryptionAlgorithm(array $instructions, array $protected_header = [], array $unprotected_header = [])
483
    {
484
        $algorithm = null;
485
        foreach ($instructions as $instruction) {
486
            $recipient_header = $instruction->getRecipientUnprotectedHeader();
487
            $complete_header = array_merge($protected_header, $unprotected_header, $recipient_header);
488
            if (!array_key_exists('enc', $complete_header)) {
489
                throw new \InvalidArgumentException('Parameter "enc" is missing.');
490
            }
491
            if (null === $algorithm) {
492
                $algorithm = $complete_header['enc'];
493
            } else {
494
                if ($algorithm !== $complete_header['enc']) {
495
                    throw new \InvalidArgumentException('Foreign "enc" parameter forbidden.');
496
                }
497
            }
498
        }
499
500
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
501
        if (!$content_encryption_algorithm instanceof ContentEncryptionInterface) {
502
            throw new \RuntimeException(sprintf('The algorithm "%s" does not implement ContentEncryptionInterface.', $algorithm));
503
        }
504
505
        return $content_encryption_algorithm;
506
    }
507
508
    private function createCEK($size)
509
    {
510
        return $this->generateRandomString($size / 8);
511
    }
512
513
    private function createIV($size)
514
    {
515
        return $this->generateRandomString($size / 8);
516
    }
517
518
    /**
519
     * @param int $length
520
     *
521
     * @return string
522
     */
523
    private function generateRandomString($length)
524
    {
525
        if (function_exists('random_bytes')) {
526
            return random_bytes($length);
527
        } else {
528
            return openssl_random_pseudo_bytes($length);
529
        }
530
    }
531
}
532