Failed Conditions
Push — master ( 199749...882a68 )
by Florent
03:09
created

Encrypter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 8
rs 9.4285
cc 1
eloc 6
nc 1
nop 3
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 Assert\Assertion;
15
use Base64Url\Base64Url;
16
use Jose\Algorithm\ContentEncryptionAlgorithmInterface;
17
use Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface;
18
use Jose\Algorithm\KeyEncryption\KeyEncryptionInterface;
19
use Jose\Algorithm\KeyEncryption\KeyWrappingInterface;
20
use Jose\Algorithm\KeyEncryptionAlgorithmInterface;
21
use Jose\Behaviour\CommonCipheringMethods;
22
use Jose\Behaviour\HasCompressionManager;
23
use Jose\Behaviour\HasJWAManager;
24
use Jose\Behaviour\HasKeyChecker;
25
use Jose\Compression\CompressionInterface;
26
use Jose\Factory\AlgorithmManagerFactory;
27
use Jose\Factory\CompressionManagerFactory;
28
use Jose\Object\JWEInterface;
29
use Jose\Object\JWKInterface;
30
use Jose\Object\Recipient;
31
use Jose\Object\RecipientInterface;
32
33
final class Encrypter implements EncrypterInterface
34
{
35
    use HasKeyChecker;
36
    use HasJWAManager;
37
    use HasCompressionManager;
38
    use CommonCipheringMethods;
39
40
    /**
41
     * {@inheritdoc}
42
     */
43
    public static function createEncrypter(array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF', 'ZLIB', 'GZ'])
44
    {
45
        $encrypter = new self($key_encryption_algorithms, $content_encryption_algorithms, $compression_methods);
46
47
        return $encrypter;
48
    }
49
50
    /**
51
     * Decrypter constructor.
52
     *
53
     * @param string[]|\Jose\Algorithm\KeyEncryptionAlgorithmInterface[]     $key_encryption_algorithms
54
     * @param string[]|\Jose\Algorithm\ContentEncryptionAlgorithmInterface[] $content_encryption_algorithms
55
     * @param string[]|\Jose\Compression\CompressionInterface[]              $compression_methods
56
     */
57
    public function __construct(array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods)
58
    {
59
        $this->setKeyEncryptionAlgorithms($key_encryption_algorithms);
60
        $this->setContentEncryptionAlgorithms($content_encryption_algorithms);
61
        $this->setCompressionMethods($compression_methods);
62
        $this->setJWAManager(AlgorithmManagerFactory::createAlgorithmManager(array_merge($key_encryption_algorithms, $content_encryption_algorithms)));
63
        $this->setCompressionManager(CompressionManagerFactory::createCompressionManager($compression_methods));
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function encrypt(JWEInterface &$jwe)
70
    {
71
        Assertion::false($jwe->isEncrypted(), 'The JWE is already encrypted.');
72
        Assertion::greaterThan($jwe->countRecipients(), 0, 'The JWE does not contain recipient.');
73
74
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($jwe);
75
        $compression_method = $this->getCompressionMethod($jwe);
76
        $key_management_mode = $this->getKeyManagementMode($jwe);
77
        $additional_headers = [];
78
        $cek = $this->determineCEK($jwe, $content_encryption_algorithm, $key_management_mode, $additional_headers);
79
80
        $nb_recipients = $jwe->countRecipients();
81
82
        for ($i = 0; $i < $nb_recipients; $i++) {
83
            $this->processRecipient($jwe, $jwe->getRecipient($i), $cek, $content_encryption_algorithm, $additional_headers);
84
        }
85
86
        if (!empty($additional_headers) && 1 === $jwe->countRecipients()) {
87
            $jwe = $jwe->withSharedProtectedHeaders(array_merge($jwe->getSharedProtectedHeaders(), $additional_headers));
88
        }
89
90
        $iv_size = $content_encryption_algorithm->getIVSize();
91
        $iv = $this->createIV($iv_size);
92
93
        $this->encryptJWE($jwe, $content_encryption_algorithm, $cek, $iv, $compression_method);
94
    }
95
96
    /**
97
     * @param \Jose\Object\JWEInterface                           $jwe
98
     * @param \Jose\Object\RecipientInterface                     $recipient
99
     * @param string                                              $cek
100
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
101
     * @param array                                               $additional_headers
102
     */
103
    private function processRecipient(JWEInterface $jwe, RecipientInterface &$recipient, $cek, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers)
104
    {
105
        if (null === $recipient->getRecipientKey()) {
106
            return;
107
        }
108
        $complete_headers = array_merge($jwe->getSharedProtectedHeaders(), $jwe->getSharedHeaders(), $recipient->getHeaders());
109
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
110
        $this->checkKeys($key_encryption_algorithm, $content_encryption_algorithm, $recipient->getRecipientKey());
0 ignored issues
show
Bug introduced by
It seems like $key_encryption_algorithm defined by $this->findKeyEncryption...ithm($complete_headers) on line 109 can be null; however, Jose\Encrypter::checkKeys() 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...
111
        $encrypted_content_encryption_key = $this->getEncryptedKey($complete_headers, $cek, $key_encryption_algorithm, $content_encryption_algorithm, $additional_headers, $recipient->getRecipientKey());
0 ignored issues
show
Bug introduced by
It seems like $key_encryption_algorithm defined by $this->findKeyEncryption...ithm($complete_headers) on line 109 can be null; however, Jose\Encrypter::getEncryptedKey() 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...
112
        $recipient_headers = $recipient->getHeaders();
113
        if (!empty($additional_headers) && 1 !== $jwe->countRecipients()) {
114
            $recipient_headers = array_merge($recipient_headers, $additional_headers);
115
            $additional_headers = [];
116
        }
117
118
        $recipient = Recipient::createRecipientFromLoadedJWE($recipient_headers, $encrypted_content_encryption_key);
119
    }
120
121
    /**
122
     * @param \Jose\Object\JWEInterface                           $jwe
123
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
124
     * @param string                                              $key_management_mode
125
     * @param array                                               $additional_headers
126
     *
127
     * @return string
128
     */
129
    private function determineCEK(JWEInterface $jwe, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, $key_management_mode, array &$additional_headers)
130
    {
131
        switch ($key_management_mode) {
132
            case KeyEncryptionInterface::MODE_ENCRYPT:
133
            case KeyEncryptionInterface::MODE_WRAP:
134
                return $this->createCEK($content_encryption_algorithm->getCEKSize());
135
            case KeyEncryptionInterface::MODE_AGREEMENT:
136
                Assertion::eq(1, $jwe->countRecipients(), 'Unable to encrypt for multiple recipients using key agreement algorithms.');
137
                $complete_headers = array_merge($jwe->getSharedProtectedHeaders(), $jwe->getSharedHeaders(), $jwe->getRecipient(0)->getHeaders());
138
                $algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
139
140
                return $algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $content_encryption_algorithm->getAlgorithmName(), $jwe->getRecipient(0)->getRecipientKey(), $complete_headers, $additional_headers);
141
            case KeyEncryptionInterface::MODE_DIRECT:
142
                Assertion::eq(1, $jwe->countRecipients(), 'Unable to encrypt for multiple recipients using key agreement algorithms.');
143
                Assertion::eq($jwe->getRecipient(0)->getRecipientKey()->get('kty'), 'oct', 'Wrong key type.');
144
                Assertion::true($jwe->getRecipient(0)->getRecipientKey()->has('k'), 'The key parameter "k" is missing.');
145
146
                return Base64Url::decode($jwe->getRecipient(0)->getRecipientKey()->get('k'));
147
            default:
148
                throw new \InvalidArgumentException(sprintf('Unsupported key management mode "%s".', $key_management_mode));
149
        }
150
    }
151
152
    /**
153
     * @param \Jose\Object\JWEInterface $jwe
154
     *
155
     * @return string
156
     */
157
    private function getKeyManagementMode(JWEInterface $jwe)
158
    {
159
        $mode = null;
160
        $recipients = $jwe->getRecipients();
161
162
        foreach ($recipients as $recipient) {
163
            $complete_headers = array_merge($jwe->getSharedProtectedHeaders(), $jwe->getSharedHeaders(), $recipient->getHeaders());
164
            Assertion::keyExists($complete_headers, 'alg', 'Parameter "alg" is missing.');
165
166
            $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
167
            Assertion::isInstanceOf($key_encryption_algorithm, KeyEncryptionAlgorithmInterface::class, sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_headers['alg']));
168
169
            if (null === $mode) {
170
                $mode = $key_encryption_algorithm->getKeyManagementMode();
171
            } else {
172
                Assertion::true($this->areKeyManagementModesCompatible($mode, $key_encryption_algorithm->getKeyManagementMode()), 'Foreign key management mode forbidden.');
173
            }
174
        }
175
176
        return $mode;
177
    }
178
179
    /**
180
     * @param \Jose\Object\JWEInterface $jwe
181
     *
182
     * @return \Jose\Compression\CompressionInterface|null
183
     */
184
    private function getCompressionMethod(JWEInterface $jwe)
185
    {
186
        $method = null;
187
        $nb_recipients = $jwe->countRecipients();
188
189
        for ($i = 0; $i < $nb_recipients; $i++) {
190
            $complete_headers = array_merge($jwe->getSharedProtectedHeaders(), $jwe->getSharedHeaders(), $jwe->getRecipient($i)->getHeaders());
191
            if (array_key_exists('zip', $complete_headers)) {
192
                if (null === $method) {
193
                    if (0 === $i) {
194
                        $method = $complete_headers['zip'];
195
                    } else {
196
                        throw new \InvalidArgumentException('Inconsistent "zip" parameter.');
197
                    }
198
                } else {
199
                    Assertion::eq($method, $complete_headers['zip'], 'Inconsistent "zip" parameter.');
200
                }
201
            } else {
202
                Assertion::eq(null, $method, 'Inconsistent "zip" parameter.');
203
            }
204
        }
205
206
        if (null === $method) {
207
            return;
208
        }
209
210
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
211
        Assertion::isInstanceOf($compression_method, CompressionInterface::class, sprintf('Compression method "%s" not supported.', $method));
212
213
        return $compression_method;
214
    }
215
216
    /**
217
     * @param \Jose\Object\JWEInterface $jwe
218
     *
219
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
220
     */
221
    private function getContentEncryptionAlgorithm(JWEInterface $jwe)
222
    {
223
        $algorithm = null;
224
225
        foreach ($jwe->getRecipients() as $recipient) {
226
            $complete_headers = array_merge($jwe->getSharedProtectedHeaders(), $jwe->getSharedHeaders(), $recipient->getHeaders());
227
            Assertion::keyExists($complete_headers, 'enc', 'Parameter "enc" is missing.');
228
            if (null === $algorithm) {
229
                $algorithm = $complete_headers['enc'];
230
            } else {
231
                Assertion::eq($algorithm, $complete_headers['enc'], 'Foreign content encryption algorithms are not allowed.');
232
            }
233
        }
234
235
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
236
        Assertion::isInstanceOf($content_encryption_algorithm, ContentEncryptionAlgorithmInterface::class, sprintf('The content encryption algorithm "%s" is not supported or not a content encryption algorithm instance.', $algorithm));
237
238
        return $content_encryption_algorithm;
239
    }
240
241
    /**
242
     * @param \Jose\Object\JWEInterface                           $jwe
243
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
244
     * @param string                                              $cek
245
     * @param string                                              $iv
246
     * @param \Jose\Compression\CompressionInterface|null         $compression_method
247
     */
248
    private function encryptJWE(JWEInterface &$jwe, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, $cek, $iv, CompressionInterface $compression_method = null)
249
    {
250
        if (!empty($jwe->getSharedProtectedHeaders())) {
251
            $jwe = $jwe->withEncodedSharedProtectedHeaders(Base64Url::encode(json_encode($jwe->getSharedProtectedHeaders())));
252
        }
253
254
        $tag = null;
255
        $payload = $this->preparePayload($jwe->getPayload(), $compression_method);
256
        $aad = null === $jwe->getAAD() ? null : Base64Url::encode($jwe->getAAD());
257
        $ciphertext = $content_encryption_algorithm->encryptContent($payload, $cek, $iv, $aad, $jwe->getEncodedSharedProtectedHeaders(), $tag);
258
        $jwe = $jwe->withCiphertext($ciphertext);
259
        $jwe = $jwe->withIV($iv);
260
261
        if (null !== $tag) {
262
            $jwe = $jwe->withTag($tag);
263
        }
264
    }
265
266
    /**
267
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
268
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
269
     * @param \Jose\Object\JWKInterface                           $recipient_key
270
     */
271
    private function checkKeys(KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key)
272
    {
273
        $this->checkKeyUsage($recipient_key, 'encryption');
274
        if ('dir' !== $key_encryption_algorithm->getAlgorithmName()) {
275
            $this->checkKeyAlgorithm($recipient_key, $key_encryption_algorithm->getAlgorithmName());
276
        } else {
277
            $this->checkKeyAlgorithm($recipient_key, $content_encryption_algorithm->getAlgorithmName());
278
        }
279
    }
280
281
    /**
282
     * @param string $current
283
     * @param string $new
284
     *
285
     * @return bool
286
     */
287
    private function areKeyManagementModesCompatible($current, $new)
288
    {
289
        $agree = KeyEncryptionAlgorithmInterface::MODE_AGREEMENT;
290
        $dir = KeyEncryptionAlgorithmInterface::MODE_DIRECT;
291
        $enc = KeyEncryptionAlgorithmInterface::MODE_ENCRYPT;
292
        $wrap = KeyEncryptionAlgorithmInterface::MODE_WRAP;
293
        $supported_key_management_mode_combinations = [$enc.$enc     => true,$enc.$wrap    => true,$wrap.$enc    => true,$wrap.$wrap   => true,$agree.$agree => false,$agree.$dir   => false,$agree.$enc   => false,$agree.$wrap  => false,$dir.$agree   => false,$dir.$dir     => false,$dir.$enc     => false,$dir.$wrap    => false,$enc.$agree   => false,$enc.$dir     => false,$wrap.$agree  => false,$wrap.$dir    => false,];
294
295
        if (array_key_exists($current.$new, $supported_key_management_mode_combinations)) {
296
            return $supported_key_management_mode_combinations[$current.$new];
297
        }
298
299
        return false;
300
    }
301
302
    /**
303
     * @param string                                      $payload
304
     * @param \Jose\Compression\CompressionInterface|null $compression_method
305
     *
306
     * @return string
307
     */
308
    private function preparePayload($payload, CompressionInterface $compression_method = null)
309
    {
310
        $prepared = is_string($payload) ? $payload : json_encode($payload);
311
        Assertion::notNull($prepared, 'The payload is empty or cannot encoded into JSON.');
312
313
        if (null === $compression_method) {
314
            return $prepared;
315
        }
316
        $compressed_payload = $compression_method->compress($prepared);
317
        Assertion::string($compressed_payload, 'Compression failed.');
318
319
        return $compressed_payload;
320
    }
321
322
    /**
323
     * @param array                                               $complete_headers
324
     * @param string                                              $cek
325
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
326
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
327
     * @param \Jose\Object\JWKInterface                           $recipient_key
328
     * @param array                                               $additional_headers
329
     *
330
     * @return string|null
331
     */
332
    private function getEncryptedKey(array $complete_headers, $cek, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key)
333
    {
334
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
335
            return $this->getEncryptedKeyFromKeyEncryptionAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key, $additional_headers);
336
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
337
            return $this->getEncryptedKeyFromKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $recipient_key, $additional_headers);
338
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
339
            return $this->getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm($complete_headers, $cek, $key_encryption_algorithm, $content_encryption_algorithm, $additional_headers, $recipient_key);
340
        }
341
    }
342
343
    /**
344
     * @param array                                                       $complete_headers
345
     * @param string                                                      $cek
346
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface $key_encryption_algorithm
347
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface         $content_encryption_algorithm
348
     * @param array                                                       $additional_headers
349
     * @param \Jose\Object\JWKInterface                                   $recipient_key
350
     *
351
     * @return string
352
     */
353
    private function getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm(array $complete_headers, $cek, KeyAgreementWrappingInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key)
354
    {
355
        $jwt_cek = $key_encryption_algorithm->wrapAgreementKey($recipient_key, $cek, $content_encryption_algorithm->getCEKSize(), $complete_headers, $additional_headers);
356
357
        return $jwt_cek;
358
    }
359
360
    /**
361
     * @param array                                                $complete_headers
362
     * @param string                                               $cek
363
     * @param \Jose\Algorithm\KeyEncryption\KeyEncryptionInterface $key_encryption_algorithm
364
     * @param \Jose\Object\JWKInterface                            $recipient_key
365
     * @param array                                                $additional_headers
366
     *
367
     * @return string
368
     */
369
    private function getEncryptedKeyFromKeyEncryptionAlgorithm(array $complete_headers, $cek, KeyEncryptionInterface $key_encryption_algorithm, JWKInterface $recipient_key, array &$additional_headers)
370
    {
371
        return $key_encryption_algorithm->encryptKey($recipient_key,  $cek, $complete_headers, $additional_headers);
372
    }
373
374
    /**
375
     * @param array                                              $complete_headers
376
     * @param string                                             $cek
377
     * @param \Jose\Algorithm\KeyEncryption\KeyWrappingInterface $key_encryption_algorithm
378
     * @param \Jose\Object\JWKInterface                          $recipient_key
379
     * @param array                                              $additional_headers
380
     *
381
     * @return string
382
     */
383
    private function getEncryptedKeyFromKeyWrappingAlgorithm(array $complete_headers, $cek, KeyWrappingInterface $key_encryption_algorithm, JWKInterface $recipient_key, &$additional_headers)
384
    {
385
        return $key_encryption_algorithm->wrapKey($recipient_key, $cek, $complete_headers, $additional_headers);
386
    }
387
388
    /**
389
     * @param array $complete_headers
390
     *
391
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
392
     */
393
    private function findKeyEncryptionAlgorithm(array $complete_headers)
394
    {
395
        Assertion::keyExists($complete_headers, 'alg', 'Parameter "alg" is missing.');
396
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
397
        Assertion::isInstanceOf($key_encryption_algorithm, KeyEncryptionAlgorithmInterface::class, sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_headers['alg']));
398
399
        return $key_encryption_algorithm;
400
    }
401
402
    /**
403
     * @param int $size
404
     *
405
     * @return string
406
     */
407
    private function createCEK($size)
408
    {
409
        return random_bytes($size / 8);
410
    }
411
412
    /**
413
     * @param int $size
414
     *
415
     * @return string
416
     */
417
    private function createIV($size)
418
    {
419
        return random_bytes($size / 8);
420
    }
421
}
422