Failed Conditions
Push — master ( d4c253...68b1e6 )
by Florent
03:55
created

ECDHES::convertDecToBin()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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