Files
w4rpservices/vendor/web-token/jwt-library/Encryption/Algorithm/KeyEncryption/AbstractECDH.php
T
2026-02-18 20:17:19 -06:00

323 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
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 Override;
use RuntimeException;
use Throwable;
use function array_key_exists;
use function extension_loaded;
use function function_exists;
use function in_array;
use function is_array;
use function is_string;
use function sprintf;
use function strlen;
abstract readonly class AbstractECDH implements KeyAgreement
{
#[Override]
public function allowedKeyTypes(): array
{
return ['EC', 'OKP'];
}
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
#[Override]
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,
$senderKey,
$additional_header_values
);
}
$agreed_key = $this->calculateAgreementKey($private_key, $public_key);
$apu = array_key_exists('apu', $complete_header) ? $complete_header['apu'] : '';
is_string($apu) || throw new InvalidArgumentException('Invalid APU.');
$apv = array_key_exists('apv', $complete_header) ? $complete_header['apv'] : '';
is_string($apv) || throw new InvalidArgumentException('Invalid APU.');
return ConcatKDF::generate($agreed_key, $algorithm, $encryptionKeyLength, $apu, $apv);
}
#[Override]
public function getKeyManagementMode(): string
{
return self::MODE_AGREEMENT;
}
protected function calculateAgreementKey(JWK $private_key, JWK $public_key): string
{
$crv = $public_key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
switch ($crv) {
case 'P-256':
case 'P-384':
case 'P-521':
$curve = $this->getCurve($crv);
if (function_exists('openssl_pkey_derive')) {
try {
$publicPem = ECKey::convertPublicKeyToPEM($public_key);
$privatePem = ECKey::convertPrivateKeyToPEM($private_key);
$res = openssl_pkey_derive($publicPem, $privatePem);
if ($res === false) {
throw new RuntimeException('Unable to derive the key');
}
return $res;
} catch (Throwable) {
//Does nothing. Will fallback to the pure PHP function
}
}
$x = $public_key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid key parameter "x"');
}
$y = $public_key->get('y');
if (! is_string($y)) {
throw new InvalidArgumentException('Invalid key parameter "y"');
}
$d = $private_key->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Invalid key parameter "d"');
}
$rec_x = $this->convertBase64ToBigInteger($x);
$rec_y = $this->convertBase64ToBigInteger($y);
$sen_d = $this->convertBase64ToBigInteger($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':
$this->checkSodiumExtensionIsAvailable();
$x = $public_key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid key parameter "x"');
}
$d = $private_key->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Invalid key parameter "d"');
}
$sKey = Base64UrlSafe::decodeNoPadding($d);
$recipientPublickey = Base64UrlSafe::decodeNoPadding($x);
return sodium_crypto_scalarmult($sKey, $recipientPublickey);
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
}
}
/**
* @param array<string, mixed> $additional_header_values
* @return JWK[]
*/
protected function getKeysFromPublicKey(
JWK $recipient_key,
?JWK $senderKey,
array &$additional_header_values
): array {
$this->checkKey($recipient_key, false);
$public_key = $recipient_key;
$crv = $public_key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
$private_key = match ($crv) {
'P-256', 'P-384', 'P-521' => $senderKey ?? ECKey::createECKey($crv),
'X25519' => $senderKey ?? $this->createOKPKey('X25519'),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)),
};
$epk = $private_key->toPublic()
->all();
$additional_header_values['epk'] = $epk;
return [$public_key, $private_key];
}
/**
* @param array<string, mixed> $complete_header
* @return JWK[]
*/
protected 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];
}
/**
* @param array<string, mixed> $complete_header
*/
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;
}
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));
}
}
$crv = $key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
switch ($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', $crv));
}
if ($is_private === true && ! $key->has('d')) {
throw new InvalidArgumentException('The key parameter "d" is missing.');
}
}
private function getCurve(string $crv): Curve
{
return match ($crv) {
'P-256' => NistCurve::curve256(),
'P-384' => NistCurve::curve384(),
'P-521' => NistCurve::curve521(),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)),
};
}
private function convertBase64ToBigInteger(string $value): BigInteger
{
$data = unpack('H*', Base64UrlSafe::decodeNoPadding($value));
if (! is_array($data) || ! isset($data[1]) || ! is_string($data[1])) {
throw new InvalidArgumentException('Unable to convert base64 to integer');
}
return BigInteger::fromBase($data[1], 16);
}
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 (strlen($hex) % 2 !== 0) {
$hex = '0' . $hex;
}
$bin = hex2bin($hex);
if ($bin === false) {
throw new InvalidArgumentException('Unable to convert integer to string');
}
return $bin;
}
/**
* @param string $curve The curve
*/
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();
$secret = sodium_crypto_sign_secretkey($keyPair);
$secretLength = strlen($secret);
$d = substr($secret, 0, -$secretLength / 2);
$x = sodium_crypto_sign_publickey($keyPair);
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
}
return new JWK([
'kty' => 'OKP',
'crv' => $curve,
'x' => Base64UrlSafe::encodeUnpadded($x),
'd' => Base64UrlSafe::encodeUnpadded($d),
]);
}
private function checkSodiumExtensionIsAvailable(): void
{
if (! extension_loaded('sodium')) {
throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
}
}
}