HPKE (Hybrid Public Key Encryption, RFC 9180)
pki.hpke.setupS
pki.hpke.setupS(suiteIds, recipientPublicKey, opts?) -> { enc, context, sharedSecret }
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 (the uncompressed point, the raw Montgomery coordinate, or the ML-KEM encapsulation key). 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; an option that belongs to another mode, such as senderKey in base mode, is refused with hpke/bad-input rather than left unused. The auth modes exist for the DHKEM suites only: an ML-KEM suite throws hpke/auth-unsupported. On a single-stage KDF (SHAKE128, SHAKE256) opts.info, opts.psk and opts.pskId are capped at 65535 bytes each and a longer one throws hpke/input-length.
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
- spec RFC 9180
pki.hpke.setupR
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, recovering the same shared secret and key schedule. The private key is a node KeyObject, or { skm, pkm } where skm is the raw private scalar of a DHKEM suite or the 64-byte seed of an ML-KEM suite; pkm is optional, and when given it must be the public key derived from skm or the call throws hpke/bad-key. 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 (DHKEM suites only). 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
- spec RFC 9180
pki.hpke.seal
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
// requires: mlKemPkR -- the recipient's ML-KEM-768 encapsulation key (1184 bytes)
var s = pki.hpke.suites, ids = { kem: s.KEM.ML_KEM_768, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
var out = pki.hpke.seal(ids, mlKemPkR, {}, Buffer.from("aad"), Buffer.from("msg"));
References
- spec RFC 9180
pki.hpke.open
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
// requires: mlKemPkR -- the recipient's ML-KEM-768 encapsulation key (1184 bytes)
// requires: mlKemSkR -- { skm } holding the recipient's 64-byte ML-KEM-768 seed
var s = pki.hpke.suites, ids = { kem: s.KEM.ML_KEM_768, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
var o = pki.hpke.seal(ids, mlKemPkR, {}, Buffer.alloc(0), Buffer.from("m"));
var pt = pki.hpke.open(ids, o.enc, mlKemSkR, {}, Buffer.alloc(0), o.ct);
References
- spec RFC 9180
pki.hpke.deriveKeyPair
pki.hpke.deriveKeyPair(kemId, ikm) -> { publicKey, privateKey }
The KEM's DeriveKeyPair: a key pair derived deterministically from input keying material, returned as raw byte strings in the encodings the KEM registers. The same ikm always gives the same pair, so a key can be rebuilt from backed-up material instead of stored in full, and the pair matches what any other conforming implementation derives from the same input.
ikm is keying material, not a private key. Each KEM runs it through its own derivation first: a DHKEM through LabeledExtract and LabeledExpand, with the prime curves rejection-sampling candidates until one lands in [1, order) (RFC 9180 section 7.1.3); an ML-KEM and a PQ/T hybrid through SHAKE256.LabeledDerive to the KEM's seed, which is then expanded (draft-ietf-hpke-pq sections 3 and 4). To use a private key you already hold, pass it as { skm } to decap, setupR or open instead; that door takes the KEM's own private encoding and checks its width.
Any length of ikm is accepted. The hybrid suites state a 32-byte minimum as a SHOULD, which is not enforced here, and an ML-KEM takes input of any length.
privateKey is the KEM's private encoding: the 32-byte seed for a hybrid suite, the 64-byte seed for a bare ML-KEM, and the scalar for a DHKEM.
Example
var ikm = Buffer.alloc(32, 7);
var kp = pki.hpke.deriveKeyPair(pki.hpke.suites.KEM.MLKEM768_X25519, ikm);
kp.publicKey.length; // -> 1216, the ML-KEM-768 key then the X25519 key
kp.privateKey.length; // -> 32, the seed the ikm derived
References
- spec RFC 9180 sec. 7.1.3
- spec draft-ietf-hpke-pq sec. 3
- spec draft-ietf-hpke-pq sec. 4
- defends
kem-key-nondeterminism (CWE-330)
pki.hpke.generateKeyPair
pki.hpke.generateKeyPair(kemId) -> { publicKey, privateKey }
A fresh KEM key pair, as raw byte strings in the encodings the KEM registers. For a hybrid suite the private key is a 32-byte seed the whole pair expands from, and for the others it is the KEM's own private encoding.
Example
var kp = pki.hpke.generateKeyPair(pki.hpke.suites.KEM.MLKEM768_P256);
kp.publicKey.length; // -> 1249
kp.privateKey.length; // -> 32
References
- spec RFC 9180 sec. 4
- spec draft-ietf-hpke-pq sec. 3
- defends
kem-key-reuse (CWE-330)
pki.hpke.encap
pki.hpke.encap(kemId, recipientPublicKey) -> { enc, sharedSecret }
The KEM half of HPKE on its own: encapsulate to a recipient public key, returning the encapsulated key to send and the shared secret it establishes. pki.hpke.setupS runs this and then the key schedule; this verb is for a caller driving its own.
For a hybrid KEM the encapsulated key is the PQ ciphertext followed by the traditional group element, and the shared secret is the two component secrets combined with the traditional ciphertext, the traditional public key and the suite label, which is the C2PRI combiner that framework specifies.
Example
var kp = pki.hpke.generateKeyPair(pki.hpke.suites.KEM.MLKEM768_X25519);
var e = pki.hpke.encap(pki.hpke.suites.KEM.MLKEM768_X25519, kp.publicKey);
e.enc.length; // -> 1120
e.sharedSecret.length; // -> 32
References
- spec RFC 9180 sec. 4
- spec draft-irtf-cfrg-hybrid-kems sec. 5.2
- defends
kem-shared-secret-confusion (CWE-347)
pki.hpke.decap
pki.hpke.decap(kemId, enc, recipientPrivateKey) -> sharedSecret
The receiving half of the KEM: recover the shared secret from an encapsulated key. A KEM does not report failure by throwing where the specification says it returns a value, so a ciphertext this key did not receive yields a different secret rather than an error, and the AEAD above it is what refuses. A structurally wrong input, such as an encapsulated key of the wrong length, is refused.
Example
var id = pki.hpke.suites.KEM.MLKEM768_X25519;
var kp = pki.hpke.generateKeyPair(id);
var e = pki.hpke.encap(id, kp.publicKey);
pki.hpke.decap(id, e.enc, { skm: kp.privateKey }).equals(e.sharedSecret); // -> true
References
- spec RFC 9180 sec. 4
- spec draft-irtf-cfrg-hybrid-kems sec. 5.2
- defends
kem-shared-secret-confusion (CWE-347)