| 
<?phpdeclare(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()
 );
 }
 }
 
 |