Failed Conditions
Push — v7 ( 629225...6b9564 )
by Florent
02:02
created

JWELoader   C

Complexity

Total Complexity 39

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 17
dl 0
loc 290
rs 6.5773
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A load() 0 4 1
A getSupportedCompressionMethods() 0 4 1
A getSupportedKeyEncryptionAlgorithms() 0 4 1
A getSupportedContentEncryptionAlgorithms() 0 4 1
A decryptUsingKey() 0 9 1
A decryptUsingKeySet() 0 19 3
B decryptRecipientKey() 0 29 5
A checkRecipients() 0 6 2
A checkPayload() 0 6 2
A checkJWKSet() 0 6 2
B decryptCEK() 0 16 6
A decryptPayload() 0 9 3
A decompressIfNeeded() 0 12 3
A checkCompleteHeader() 0 8 3
A getKeyEncryptionAlgorithm() 0 9 2
A getContentEncryptionAlgorithm() 0 9 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2017 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Encryption;
15
16
use Jose\Component\Checker\HeaderCheckerManager;
17
use Jose\Component\Core\AlgorithmManager;
18
use Jose\Component\Core\JWKSet;
19
use Jose\Component\Encryption\Compression\CompressionMethodManager;
20
use Base64Url\Base64Url;
21
use Jose\Component\Core\AlgorithmInterface;
22
use Jose\Component\Core\JWK;
23
use Jose\Component\Core\Util\KeyChecker;
24
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithmInterface;
25
use Jose\Component\Encryption\Algorithm\KeyEncryption\DirectEncryptionInterface;
26
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreementInterface;
27
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreementWrappingInterface;
28
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyEncryptionInterface;
29
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrappingInterface;
30
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithmInterface;
31
32
/**
33
 * Class JWELoader.
34
 */
35
final class JWELoader
36
{
37
    /**
38
     * @var HeaderCheckerManager
39
     */
40
    private $headerCheckerManager;
41
42
    /**
43
     * @var AlgorithmManager
44
     */
45
    private $keyEncryptionAlgorithmManager;
46
47
    /**
48
     * @var AlgorithmManager
49
     */
50
    private $contentEncryptionAlgorithmManager;
51
52
    /**
53
     * @var CompressionMethodManager
54
     */
55
    private $compressionMethodManager;
56
57
    /**
58
     * JWELoader constructor.
59
     *
60
     * @param AlgorithmManager         $keyEncryptionAlgorithmManager
61
     * @param AlgorithmManager         $contentEncryptionAlgorithmManager
62
     * @param CompressionMethodManager $compressionMethodManager
63
     * @param HeaderCheckerManager     $headerCheckerManager
64
     */
65
    public function __construct(AlgorithmManager $keyEncryptionAlgorithmManager, AlgorithmManager $contentEncryptionAlgorithmManager, CompressionMethodManager $compressionMethodManager, HeaderCheckerManager $headerCheckerManager)
66
    {
67
        $this->keyEncryptionAlgorithmManager = $keyEncryptionAlgorithmManager;
68
        $this->contentEncryptionAlgorithmManager = $contentEncryptionAlgorithmManager;
69
        $this->compressionMethodManager = $compressionMethodManager;
70
        $this->headerCheckerManager = $headerCheckerManager;
71
    }
72
73
    /**
74
     * @param string $input
75
     *
76
     * @return JWE
77
     */
78
    public function load(string $input): JWE
79
    {
80
        return JWEParser::parse($input);
81
    }
82
83
    /**
84
     * @return string[]
85
     */
86
    public function getSupportedCompressionMethods(): array
87
    {
88
        return $this->compressionMethodManager->list();
89
    }
90
91
    /**
92
     * @return string[]
93
     */
94
    public function getSupportedKeyEncryptionAlgorithms(): array
95
    {
96
        return $this->keyEncryptionAlgorithmManager->list();
97
    }
98
99
    /**
100
     * @return string[]
101
     */
102
    public function getSupportedContentEncryptionAlgorithms(): array
103
    {
104
        return $this->contentEncryptionAlgorithmManager->list();
105
    }
106
107
    /**
108
     * @param JWE      $jwe            A JWE object to decrypt
109
     * @param JWK      $jwk            The key used to decrypt the input
110
     * @param null|int $recipientIndex If the JWE has been decrypted, an integer that represents the ID of the recipient is set
111
     *
112
     * @return JWE
113
     */
114
    public function decryptUsingKey(JWE $jwe, JWK $jwk, ?int &$recipientIndex = null): JWE
115
    {
116
        $jwkset = JWKSet::createFromKeys([$jwk]);
117
118
        $jwe = $this->decryptUsingKeySet($jwe, $jwkset, $recipientIndex);
119
        $this->headerCheckerManager->check($jwe, $recipientIndex);
120
121
        return $jwe;
122
    }
123
124
    /**
125
     * @param JWE      $jwe            A JWE object to decrypt
126
     * @param JWKSet   $jwkset         The key set used to decrypt the input
127
     * @param null|int $recipientIndex If the JWE has been decrypted, an integer that represents the ID of the recipient is set
128
     *
129
     * @return JWE
130
     */
131
    public function decryptUsingKeySet(JWE $jwe, JWKSet $jwkset, ?int &$recipientIndex = null): JWE
132
    {
133
        $this->checkJWKSet($jwkset);
134
        $this->checkPayload($jwe);
135
        $this->checkRecipients($jwe);
136
137
        $nb_recipients = $jwe->countRecipients();
138
139
        for ($i = 0; $i < $nb_recipients; ++$i) {
140
            $plaintext = $this->decryptRecipientKey($jwe, $jwkset, $i);
141
            if (null !== $plaintext) {
142
                $recipientIndex = $i;
143
144
                return $jwe->withPayload($plaintext);
145
            }
146
        }
147
148
        throw new \InvalidArgumentException('Unable to decrypt the JWE.');
149
    }
150
151
    /**
152
     * @param JWE    $jwe
153
     * @param JWKSet $jwkset
154
     * @param int    $i
155
     *
156
     * @return string|null
157
     */
158
    private function decryptRecipientKey(JWE $jwe, JWKSet $jwkset, int $i): ?string
159
    {
160
        $recipient = $jwe->getRecipient($i);
161
        $complete_headers = array_merge($jwe->getSharedProtectedHeaders(), $jwe->getSharedHeaders(), $recipient->getHeaders());
162
        $this->checkCompleteHeader($complete_headers);
163
164
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_headers);
165
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($complete_headers);
166
167
        foreach ($jwkset as $jwk) {
168
            try {
169
                KeyChecker::checkKeyUsage($jwk, 'decryption');
0 ignored issues
show
Bug introduced by
It seems like $jwk defined by $jwk on line 167 can be null; however, Jose\Component\Core\Util...hecker::checkKeyUsage() 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...
170
                if ('dir' !== $key_encryption_algorithm->name()) {
171
                    KeyChecker::checkKeyAlgorithm($jwk, $key_encryption_algorithm->name());
0 ignored issues
show
Bug introduced by
It seems like $jwk defined by $jwk on line 167 can be null; however, Jose\Component\Core\Util...er::checkKeyAlgorithm() 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...
172
                } else {
173
                    KeyChecker::checkKeyAlgorithm($jwk, $content_encryption_algorithm->name());
0 ignored issues
show
Bug introduced by
It seems like $jwk defined by $jwk on line 167 can be null; however, Jose\Component\Core\Util...er::checkKeyAlgorithm() 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...
174
                }
175
                $cek = $this->decryptCEK($key_encryption_algorithm, $content_encryption_algorithm, $jwk, $recipient, $complete_headers);
0 ignored issues
show
Bug introduced by
It seems like $jwk defined by $jwk on line 167 can be null; however, Jose\Component\Encryption\JWELoader::decryptCEK() 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
                if (null !== $cek) {
177
                    return $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $complete_headers);
178
                }
179
            } catch (\Exception $e) {
180
                //We do nothing, we continue with other keys
181
                continue;
182
            }
183
        }
184
185
        return null;
186
    }
187
188
    /**
189
     * @param JWE $jwe
190
     */
191
    private function checkRecipients(JWE $jwe)
192
    {
193
        if (0 === $jwe->countRecipients()) {
194
            throw new \InvalidArgumentException('The JWE does not contain any recipient.');
195
        }
196
    }
197
198
    /**
199
     * @param JWE $jwe
200
     */
201
    private function checkPayload(JWE $jwe)
202
    {
203
        if (null !== $jwe->getPayload()) {
204
            throw new \InvalidArgumentException('The JWE is already decrypted.');
205
        }
206
    }
207
208
    /**
209
     * @param JWKSet $jwkset
210
     */
211
    private function checkJWKSet(JWKSet $jwkset)
212
    {
213
        if (0 === $jwkset->count()) {
214
            throw new \InvalidArgumentException('No key in the key set.');
215
        }
216
    }
217
218
    /**
219
     * @param AlgorithmInterface                  $key_encryption_algorithm
220
     * @param ContentEncryptionAlgorithmInterface $content_encryption_algorithm
221
     * @param JWK                                 $key
222
     * @param Recipient                           $recipient
223
     * @param array                               $complete_headers
224
     *
225
     * @return null|string
226
     */
227
    private function decryptCEK(AlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWK $key, Recipient $recipient, array $complete_headers): ?string
228
    {
229
        if ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
230
            return $key_encryption_algorithm->getCEK($key);
231
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
232
            return $key_encryption_algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $content_encryption_algorithm->name(), $key, $complete_headers);
233
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
234
            return $key_encryption_algorithm->unwrapAgreementKey($key, $recipient->getEncryptedKey(), $content_encryption_algorithm->getCEKSize(), $complete_headers);
235
        } elseif ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
236
            return $key_encryption_algorithm->decryptKey($key, $recipient->getEncryptedKey(), $complete_headers);
237
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
238
            return $key_encryption_algorithm->unwrapKey($key, $recipient->getEncryptedKey(), $complete_headers);
239
        } else {
240
            throw new \InvalidArgumentException('Unsupported CEK generation');
241
        }
242
    }
243
244
    /**
245
     * @param JWE                                 $jwe
246
     * @param string                              $cek
247
     * @param ContentEncryptionAlgorithmInterface $content_encryption_algorithm
248
     * @param array                               $complete_headers
249
     *
250
     * @return string
251
     */
252
    private function decryptPayload(JWE $jwe, string $cek, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array $complete_headers): string
253
    {
254
        $payload = $content_encryption_algorithm->decryptContent($jwe->getCiphertext(), $cek, $jwe->getIV(), null === $jwe->getAAD() ? null : Base64Url::encode($jwe->getAAD()), $jwe->getEncodedSharedProtectedHeaders(), $jwe->getTag());
255
        if (null === $payload) {
256
            throw new \RuntimeException('Unable to decrypt the JWE.');
257
        }
258
259
        return $this->decompressIfNeeded($payload, $complete_headers);
260
    }
261
262
    /**
263
     * @param string $payload
264
     * @param array  $complete_headers
265
     *
266
     * @return string
267
     */
268
    private function decompressIfNeeded(string $payload, array $complete_headers): string
269
    {
270
        if (array_key_exists('zip', $complete_headers)) {
271
            $compression_method = $this->compressionMethodManager->get($complete_headers['zip']);
272
            $payload = $compression_method->uncompress($payload);
273
            if (!is_string($payload)) {
274
                throw new \InvalidArgumentException('Decompression failed');
275
            }
276
        }
277
278
        return $payload;
279
    }
280
281
    /**
282
     * @param array $complete_headers
283
     *
284
     * @throws \InvalidArgumentException
285
     */
286
    private function checkCompleteHeader(array $complete_headers)
287
    {
288
        foreach (['enc', 'alg'] as $key) {
289
            if (!array_key_exists($key, $complete_headers)) {
290
                throw new \InvalidArgumentException(sprintf("Parameters '%s' is missing.", $key));
291
            }
292
        }
293
    }
294
295
    /**
296
     * @param array $complete_headers
297
     *
298
     * @return KeyEncryptionAlgorithmInterface
299
     */
300
    private function getKeyEncryptionAlgorithm(array $complete_headers): KeyEncryptionAlgorithmInterface
301
    {
302
        $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($complete_headers['alg']);
303
        if (!$key_encryption_algorithm instanceof KeyEncryptionAlgorithmInterface) {
304
            throw new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithmInterface.', $complete_headers['alg']));
305
        }
306
307
        return $key_encryption_algorithm;
308
    }
309
310
    /**
311
     * @param array $complete_headers
312
     *
313
     * @return ContentEncryptionAlgorithmInterface
314
     */
315
    private function getContentEncryptionAlgorithm(array $complete_headers): ContentEncryptionAlgorithmInterface
316
    {
317
        $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($complete_headers['enc']);
318
        if (!$content_encryption_algorithm instanceof ContentEncryptionAlgorithmInterface) {
319
            throw new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or does not implement ContentEncryptionInterface.', $complete_headers['enc']));
320
        }
321
322
        return $content_encryption_algorithm;
323
    }
324
}
325