Completed
Push — master ( d8c018...19ac78 )
by Florent
06:38
created

Decrypter   C

Complexity

Total Complexity 41

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 19
Bugs 12 Features 1
Metric Value
wmc 41
c 19
b 12
f 1
lcom 1
cbo 21
dl 0
loc 336
rs 5.214

15 Methods

Rating   Name   Duplication   Size   Complexity  
B decryptRecipientKey() 0 34 6
A checkRecipients() 0 8 2
A checkPayload() 0 9 2
A checkJWKSet() 0 10 2
A decompressIfNeeded() 0 17 3
A getCompressionMethod() 0 14 2
A decryptUsingKey() 0 7 1
B decryptCEK() 0 34 6
A checkCompleteHeader() 0 12 3
A getKeyEncryptionAlgorithm() 0 14 2
A getContentEncryptionAlgorithm() 0 14 2
A decryptPayload() 0 21 4
A __construct() 0 14 1
A createDecrypter() 0 10 2
A decryptUsingKeySet() 0 20 3

How to fix   Complexity   

Complex Class

Complex classes like Decrypter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Decrypter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2016 Spomky-Labs
7
 *
8
 * This software may be modified and distributed under the terms
9
 * of the MIT license.  See the LICENSE file for details.
10
 */
11
12
namespace Jose;
13
14
use Base64Url\Base64Url;
15
use Jose\Algorithm\ContentEncryptionAlgorithmInterface;
16
use Jose\Algorithm\JWAInterface;
17
use Jose\Algorithm\KeyEncryption\DirectEncryptionInterface;
18
use Jose\Algorithm\KeyEncryption\KeyAgreementInterface;
19
use Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface;
20
use Jose\Algorithm\KeyEncryption\KeyEncryptionInterface;
21
use Jose\Algorithm\KeyEncryption\KeyWrappingInterface;
22
use Jose\Algorithm\KeyEncryptionAlgorithmInterface;
23
use Jose\Behaviour\CommonCipheringMethods;
24
use Jose\Behaviour\HasCompressionManager;
25
use Jose\Behaviour\HasJWAManager;
26
use Jose\Behaviour\HasKeyChecker;
27
use Jose\Behaviour\HasLogger;
28
use Jose\Factory\AlgorithmManagerFactory;
29
use Jose\Factory\CompressionManagerFactory;
30
use Jose\Object\JWEInterface;
31
use Jose\Object\JWKInterface;
32
use Jose\Object\JWKSet;
33
use Jose\Object\JWKSetInterface;
34
use Jose\Object\RecipientInterface;
35
use Psr\Log\LoggerInterface;
36
use Psr\Log\LogLevel;
37
38
final class Decrypter implements DecrypterInterface
39
{
40
    use HasKeyChecker;
41
    use HasJWAManager;
42
    use HasCompressionManager;
43
    use HasLogger;
44
    use CommonCipheringMethods;
45
46
    /**
47
     * {@inheritdoc}
48
     */
49
    public static function createDecrypter(array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF', 'ZLIB', 'GZ'], LoggerInterface $logger = null)
50
    {
51
        $decrypter = new self($key_encryption_algorithms, $content_encryption_algorithms, $compression_methods);
52
53
        if (null !== $logger) {
54
            $decrypter->enableLogging($logger);
55
        }
56
57
        return $decrypter;
58
    }
59
60
    /**
61
     * Decrypter constructor.
62
     *
63
     * @param string[]|\Jose\Algorithm\KeyEncryptionAlgorithmInterface[]     $key_encryption_algorithms
64
     * @param string[]|\Jose\Algorithm\ContentEncryptionAlgorithmInterface[] $content_encryption_algorithms
65
     * @param string[]|\Jose\Compression\CompressionInterface[]              $compression_methods
66
     */
67
    public function __construct(
68
        array $key_encryption_algorithms,
69
        array $content_encryption_algorithms,
70
        array $compression_methods
71
    ) {
72
        $this->setKeyEncryptionAlgorithms($key_encryption_algorithms);
73
        $this->setContentEncryptionAlgorithms($content_encryption_algorithms);
74
        $this->setCompressionMethods($compression_methods);
75
        $this->setJWAManager(AlgorithmManagerFactory::createAlgorithmManager(array_merge(
76
            $key_encryption_algorithms,
77
            $content_encryption_algorithms
78
        )));
79
        $this->setCompressionManager(CompressionManagerFactory::createCompressionManager($compression_methods));
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function decryptUsingKey(JWEInterface &$jwe, JWKInterface $jwk, &$recipient_index = null)
86
    {
87
        $jwk_set = new JWKSet();
88
        $jwk_set->addKey($jwk);
89
90
        $this->decryptUsingKeySet($jwe, $jwk_set, $recipient_index);
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function decryptUsingKeySet(JWEInterface &$jwe, JWKSetInterface $jwk_set, &$recipient_index = null)
97
    {
98
        $this->log(LogLevel::DEBUG, 'Trying to decrypt the JWE.');
99
        $this->checkJWKSet($jwk_set);
100
        $this->checkPayload($jwe);
101
        $this->checkRecipients($jwe);
102
103
        $nb_recipients = $jwe->countRecipients();
104
        $this->log(LogLevel::DEBUG, 'The JWE contains {nb} recipient(s).', ['nb' => $nb_recipients]);
105
106
        for ($i = 0; $i < $nb_recipients; $i++) {
107
            if (is_int($result = $this->decryptRecipientKey($jwe, $jwk_set, $i))) {
108
                $recipient_index = $result;
109
110
                return;
111
            }
112
        }
113
114
        throw new \InvalidArgumentException('Unable to decrypt the JWE.');
115
    }
116
117
    /**
118
     * @param \Jose\Object\JWEInterface    $jwe
119
     * @param \Jose\Object\JWKSetInterface $jwk_set
120
     * @param int                          $i
121
     *
122
     * @return mixed
123
     */
124
    private function decryptRecipientKey(JWEInterface &$jwe, JWKSetInterface $jwk_set, $i)
125
    {
126
        $this->log(LogLevel::DEBUG, 'Trying to decrypt the encrypted key of recipient #{nb}.', ['nb' => $i]);
127
        $recipient = $jwe->getRecipient($i);
128
        $complete_headers = array_merge(
129
            $jwe->getSharedProtectedHeaders(),
130
            $jwe->getSharedHeaders(),
131
            $recipient->getHeaders()
132
        );
133
        $this->checkCompleteHeader($complete_headers);
134
135
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_headers);
136
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($complete_headers);
137
138
        foreach ($jwk_set as $jwk) {
139
            try {
140
                $this->checkKeyUsage($jwk, 'decryption');
141
                if ('dir' !== $key_encryption_algorithm->getAlgorithmName()) {
142
                    $this->checkKeyAlgorithm($jwk, $key_encryption_algorithm->getAlgorithmName());
143
                } else {
144
                    $this->checkKeyAlgorithm($jwk, $content_encryption_algorithm->getAlgorithmName());
145
                }
146
                $cek = $this->decryptCEK($key_encryption_algorithm, $content_encryption_algorithm, $jwk, $recipient, $complete_headers);
147
                if (null !== $cek) {
148
                    if (true === $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $complete_headers)) {
149
                        return $i;
150
                    }
151
                }
152
            } catch (\Exception $e) {
153
                //We do nothing, we continue with other keys
154
                continue;
155
            }
156
        }
157
    }
158
159
    /**
160
     * @param \Jose\Object\JWEInterface $jwe
161
     */
162
    private function checkRecipients(JWEInterface $jwe)
163
    {
164
        if (0 === $jwe->countRecipients()) {
165
            $exception = new \InvalidArgumentException('The JWE does not contain any recipient.');
166
            $this->log(LogLevel::ERROR, 'The JWE does not contain any recipient.', ['exception' => $exception]);
167
            throw $exception;
168
        }
169
    }
170
171
    /**
172
     * @param \Jose\Object\JWEInterface $jwe
173
     */
174
    private function checkPayload(JWEInterface $jwe)
175
    {
176
        $this->log(LogLevel::DEBUG, 'Checking the payload.', ['payload' => $jwe->getPayload()]);
177
        if (null !== $jwe->getPayload()) {
178
            $exception = new \InvalidArgumentException('The JWE is already decrypted.');
179
            $this->log(LogLevel::ERROR, 'The JWE is already decrypted.', ['exception' => $exception]);
180
            throw $exception;
181
        }
182
    }
183
184
    /**
185
     * @param \Jose\Object\JWKSetInterface $jwk_set
186
     */
187
    private function checkJWKSet(JWKSetInterface $jwk_set)
188
    {
189
        $this->log(LogLevel::DEBUG, 'Checking the key set.', ['jwkset' => $jwk_set]);
190
        if (0 === count($jwk_set)) {
191
            $exception = new \InvalidArgumentException('No key in the key set.');
192
            $this->log(LogLevel::ERROR, 'There is no key in the key set.', ['exception' => $exception]);
193
            throw $exception;
194
        }
195
        $this->log(LogLevel::DEBUG, 'Checking the key set.', ['jwkset' => $jwk_set]);
196
    }
197
198
    /**
199
     * @param \Jose\Algorithm\JWAInterface                        $key_encryption_algorithm
200
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
201
     * @param \Jose\Object\JWKInterface                           $key
202
     * @param \Jose\Object\RecipientInterface                     $recipient
203
     * @param array                                               $complete_headers
204
     *
205
     * @return null|string
206
     */
207
    private function decryptCEK(JWAInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $key, RecipientInterface $recipient, array $complete_headers)
208
    {
209
        if ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
210
            return $key_encryption_algorithm->getCEK($key);
211
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
212
            return $key_encryption_algorithm->getAgreementKey(
213
                $content_encryption_algorithm->getCEKSize(),
214
                $content_encryption_algorithm->getAlgorithmName(),
215
                $key,
216
                $complete_headers
217
            );
218
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
219
            return $key_encryption_algorithm->unwrapAgreementKey(
220
                $key,
221
                $recipient->getEncryptedKey(),
222
                $content_encryption_algorithm->getCEKSize(),
223
                $complete_headers
224
            );
225
        } elseif ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
226
            return $key_encryption_algorithm->decryptKey(
227
                $key,
228
                $recipient->getEncryptedKey(),
229
                $complete_headers
230
            );
231
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
232
            return $key_encryption_algorithm->unwrapKey(
233
                $key,
234
                $recipient->getEncryptedKey(),
235
                $complete_headers
236
            );
237
        } else {
238
            throw new \InvalidArgumentException('Unsupported CEK generation');
239
        }
240
    }
241
242
    /**
243
     * @param \Jose\Object\JWEInterface                           $jwe
244
     * @param string                                              $cek
245
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
246
     * @param array                                               $complete_headers
247
     *
248
     * @return bool
249
     */
250
    private function decryptPayload(JWEInterface &$jwe, $cek, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array $complete_headers)
251
    {
252
        $payload = $content_encryption_algorithm->decryptContent(
253
            $jwe->getCiphertext(),
254
            $cek,
255
            $jwe->getIV(),
256
            null === $jwe->getAAD() ? null : Base64Url::encode($jwe->getAAD()),
257
            $jwe->getEncodedSharedProtectedHeaders(),
258
            $jwe->getTag()
259
        );
260
        if (null === $payload) {
261
            return false;
262
        }
263
264
        $this->decompressIfNeeded($payload, $complete_headers);
265
266
        $decoded = json_decode($payload, true);
267
        $jwe = $jwe->withPayload(null === $decoded ? $payload : $decoded);
268
269
        return true;
270
    }
271
272
    /**
273
     * @param string $payload
274
     * @param array  $complete_headers
275
     */
276
    private function decompressIfNeeded(&$payload, array $complete_headers)
277
    {
278
        $this->log(LogLevel::DEBUG, 'Checking if payload is compressed.', ['payload' => $payload]);
279
        if (array_key_exists('zip', $complete_headers)) {
280
            $this->log(LogLevel::DEBUG, 'Header indicates the payload is compressed with method {compress}.', ['compress' => $complete_headers['zip']]);
281
            $compression_method = $this->getCompressionMethod($complete_headers['zip']);
282
            $payload = $compression_method->uncompress($payload);
283
            if (!is_string($payload)) {
284
                $exception = new \InvalidArgumentException('Decompression failed');
285
                $this->log(LogLevel::ERROR, 'Unable to decompress the payload.', ['exception' => $exception]);
286
                throw $exception;
287
            }
288
            $this->log(LogLevel::DEBUG, 'The payload has been decompressed.');
289
        } else {
290
            $this->log(LogLevel::DEBUG, 'The payload is not compressed.');
291
        }
292
    }
293
294
    /**
295
     * @param array $complete_headers
296
     *
297
     * @throws \InvalidArgumentException
298
     */
299
    private function checkCompleteHeader(array $complete_headers)
300
    {
301
        $this->log(LogLevel::DEBUG, 'Checking for mandatory parameters.');
302
        foreach (['enc', 'alg'] as $key) {
303
            if (!array_key_exists($key, $complete_headers)) {
304
                $exception = new \InvalidArgumentException(sprintf("Parameters '%s' is missing.", $key));
305
                $this->log(LogLevel::ERROR, 'Parameter is missing.', ['exception' => $exception]);
306
                throw $exception;
307
            }
308
        }
309
        $this->log(LogLevel::DEBUG, 'Mandatory parameters found.');
310
    }
311
312
    /**
313
     * @param array $complete_headers
314
     *
315
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
316
     */
317
    private function getKeyEncryptionAlgorithm(array $complete_headers)
318
    {
319
        $this->log(LogLevel::DEBUG, 'Trying to find key encryption algorithm');
320
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
321
        if (!$key_encryption_algorithm instanceof KeyEncryptionAlgorithmInterface) {
322
            $exception = new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithmInterface.', $complete_headers['alg']));
323
            $this->log(LogLevel::ERROR, 'The key encryption algorithm {alg} is not supported or does not implement KeyEncryptionAlgorithmInterface.', ['alg' => $complete_headers['alg'], 'exception' => $exception]);
324
            throw $exception;
325
        }
326
327
        $this->log(LogLevel::DEBUG, 'Key encryption algorithm {algorithm} found', ['algorithm' => $complete_headers['alg'], 'service' > $key_encryption_algorithm]);
328
329
        return $key_encryption_algorithm;
330
    }
331
332
    /**
333
     * @param array $complete_headers
334
     *
335
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
336
     */
337
    private function getContentEncryptionAlgorithm(array $complete_headers)
338
    {
339
        $this->log(LogLevel::DEBUG, 'Trying to find content encryption algorithm');
340
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['enc']);
341
        if (!$content_encryption_algorithm instanceof ContentEncryptionAlgorithmInterface) {
342
            $exception = new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or does not implement ContentEncryptionInterface.', $complete_headers['enc']));
343
            $this->log(LogLevel::ERROR, 'The key encryption algorithm {alg} is not supported or does not implement ContentEncryptionInterface.', ['alg' => $complete_headers['enc'], 'exception' => $exception]);
344
            throw $exception;
345
        }
346
347
        $this->log(LogLevel::DEBUG, 'Content encryption algorithm {algorithm} found', ['algorithm' => $complete_headers['enc'], 'service' > $content_encryption_algorithm]);
348
349
        return $content_encryption_algorithm;
350
    }
351
352
    /**
353
     * @param string $method
354
     *
355
     * @throws \InvalidArgumentException
356
     *
357
     * @return \Jose\Compression\CompressionInterface
358
     */
359
    private function getCompressionMethod($method)
360
    {
361
        $this->log(LogLevel::DEBUG, 'Trying to find compression method {method}', ['method' => $method]);
362
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
363
        if (null === $compression_method) {
364
            $exception = new \InvalidArgumentException(sprintf('Compression method "%s" not supported', $method));
365
            $this->log(LogLevel::ERROR, 'Compression method {method} not supported', ['method' => $method, 'exception' => $exception]);
366
            throw $exception;
367
        }
368
369
        $this->log(LogLevel::DEBUG, 'Compression method {method} found', ['method' => $method, 'service' => $compression_method]);
370
371
        return $compression_method;
372
    }
373
}
374