Failed Conditions
Push — master ( 924f8c...c830be )
by Florent
01:51
created

ECDHES::createOKPKey()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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