Failed Conditions
Push — master ( 5537f8...d96005 )
by Florent
02:00
created

JWEDecrypter::checkRecipients()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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