<?php
declare(strict_types=1);
namespace ParagonIE\HPKE\KEM;
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface;
use Mdanter\Ecc\Exception\InsecureCurveException;
use Mdanter\Ecc\Serializer\Point\UncompressedPointSerializer;
use ParagonIE\EasyECC\EasyECC;
use ParagonIE\EasyECC\Exception\NotImplementedException;
use ParagonIE\HPKE\{
HPKE,
HPKEException,
SymmetricKey,
Util
};
use ParagonIE\HPKE\Interfaces\{
DecapsKeyInterface,
EncapsKeyInterface,
KDFInterface,
KemInterface,
SymmetricKeyInterface
};
use ParagonIE\HPKE\KEM\DHKEM\{
Curve,
DecapsKey,
EncapsKey
};
use SodiumException;
use TypeError;
class DiffieHellmanKEM implements KemInterface
{
protected ?HPKE $hpke = null;
public function __construct(
public readonly Curve $curve,
public readonly KDFInterface $kdf,
) {}
/**
* This is called Npk in the HPKE spec..
*
* @return int
*/
public function getPublicKeyLength(): int
{
return match ($this->curve) {
Curve::Secp256k1, Curve::NistP256 => 65,
Curve::NistP384 => 97,
Curve::NistP521 => 133,
Curve::X25519 => 32
};
}
/**
* Thisi s called Nsec in the HPKE spec.
*/
public function getSecretLength(): int
{
return match ($this->curve) {
Curve::Secp256k1, Curve::NistP256, Curve::X25519 => 32,
Curve::NistP384 => 48,
Curve::NistP521 => 64
};
}
/**
* Thisi s called Nsk in the HPKE spec.
*/
public function getSecretKeyLength(): int
{
return match ($this->curve) {
Curve::Secp256k1, Curve::NistP256, Curve::X25519 => 32,
Curve::NistP384 => 48,
Curve::NistP521 => 66
};
}
/**
* This is called Nenc in the HPKE spec.
*
* @return int
*/
public function getHeaderLength(): int
{
return $this->curve->encapsKeyLength();
}
public function getKemId(): string
{
// https://www.iana.org/assignments/hpke/hpke.xhtml
return match ($this->curve) {
Curve::X25519 => "\x00\x20",
Curve::NistP256 => "\x00\x10",
Curve::NistP384 => "\x00\x11",
Curve::NistP521 => "\x00\x12",
Curve::Secp256k1 => "\x00\x16"
};
}
/**
* This is different from the HPKE Suite ID
*
* @return string
*/
public function getSuiteId(): string
{
return 'KEM' . $this->getKemId();
}
/**
* Stubbed out so it can be overridden in unit tests
*
* @throws NotImplementedException
* @throws SodiumException
*/
public function generatePrivateKey(EasyECC $ecc): string|PrivateKeyInterface
{
if ($ecc->getCurveName() === 'sodium') {
return sodium_crypto_box_keypair();
}
return $ecc->generatePrivateKey();
}
/**
* Generate a keypair for Key Encapsulation.
*
* @return array{0: DecapsKeyInterface, 1: EncapsKeyInterface}
*
* @throws HPKEException
* @throws NotImplementedException
* @throws SodiumException
*/
public function generateKeys(): array
{
$ecc = $this->curve->getEasyECC();
$keypair = $this->generatePrivateKey($ecc);
// Special handling for libsodium
if (is_string($keypair)) {
return [
new DecapsKey($this->curve, sodium_crypto_box_secretkey($keypair)),
new EncapsKey($this->curve, sodium_crypto_box_publickey($keypair))
];
}
// Handle all other elliptic curves
$sk = Util::gmpToBytes($keypair->getSecret(), $this->curve->decapsKeyLength());
$ser = (new UncompressedPointSerializer());
$pk = sodium_hex2bin($ser->serialize($keypair->getPublicKey()->getPoint()));
return [
new DecapsKey($this->curve, $sk),
new EncapsKey($this->curve, $pk)
];
}
/**
* @param EncapsKey $encapsKey
* @return array{0: SymmetricKeyInterface, 1: string}
*
* @throws HPKEException
* @throws InsecureCurveException
* @throws NotImplementedException
* @throws SodiumException
*/
public function encapsulate(EncapsKeyInterface $encapsKey): array
{
if (is_null($this->hpke)) {
throw new HPKEException('HPKE not injected');
}
if (!hash_equals($encapsKey->curve->name, $this->curve->name)) {
throw new TypeError('Encapsulation key must be meant for this curve');
}
[$ephSecret, $ephPublic] = $this->generateKeys();
if (!$ephSecret instanceof DecapsKey || !$ephPublic instanceof EncapsKey) {
throw new TypeError('Ephemeral key pair error');
}
$dh = $this->scalarMult($ephSecret, $encapsKey);
$enc = $ephPublic->serializeForHeader();
$kem_context = $enc . $encapsKey->serializeForHeader();
$secret_length = $this->getSecretLength();
$shared_secret = new SymmetricKey(
$this->kdf->extractAndExpand(
suiteId: $this->getSuiteId(),
dh: $dh,
kemContext: $kem_context,
length: $secret_length
)
);
return [$shared_secret, $enc];
}
/**
* @param DecapsKey $decapsKey
* @param string $enc
* @return SymmetricKeyInterface
*
* @throws HPKEException
* @throws InsecureCurveException
* @throws SodiumException
*/
public function decapsulate(
DecapsKeyInterface $decapsKey,
string $enc
): SymmetricKeyInterface {
if (is_null($this->hpke)) {
throw new HPKEException('HPKE not injected');
}
if (!hash_equals($decapsKey->curve->name, $this->curve->name)) {
throw new TypeError('Encapsulation key must be meant for this curve');
}
$ephPublic = new EncapsKey($decapsKey->curve, $enc);
$dh = $this->scalarMult($decapsKey, $ephPublic);
$kem_context = $enc . $decapsKey->getEncapsKey()->bytes;
$secret_length = $this->getSecretLength();
return new SymmetricKey(
$this->kdf->extractAndExpand($this->getSuiteId(), $dh, $kem_context, $secret_length)
);
}
/**
* @param EncapsKey $encapsKey
* @param DecapsKey $decapsKey
* @return array{0: SymmetricKeyInterface, 1: string}
*
* @throws HPKEException
* @throws NotImplementedException
* @throws SodiumException
* @throws InsecureCurveException
*/
public function authEncaps(EncapsKeyInterface $encapsKey, DecapsKeyInterface $decapsKey): array
{
if (is_null($this->hpke)) {
throw new HPKEException('HPKE not injected');
}
if (!hash_equals($encapsKey->curve->name, $this->curve->name)) {
throw new TypeError('Encapsulation key must be meant for this curve');
}
[$ephSecret, $ephPublic] = $this->generateKeys();
if (!$ephSecret instanceof DecapsKey || !$ephPublic instanceof EncapsKey) {
throw new TypeError('Ephemeral key pair error');
}
$dh = $this->scalarMult($ephSecret, $encapsKey) . $this->scalarMult($decapsKey, $encapsKey);
$enc = $ephPublic->serializeForHeader();
$kem_context = $enc .
$encapsKey->serializeForHeader() .
$decapsKey->getEncapsKey()->serializeForHeader();
$secret_length = $this->curve->secretLength();
$shared_secret = new SymmetricKey($this->kdf->extractAndExpand(
$this->getSuiteId(), $dh, $kem_context, $secret_length
));
return [$shared_secret, $enc];
}
/**
* @param DecapsKey $decapsKey
* @param EncapsKey $encapsKey
* @param string $enc
* @return SymmetricKeyInterface
*
* @throws HPKEException
* @throws InsecureCurveException
* @throws SodiumException
*/
public function authDecaps(
DecapsKeyInterface $decapsKey,
EncapsKeyInterface $encapsKey,
string $enc
): SymmetricKeyInterface {
if (is_null($this->hpke)) {
throw new HPKEException('HPKE not injected');
}
if (!hash_equals($decapsKey->curve->name, $this->curve->name)) {
throw new TypeError('Encapsulation key must be meant for this curve');
}
$ephPublic = new EncapsKey($decapsKey->curve, $enc);
$dh = $this->scalarMult($decapsKey, $ephPublic) . $this->scalarMult($decapsKey, $encapsKey);
$kem_context = $enc .
$encapsKey->serializeForHeader() .
$decapsKey->getEncapsKey()->serializeForHeader();
$secret_length = $this->curve->secretLength();
return new SymmetricKey(
$this->kdf->extractAndExpand($this->getSuiteId(), $dh, $kem_context, $secret_length)
);
}
/**
* Inject a reference to the HPKE class.
*
* @param HPKE $hpke
* @return static
*/
public function withHPKE(HPKE $hpke): static
{
$this->hpke = $hpke;
return $this;
}
/**
* @param DecapsKey $decapsKey
* @param EncapsKey $encapsKey
* @return string
*
* @throws HPKEException
* @throws InsecureCurveException
* @throws SodiumException
*/
protected function scalarMult(DecapsKey $decapsKey, EncapsKey $encapsKey): string
{
return $this->curve->getEasyECC()->scalarmult(
$decapsKey->toPrivateKey(),
$encapsKey->toPublicKey()
);
}
}
|