HPKE

Hybrid Public Key Encryption (RFC 9180) -- the standard construction behind TLS Encrypted Client Hello, MLS, and OHTTP that turns a recipient KEM public key into an encapsulated key plus an AEAD-encrypting context. This is the RFC 9180 base construction: the DHKEM suites P-256, P-521, X25519, and X448, HKDF-SHA256 / HKDF-SHA512, the three AEADs plus export-only, and all four modes (base / psk / auth / auth-psk), each proven against the RFC 9180 Appendix A known-answer vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no test vector for them, so they are omitted (a request fails closed) until an authoritative KAT is available. Pure composition over node:crypto -- no ASN.1, no schema engine. Post-quantum KEMs (ML-KEM, X-Wing) are a data-row extension the registry is shaped to admit once their specifications stabilize. The RFC 9180 sec. 7 registry code points live in pki.hpke.suites (KEM / KDF / AEAD / MODE), passed to the setup functions to select a suite.

pki.hpke.setupS

since 0.2.2 experimental
pki.hpke.setupS(suiteIds, recipientPublicKey, opts?) -> { enc, context }

Establish a sender HPKE context for a recipient KEM public key: encapsulate a shared secret and run the key schedule, returning the encapsulated key enc (send it to the recipient) and a context whose .seal(aad, pt) / .export(ctx, L) encrypt and derive further secrets. suiteIds is { kem, kdf, aead } from pki.hpke.suites; recipientPublicKey is a node KeyObject or the serialized public key bytes. opts.mode selects base / psk / auth / auth-psk (default base); opts.info, opts.psk/opts.pskId, and opts.senderKey (auth modes) supply the corresponding inputs.

Example

var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
var sender = pki.hpke.setupS(ids, pkR, { info: Buffer.from("app") });
var ct = sender.context.seal(Buffer.from("aad"), Buffer.from("secret"));

References

pki.hpke.setupR

since 0.2.2 experimental
pki.hpke.setupR(suiteIds, enc, recipientPrivateKey, opts?) -> context

Establish the recipient HPKE context from the sender's encapsulated key enc and the recipient KEM private key (a node KeyObject or { skm, pkm } -- the raw private scalar plus its public point), recovering the same shared secret and key schedule. The returned context .open(aad, ct) decrypts and .export(ctx, L) derives secrets. opts mirrors setupS (mode / info / psk / pskId), with opts.senderPublicKey for the auth modes. A ciphertext whose tag does not verify throws hpke/open-failed.

Example

var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
var skR = Buffer.from("009f2181fba5f8908632c10ea1137c40a849728fde016c4602458b943a5dc048", "hex");
var sender = pki.hpke.setupS(ids, pkR);
var recipient = pki.hpke.setupR(ids, sender.enc, { skm: skR, pkm: pkR });
var pt = recipient.open(Buffer.alloc(0), sender.context.seal(Buffer.alloc(0), Buffer.from("hi")));

References

pki.hpke.seal

since 0.2.2 experimental
pki.hpke.seal(suiteIds, recipientPublicKey, opts, aad, pt) -> { enc, ct }

Single-shot HPKE encryption (RFC 9180 sec. 6): set up a sender context and encrypt one plaintext, returning the encapsulated key enc and ciphertext ct. Equivalent to setupS followed by one context.seal. opts is the setupS options object (mode / info / psk / senderKey).

Example

var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
var out = pki.hpke.seal(ids, pkR, {}, Buffer.from("aad"), Buffer.from("msg"));

References

pki.hpke.open

since 0.2.2 experimental
pki.hpke.open(suiteIds, enc, recipientPrivateKey, opts, aad, ct) -> pt

Single-shot HPKE decryption (RFC 9180 sec. 6): set up a recipient context from enc and decrypt one ciphertext, returning the plaintext. A tag that does not verify throws hpke/open-failed and returns no plaintext.

Example

var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
var skR = Buffer.from("009f2181fba5f8908632c10ea1137c40a849728fde016c4602458b943a5dc048", "hex");
var o = pki.hpke.seal(ids, pkR, {}, Buffer.alloc(0), Buffer.from("m"));
var pt = pki.hpke.open(ids, o.enc, { skm: skR, pkm: pkR }, {}, Buffer.alloc(0), o.ct);

References