Completed
Push — v2.0.x ( 59a3d4...b06cab )
by Florent
03:22
created

Encrypter::computeRecipient()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
c 2
b 1
f 1
dl 0
loc 26
rs 8.5806
cc 4
eloc 18
nc 4
nop 7
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 Base64Url\Base64Url;
15
use Jose\Algorithm\ContentEncryptionAlgorithmInterface;
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\Compression\CompressionManagerInterface;
27
use Jose\Object\JWEInterface;
28
use Jose\Object\JWKInterface;
29
use Jose\Object\Recipient;
30
31
/**
32
 */
33
final class Encrypter implements EncrypterInterface
34
{
35
    use HasKeyChecker;
36
    use HasJWAManager;
37
    use HasCompressionManager;
38
39
    /**
40
     * Encrypter constructor.
41
     *
42
     * @param \Jose\Algorithm\JWAManagerInterface           $jwa_manager
43
     * @param \Jose\Compression\CompressionManagerInterface $compression_manager
44
     */
45
    public function __construct(
46
        JWAManagerInterface $jwa_manager,
47
        CompressionManagerInterface $compression_manager)
48
    {
49
        $this->setJWAManager($jwa_manager);
50
        $this->setCompressionManager($compression_manager);
51
    }
52
53
    /**
54
     * @param \Jose\Object\JWEInterface      $jwe
55
     * @param \Jose\Object\JWKInterface      $recipient_key
56
     * @param \Jose\Object\JWKInterface|null $sender_key
57
     * @param array                          $recipient_headers
58
     *
59
     * @return \Jose\Object\JWEInterface
60
     */
61
    public function addRecipient(JWEInterface $jwe, JWKInterface $recipient_key, JWKInterface $sender_key = null, array $recipient_headers = [])
62
    {
63
        $complete_headers = array_merge(
64
            $jwe->getSharedProtectedHeaders(),
65
            $jwe->getSharedHeaders(),
66
            $recipient_headers
67
        );
68
69
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
70
71
        $this->checkKeyUsage($recipient_key, 'encryption');
72
        $this->checkKeyAlgorithm($recipient_key, $key_encryption_algorithm->getAlgorithmName());
73
        if ($sender_key instanceof JWKInterface) {
74
            $this->checkKeyUsage($sender_key, 'encryption');
75
            $this->checkKeyAlgorithm($sender_key, $key_encryption_algorithm->getAlgorithmName());
76
        }
77
78
        // Content Encryption Algorithm
79
        $content_encryption_algorithm = $this->findContentEncryptionAlgorithm($complete_headers);
80
81
        if (null === $jwe->getCiphertext()) {
82
            // the content is not yet encrypted (no recipient)
83
84
            if (!empty($jwe->getSharedProtectedHeaders())) {
85
                $jwe = $jwe->withEncodedSharedProtectedHeaders(Base64Url::encode(json_encode($jwe->getSharedProtectedHeaders())));
86
            }
87
88
            // CEK
89
            $content_encryption_key = $this->getCEK(
90
                $complete_headers,
91
                $key_encryption_algorithm,
92
                $content_encryption_algorithm,
93
                $recipient_key,
94
                $sender_key
95
            );
96
            $jwe = $jwe->withContentEncryptionKey($content_encryption_key);
97
98
            // IV
99
            if (null !== $iv_size = $content_encryption_algorithm->getIVSize()) {
100
                $iv = $this->createIV($iv_size);
101
                $jwe = $jwe->withIV($iv);
102
            }
103
104
            // We encrypt the payload and get the tag
105
            $tag = null;
106
            $payload = $this->preparePayload($jwe->getPayload(), $complete_headers);
107
108
            $ciphertext = $content_encryption_algorithm->encryptContent(
109
                $payload,
110
                $content_encryption_key,
111
                $jwe->getIV(),
112
                $jwe->getAAD(),
113
                $jwe->getEncodedSharedProtectedHeaders(),
114
                $tag
115
            );
116
            $jwe = $jwe->withCiphertext($ciphertext);
117
118
            // Tag
119
            if (null !== $tag) {
120
                $jwe = $jwe->withTag($tag);
121
            }
122
123
            $recipient = $this->computeRecipient(
124
                $jwe,
125
                $key_encryption_algorithm,
126
                $content_encryption_algorithm,
127
                $complete_headers,
128
                $recipient_headers,
129
                $recipient_key,
130
                $sender_key
131
            );
132
133
            $jwe = $jwe->addRecipient($recipient);
134
        } else {
135
            if (0 === $jwe->countRecipients()) {
136
                throw new \InvalidArgumentException('Invalid JWE. The payload is encrypted but no recipient is available.');
137
            }
138
            if (null === $jwe->getContentEncryptionKey()) {
139
                throw new \InvalidArgumentException('Unable to add a recipient. The JWE must be decrypted first.');
140
            }
141
            $current_key_management_mode = $this->getCurrentKeyManagementMode($jwe);
142
143
            if (false === $this->areKeyManagementModesCompatible($current_key_management_mode, $key_encryption_algorithm->getKeyManagementMode())) {
144
                throw new \InvalidArgumentException('Foreign key management mode forbidden.');
145
            }
146
147
            $recipient = $this->computeRecipient(
148
                $jwe,
149
                $key_encryption_algorithm,
150
                $content_encryption_algorithm,
151
                $complete_headers,
152
                $recipient_headers,
153
                $recipient_key,
154
                $sender_key
155
            );
156
157
            $jwe = $jwe->addRecipient($recipient);
158
        }
159
160
        return $jwe;
161
    }
162
163
    /**
164
     * @param \Jose\Object\JWEInterface $jwe
165
     *
166
     * @return string
167
     */
168
    private function getCurrentKeyManagementMode(JWEInterface $jwe)
169
    {
170
        $complete_headers = array_merge(
171
            $jwe->getSharedProtectedHeaders(),
172
            $jwe->getSharedHeaders(),
173
            $jwe->getRecipient(0)->getHeaders()
174
        );
175
        $key_encryption_algorithm = $this->findKeyEncryptionAlgorithm($complete_headers);
176
177
        return $key_encryption_algorithm->getKeyManagementMode();
178
    }
179
180
    /**
181
     * @param \Jose\Object\JWEInterface                           $jwe
182
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
183
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
184
     * @param array                                               $complete_headers
185
     * @param array                                               $recipient_headers
186
     * @param \Jose\Object\JWKInterface                           $recipient_key
187
     * @param \Jose\Object\JWKInterface|null                      $sender_key
188
     *
189
     * @return \Jose\Object\RecipientInterface
190
     */
191
    private function computeRecipient(JWEInterface $jwe, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array $complete_headers, array $recipient_headers, JWKInterface $recipient_key, JWKInterface $sender_key = null)
192
    {
193
        $recipient = new Recipient();
194
        $recipient = $recipient->withHeaders($recipient_headers);
195
196
        $additional_headers = [];
197
        $encrypted_content_encryption_key = $this->getEncryptedKey(
198
            $complete_headers,
199
            $jwe->getContentEncryptionKey(),
200
            $key_encryption_algorithm,
201
            $content_encryption_algorithm,
202
            $additional_headers,
203
            $recipient_key,
204
            $sender_key
205
        );
206
        if (!empty($additional_headers)) {
207
            foreach ($additional_headers as $key=>$value) {
208
                $recipient = $recipient->withHeader($key, $value);
209
            }
210
        }
211
        if (null !== $encrypted_content_encryption_key) {
212
            $recipient = $recipient->withEncryptedKey($encrypted_content_encryption_key);
213
        }
214
215
        return $recipient;
216
    }
217
218
    /**
219
     * @param $current
220
     * @param $new
221
     *
222
     * @return bool
223
     */
224
    private function areKeyManagementModesCompatible($current, $new)
225
    {
226
        $agree = KeyEncryptionAlgorithmInterface::MODE_AGREEMENT;
227
        $dir = KeyEncryptionAlgorithmInterface::MODE_DIRECT;
228
        $enc = KeyEncryptionAlgorithmInterface::MODE_ENCRYPT;
229
        $wrap = KeyEncryptionAlgorithmInterface::MODE_WRAP;
230
        switch ($current.$new) {
231
            case $agree.$enc:
232
            case $agree.$wrap:
233
            case $dir.$enc:
234
            case $dir.$wrap:
235
            case $enc.$enc:
236
            case $enc.$wrap:
237
            case $wrap.$enc:
238
            case $wrap.$wrap:
239
                return true;
240
            case $agree.$agree:
241
            case $agree.$dir:
242
            case $dir.$agree:
243
            case $dir.$dir:
244
            case $enc.$agree:
245
            case $enc.$dir:
246
            case $wrap.$agree:
247
            case $wrap.$dir:
248
            default:
249
                return false;
250
        }
251
    }
252
253
    /**
254
     * @param string $payload
255
     * @param array  $complete_headers
256
     *
257
     * @return string
258
     */
259
    private function preparePayload($payload, array $complete_headers)
260
    {
261
        $prepared = is_string($payload)?$payload:json_encode($payload);
262
263
        if(null === $prepared) {
264
            throw new \InvalidArgumentException('The payload is empty or cannot encoded into JSON.');
265
        }
266
        if (!array_key_exists('zip', $complete_headers)) {
267
            return $prepared;
268
        }
269
270
        $compression_method = $this->getCompressionManager()->getCompressionAlgorithm($complete_headers['zip']);
271
        if (null === $compression_method) {
272
            throw new \RuntimeException(sprintf('Compression method "%s" not supported', $complete_headers['zip']));
273
        }
274
        $compressed_payload = $compression_method->compress($prepared);
275
        if (!is_string($compressed_payload)) {
276
            throw new \RuntimeException('Compression failed.');
277
        }
278
279
        return $compressed_payload;
280
    }
281
282
    /**
283
     * @param array                                               $complete_headers
284
     * @param string                                              $cek
285
     * @param \Jose\Algorithm\KeyEncryptionAlgorithmInterface     $key_encryption_algorithm
286
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
287
     * @param \Jose\Object\JWKInterface                           $recipient_key
288
     * @param \Jose\Object\JWKInterface|null                      $sender_key
289
     * @param array                                               $additional_headers
290
     *
291
     * @return string
292
     */
293
    private function getEncryptedKey(array $complete_headers, $cek, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, array &$additional_headers, JWKInterface $recipient_key, JWKInterface $sender_key = null)
294
    {
295
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
296
            return $key_encryption_algorithm->encryptKey(
297
                    $recipient_key,
298
                    $cek,
299
                    $complete_headers
300
                );
301
        } elseif ($key_encryption_algorithm instanceof KeyWrappingInterface) {
302
            return $key_encryption_algorithm->wrapKey(
303
                $recipient_key,
304
                $cek,
305
                $complete_headers
306
            );
307
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
308
            if (!$sender_key instanceof JWKInterface) {
309
                throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
310
            }
311
            $jwt_cek = $key_encryption_algorithm->wrapAgreementKey($sender_key, $recipient_key, $cek, $content_encryption_algorithm->getCEKSize(), $complete_headers, $additional_headers);
312
313
            return $jwt_cek;
314
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
315
            $jwt_cek = $key_encryption_algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $sender_key, $recipient_key, $complete_headers, $additional_headers);
0 ignored issues
show
Bug introduced by
It seems like $sender_key defined by parameter $sender_key on line 293 can be null; however, Jose\Algorithm\KeyEncryp...face::getAgreementKey() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
316
317
            return $jwt_cek;
318
        }
319
    }
320
321
    private function getCEK(array $complete_headers, KeyEncryptionAlgorithmInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key, JWKInterface $sender_key = null)
322
    {
323
        if ($key_encryption_algorithm instanceof KeyEncryptionInterface) {
324
            return $this->createCEK($content_encryption_algorithm->getCEKSize());
325
        } elseif ($key_encryption_algorithm instanceof KeyAgreementWrappingInterface) {
326
            return $this->createCEK($content_encryption_algorithm->getCEKSize());
327
        } elseif ($key_encryption_algorithm instanceof KeyAgreementInterface) {
328
            return $this->calculateAgreementKey($complete_headers, $key_encryption_algorithm, $content_encryption_algorithm, $recipient_key, $sender_key);
329
        } elseif ($key_encryption_algorithm instanceof DirectEncryptionInterface) {
330
            return $key_encryption_algorithm->getCEK($recipient_key);
331
        } else {
332
            throw new \RuntimeException('Unable to get key management mode.');
333
        }
334
    }
335
336
    /**
337
     * @param array $complete_headers
338
     *
339
     * @return \Jose\Algorithm\KeyEncryptionAlgorithmInterface
340
     */
341
    private function findKeyEncryptionAlgorithm(array $complete_headers)
342
    {
343
        if (!array_key_exists('alg', $complete_headers)) {
344
            throw new \InvalidArgumentException('Parameter "alg" is missing.');
345
        }
346
        $key_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['alg']);
347
        if ($key_encryption_algorithm instanceof KeyEncryptionAlgorithmInterface) {
348
            return $key_encryption_algorithm;
349
        }
350
        throw new \InvalidArgumentException(sprintf('The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', $complete_headers['alg']));
351
    }
352
353
    /**
354
     * @param array $complete_headers
355
     *
356
     * @return \Jose\Algorithm\ContentEncryptionAlgorithmInterface
357
     */
358
    private function findContentEncryptionAlgorithm(array $complete_headers)
359
    {
360
        if (!array_key_exists('enc', $complete_headers)) {
361
            throw new \InvalidArgumentException('Parameter "enc" is missing.');
362
        }
363
364
        $content_encryption_algorithm = $this->getJWAManager()->getAlgorithm($complete_headers['enc']);
365
        if (!$content_encryption_algorithm instanceof ContentEncryptionAlgorithmInterface) {
366
            throw new \RuntimeException(sprintf('The algorithm "%s" is not enabled or does not implement ContentEncryptionInterface.', $complete_headers['enc']));
367
        }
368
369
        return $content_encryption_algorithm;
370
    }
371
372
    /**
373
     * @param array                                               $complete_headers
374
     * @param \Jose\Algorithm\KeyEncryption\KeyAgreementInterface $key_encryption_algorithm
375
     * @param \Jose\Algorithm\ContentEncryptionAlgorithmInterface $content_encryption_algorithm
376
     * @param \Jose\Object\JWKInterface                           $recipient_key
377
     * @param \Jose\Object\JWKInterface|null                      $sender_key
378
     *
379
     * @return string
380
     */
381
    private function calculateAgreementKey(array $complete_headers, KeyAgreementInterface $key_encryption_algorithm, ContentEncryptionAlgorithmInterface $content_encryption_algorithm, JWKInterface $recipient_key, JWKInterface $sender_key = null)
382
    {
383
        if (!$sender_key instanceof JWKInterface) {
384
            throw new \RuntimeException('The sender key must be set using Key Agreement or Key Agreement with Wrapping algorithms.');
385
        }
386
        $additional_header_values = [];
387
        $cek = $key_encryption_algorithm->getAgreementKey($content_encryption_algorithm->getCEKSize(), $sender_key, $recipient_key, $complete_headers, $additional_header_values);
388
389
        return $cek;
390
    }
391
392
    /**
393
     * @param int $size
394
     *
395
     * @return string
396
     */
397
    private function createCEK($size)
398
    {
399
        return $this->generateRandomString($size / 8);
400
    }
401
402
    /**
403
     * @param int $size
404
     *
405
     * @return string
406
     */
407
    private function createIV($size)
408
    {
409
        return $this->generateRandomString($size / 8);
410
    }
411
412
    /**
413
     * @param int $length
414
     *
415
     * @return string
416
     */
417
    private function generateRandomString($length)
418
    {
419
        if (function_exists('random_bytes')) {
420
            return random_bytes($length);
421
        } else {
422
            return openssl_random_pseudo_bytes($length);
423
        }
424
    }
425
}
426