ECDHES::checkKey()   B
last analyzed

Complexity

Conditions 11
Paths 26

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 7.3166
c 0
b 0
f 0
cc 11
nc 26
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 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\Algorithm\KeyEncryption;
15
16
use Base64Url\Base64Url;
17
use GMP;
18
use InvalidArgumentException;
19
use Jose\Component\Core\JWK;
20
use Jose\Component\Core\Util\Ecc\Curve;
21
use Jose\Component\Core\Util\Ecc\EcDH;
22
use Jose\Component\Core\Util\Ecc\NistCurve;
23
use Jose\Component\Core\Util\Ecc\PrivateKey;
24
use Jose\Component\Core\Util\ECKey;
25
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\ConcatKDF;
26
use RuntimeException;
27
28
final class ECDHES implements KeyAgreement
29
{
30
    public function allowedKeyTypes(): array
31
    {
32
        return ['EC', 'OKP'];
33
    }
34
35
    public function getAgreementKey(int $encryptionKeyLength, string $algorithm, JWK $recipientKey, ?JWK $senderKey, array $complete_header = [], array &$additional_header_values = []): string
36
    {
37
        if ($recipientKey->has('d')) {
38
            list($public_key, $private_key) = $this->getKeysFromPrivateKeyAndHeader($recipientKey, $complete_header);
39
        } else {
40
            list($public_key, $private_key) = $this->getKeysFromPublicKey($recipientKey, $additional_header_values);
41
        }
42
43
        $agreed_key = $this->calculateAgreementKey($private_key, $public_key);
44
45
        $apu = \array_key_exists('apu', $complete_header) ? $complete_header['apu'] : '';
46
        $apv = \array_key_exists('apv', $complete_header) ? $complete_header['apv'] : '';
47
48
        return ConcatKDF::generate($agreed_key, $algorithm, $encryptionKeyLength, $apu, $apv);
49
    }
50
51
    /**
52
     * @throws InvalidArgumentException if the curve is not supported
53
     */
54
    public function calculateAgreementKey(JWK $private_key, JWK $public_key): string
55
    {
56
        switch ($public_key->get('crv')) {
57
            case 'P-256':
58
            case 'P-384':
59
            case 'P-521':
60
                $curve = $this->getCurve($public_key->get('crv'));
61
                if (\function_exists('openssl_pkey_derive')) {
62
                    try {
63
                        $publicPem = ECKey::convertPublicKeyToPEM($public_key);
64
                        $privatePem = ECKey::convertPrivateKeyToPEM($private_key);
65
66
                        return openssl_pkey_derive($publicPem, $privatePem, $curve->getSize());
67
                    } catch (\Throwable $throwable) {
68
                        //Does nothing. Will fallback to the pure PHP function
69
                    }
70
                }
71
72
                $rec_x = $this->convertBase64ToGmp($public_key->get('x'));
73
                $rec_y = $this->convertBase64ToGmp($public_key->get('y'));
74
                $sen_d = $this->convertBase64ToGmp($private_key->get('d'));
75
76
                $priv_key = PrivateKey::create($sen_d);
77
                $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
78
79
                return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key));
80
            case 'X25519':
81
                $sKey = Base64Url::decode($private_key->get('d'));
82
                $recipientPublickey = Base64Url::decode($public_key->get('x'));
83
84
                return sodium_crypto_scalarmult($sKey, $recipientPublickey);
85
            default:
86
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv')));
87
        }
88
    }
89
90
    public function name(): string
91
    {
92
        return 'ECDH-ES';
93
    }
94
95
    public function getKeyManagementMode(): string
96
    {
97
        return self::MODE_AGREEMENT;
98
    }
99
100
    /**
101
     * @throws InvalidArgumentException if the curve is not supported
102
     *
103
     * @return JWK[]
104
     */
105
    private function getKeysFromPublicKey(JWK $recipient_key, array &$additional_header_values): array
106
    {
107
        $this->checkKey($recipient_key, false);
108
        $public_key = $recipient_key;
109
        switch ($public_key->get('crv')) {
110
            case 'P-256':
111
            case 'P-384':
112
            case 'P-521':
113
                $private_key = ECKey::createECKey($public_key->get('crv'));
114
115
                break;
116
            case 'X25519':
117
                $this->checkSodiumExtensionIsAvailable();
118
                $private_key = $this->createOKPKey('X25519');
119
120
                break;
121
            default:
122
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv')));
123
        }
124
        $epk = $private_key->toPublic()->all();
125
        $additional_header_values['epk'] = $epk;
126
127
        return [$public_key, $private_key];
128
    }
129
130
    /**
131
     * @throws InvalidArgumentException if the curves are different
132
     *
133
     * @return JWK[]
134
     */
135
    private function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array
136
    {
137
        $this->checkKey($recipient_key, true);
138
        $private_key = $recipient_key;
139
        $public_key = $this->getPublicKey($complete_header);
140
        if ($private_key->get('crv') !== $public_key->get('crv')) {
141
            throw new InvalidArgumentException('Curves are different');
142
        }
143
144
        return [$public_key, $private_key];
145
    }
146
147
    /**
148
     * @throws InvalidArgumentException if the ephemeral public key is missing or invalid
149
     */
150
    private function getPublicKey(array $complete_header): JWK
151
    {
152
        if (!isset($complete_header['epk'])) {
153
            throw new InvalidArgumentException('The header parameter "epk" is missing.');
154
        }
155
        if (!\is_array($complete_header['epk'])) {
156
            throw new InvalidArgumentException('The header parameter "epk" is not an array of parameters');
157
        }
158
        $public_key = new JWK($complete_header['epk']);
159
        $this->checkKey($public_key, false);
160
161
        return $public_key;
162
    }
163
164
    /**
165
     * @throws InvalidArgumentException if the key is invalid
166
     */
167
    private function checkKey(JWK $key, bool $is_private): void
168
    {
169
        if (!\in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
170
            throw new InvalidArgumentException('Wrong key type.');
171
        }
172
        foreach (['x', 'crv'] as $k) {
173
            if (!$key->has($k)) {
174
                throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
175
            }
176
        }
177
178
        switch ($key->get('crv')) {
179
            case 'P-256':
180
            case 'P-384':
181
            case 'P-521':
182
                if (!$key->has('y')) {
183
                    throw new InvalidArgumentException('The key parameter "y" is missing.');
184
                }
185
186
                break;
187
            case 'X25519':
188
                break;
189
            default:
190
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $key->get('crv')));
191
        }
192
        if (true === $is_private && !$key->has('d')) {
193
            throw new InvalidArgumentException('The key parameter "d" is missing.');
194
        }
195
    }
196
197
    /**
198
     * @throws InvalidArgumentException if the curve is not supported
199
     */
200
    private function getCurve(string $crv): Curve
201
    {
202
        switch ($crv) {
203
            case 'P-256':
204
                return NistCurve::curve256();
205
            case 'P-384':
206
                return NistCurve::curve384();
207
            case 'P-521':
208
                return NistCurve::curve521();
209
            default:
210
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
211
        }
212
    }
213
214
    private function convertBase64ToGmp(string $value): GMP
215
    {
216
        $value = unpack('H*', Base64Url::decode($value));
217
218
        return gmp_init($value[1], 16);
219
    }
220
221
    /**
222
     * @throws InvalidArgumentException if the data cannot be converted
223
     */
224
    private function convertDecToBin(GMP $dec): string
225
    {
226
        if (gmp_cmp($dec, 0) < 0) {
227
            throw new InvalidArgumentException('Unable to convert negative integer to string');
228
        }
229
        $hex = gmp_strval($dec, 16);
230
231
        if (0 !== mb_strlen($hex, '8bit') % 2) {
232
            $hex = '0'.$hex;
233
        }
234
235
        return hex2bin($hex);
236
    }
237
238
    /**
239
     * @param string $curve The curve
240
     *
241
     * @throws InvalidArgumentException if the curve is not supported
242
     */
243
    private function createOKPKey(string $curve): JWK
244
    {
245
        $this->checkSodiumExtensionIsAvailable();
246
        switch ($curve) {
247
            case 'X25519':
248
                $keyPair = sodium_crypto_box_keypair();
249
                $d = sodium_crypto_box_secretkey($keyPair);
250
                $x = sodium_crypto_box_publickey($keyPair);
251
252
                break;
253
            case 'Ed25519':
254
                $keyPair = sodium_crypto_sign_keypair();
255
                $d = sodium_crypto_sign_secretkey($keyPair);
256
                $x = sodium_crypto_sign_publickey($keyPair);
257
258
                break;
259
            default:
260
                throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
261
        }
262
263
        return new JWK([
264
            'kty' => 'OKP',
265
            'crv' => $curve,
266
            'x' => Base64Url::encode($x),
267
            'd' => Base64Url::encode($d),
268
        ]);
269
    }
270
271
    /**
272
     * @throws RuntimeException if the extension "sodium" is not available
273
     */
274
    private function checkSodiumExtensionIsAvailable(): void
275
    {
276
        if (!\extension_loaded('sodium')) {
277
            throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
278
        }
279
    }
280
}
281