관리-도구
편집 파일: ECDHES.php
<?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2020 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Jose\Component\Encryption\Algorithm\KeyEncryption; use function array_key_exists; use Base64Url\Base64Url; use Brick\Math\BigInteger; use function extension_loaded; use function function_exists; use function in_array; use InvalidArgumentException; use function is_array; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Ecc\Curve; use Jose\Component\Core\Util\Ecc\EcDH; use Jose\Component\Core\Util\Ecc\NistCurve; use Jose\Component\Core\Util\Ecc\PrivateKey; use Jose\Component\Core\Util\ECKey; use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\ConcatKDF; use RuntimeException; use Throwable; final class ECDHES implements KeyAgreement { public function allowedKeyTypes(): array { return ['EC', 'OKP']; } public function getAgreementKey(int $encryptionKeyLength, string $algorithm, JWK $recipientKey, ?JWK $senderKey, array $complete_header = [], array &$additional_header_values = []): string { if ($recipientKey->has('d')) { [$public_key, $private_key] = $this->getKeysFromPrivateKeyAndHeader($recipientKey, $complete_header); } else { [$public_key, $private_key] = $this->getKeysFromPublicKey($recipientKey, $additional_header_values); } $agreed_key = $this->calculateAgreementKey($private_key, $public_key); $apu = array_key_exists('apu', $complete_header) ? $complete_header['apu'] : ''; $apv = array_key_exists('apv', $complete_header) ? $complete_header['apv'] : ''; return ConcatKDF::generate($agreed_key, $algorithm, $encryptionKeyLength, $apu, $apv); } /** * @throws InvalidArgumentException if the curve is not supported */ public function calculateAgreementKey(JWK $private_key, JWK $public_key): string { switch ($public_key->get('crv')) { case 'P-256': case 'P-384': case 'P-521': $curve = $this->getCurve($public_key->get('crv')); if (function_exists('openssl_pkey_derive')) { try { $publicPem = ECKey::convertPublicKeyToPEM($public_key); $privatePem = ECKey::convertPrivateKeyToPEM($private_key); $res = openssl_pkey_derive($publicPem, $privatePem, $curve->getSize()); if (false === $res) { throw new RuntimeException('Unable to derive the key'); } return $res; } catch (Throwable $throwable) { //Does nothing. Will fallback to the pure PHP function } } $rec_x = $this->convertBase64ToBigInteger($public_key->get('x')); $rec_y = $this->convertBase64ToBigInteger($public_key->get('y')); $sen_d = $this->convertBase64ToBigInteger($private_key->get('d')); $priv_key = PrivateKey::create($sen_d); $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y); return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key)); case 'X25519': $sKey = Base64Url::decode($private_key->get('d')); $recipientPublickey = Base64Url::decode($public_key->get('x')); return sodium_crypto_scalarmult($sKey, $recipientPublickey); default: throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv'))); } } public function name(): string { return 'ECDH-ES'; } public function getKeyManagementMode(): string { return self::MODE_AGREEMENT; } /** * @throws InvalidArgumentException if the curve is not supported * * @return JWK[] */ private function getKeysFromPublicKey(JWK $recipient_key, array &$additional_header_values): array { $this->checkKey($recipient_key, false); $public_key = $recipient_key; switch ($public_key->get('crv')) { case 'P-256': case 'P-384': case 'P-521': $private_key = ECKey::createECKey($public_key->get('crv')); break; case 'X25519': $this->checkSodiumExtensionIsAvailable(); $private_key = $this->createOKPKey('X25519'); break; default: throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $public_key->get('crv'))); } $epk = $private_key->toPublic()->all(); $additional_header_values['epk'] = $epk; return [$public_key, $private_key]; } /** * @throws InvalidArgumentException if the curves are different * * @return JWK[] */ private function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array { $this->checkKey($recipient_key, true); $private_key = $recipient_key; $public_key = $this->getPublicKey($complete_header); if ($private_key->get('crv') !== $public_key->get('crv')) { throw new InvalidArgumentException('Curves are different'); } return [$public_key, $private_key]; } /** * @throws InvalidArgumentException if the ephemeral public key is missing or invalid */ private function getPublicKey(array $complete_header): JWK { if (!isset($complete_header['epk'])) { throw new InvalidArgumentException('The header parameter "epk" is missing.'); } if (!is_array($complete_header['epk'])) { throw new InvalidArgumentException('The header parameter "epk" is not an array of parameters'); } $public_key = new JWK($complete_header['epk']); $this->checkKey($public_key, false); return $public_key; } /** * @throws InvalidArgumentException if the key is invalid */ private function checkKey(JWK $key, bool $is_private): void { if (!in_array($key->get('kty'), $this->allowedKeyTypes(), true)) { throw new InvalidArgumentException('Wrong key type.'); } foreach (['x', 'crv'] as $k) { if (!$key->has($k)) { throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k)); } } switch ($key->get('crv')) { case 'P-256': case 'P-384': case 'P-521': if (!$key->has('y')) { throw new InvalidArgumentException('The key parameter "y" is missing.'); } break; case 'X25519': break; default: throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $key->get('crv'))); } if (true === $is_private && !$key->has('d')) { throw new InvalidArgumentException('The key parameter "d" is missing.'); } } /** * @throws InvalidArgumentException if the curve is not supported */ private function getCurve(string $crv): Curve { switch ($crv) { case 'P-256': return NistCurve::curve256(); case 'P-384': return NistCurve::curve384(); case 'P-521': return NistCurve::curve521(); default: throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)); } } private function convertBase64ToBigInteger(string $value): BigInteger { $data = unpack('H*', Base64Url::decode($value)); if (!is_array($data) || !isset($data[1])) { throw new InvalidArgumentException('Unable to convert base64 to integer'); } return BigInteger::fromBase($data[1], 16); } /** * @throws InvalidArgumentException if the data cannot be converted */ private function convertDecToBin(BigInteger $dec): string { if ($dec->compareTo(BigInteger::zero()) < 0) { throw new InvalidArgumentException('Unable to convert negative integer to string'); } $hex = $dec->toBase(16); if (0 !== mb_strlen($hex, '8bit') % 2) { $hex = '0'.$hex; } $bin = hex2bin($hex); if (false === $bin) { throw new InvalidArgumentException('Unable to convert integer to string'); } return $bin; } /** * @param string $curve The curve * * @throws InvalidArgumentException if the curve is not supported */ private function createOKPKey(string $curve): JWK { $this->checkSodiumExtensionIsAvailable(); switch ($curve) { case 'X25519': $keyPair = sodium_crypto_box_keypair(); $d = sodium_crypto_box_secretkey($keyPair); $x = sodium_crypto_box_publickey($keyPair); break; case 'Ed25519': $keyPair = sodium_crypto_sign_keypair(); $d = sodium_crypto_sign_secretkey($keyPair); $x = sodium_crypto_sign_publickey($keyPair); break; default: throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve)); } return new JWK([ 'kty' => 'OKP', 'crv' => $curve, 'x' => Base64Url::encode($x), 'd' => Base64Url::encode($d), ]); } /** * @throws RuntimeException if the extension "sodium" is not available */ private function checkSodiumExtensionIsAvailable(): void { if (!extension_loaded('sodium')) { throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method'); } } }