Completed
Push — master ( 672c87...f5002a )
by Florent
04:13 queued 02:04
created

ECDHES::getAgreementKey()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 8
nop 5
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 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 Jose\Component\Core\JWK;
18
use Jose\Component\Core\Util\Ecc\Curve;
19
use Jose\Component\Core\Util\Ecc\NistCurve;
20
use Jose\Component\Core\Util\Ecc\PrivateKey;
21
use Jose\Component\Encryption\Util\ConcatKDF;
22
use Jose\Component\Encryption\Util\Ecc\EcDH;
23
24
final class ECDHES implements KeyAgreement
25
{
26
    /**
27
     * {@inheritdoc}
28
     */
29
    public function allowedKeyTypes(): array
30
    {
31
        return ['EC', 'OKP'];
32
    }
33
34
    /**
35
     * {@inheritdoc}
36
     */
37
    public function getAgreementKey(int $encryption_key_length, string $algorithm, JWK $recipient_key, array $complete_header = [], array &$additional_header_values = []): string
38
    {
39
        if ($recipient_key->has('d')) {
40
            list($public_key, $private_key) = $this->getKeysFromPrivateKeyAndHeader($recipient_key, $complete_header);
41
        } else {
42
            list($public_key, $private_key) = $this->getKeysFromPublicKey($recipient_key, $additional_header_values);
43
        }
44
45
        $agreed_key = $this->calculateAgreementKey($private_key, $public_key);
46
47
        $apu = array_key_exists('apu', $complete_header) ? $complete_header['apu'] : '';
48
        $apv = array_key_exists('apv', $complete_header) ? $complete_header['apv'] : '';
49
50
        return ConcatKDF::generate($agreed_key, $algorithm, $encryption_key_length, $apu, $apv);
51
    }
52
53
    /**
54
     * @param JWK   $recipient_key
55
     * @param array $additional_header_values
56
     *
57
     * @return JWK[]
58
     */
59
    private function getKeysFromPublicKey(JWK $recipient_key, array &$additional_header_values): array
60
    {
61
        $this->checkKey($recipient_key, false);
62
        $public_key = $recipient_key;
63
        switch ($public_key->get('crv')) {
64
            case 'P-256':
65
            case 'P-384':
66
            case 'P-521':
67
                $private_key = $this->createECKey($public_key->get('crv'));
68
69
                break;
70
            case 'X25519':
71
                $private_key = $this->createOKPKey('X25519');
72
73
                break;
74
            default:
75
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv')));
76
        }
77
        $epk = $private_key->toPublic()->all();
78
        $additional_header_values['epk'] = $epk;
79
80
        return [$public_key, $private_key];
81
    }
82
83
    /**
84
     * @param JWK   $recipient_key
85
     * @param array $complete_header
86
     *
87
     * @return JWK[]
88
     */
89
    private function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array
90
    {
91
        $this->checkKey($recipient_key, true);
92
        $private_key = $recipient_key;
93
        $public_key = $this->getPublicKey($complete_header);
94
        if ($private_key->get('crv') !== $public_key->get('crv')) {
95
            throw new \InvalidArgumentException('Curves are different');
96
        }
97
98
        return [$public_key, $private_key];
99
    }
100
101
    /**
102
     * @param JWK $private_key
103
     * @param JWK $public_key
104
     *
105
     * @throws \InvalidArgumentException
106
     *
107
     * @return string
108
     */
109
    public function calculateAgreementKey(JWK $private_key, JWK $public_key): string
110
    {
111
        switch ($public_key->get('crv')) {
112
            case 'P-256':
113
            case 'P-384':
114
            case 'P-521':
115
                $curve = $this->getCurve($public_key->get('crv'));
116
117
                $rec_x = $this->convertBase64ToGmp($public_key->get('x'));
118
                $rec_y = $this->convertBase64ToGmp($public_key->get('y'));
119
                $sen_d = $this->convertBase64ToGmp($private_key->get('d'));
120
121
                $priv_key = PrivateKey::create($sen_d);
122
                $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
123
124
                return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key));
125
            case 'X25519':
126
                $sKey = Base64Url::decode($private_key->get('d'));
127
                $recipientPublickey = Base64Url::decode($public_key->get('x'));
128
129
                return sodium_crypto_scalarmult($sKey, $recipientPublickey);
130
            default:
131
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv')));
132
        }
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function name(): string
139
    {
140
        return 'ECDH-ES';
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function getKeyManagementMode(): string
147
    {
148
        return self::MODE_AGREEMENT;
149
    }
150
151
    /**
152
     * @param array $complete_header
153
     *
154
     * @return JWK
155
     */
156
    private function getPublicKey(array $complete_header)
157
    {
158
        if (!array_key_exists('epk', $complete_header)) {
159
            throw new \InvalidArgumentException('The header parameter "epk" is missing');
160
        }
161
        if (!is_array($complete_header['epk'])) {
162
            throw new \InvalidArgumentException('The header parameter "epk" is not an array of parameter');
163
        }
164
165
        $public_key = JWK::create($complete_header['epk']);
166
        $this->checkKey($public_key, false);
167
168
        return $public_key;
169
    }
170
171
    /**
172
     * @param JWK  $key
173
     * @param bool $is_private
174
     */
175
    private function checkKey(JWK $key, $is_private)
176
    {
177
        if (!in_array($key->get('kty'), $this->allowedKeyTypes())) {
178
            throw new \InvalidArgumentException('Wrong key type.');
179
        }
180
        foreach (['x', 'crv'] as $k) {
181
            if (!$key->has($k)) {
182
                throw new \InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
183
            }
184
        }
185
186
        switch ($key->get('crv')) {
187
            case 'P-256':
188
            case 'P-384':
189
            case 'P-521':
190
                if (!$key->has('y')) {
191
                    throw new \InvalidArgumentException('The key parameter "y" is missing.');
192
                }
193
194
                break;
195
            case 'X25519':
196
                break;
197
            default:
198
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported', $key->get('crv')));
199
        }
200
        if (true === $is_private) {
201
            if (!$key->has('d')) {
202
                throw new \InvalidArgumentException('The key parameter "d" is missing.');
203
            }
204
        }
205
    }
206
207
    /**
208
     * @param string $crv
209
     *
210
     * @throws \InvalidArgumentException
211
     *
212
     * @return Curve
213
     */
214
    private function getCurve(string $crv): Curve
215
    {
216
        switch ($crv) {
217
            case 'P-256':
218
                return NistCurve::curve256();
219
            case 'P-384':
220
                return NistCurve::curve384();
221
            case 'P-521':
222
                return NistCurve::curve521();
223
            default:
224
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
225
        }
226
    }
227
228
    /**
229
     * @param string $value
230
     *
231
     * @return \GMP
232
     */
233
    private function convertBase64ToGmp(string $value): \GMP
234
    {
235
        $value = unpack('H*', Base64Url::decode($value));
236
237
        return gmp_init($value[1], 16);
238
    }
239
240
    /**
241
     * @param \GMP $dec
242
     *
243
     * @return string
244
     */
245
    private function convertDecToBin(\GMP $dec): string
246
    {
247
        if (gmp_cmp($dec, 0) < 0) {
248
            throw new \InvalidArgumentException('Unable to convert negative integer to string');
249
        }
250
251
        $hex = gmp_strval($dec, 16);
252
253
        if (0 !== mb_strlen($hex, '8bit') % 2) {
254
            $hex = '0'.$hex;
255
        }
256
257
        return hex2bin($hex);
258
    }
259
260
    /**
261
     * @param string $crv The curve
262
     *
263
     * @return JWK
264
     */
265
    public function createECKey(string $crv): JWK
266
    {
267
        try {
268
            $jwk = self::createECKeyUsingOpenSSL($crv);
269
        } catch (\Exception $e) {
270
            $jwk = self::createECKeyUsingPurePhp($crv);
271
        }
272
273
        return JWK::create($jwk);
274
    }
275
276
    /**
277
     * @param string $curve The curve
278
     *
279
     * @return JWK
280
     */
281
    public static function createOKPKey(string $curve): JWK
282
    {
283
        switch ($curve) {
284
            case 'X25519':
285
                $keyPair = sodium_crypto_box_keypair();
286
                $d = sodium_crypto_box_secretkey($keyPair);
287
                $x = sodium_crypto_box_publickey($keyPair);
288
289
                break;
290
            case 'Ed25519':
291
                $keyPair = sodium_crypto_sign_keypair();
292
                $d = sodium_crypto_sign_secretkey($keyPair);
293
                $x = sodium_crypto_sign_publickey($keyPair);
294
295
                break;
296
            default:
297
                throw new \InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
298
        }
299
300
        return JWK::create([
301
            'kty' => 'OKP',
302
            'crv' => $curve,
303
            'x'   => Base64Url::encode($x),
304
            'd'   => Base64Url::encode($d),
305
        ]);
306
    }
307
308
    /**
309
     * @param string $curve
310
     *
311
     * @return array
312
     */
313
    private static function createECKeyUsingPurePhp(string $curve): array
314
    {
315
        switch ($curve) {
316
            case 'P-256':
317
                $nistCurve = NistCurve::curve256();
318
319
                break;
320
            case 'P-384':
321
                $nistCurve = NistCurve::curve384();
322
323
                break;
324
            case 'P-521':
325
                $nistCurve = NistCurve::curve521();
326
327
                break;
328
            default:
329
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
330
        }
331
332
        $privateKey = $nistCurve->createPrivateKey();
333
        $publicKey = $nistCurve->createPublicKey($privateKey);
334
335
        return [
336
            'kty' => 'EC',
337
            'crv' => $curve,
338
            'd'   => Base64Url::encode(gmp_export($privateKey->getSecret())),
339
            'x'   => Base64Url::encode(gmp_export($publicKey->getPoint()->getX())),
340
            'y'   => Base64Url::encode(gmp_export($publicKey->getPoint()->getY())),
341
        ];
342
    }
343
344
    /**
345
     * @param string $curve
346
     *
347
     * @return array
348
     */
349
    private static function createECKeyUsingOpenSSL(string $curve): array
350
    {
351
        $key = openssl_pkey_new([
352
            'curve_name'       => self::getOpensslCurveName($curve),
353
            'private_key_type' => OPENSSL_KEYTYPE_EC,
354
        ]);
355
        $res = openssl_pkey_export($key, $out);
356
        if (false === $res) {
357
            throw new \RuntimeException('Unable to create the key');
358
        }
359
        $res = openssl_pkey_get_private($out);
360
361
        $details = openssl_pkey_get_details($res);
362
363
        return [
364
            'kty' => 'EC',
365
            'crv' => $curve,
366
            'd'   => Base64Url::encode($details['ec']['d']),
367
            'x'   => Base64Url::encode($details['ec']['x']),
368
            'y'   => Base64Url::encode($details['ec']['y']),
369
        ];
370
    }
371
372
    /**
373
     * @param string $curve
374
     *
375
     * @return string
376
     */
377
    private static function getOpensslCurveName(string $curve): string
378
    {
379
        switch ($curve) {
380
            case 'P-256':
381
                return 'prime256v1';
382
            case 'P-384':
383
                return 'secp384r1';
384
            case 'P-521':
385
                return 'secp521r1';
386
            default:
387
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
388
        }
389
    }
390
}
391