Completed
Push — master ( 7b61ef...71e756 )
by Florent
08:30 queued 07:08
created

ECDHES::convertBase64ToGmp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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
    public function calculateAgreementKey(JWK $private_key, JWK $public_key): string
52
    {
53
        switch ($public_key->get('crv')) {
54
            case 'P-256':
55
            case 'P-384':
56
            case 'P-521':
57
                $curve = $this->getCurve($public_key->get('crv'));
58
                if (\function_exists('openssl_pkey_derive')) {
59
                    try {
60
                        $publicPem = ECKey::convertPublicKeyToPEM($public_key);
61
                        $privatePem = ECKey::convertPrivateKeyToPEM($private_key);
62
63
                        return openssl_pkey_derive($publicPem, $privatePem, $curve->getSize());
64
                    } catch (\Throwable $throwable) {
65
                        //Does nothing. Will fallback to the pure PHP function
66
                    }
67
                }
68
69
                $rec_x = $this->convertBase64ToGmp($public_key->get('x'));
70
                $rec_y = $this->convertBase64ToGmp($public_key->get('y'));
71
                $sen_d = $this->convertBase64ToGmp($private_key->get('d'));
72
73
                $priv_key = PrivateKey::create($sen_d);
74
                $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
75
76
                return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key));
77
            case 'X25519':
78
                $sKey = Base64Url::decode($private_key->get('d'));
79
                $recipientPublickey = Base64Url::decode($public_key->get('x'));
80
81
                return sodium_crypto_scalarmult($sKey, $recipientPublickey);
82
            default:
83
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv')));
84
        }
85
    }
86
87
    public function name(): string
88
    {
89
        return 'ECDH-ES';
90
    }
91
92
    public function getKeyManagementMode(): string
93
    {
94
        return self::MODE_AGREEMENT;
95
    }
96
97
    /**
98
     * @return JWK[]
99
     */
100
    private function getKeysFromPublicKey(JWK $recipient_key, array &$additional_header_values): array
101
    {
102
        $this->checkKey($recipient_key, false);
103
        $public_key = $recipient_key;
104
        switch ($public_key->get('crv')) {
105
            case 'P-256':
106
            case 'P-384':
107
            case 'P-521':
108
                $private_key = ECKey::createECKey($public_key->get('crv'));
109
110
                break;
111
            case 'X25519':
112
                $this->checkSodiumExtensionIsAvailable();
113
                $private_key = $this->createOKPKey('X25519');
114
115
                break;
116
            default:
117
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv')));
118
        }
119
        $epk = $private_key->toPublic()->all();
120
        $additional_header_values['epk'] = $epk;
121
122
        return [$public_key, $private_key];
123
    }
124
125
    /**
126
     * @return JWK[]
127
     */
128
    private function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array
129
    {
130
        $this->checkKey($recipient_key, true);
131
        $private_key = $recipient_key;
132
        $public_key = $this->getPublicKey($complete_header);
133
        if ($private_key->get('crv') !== $public_key->get('crv')) {
134
            throw new InvalidArgumentException('Curves are different');
135
        }
136
137
        return [$public_key, $private_key];
138
    }
139
140
    private function getPublicKey(array $complete_header): JWK
141
    {
142
        if (!isset($complete_header['epk'])) {
143
            throw new InvalidArgumentException('The header parameter "epk" is missing.');
144
        }
145
        if (!\is_array($complete_header['epk'])) {
146
            throw new InvalidArgumentException('The header parameter "epk" is not an array of parameters');
147
        }
148
        $public_key = new JWK($complete_header['epk']);
149
        $this->checkKey($public_key, false);
150
151
        return $public_key;
152
    }
153
154
    private function checkKey(JWK $key, bool $is_private): void
155
    {
156
        if (!\in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
157
            throw new InvalidArgumentException('Wrong key type.');
158
        }
159
        foreach (['x', 'crv'] as $k) {
160
            if (!$key->has($k)) {
161
                throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
162
            }
163
        }
164
165
        switch ($key->get('crv')) {
166
            case 'P-256':
167
            case 'P-384':
168
            case 'P-521':
169
                if (!$key->has('y')) {
170
                    throw new InvalidArgumentException('The key parameter "y" is missing.');
171
                }
172
173
                break;
174
            case 'X25519':
175
                break;
176
            default:
177
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $key->get('crv')));
178
        }
179
        if (true === $is_private && !$key->has('d')) {
180
            throw new InvalidArgumentException('The key parameter "d" is missing.');
181
        }
182
    }
183
184
    private function getCurve(string $crv): Curve
185
    {
186
        switch ($crv) {
187
            case 'P-256':
188
                return NistCurve::curve256();
189
            case 'P-384':
190
                return NistCurve::curve384();
191
            case 'P-521':
192
                return NistCurve::curve521();
193
            default:
194
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
195
        }
196
    }
197
198
    private function convertBase64ToGmp(string $value): GMP
199
    {
200
        $value = unpack('H*', Base64Url::decode($value));
201
202
        return gmp_init($value[1], 16);
203
    }
204
205
    private function convertDecToBin(GMP $dec): string
206
    {
207
        if (gmp_cmp($dec, 0) < 0) {
208
            throw new \InvalidArgumentException('Unable to convert negative integer to string');
209
        }
210
        $hex = gmp_strval($dec, 16);
211
212
        if (0 !== mb_strlen($hex, '8bit') % 2) {
213
            $hex = '0'.$hex;
214
        }
215
216
        return hex2bin($hex);
217
    }
218
219
    /**
220
     * @param string $curve The curve
221
     */
222
    private function createOKPKey(string $curve): JWK
223
    {
224
        $this->checkSodiumExtensionIsAvailable();
225
        switch ($curve) {
226
            case 'X25519':
227
                $keyPair = sodium_crypto_box_keypair();
228
                $d = sodium_crypto_box_secretkey($keyPair);
229
                $x = sodium_crypto_box_publickey($keyPair);
230
231
                break;
232
            case 'Ed25519':
233
                $keyPair = sodium_crypto_sign_keypair();
234
                $d = sodium_crypto_sign_secretkey($keyPair);
235
                $x = sodium_crypto_sign_publickey($keyPair);
236
237
                break;
238
            default:
239
                throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
240
        }
241
242
        return new JWK([
243
            'kty' => 'OKP',
244
            'crv' => $curve,
245
            'x' => Base64Url::encode($x),
246
            'd' => Base64Url::encode($d),
247
        ]);
248
    }
249
250
    private function checkSodiumExtensionIsAvailable(): void
251
    {
252
        if (!\extension_loaded('sodium')) {
253
            throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
254
        }
255
    }
256
}
257