3 declare(strict_types=1);
5 namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
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;
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;
26 abstract class AbstractECDH implements KeyAgreement
28 public function allowedKeyTypes(): array
34 * @param array<string, mixed> $complete_header
35 * @param array<string, mixed> $additional_header_values
37 public function getAgreementKey(
38 int $encryptionKeyLength,
42 array $complete_header = [],
43 array &$additional_header_values = []
45 if ($recipientKey->has('d')) {
46 [$public_key, $private_key] = $this->getKeysFromPrivateKeyAndHeader($recipientKey, $complete_header);
48 [$public_key, $private_key] = $this->getKeysFromPublicKey(
51 $additional_header_values
55 $agreed_key = $this->calculateAgreementKey($private_key, $public_key);
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.');
62 return ConcatKDF::generate($agreed_key, $algorithm, $encryptionKeyLength, $apu, $apv);
65 public function getKeyManagementMode(): string
67 return self::MODE_AGREEMENT;
70 protected function calculateAgreementKey(JWK $private_key, JWK $public_key): string
72 $crv = $public_key->get('crv');
73 if (! is_string($crv)) {
74 throw new InvalidArgumentException('Invalid key parameter "crv"');
80 $curve = $this->getCurve($crv);
81 if (function_exists('openssl_pkey_derive')) {
83 $publicPem = ECKey::convertPublicKeyToPEM($public_key);
84 $privatePem = ECKey::convertPrivateKeyToPEM($private_key);
86 $res = openssl_pkey_derive($publicPem, $privatePem, $curve->getSize());
88 throw new RuntimeException('Unable to derive the key');
93 //Does nothing. Will fallback to the pure PHP function
96 $x = $public_key->get('x');
97 if (! is_string($x)) {
98 throw new InvalidArgumentException('Invalid key parameter "x"');
100 $y = $public_key->get('y');
101 if (! is_string($y)) {
102 throw new InvalidArgumentException('Invalid key parameter "y"');
104 $d = $private_key->get('d');
105 if (! is_string($d)) {
106 throw new InvalidArgumentException('Invalid key parameter "d"');
109 $rec_x = $this->convertBase64ToBigInteger($x);
110 $rec_y = $this->convertBase64ToBigInteger($y);
111 $sen_d = $this->convertBase64ToBigInteger($d);
113 $priv_key = PrivateKey::create($sen_d);
114 $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
116 return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key));
119 $this->checkSodiumExtensionIsAvailable();
120 $x = $public_key->get('x');
121 if (! is_string($x)) {
122 throw new InvalidArgumentException('Invalid key parameter "x"');
124 $d = $private_key->get('d');
125 if (! is_string($d)) {
126 throw new InvalidArgumentException('Invalid key parameter "d"');
128 $sKey = Base64UrlSafe::decodeNoPadding($d);
129 $recipientPublickey = Base64UrlSafe::decodeNoPadding($x);
131 return sodium_crypto_scalarmult($sKey, $recipientPublickey);
134 throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
139 * @param array<string, mixed> $additional_header_values
142 protected function getKeysFromPublicKey(
145 array &$additional_header_values
147 $this->checkKey($recipient_key, false);
148 $public_key = $recipient_key;
150 $crv = $public_key->get('crv');
151 if (! is_string($crv)) {
152 throw new InvalidArgumentException('Invalid key parameter "crv"');
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)),
159 $epk = $private_key->toPublic()
161 $additional_header_values['epk'] = $epk;
163 return [$public_key, $private_key];
167 * @param array<string, mixed> $complete_header
170 protected function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array
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');
179 return [$public_key, $private_key];
183 * @param array<string, mixed> $complete_header
185 private function getPublicKey(array $complete_header): JWK
187 if (! isset($complete_header['epk'])) {
188 throw new InvalidArgumentException('The header parameter "epk" is missing.');
190 if (! is_array($complete_header['epk'])) {
191 throw new InvalidArgumentException('The header parameter "epk" is not an array of parameters');
193 $public_key = new JWK($complete_header['epk']);
194 $this->checkKey($public_key, false);
199 private function checkKey(JWK $key, bool $is_private): void
201 if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
202 throw new InvalidArgumentException('Wrong key type.');
204 foreach (['x', 'crv'] as $k) {
205 if (! $key->has($k)) {
206 throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
210 $crv = $key->get('crv');
211 if (! is_string($crv)) {
212 throw new InvalidArgumentException('Invalid key parameter "crv"');
218 if (! $key->has('y')) {
219 throw new InvalidArgumentException('The key parameter "y" is missing.');
228 throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
230 if ($is_private === true && ! $key->has('d')) {
231 throw new InvalidArgumentException('The key parameter "d" is missing.');
235 private function getCurve(string $crv): Curve
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)),
245 private function convertBase64ToBigInteger(string $value): BigInteger
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');
252 return BigInteger::fromBase($data[1], 16);
255 private function convertDecToBin(BigInteger $dec): string
257 if ($dec->compareTo(BigInteger::zero()) < 0) {
258 throw new InvalidArgumentException('Unable to convert negative integer to string');
260 $hex = $dec->toBase(16);
262 if (mb_strlen($hex, '8bit') % 2 !== 0) {
266 $bin = hex2bin($hex);
267 if ($bin === false) {
268 throw new InvalidArgumentException('Unable to convert integer to string');
275 * @param string $curve The curve
277 private function createOKPKey(string $curve): JWK
279 $this->checkSodiumExtensionIsAvailable();
283 $keyPair = sodium_crypto_box_keypair();
284 $d = sodium_crypto_box_secretkey($keyPair);
285 $x = sodium_crypto_box_publickey($keyPair);
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);
299 throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
305 'x' => Base64UrlSafe::encodeUnpadded($x),
306 'd' => Base64UrlSafe::encodeUnpadded($d),
310 private function checkSodiumExtensionIsAvailable(): void
312 if (! extension_loaded('sodium')) {
313 throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');