Completed
Push — master ( 4b8ee0...7b8765 )
by Florent
02:32
created

Decrypter::decryptUsingKeySet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 7 Features 1
Metric Value
c 11
b 7
f 1
dl 0
loc 18
rs 9.4285
cc 3
eloc 11
nc 3
nop 2
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
            if (is_int($result = $this->decryptRecipientKey($jwe, $jwk_set, $i))) {
91
                return $result;
92
            }
93
        }
94
95
        throw new \InvalidArgumentException('Unable to decrypt the JWE. Please verify the key or keyset used is correct');
96
    }
97
98
    /**
99
     * @param \Jose\Object\JWEInterface    $jwe
100
     * @param \Jose\Object\JWKSetInterface $jwk_set
101
     * @param int                          $i
102
     *
103
     * @return mixed
104
     */
105
    private function decryptRecipientKey(JWEInterface &$jwe, JWKSetInterface $jwk_set, $i)
106
    {
107
        $this->log(LogLevel::DEBUG, 'Trying to decrypt the encrypted key of recipient #{nb}.', ['nb' => $i]);
108
        $recipient = $jwe->getRecipient($i);
109
        $complete_headers = array_merge(
110
            $jwe->getSharedProtectedHeaders(),
111
            $jwe->getSharedHeaders(),
112
            $recipient->getHeaders()
113
        );
114
        $this->checkCompleteHeader($complete_headers);
115
116
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_headers);
117
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($complete_headers);
118
119
        foreach ($jwk_set as $jwk) {
120
            try {
121
                $this->checkKeyUsage($jwk, 'decryption');
122
                if ('dir' !== $key_encryption_algorithm->getAlgorithmName()) {
123
                    $this->checkKeyAlgorithm($jwk, $key_encryption_algorithm->getAlgorithmName());
124
                } else {
125
                    $this->checkKeyAlgorithm($jwk, $content_encryption_algorithm->getAlgorithmName());
126
                }
127
                $cek = $this->decryptCEK($key_encryption_algorithm, $content_encryption_algorithm, $jwk, $recipient, $complete_headers);
128
                if (null !== $cek) {
129
                    if (true === $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $complete_headers)) {
130
                        return $i;
131
                    };
132
                }
133
            } catch (\Exception $e) {
134
                //We do nothing, we continue with other keys
135
                continue;
136
            }
137
        }
138
    }
139
140
    /**
141
     * @param \Jose\Object\JWEInterface $jwe
142
     */
143
    private function checkRecipients(JWEInterface $jwe)
144
    {
145
        if (0 === $jwe->countRecipients()) {
146
            $exception = new \InvalidArgumentException('The JWE does not contain any recipient.');
147
            $this->log(LogLevel::ERROR, 'The JWE does not contain any recipient.', ['exception' => $exception]);
148
            throw $exception;
149
        }
150
    }
151
152
    /**
153
     * @param \Jose\Object\JWEInterface $jwe
154
     */
155
    private function checkPayload(JWEInterface $jwe)
156
    {
157
        $this->log(LogLevel::DEBUG, 'Checking the payload.', ['payload' => $jwe->getPayload()]);
158
        if (null !== $jwe->getPayload()) {
159
            $exception = new \InvalidArgumentException('The JWE is already decrypted.');
160
            $this->log(LogLevel::ERROR, 'The JWE is already decrypted.', ['exception' => $exception]);
161
            throw $exception;
162
        }
163
    }
164
165
    /**
166
     * @param \Jose\Object\JWKSetInterface $jwk_set
167
     */
168
    private function checkJWKSet(JWKSetInterface $jwk_set)
169
    {
170
        $this->log(LogLevel::DEBUG, 'Checking the key set.', ['jwkset' => $jwk_set]);
171
        if (0 === count($jwk_set)) {
172
            $exception = new \InvalidArgumentException('No key in the key set.');
173
            $this->log(LogLevel::ERROR, 'There is no key in the key set.', ['exception' => $exception]);
174
            throw $exception;
175
        }
176
        $this->log(LogLevel::DEBUG, 'Checking the key set.', ['jwkset' => $jwk_set]);
177
    }
178
179
    /**
180
     * @param \Jose\Algorithm\JWAInterface                        $key_encryption_algorithm
181
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
182
     * @param \Jose\Object\JWKInterface                           $key
183
     * @param \Jose\Object\RecipientInterface                     $recipient
184
     * @param array                                               $complete_headers
185
     *
186
     * @return null|string
187
     */
188
    private function decryptCEK(JWAInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $key, RecipientInterface $recipient, array $complete_headers)
189
    {
190
        if ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
191
            return $key_encryption_algorithm->getCEK($key);
192
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
193
            return $key_encryption_algorithm->getAgreementKey(
194
                $content_encryption_algorithm->getCEKSize(),
195
                $content_encryption_algorithm->getAlgorithmName(),
196
                $key,
197
                null,
198
                $complete_headers
199
            );
200
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
201
            return $key_encryption_algorithm->unwrapAgreementKey(
202
                $key,
203
                $recipient->getEncryptedKey(),
204
                $content_encryption_algorithm->getCEKSize(),
205
                $complete_headers
206
            );
207
        } elseif ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
208
            return $key_encryption_algorithm->decryptKey(
209
                $key,
210
                $recipient->getEncryptedKey(),
211
                $complete_headers
212
            );
213
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
214
            return $key_encryption_algorithm->unwrapKey(
215
                $key,
216
                $recipient->getEncryptedKey(),
217
                $complete_headers
218
            );
219
        } else {
220
            throw new \InvalidArgumentException('Unsupported CEK generation');
221
        }
222
    }
223
224
    /**
225
     * @param \Jose\Object\JWEInterface                           $jwe
226
     * @param string                                              $cek
227
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
228
     * @param array                                               $complete_headers
229
     *
230
     * @return bool
231
     */
232
    private function decryptPayload(JWEInterface &$jwe, $cek, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array $complete_headers)
233
    {
234
        $payload = $content_encryption_algorithm->decryptContent(
235
            $jwe->getCiphertext(),
236
            $cek,
237
            $jwe->getIV(),
238
            $jwe->getAAD(),
239
            $jwe->getEncodedSharedProtectedHeaders(),
240
            $jwe->getTag()
241
        );
242
        if (null === $payload) {
243
            return false;
244
        }
245
246
        $jwe = $jwe->withContentEncryptionKey($cek);
247
248
        $this->decompressIfNeeded($payload, $complete_headers);
249
250
        $decoded = json_decode($payload, true);
251
        $jwe = $jwe->withPayload(null === $decoded ? $payload : $decoded);
252
253
        return true;
254
    }
255
256
    /**
257
     * @param string $payload
258
     * @param array  $complete_headers
259
     */
260
    private function decompressIfNeeded(&$payload, array $complete_headers)
261
    {
262
        $this->log(LogLevel::DEBUG, 'Checking if payload is compressed.', ['payload' => $payload]);
263
        if (array_key_exists('zip', $complete_headers)) {
264
            $this->log(LogLevel::DEBUG, 'Header indicates the payload is compressed with method {compress}.', ['compress' => $complete_headers['zip']]);
265
            $compression_method = $this->getCompressionMethod($complete_headers['zip']);
266
            $payload = $compression_method->uncompress($payload);
267
            if (!is_string($payload)) {
268
                $exception = new \InvalidArgumentException('Decompression failed');
269
                $this->log(LogLevel::ERROR, 'Unable to decompress the payload.', ['exception' => $exception]);
270
                throw $exception;
271
            }
272
            $this->log(LogLevel::DEBUG, 'The payload has been decompressed.');
273
        } else {
274
            $this->log(LogLevel::DEBUG, 'The payload is not compressed.');
275
        }
276
    }
277
278
    /**
279
     * @param array $complete_headers
280
     *
281
     * @throws \InvalidArgumentException
282
     */
283
    private function checkCompleteHeader(array $complete_headers)
284
    {
285
        $this->log(LogLevel::DEBUG, 'Checking for mandatory parameters.');
286
        foreach (['enc', 'alg'] as $key) {
287
            if (!array_key_exists($key, $complete_headers)) {
288
                $exception = new \InvalidArgumentException(sprintf("Parameters '%s' is missing.", $key));
289
                $this->log(LogLevel::ERROR, 'Parameter is missing.', ['exception' => $exception]);
290
                throw $exception;
291
            }
292
        }
293
        $this->log(LogLevel::DEBUG, 'Mandatory parameters found.');
294
    }
295
296
    /**
297
     * @param array $complete_headers
298
     *
299
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
300
     */
301
    private function getKeyEncryptionAlgorithm(array $complete_headers)
302
    {
303
        $this->log(LogLevel::DEBUG, 'Trying to find key encryption algorithm');
304
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
305
        if (!$key_encryption_algorithm instanceof KeyEncryptionAlgorithmInterface) {
306
            $exception = new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithmInterface.', $complete_headers['alg']));
307
            $this->log(LogLevel::ERROR, 'The key encryption algorithm {alg} is not supported or does not implement KeyEncryptionAlgorithmInterface.', ['alg' => $complete_headers['alg'], 'exception' => $exception]);
308
            throw $exception;
309
        }
310
311
        $this->log(LogLevel::DEBUG, 'Key encryption algorithm {algorithm} found', ['algorithm' => $complete_headers['alg'], 'service' > $key_encryption_algorithm]);
312
313
        return $key_encryption_algorithm;
314
    }
315
316
    /**
317
     * @param array $complete_headers
318
     *
319
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
320
     */
321
    private function getContentEncryptionAlgorithm(array $complete_headers)
322
    {
323
        $this->log(LogLevel::DEBUG, 'Trying to find content encryption algorithm');
324
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['enc']);
325
        if (!$content_encryption_algorithm instanceof ContentEncryptionAlgorithmInterface) {
326
            $exception = new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or does not implement ContentEncryptionInterface.', $complete_headers['enc']));
327
            $this->log(LogLevel::ERROR, 'The key encryption algorithm {alg} is not supported or does not implement ContentEncryptionInterface.', ['alg' => $complete_headers['enc'], 'exception' => $exception]);
328
            throw $exception;
329
        }
330
331
        $this->log(LogLevel::DEBUG, 'Content encryption algorithm {algorithm} found', ['algorithm' => $complete_headers['enc'], 'service' > $content_encryption_algorithm]);
332
333
        return $content_encryption_algorithm;
334
    }
335
336
    /**
337
     * @param string $method
338
     *
339
     * @throws \InvalidArgumentException
340
     *
341
     * @return \Jose\Compression\CompressionInterface
342
     */
343
    private function getCompressionMethod($method)
344
    {
345
        $this->log(LogLevel::DEBUG, 'Trying to find compression method {method}', ['method' => $method]);
346
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
347
        if (null === $compression_method) {
348
            $exception = new \InvalidArgumentException(sprintf('Compression method "%s" not supported', $method));
349
            $this->log(LogLevel::ERROR, 'Compression method {method} not supported', ['method' => $method, 'exception' => $exception]);
350
            throw $exception;
351
        }
352
353
        $this->log(LogLevel::DEBUG, 'Compression method {method} found', ['method' => $method, 'service' => $compression_method]);
354
355
        return $compression_method;
356
    }
357
}
358