Completed
Push — master ( f67525...5cf1da )
by Florent
02:34
created

Loader::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 17
rs 9.4286
cc 1
eloc 15
nc 1
nop 7
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2015 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 Base64Url\Base64Url;
15
use Jose\Behaviour\HasCheckerManager;
16
use Jose\Behaviour\HasCompressionManager;
17
use Jose\Behaviour\HasJWAManager;
18
use Jose\Behaviour\HasJWKManager;
19
use Jose\Behaviour\HasJWKSetManager;
20
use Jose\Behaviour\HasJWTManager;
21
use Jose\Behaviour\HasKeyChecker;
22
use Jose\Behaviour\HasPayloadConverter;
23
use Jose\Checker\CheckerManagerInterface;
24
use Jose\Compression\CompressionManagerInterface;
25
use Jose\Operation\ContentEncryptionInterface;
26
use Jose\Operation\DirectEncryptionInterface;
27
use Jose\Operation\KeyAgreementInterface;
28
use Jose\Operation\KeyAgreementWrappingInterface;
29
use Jose\Operation\KeyEncryptionInterface;
30
use Jose\Operation\SignatureInterface;
31
use Jose\Payload\PayloadConverterManagerInterface;
32
use Jose\Util\Converter;
33
34
/**
35
 * Class able to load JWS or JWE.
36
 * JWS object can also be verified.
37
 */
38
class Loader implements LoaderInterface
39
{
40
    use HasKeyChecker;
41
    use HasJWAManager;
42
    use HasJWTManager;
43
    use HasJWKManager;
44
    use HasJWKSetManager;
45
    use HasCheckerManager;
46
    use HasPayloadConverter;
47
    use HasCompressionManager;
48
49
    /**
50
     * Loader constructor.
51
     *
52
     * @param \Jose\JWTManagerInterface                      $jwt_manager
53
     * @param \Jose\JWAManagerInterface                      $jwa_manager
54
     * @param \Jose\JWKManagerInterface                      $jwk_manager
55
     * @param \Jose\JWKSetManagerInterface                   $jwkset_manager
56
     * @param \Jose\Payload\PayloadConverterManagerInterface $payload_converter_manager
57
     * @param \Jose\Compression\CompressionManagerInterface  $compression_manager
58
     * @param \Jose\Checker\CheckerManagerInterface          $checker_manager
59
     */
60
    public function __construct(
61
        JWTManagerInterface $jwt_manager,
62
        JWAManagerInterface $jwa_manager,
63
        JWKManagerInterface $jwk_manager,
64
        JWKSetManagerInterface $jwkset_manager,
65
        PayloadConverterManagerInterface $payload_converter_manager,
66
        CompressionManagerInterface $compression_manager,
67
        CheckerManagerInterface $checker_manager)
68
    {
69
        $this->setJWTManager($jwt_manager);
70
        $this->setJWAManager($jwa_manager);
71
        $this->setJWKManager($jwk_manager);
72
        $this->setJWKSetManager($jwkset_manager);
73
        $this->setPayloadConverter($payload_converter_manager);
74
        $this->setCompressionManager($compression_manager);
75
        $this->setCheckerManager($checker_manager);
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function load($input)
82
    {
83
        $json = Converter::convert($input, JSONSerializationModes::JSON_SERIALIZATION, false);
84
        if (is_array($json)) {
85
            if (array_key_exists('signatures', $json)) {
86
                return $this->loadSerializedJsonJWS($json, $input);
87
            }
88
            if (array_key_exists('recipients', $json)) {
89
                return $this->loadSerializedJsonJWE($json, $input);
90
            }
91
        }
92
        throw new \InvalidArgumentException('Unable to load the input');
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function decrypt(JWEInterface &$jwe, JWKSetInterface $jwk_set = null)
99
    {
100
        $complete_header = array_merge(
101
            $jwe->getProtectedHeader(),
102
            $jwe->getUnprotectedHeader()
103
        );
104
105
        $this->checkCompleteHeader($complete_header);
106
107
        if (null === $jwk_set) {
108
            $jwk_set = $this->getKeysFromCompleteHeader($complete_header);
109
        }
110
        $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($complete_header['alg']);
111
        $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($complete_header['enc']);
112
113
        foreach ($jwk_set as $jwk) {
114
            if (!$this->checkKeyUsage($jwk, 'decryption')) {
115
                continue;
116
            }
117
            if (!$this->checkKeyAlgorithm($jwk, $key_encryption_algorithm->getAlgorithmName())) {
118
                continue;
119
            }
120
            try {
121
                $cek = $this->decryptCEK($key_encryption_algorithm, $content_encryption_algorithm, $jwk, $jwe->getEncryptedKey(), $complete_header);
122
123
                if (null !== $cek) {
124
                    if (true === $this->decryptPayload($jwe, $cek, $content_encryption_algorithm)) {
125
                        return true;
126
                    }
127
                }
128
            } catch (\InvalidArgumentException $e) {
129
                //We do nothing, we continue with other keys
130
            }
131
        }
132
133
        return false;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     *
139
     * @throws \InvalidArgumentException
140
     */
141
    public function verifySignature(JWSInterface $jws, JWKSetInterface $jwk_set = null, $detached_payload = null)
142
    {
143
        if (null !== $detached_payload && !empty($jws->getPayload())) {
144
            throw new \InvalidArgumentException('A detached payload is set, but the JWS already has a payload');
145
        }
146
        $complete_header = array_merge($jws->getProtectedHeader(), $jws->getUnprotectedHeader());
147
        if (null === $jwk_set) {
148
            $jwk_set = $this->getKeysFromCompleteHeader($complete_header);
149
        }
150
151
        $input = $jws->getEncodedProtectedHeader().'.'.(null === $detached_payload ? $jws->getEncodedPayload() : $detached_payload);
152
153
        if (0 === count($jwk_set)) {
154
            return false;
155
        }
156
        foreach ($jwk_set->getKeys() as $jwk) {
157
            $algorithm = $this->getAlgorithm($complete_header, $jwk);
158
            if (!$this->checkKeyUsage($jwk, 'verification')) {
159
                continue;
160
            }
161
            if (!$this->checkKeyAlgorithm($jwk, $algorithm->getAlgorithmName())) {
162
                continue;
163
            }
164
            try {
165
                if (true === $algorithm->verify($jwk, $input, $jws->getSignature())) {
166
                    return true;
167
                }
168
            } catch (\InvalidArgumentException $e) {
169
                //We do nothing, we continue with other keys
170
            }
171
        }
172
173
        return false;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function verify(JWTInterface $jwt)
180
    {
181
        $this->getCheckerManager()->checkJWT($jwt);
182
183
        return true;
184
    }
185
186
    /**
187
     * @param array  $data
188
     * @param string $input
189
     *
190
     * @return \Jose\JWSInterface|\Jose\JWSInterface[]
191
     */
192
    protected function loadSerializedJsonJWS(array $data, $input)
193
    {
194
        $encoded_payload = isset($data['payload']) ? $data['payload'] : '';
195
        $payload = Base64Url::decode($encoded_payload);
196
197
        $jws = [];
198
        foreach ($data['signatures'] as $signature) {
199
            if (array_key_exists('protected', $signature)) {
200
                $encoded_protected_header = $signature['protected'];
201
                $protected_header = json_decode(Base64Url::decode($encoded_protected_header), true);
202
            } else {
203
                $encoded_protected_header = null;
204
                $protected_header = [];
205
            }
206
            $unprotected_header = isset($signature['header']) ? $signature['header'] : [];
207
208
            $result = $this->createJWS($encoded_protected_header, $encoded_payload, $protected_header, $unprotected_header, $payload, Base64Url::decode($signature['signature']));
209
            $result = $result->withInput($input);
210
            $jws[] = $result;
211
        }
212
213
        return count($jws) > 1 ? $jws : current($jws);
214
    }
215
216
    /**
217
     * @param string $encoded_protected_header
218
     * @param string $encoded_payload
219
     * @param array  $protected_header
220
     * @param array  $unprotected_header
221
     * @param string $payload
222
     * @param string $signature
223
     *
224
     * @throws \Exception
225
     *
226
     * @return \Jose\JWSInterface
227
     */
228
    protected function createJWS($encoded_protected_header, $encoded_payload, $protected_header, $unprotected_header, $payload, $signature)
229
    {
230
        $complete_header = array_merge($protected_header, $unprotected_header);
231
        $payload = $this->getPayloadConverter()->convertStringToPayload($complete_header, $payload);
232
        $jws = $this->getJWTManager()->createJWS();
233
        $jws = $jws->withSignature($signature);
234
        $jws = $jws->withPayload($payload);
235
        $jws = $jws->withEncodedProtectedHeader($encoded_protected_header);
236
        $jws = $jws->withEncodedPayload($encoded_payload);
237
        if (!empty($protected_header)) {
238
            $jws = $jws->withProtectedHeader($protected_header);
239
        }
240
        if (!empty($unprotected_header)) {
241
            $jws = $jws->withUnprotectedHeader($unprotected_header);
242
        }
243
244
        return $jws;
245
    }
246
247
    /**
248
     * @param array              $header
249
     * @param \Jose\JWKInterface $key
250
     *
251
     * @return \Jose\Operation\SignatureInterface|null
252
     */
253
    protected function getAlgorithm(array $header, JWKInterface $key)
254
    {
255
        if (!array_key_exists('alg', $header)) {
256
            if (null === $key->getAlgorithm()) {
257
                throw new \InvalidArgumentException("No 'alg' parameter set in the header or the key.");
258
            } else {
259
                $alg = $key->getAlgorithm();
260
            }
261
        } else {
262
            $alg = $header['alg'];
263
        }
264
265
        $algorithm = $this->getJWAManager()->getAlgorithm($alg);
266
        if (!$algorithm instanceof SignatureInterface) {
267
            throw new \RuntimeException("The algorithm '$alg' is not supported or does not implement SignatureInterface.");
268
        }
269
270
        return $algorithm;
271
    }
272
273
    /**
274
     * @param \Jose\JWAInterface                         $key_encryption_algorithm
275
     * @param \Jose\Operation\ContentEncryptionInterface $content_encryption_algorithm
276
     * @param \Jose\JWKInterface                         $key
277
     * @param string|null                                $encrypted_cek
278
     * @param array                                      $header
279
     *
280
     * @return string|null
281
     */
282
    public function decryptCEK(JWAInterface $key_encryption_algorithm, ContentEncryptionInterface $content_encryption_algorithm, JWKInterface $key, $encrypted_cek, array $header)
283
    {
284
        if ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
285
            return $key_encryption_algorithm->getCEK($key, $header);
286
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
287
            return $key_encryption_algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $key, null, $header);
288
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
289
            return $key_encryption_algorithm->unwrapAgreementKey($key, $encrypted_cek, $content_encryption_algorithm->getCEKSize(), $header);
290
        } elseif ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
291
            return $key_encryption_algorithm->decryptKey($key, $encrypted_cek, $header);
292
        } else {
293
            throw new \RuntimeException('Unsupported CEK generation');
294
        }
295
    }
296
297
    /**
298
     * @param array  $data
299
     * @param string $input
300
     *
301
     * @return \Jose\JWEInterface|\Jose\JWEInterface[]
302
     */
303
    protected function loadSerializedJsonJWE(array $data, $input)
304
    {
305
        $result = [];
306
        foreach ($data['recipients'] as $recipient) {
307
            $encoded_protected_header = array_key_exists('protected', $data) ? $data['protected'] : '';
308
            $protected_header = empty($encoded_protected_header) ? [] : json_decode(Base64Url::decode($encoded_protected_header), true);
309
            $unprotected_header = array_key_exists('unprotected', $data) ? $data['unprotected'] : [];
310
            $header = array_key_exists('header', $recipient) ? $recipient['header'] : [];
311
312
            $jwe = $this->getJWTManager()->createJWE();
313
            $jwe = $jwe->withAAD(array_key_exists('aad', $data) ? Base64Url::decode($data['aad']) : null);
314
            $jwe = $jwe->withCiphertext(Base64Url::decode($data['ciphertext']));
315
            $jwe = $jwe->withEncryptedKey(array_key_exists('encrypted_key', $recipient) ? Base64Url::decode($recipient['encrypted_key']) : null);
316
            $jwe = $jwe->withIV(array_key_exists('iv', $data) ? Base64Url::decode($data['iv']) : null);
317
            $jwe = $jwe->withTag(array_key_exists('tag', $data) ? Base64Url::decode($data['tag']) : null);
318
            $jwe = $jwe->withProtectedHeader($protected_header);
319
            $jwe = $jwe->withEncodedProtectedHeader($encoded_protected_header);
320
            $jwe = $jwe->withUnprotectedHeader(array_merge($unprotected_header, $header));
321
            $jwe = $jwe->withInput($input);
322
            $result[] = $jwe;
323
        }
324
325
        return count($result) > 1 ? $result : current($result);
326
    }
327
328
    /**
329
     * @param \Jose\JWEInterface                         $jwe
330
     * @param string                                     $cek
331
     * @param \Jose\Operation\ContentEncryptionInterface $content_encryption_algorithm
332
     *
333
     * @return \Jose\JWEInterface
334
     */
335
    protected function decryptPayload(JWEInterface &$jwe, $cek, $content_encryption_algorithm)
336
    {
337
        $payload = $content_encryption_algorithm->decryptContent(
338
            $jwe->getCiphertext(),
339
            $cek,
340
            $jwe->getIV(),
341
            $jwe->getAAD(),
342
            $jwe->getEncodedProtectedHeader(),
343
            $jwe->getTag()
344
        );
345
346
        if (null === $payload) {
347
            return false;
348
        }
349
350
        if (null !== $jwe->getZip()) {
351
            $compression_method = $this->getCompressionMethod($jwe->getZip());
352
            $payload = $compression_method->uncompress($payload);
353
            if (!is_string($payload)) {
354
                throw new \RuntimeException('Decompression failed');
355
            }
356
        }
357
358
        $payload = $this->getPayloadConverter()->convertStringToPayload(array_merge($jwe->getProtectedHeader(), $jwe->getUnprotectedHeader()), $payload);
359
360
        $jwe = $jwe->withPayload($payload);
361
362
        return true;
363
    }
364
365
    /**
366
     * @param array $complete_header
367
     *
368
     * @throws \InvalidArgumentException
369
     */
370
    protected function checkCompleteHeader(array $complete_header)
371
    {
372
        foreach (['enc', 'alg'] as $key) {
373
            if (!array_key_exists($key, $complete_header)) {
374
                throw new \InvalidArgumentException(sprintf("Parameters '%s' is missing.", $key));
375
            }
376
        }
377
    }
378
379
    /**
380
     * @param string $algorithm
381
     *
382
     * @return \Jose\Operation\DirectEncryptionInterface|\Jose\Operation\KeyEncryptionInterface|\Jose\Operation\KeyAgreementInterface|\Jose\Operation\KeyAgreementWrappingInterface
383
     */
384
    protected function getKeyEncryptionAlgorithm($algorithm)
385
    {
386
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
387
        foreach ([
388
                    '\Jose\Operation\DirectEncryptionInterface',
389
                    '\Jose\Operation\KeyEncryptionInterface',
390
                    '\Jose\Operation\KeyAgreementInterface',
391
                    '\Jose\Operation\KeyAgreementWrappingInterface',
392
                ] as $class) {
393
            if ($key_encryption_algorithm instanceof $class) {
394
                return $key_encryption_algorithm;
395
            }
396
        }
397
        throw new \RuntimeException(sprintf("The key encryption algorithm '%s' is not supported or not a key encryption algorithm instance.", $algorithm));
398
    }
399
400
    /**
401
     * @param $algorithm
402
     *
403
     * @return \Jose\Operation\ContentEncryptionInterface
404
     */
405
    protected function getContentEncryptionAlgorithm($algorithm)
406
    {
407
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($algorithm);
408
        if (!$content_encryption_algorithm instanceof ContentEncryptionInterface) {
409
            throw new \RuntimeException("The algorithm '".$algorithm."' does not implement ContentEncryptionInterface.");
410
        }
411
412
        return $content_encryption_algorithm;
413
    }
414
415
    protected function getCompressionMethod($method)
416
    {
417
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($method);
418
        if (null === $compression_method) {
419
            throw new \RuntimeException(sprintf("Compression method '%s' not supported"), $method);
420
        }
421
422
        return $compression_method;
423
    }
424
425
    protected function getKeysFromCompleteHeader(array $header)
426
    {
427
        $keys = $this->getJWKSetManager()->createJWKSet();
428
        $jwk = $this->getJWKManager()->findJWK($header);
429
        if ($jwk instanceof JWKInterface) {
430
            $keys->addKey($jwk);
431
        }
432
        $jwkset = $this->getJWKSetManager()->findJWKSet($header);
433
        if ($jwkset instanceof JWKSetInterface) {
434
            foreach ($jwkset as $key) {
435
                $keys->addKey($key);
436
            }
437
        }/* elseif ($jwkset instanceof JWKInterface) {
438
            $keys->addKey($jwkset);
439
        }*/
440
441
        return $keys;
442
    }
443
}
444