Failed Conditions
Push — v7 ( 73ebba...e5c6da )
by Florent
02:18
created

JWELoader::decryptRecipientKey()   B

Complexity

Conditions 5
Paths 13

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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