Completed
Push — master ( 027ea9...6fcfe6 )
by Florent
05:03
created

Encrypter::compressPayload()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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