HPKE envelope
A blamejs public-key sealing format: ML-KEM-1024 encapsulation, HKDF-SHA3-512 key derivation and XChaCha20-Poly1305 encryption. The format is specific to blamejs and does not interoperate with RFC 9180 HPKE or with any HPKE draft: the key is one HKDF-SHA3-512 output over the context "blamejs/hpke/v1" || 00 21 22 23 00 || info, there is no RFC 9180 key schedule, and XChaCha20-Poly1305 has no HPKE AEAD identifier. The bytes 21 22 23 are blamejs domain-separation bytes, not HPKE registry values. Envelopes sealed here open only with b.crypto.hpke.open.
For RFC 9180 HPKE with ML-KEM-1024 under the registered codepoints, use b.crypto.hpke.pq.connolly or b.crypto.hpke.pq.wg.
seal and open accept only ML-KEM-1024 keys and refuse a key of any other type with crypto-hpke/bad-key.
Each seal encapsulates a fresh ML-KEM-1024 shared secret, so every envelope has its own key and the all-zero 24-byte nonce is used once per key. info and aad are bound: a different value on open fails the Poly1305 tag. seal and open emit system.hpke.seal and system.hpke.open audit rows.
b.crypto.hpke.pq.connolly.seal(opts) #
{
recipientPubKey: Buffer|string|KeyObject, // 1568-byte key, PEM or KeyObject
plaintext: Buffer|string,
info: Buffer|string, // application context
aad: Buffer|string, // additional authenticated data
kdf: string, // "HKDF-SHA512" (default) | "HKDF-SHA384"
aead: string, // "ChaCha20Poly1305" (default) | "AES-256-GCM"
psk: Buffer|string, // pre-shared key, at least 32 bytes, with pskId
pskId: Buffer|string, // pre-shared key identifier, with psk
}
Encrypt one message to an ML-KEM-1024 public key under RFC 9180 base mode (psk mode when psk and pskId are given) with KEM 0x0042, as draft-connolly-cfrg-hpke-mlkem-04 specifies. Returns { enc, ciphertext }: enc is the 1568-byte ML-KEM-1024 ciphertext and ciphertext is the AEAD output for sequence number 0. The suite defaults to HKDF-SHA512 and ChaCha20Poly1305.
Throws crypto-hpke-pq/bad-opt for a missing or malformed option, crypto-hpke-pq/unsupported-suite for a KDF or AEAD outside the list in the module notes, crypto-hpke-pq/bad-key for a public key that is not an ML-KEM-1024 key, crypto-hpke-pq/inconsistent-psk when only one of psk and pskId is given, crypto-hpke-pq/weak-psk for a psk shorter than 32 bytes, and crypto-hpke-pq/export-only for the export-only AEAD.
var pair = b.crypto.hpke.pq.connolly.generateKeyPair();
var sealed = b.crypto.hpke.pq.connolly.seal({
recipientPubKey: pair.publicKey,
plaintext: "hello",
info: "app/topic",
});
sealed.enc.length; // → 1568
b.crypto.hpke.pq.connolly.open(opts) #
{
privateKey: Buffer|string|KeyObject, // 64-byte seed, PKCS#8 PEM or KeyObject
enc: Buffer, // 1568-byte ML-KEM-1024 ciphertext
ciphertext: Buffer,
info: Buffer|string,
aad: Buffer|string,
kdf: string, // as sealed; default "HKDF-SHA512"
aead: string, // as sealed; default "ChaCha20Poly1305"
psk: Buffer|string, // at least 32 bytes, as sealed
pskId: Buffer|string,
}
Decrypt one RFC 9180 message sealed to an ML-KEM-1024 key under KEM 0x0042, from connolly.seal or from any implementation of the same suite. kdf, aead, info, aad, psk and pskId must match the sender's. Returns the plaintext as a Buffer.
Throws crypto-hpke-pq/open-failed when the AEAD tag does not verify, crypto-hpke-pq/bad-opt for a missing or malformed option, crypto-hpke-pq/unsupported-suite for a KDF or AEAD outside the list in the module notes, crypto-hpke-pq/bad-key for a private key or enc that is not ML-KEM-1024, crypto-hpke-pq/inconsistent-psk when only one of psk and pskId is given, crypto-hpke-pq/weak-psk for a psk shorter than 32 bytes, and crypto-hpke-pq/export-only for the export-only AEAD.
var pair = b.crypto.hpke.pq.connolly.generateKeyPair();
var sealed = b.crypto.hpke.pq.connolly.seal({
recipientPubKey: pair.publicKey, plaintext: "hello", info: "app/topic",
});
var pt = b.crypto.hpke.pq.connolly.open({
privateKey: pair.privateKey, enc: sealed.enc,
ciphertext: sealed.ciphertext, info: "app/topic",
});
pt.toString("utf8"); // → "hello"
b.crypto.hpke.pq.connolly.setupSender(opts) #
{
recipientPubKey: Buffer|string|KeyObject,
info: Buffer|string,
kdf: string,
aead: string, // "Export-only" for a context used only to export
psk: Buffer|string, // at least 32 bytes, with pskId
pskId: Buffer|string,
}
Run the RFC 9180 sender setup once and return the context for a sequence of messages: { enc, seal(plaintext, aad?), export(exporterContext, length) }. Each seal uses the next sequence number, so the recipient opens the messages in the order they were sealed. export returns length bytes of the RFC 9180 exporter secret for exporterContext.
Throws crypto-hpke-pq/bad-opt, crypto-hpke-pq/unsupported-suite, crypto-hpke-pq/bad-key, crypto-hpke-pq/inconsistent-psk and crypto-hpke-pq/weak-psk as connolly.seal does. The context's seal throws crypto-hpke-pq/bad-opt for a missing plaintext (pass "" for an empty message), crypto-hpke-pq/export-only under the export-only AEAD and crypto-hpke-pq/message-limit when the sequence number would overflow; its export throws crypto-hpke-pq/export-length for a length above 255 times the KDF output size.
var pair = b.crypto.hpke.pq.connolly.generateKeyPair();
var sender = b.crypto.hpke.pq.connolly.setupSender({
recipientPubKey: pair.publicKey, info: "stream",
});
var c0 = sender.seal("first", "aad-0");
var c1 = sender.seal("second", "aad-1");
sender.enc.length; // → 1568
b.crypto.hpke.pq.connolly.setupRecipient(opts) #
{
privateKey: Buffer|string|KeyObject,
enc: Buffer,
info: Buffer|string,
kdf: string,
aead: string,
psk: Buffer|string, // at least 32 bytes, as given to setupSender
pskId: Buffer|string,
}
Run the RFC 9180 recipient setup for enc and return { open(ciphertext, aad?), export(exporterContext, length) }. Each open uses the next sequence number, so the messages are opened in the order they were sealed; a message opened out of order fails its tag, and a failed open does not advance the sequence number.
Throws crypto-hpke-pq/bad-opt, crypto-hpke-pq/unsupported-suite, crypto-hpke-pq/bad-key, crypto-hpke-pq/inconsistent-psk and crypto-hpke-pq/weak-psk as connolly.open does. The context's open throws crypto-hpke-pq/open-failed, crypto-hpke-pq/export-only and crypto-hpke-pq/message-limit; its export throws crypto-hpke-pq/export-length.
var pair = b.crypto.hpke.pq.connolly.generateKeyPair();
var sender = b.crypto.hpke.pq.connolly.setupSender({ recipientPubKey: pair.publicKey });
var c0 = sender.seal("first");
var recip = b.crypto.hpke.pq.connolly.setupRecipient({
privateKey: pair.privateKey, enc: sender.enc,
});
recip.open(c0).toString("utf8"); // → "first"
b.crypto.hpke.pq.connolly.generateKeyPair() #
Generate an ML-KEM-1024 key pair in the HPKE serialized forms: { publicKey, privateKey }, where publicKey is the 1568-byte encapsulation key and privateKey is the 64-byte seed (d, z) from a fresh random source. These are the forms a peer implementation exchanges. Throws crypto-hpke-pq/bad-opt when given any argument, since connolly defines no KEM other than ML-KEM-1024.
var pair = b.crypto.hpke.pq.connolly.generateKeyPair();
pair.publicKey.length; // → 1568
pair.privateKey.length; // → 64
b.crypto.hpke.pq.wg.seal(opts) #
{
recipientPubKey: Buffer|string|KeyObject, // 1568 bytes (ML-KEM-1024) or 1665 (MLKEM1024-P384)
plaintext: Buffer|string,
info: Buffer|string,
aad: Buffer|string,
kem: string, // "ML-KEM-1024" (default) | "MLKEM1024-P384"
kdf: string, // "SHAKE256" (default) | "HKDF-SHA384" | "HKDF-SHA512"
aead: string, // "ChaCha20Poly1305" (default) | "AES-256-GCM"
psk: Buffer|string, // pre-shared key, at least 32 bytes, with pskId
pskId: Buffer|string, // pre-shared key identifier, with psk
}
Encrypt one message under draft-ietf-hpke-pq-05: RFC 9180 base mode (psk mode when psk and pskId are given) with KEM ML-KEM-1024 (0x0042) or MLKEM1024-P384 (0x0051). Returns { enc, ciphertext }. The suite defaults to ML-KEM-1024, the single-stage SHAKE256 KDF and ChaCha20Poly1305.
Throws crypto-hpke-pq/bad-opt for a missing or malformed option, crypto-hpke-pq/unsupported-suite for a KEM, KDF or AEAD outside the list in the module notes, crypto-hpke-pq/bad-key for a public key that does not match the KEM, crypto-hpke-pq/inconsistent-psk when only one of psk and pskId is given, crypto-hpke-pq/weak-psk for a psk shorter than 32 bytes, crypto-hpke-pq/export-only for the export-only AEAD, and crypto-hpke-pq/input-length for an info, psk or pskId over 65,535 bytes under SHAKE256.
var pair = b.crypto.hpke.pq.wg.generateKeyPair();
var sealed = b.crypto.hpke.pq.wg.seal({
recipientPubKey: pair.publicKey,
plaintext: "hello",
});
sealed.enc.length; // → 1568
b.crypto.hpke.pq.wg.open(opts) #
{
privateKey: Buffer|string|KeyObject, // 64-byte (ML-KEM-1024) or 32-byte (MLKEM1024-P384) seed
enc: Buffer,
ciphertext: Buffer,
info: Buffer|string,
aad: Buffer|string,
kem: string,
kdf: string,
aead: string,
psk: Buffer|string, // at least 32 bytes, as sealed
pskId: Buffer|string,
}
Decrypt one draft-ietf-hpke-pq-05 message, from wg.seal or from any implementation of the same suite. kem, kdf, aead, info, aad, psk and pskId must match the sender's. Returns the plaintext as a Buffer.
Throws crypto-hpke-pq/open-failed when the AEAD tag does not verify, crypto-hpke-pq/bad-opt for a missing or malformed option, crypto-hpke-pq/unsupported-suite for a KEM, KDF or AEAD outside the list in the module notes, crypto-hpke-pq/bad-key for a private key or enc that does not match the KEM, crypto-hpke-pq/inconsistent-psk when only one of psk and pskId is given, crypto-hpke-pq/weak-psk for a psk shorter than 32 bytes, crypto-hpke-pq/export-only for the export-only AEAD, and crypto-hpke-pq/input-length for an info, psk or pskId over 65,535 bytes under SHAKE256.
var pair = b.crypto.hpke.pq.wg.generateKeyPair();
var sealed = b.crypto.hpke.pq.wg.seal({ recipientPubKey: pair.publicKey, plaintext: "hello" });
var pt = b.crypto.hpke.pq.wg.open({
privateKey: pair.privateKey, enc: sealed.enc,
ciphertext: sealed.ciphertext,
});
pt.toString("utf8"); // → "hello"
b.crypto.hpke.pq.wg.setupSender(opts) #
{
recipientPubKey: Buffer|string|KeyObject,
info: Buffer|string,
kem: string,
kdf: string,
aead: string, // "Export-only" for a context used only to export
psk: Buffer|string, // at least 32 bytes, with pskId
pskId: Buffer|string,
}
Run the draft-ietf-hpke-pq-05 sender setup once and return { enc, seal(plaintext, aad?), export(exporterContext, length) }, the RFC 9180 context for a sequence of messages. Each seal uses the next sequence number.
Throws crypto-hpke-pq/bad-opt, crypto-hpke-pq/unsupported-suite, crypto-hpke-pq/bad-key, crypto-hpke-pq/inconsistent-psk and crypto-hpke-pq/weak-psk and crypto-hpke-pq/input-length as wg.seal does. The context's seal throws crypto-hpke-pq/bad-opt for a missing plaintext (pass "" for an empty message), crypto-hpke-pq/export-only under the export-only AEAD and crypto-hpke-pq/message-limit when the sequence number would overflow; its export throws crypto-hpke-pq/export-length for a length above 65,535 under SHAKE256 or above 255 times the HKDF output size.
var pair = b.crypto.hpke.pq.wg.generateKeyPair();
var sender = b.crypto.hpke.pq.wg.setupSender({ recipientPubKey: pair.publicKey });
var key = sender.export("session key", 32);
key.length; // → 32
b.crypto.hpke.pq.wg.setupRecipient(opts) #
{
privateKey: Buffer|string|KeyObject,
enc: Buffer,
info: Buffer|string,
kem: string,
kdf: string,
aead: string,
psk: Buffer|string, // at least 32 bytes, as given to setupSender
pskId: Buffer|string,
}
Run the draft-ietf-hpke-pq-05 recipient setup for enc and return { open(ciphertext, aad?), export(exporterContext, length) }. Each open uses the next sequence number; a failed open does not advance it.
Throws crypto-hpke-pq/bad-opt, crypto-hpke-pq/unsupported-suite, crypto-hpke-pq/bad-key, crypto-hpke-pq/inconsistent-psk and crypto-hpke-pq/weak-psk and crypto-hpke-pq/input-length as wg.open does. The context's open throws crypto-hpke-pq/open-failed, crypto-hpke-pq/export-only and crypto-hpke-pq/message-limit; its export throws crypto-hpke-pq/export-length.
var pair = b.crypto.hpke.pq.wg.generateKeyPair();
var sender = b.crypto.hpke.pq.wg.setupSender({ recipientPubKey: pair.publicKey });
var recip = b.crypto.hpke.pq.wg.setupRecipient({
privateKey: pair.privateKey, enc: sender.enc,
});
recip.export("session key", 32).equals(sender.export("session key", 32)); // → true
b.crypto.hpke.pq.wg.generateKeyPair(opts?) #
{
kem: string, // "ML-KEM-1024" (default) | "MLKEM1024-P384"
}
Generate a key pair for kem in the HPKE serialized forms: { publicKey, privateKey }. For ML-KEM-1024 (the default) the public key is 1568 bytes and the private key is the 64-byte seed; for MLKEM1024-P384 the public key is 1665 bytes and the private key is a 32-byte seed. Throws crypto-hpke-pq/bad-opt for an unknown option and crypto-hpke-pq/unsupported-suite for another KEM.
var pair = b.crypto.hpke.pq.wg.generateKeyPair({ kem: "MLKEM1024-P384" });
pair.publicKey.length; // → 1665
pair.privateKey.length; // → 32
b.crypto.hpke.generateKeyPair() #
Generate an ML-KEM-1024 key pair as PEM strings: { publicKey, privateKey }, an SPKI public key and a PKCS#8 private key. The pair also works with b.crypto.hpke.pq.connolly and b.crypto.hpke.pq.wg under their ML-KEM-1024 suites.
var pair = b.crypto.hpke.generateKeyPair();
pair.publicKey.indexOf("-----BEGIN PUBLIC KEY-----"); // → 0
b.crypto.hpke.seal(opts) #
{
recipientPubKey: string, // ML-KEM-1024 SPKI PEM
plaintext: Buffer|string,
info: Buffer|string, // bound into the key derivation
aad: Buffer|string, // additional authenticated data
}
Seal plaintext to an ML-KEM-1024 public key in the blamejs envelope format and return { enc, ciphertext }: enc is the 1568-byte ML-KEM-1024 ciphertext and ciphertext is the XChaCha20-Poly1305 output, the plaintext followed by a 16-byte tag. The envelope opens only with b.crypto.hpke.open; it is not an RFC 9180 message.
Throws validate-opts/bad-object when opts is not an object, crypto-hpke/bad-opt for a missing recipientPubKey or plaintext, crypto-hpke/bad-key for a recipientPubKey that is not a readable public key or is a key of any type other than ML-KEM-1024 (RSA, EC, X25519, X448, ML-KEM-512 and ML-KEM-768 keys are refused), crypto-hpke/kem-encap-failed when encapsulation fails, and crypto-hpke/aead-encrypt-failed when encryption fails.
var pair = b.crypto.hpke.generateKeyPair();
var sealed = b.crypto.hpke.seal({
recipientPubKey: pair.publicKey,
plaintext: "hello",
info: "session-1",
});
sealed.ciphertext.length; // → 21
b.crypto.hpke.open(opts) #
{
privateKey: string, // ML-KEM-1024 PKCS#8 PEM
enc: Buffer,
ciphertext: Buffer,
info: Buffer|string,
aad: Buffer|string,
}
Open an envelope produced by b.crypto.hpke.seal and return the plaintext as a Buffer. info and aad must equal the values given to seal. A failed tag emits a system.hpke.open audit row with outcome failure.
Throws validate-opts/bad-object when opts is not an object, crypto-hpke/bad-opt for a missing privateKey or a non-Buffer enc or ciphertext, crypto-hpke/bad-key for a privateKey that is not a readable private key or is a key of any type other than ML-KEM-1024, crypto-hpke/kem-decap-failed when decapsulation fails, and crypto-hpke/aead-decrypt-failed when the tag does not verify.
var pair = b.crypto.hpke.generateKeyPair();
var sealed = b.crypto.hpke.seal({ recipientPubKey: pair.publicKey, plaintext: "hello" });
var pt = b.crypto.hpke.open({
privateKey: pair.privateKey, enc: sealed.enc, ciphertext: sealed.ciphertext,
});
pt.toString("utf8"); // → "hello"