Completed
Push — master ( 0f6cd0...ce5be5 )
by Florent
41:39 queued 33s
created

Decrypter::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
c 6
b 2
f 0
dl 0
loc 12
rs 9.4285
cc 2
eloc 8
nc 2
nop 3
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 Jose\Algorithm\ContentEncryptionAlgorithmInterface;
15
use Jose\Algorithm\JWAInterface;
16
use Jose\Algorithm\JWAManagerInterface;
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\HasCompressionManager;
24
use Jose\Behaviour\HasJWAManager;
25
use Jose\Behaviour\HasKeyChecker;
26
use Jose\Behaviour\HasLogger;
27
use Jose\Compression\CompressionManagerInterface;
28
use Jose\Object\JWEInterface;
29
use Jose\Object\JWKInterface;
30
use Jose\Object\JWKSet;
31
use Jose\Object\JWKSetInterface;
32
use Jose\Object\RecipientInterface;
33
use Psr\Log\LoggerInterface;
34
use Psr\Log\LogLevel;
35
36
/**
37
 */
38
final class Decrypter implements DecrypterInterface
39
{
40
    use HasKeyChecker;
41
    use HasJWAManager;
42
    use HasCompressionManager;
43
    use HasLogger;
44
45
    /**
46
     * Decrypter constructor.
47
     *
48
     * @param \Jose\Algorithm\JWAManagerInterface           $jwa_manager
49
     * @param \Jose\Compression\CompressionManagerInterface $compression_manager
50
     * @param \Psr\Log\LoggerInterface|null                 $logger
51
     */
52
    public function __construct(
53
        JWAManagerInterface $jwa_manager,
54
        CompressionManagerInterface $compression_manager,
55
        LoggerInterface $logger = null
56
    ) {
57
        $this->setJWAManager($jwa_manager);
58
        $this->setCompressionManager($compression_manager);
59
60
        if (null !== $logger) {
61
            $this->setLogger($logger);
62
        }
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function decryptUsingKey(JWEInterface &$jwe, JWKInterface $jwk)
69
    {
70
        $jwk_set = new JWKSet();
71
        $jwk_set = $jwk_set->addKey($jwk);
72
73
        return $this->decryptUsingKeySet($jwe, $jwk_set);
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function decryptUsingKeySet(JWEInterface &$jwe, JWKSetInterface $jwk_set)
80
    {
81
        $this->log(LogLevel::DEBUG, 'Trying to decrypt the JWE.');
82
        $this->checkJWKSet($jwk_set);
83
        $this->checkPayload($jwe);
84
        $this->checkRecipients($jwe);
85
86
        $nb_recipients = $jwe->countRecipients();
87
        $this->log(LogLevel::DEBUG, 'The JWE contains {nb} recipient(s).', ['nb' => $nb_recipients]);
88
89
        for ($i = 0; $i < $nb_recipients; $i++) {
90
            $this->log(LogLevel::DEBUG, 'Trying to decrypt the encrypted key of recipient #{nb}.', ['nb' => $i]);
91
            $recipient = $jwe->getRecipient($i);
92
            $complete_headers = array_merge(
93
                $jwe->getSharedProtectedHeaders(),
94
                $jwe->getSharedHeaders(),
95
                $recipient->getHeaders()
96
            );
97
            $this->checkCompleteHeader($complete_headers);
98
99
            $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_headers);
100
            $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($complete_headers);
101
102
            foreach ($jwk_set as $jwk) {
103
                try {
104
                    $this->checkKeyUsage($jwk, 'decryption');
105
                    if ('dir' !== $key_encryption_algorithm->getAlgorithmName()) {
106
                        $this->checkKeyAlgorithm($jwk, $key_encryption_algorithm->getAlgorithmName());
107
                    } else {
108
                        $this->checkKeyAlgorithm($jwk, $content_encryption_algorithm->getAlgorithmName());
109
                    }
110
                    $cek = $this->decryptCEK($key_encryption_algorithm, $content_encryption_algorithm, $jwk, $recipient, $complete_headers);
111
                    if (null !== $cek) {
112
                        if (true === $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $complete_headers)) {
113
                            return $i;
114
                        };
115
                    }
116
                } catch (\Exception $e) {
117
                    //We do nothing, we continue with other keys
118
                    continue;
119
                }
120
            }
121
        }
122
123
        throw new \InvalidArgumentException('Unable to decrypt the JWE. Please verify the key or keyset used is correct');
124
    }
125
126
    /**
127
     * @param \Jose\Object\JWEInterface $jwe
128
     */
129
    private function checkRecipients(JWEInterface $jwe)
130
    {
131
        if (0 === $jwe->countRecipients()) {
132
            throw new \InvalidArgumentException('The JWE does not contain any recipient.');
133
        }
134
    }
135
136
    /**
137
     * @param \Jose\Object\JWEInterface $jwe
138
     */
139
    private function checkPayload(JWEInterface $jwe)
140
    {
141
        $this->log(LogLevel::DEBUG, 'Checking the payload.', ['payload' => $jwe->getPayload()]);
142
        if (null !== $jwe->getPayload()) {
143
            $exception = new \InvalidArgumentException('The JWE is already decrypted.');
144
            $this->log(LogLevel::ERROR, 'The JWE is already decrypted.', ['exception' => $exception]);
145
            throw $exception;
146
        }
147
    }
148
149
    /**
150
     * @param \Jose\Object\JWKSetInterface $jwk_set
151
     */
152
    private function checkJWKSet(JWKSetInterface $jwk_set)
153
    {
154
        $this->log(LogLevel::DEBUG, 'Checking the key set.', ['jwkset' => $jwk_set]);
155
        if (0 === count($jwk_set)) {
156
            $exception = new \InvalidArgumentException('No key in the key set.');
157
            $this->log(LogLevel::ERROR, 'There is no key in the key set.', ['exception' => $exception]);
158
            throw $exception;
159
        }
160
        $this->log(LogLevel::DEBUG, 'Checking the key set.', ['jwkset' => $jwk_set]);
161
    }
162
163
    /**
164
     * @param \Jose\Algorithm\JWAInterface                        $key_encryption_algorithm
165
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
166
     * @param \Jose\Object\JWKInterface                           $key
167
     * @param \Jose\Object\RecipientInterface                     $recipient
168
     * @param array                                               $complete_headers
169
     *
170
     * @return null|string
171
     */
172
    private function decryptCEK(JWAInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $key, RecipientInterface $recipient, array $complete_headers)
173
    {
174
        if ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
175
            return $key_encryption_algorithm->getCEK($key);
176
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
177
            return $key_encryption_algorithm->getAgreementKey(
178
                $content_encryption_algorithm->getCEKSize(),
179
                $content_encryption_algorithm->getAlgorithmName(),
180
                $key,
181
                null,
182
                $complete_headers
183
            );
184
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
185
            return $key_encryption_algorithm->unwrapAgreementKey(
186
                $key,
187
                $recipient->getEncryptedKey(),
188
                $content_encryption_algorithm->getCEKSize(),
189
                $complete_headers
190
            );
191
        } elseif ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
192
            return $key_encryption_algorithm->decryptKey(
193
                $key,
194
                $recipient->getEncryptedKey(),
195
                $complete_headers
196
            );
197
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
198
            return $key_encryption_algorithm->unwrapKey(
199
                $key,
200
                $recipient->getEncryptedKey(),
201
                $complete_headers
202
            );
203
        } else {
204
            throw new \InvalidArgumentException('Unsupported CEK generation');
205
        }
206
    }
207
208
    /**
209
     * @param \Jose\Object\JWEInterface                           $jwe
210
     * @param string                                              $cek
211
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
212
     * @param array                                               $complete_headers
213
     *
214
     * @return bool
215
     */
216
    private function decryptPayload(JWEInterface &$jwe, $cek, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array $complete_headers)
217
    {
218
        $payload = $content_encryption_algorithm->decryptContent(
219
            $jwe->getCiphertext(),
220
            $cek,
221
            $jwe->getIV(),
222
            $jwe->getAAD(),
223
            $jwe->getEncodedSharedProtectedHeaders(),
224
            $jwe->getTag()
225
        );
226
        if (null === $payload) {
227
            return false;
228
        }
229
230
        $jwe = $jwe->withContentEncryptionKey($cek);
231
232
        $this->decompressIfNeeded($payload, $complete_headers);
233
234
        $decoded = json_decode($payload, true);
235
        $jwe = $jwe->withPayload(null === $decoded ? $payload : $decoded);
236
237
        return true;
238
    }
239
240
    /**
241
     * @param string $payload
242
     * @param array  $complete_headers
243
     */
244
    private function decompressIfNeeded(&$payload, array $complete_headers)
245
    {
246
        $this->log(LogLevel::DEBUG, 'Checking if payload is compressed.', ['payload' => $payload]);
247
        if (array_key_exists('zip', $complete_headers)) {
248
            $this->log(LogLevel::DEBUG, 'Header indicates the payload is compressed with method {compress}.', ['compress' => $complete_headers['zip']]);
249
            $compression_method = $this->getCompressionMethod($complete_headers['zip']);
250
            $payload = $compression_method->uncompress($payload);
251
            if (!is_string($payload)) {
252
                $exception = new \InvalidArgumentException('Decompression failed');
253
                $this->log(LogLevel::ERROR, 'Unable to decompress the payload.', ['exception' => $exception]);
254
                throw $exception;
255
            }
256
        }
257
    }
258
259
    /**
260
     * @param array $complete_headers
261
     *
262
     * @throws \InvalidArgumentException
263
     */
264
    private function checkCompleteHeader(array $complete_headers)
265
    {
266
        $this->log(LogLevel::DEBUG, 'Checking for mandatory parameters.');
267
        foreach (['enc', 'alg'] as $key) {
268
            if (!array_key_exists($key, $complete_headers)) {
269
                $exception = new \InvalidArgumentException(sprintf("Parameters '%s' is missing.", $key));
270
                $this->log(LogLevel::ERROR, 'Parameter is missing.', ['exception' => $exception]);
271
                throw $exception;
272
            }
273
        }
274
    }
275
276
    /**
277
     * @param array $complete_headers
278
     *
279
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
280
     */
281
    private function getKeyEncryptionAlgorithm(array $complete_headers)
282
    {
283
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
284
285
        if (!$key_encryption_algorithm instanceof KeyEncryptionAlgorithmInterface) {
286
            throw new \InvalidArgumentException(sprintf("The key encryption algorithm '%s' is not supported or does not implement KeyEncryptionAlgorithmInterface.", $complete_headers['alg']));
287
        }
288
289
        return $key_encryption_algorithm;
290
    }
291
292
    /**
293
     * @param array $complete_headers
294
     *
295
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
296
     */
297
    private function getContentEncryptionAlgorithm(array $complete_headers)
298
    {
299
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['enc']);
300
        if (!$content_encryption_algorithm instanceof ContentEncryptionAlgorithmInterface) {
301
            throw new \InvalidArgumentException(sprintf('The algorithm "%s" does not exist or does not implement ContentEncryptionInterface."', $complete_headers['enc']));
302
        }
303
304
        return $content_encryption_algorithm;
305
    }
306
307
    /**
308
     * @param string $method
309
     *
310
     * @throws \InvalidArgumentException
311
     *
312
     * @return \Jose\Compression\CompressionInterface
313
     */
314
    private function getCompressionMethod($method)
315
    {
316
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
317
        if (null === $compression_method) {
318
            throw new \InvalidArgumentException(sprintf("Compression method '%s' not supported"), $method);
319
        }
320
321
        return $compression_method;
322
    }
323
}
324