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