Failed Conditions
Push — master ( 6560de...8483ce )
by Florent
05:17
created

JWEDecrypter::getKeyEncryptionAlgorithmManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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