What is the problem this feature will solve?
crypto.encapsulate(key[, callback]) performs randomized ML-KEM encapsulation only (FIPS 203 §6.1).
There is no way to supply the encapsulation randomness, so the derandomized variant —
ML-KEM.Encaps_internal (FIPS 203 §6.2, which takes a 32-byte message m) — is not reachable from
node:crypto, even on a Node backed by OpenSSL 3.5.x.
OpenSSL already exposes it: EVP_PKEY_encapsulate_init() accepts an ikme parameter
(EVP_KEM-ML-KEM), and the symmetric half —
KeyGen_internal — is already reachable from Node via raw-seed key import. Only the encapsulation
half is missing.
Derandomized encapsulation is needed to build standardized hybrid KEMs on top of native ML-KEM — in
particular X-Wing (draft-connolly-cfrg-xwing-kem),
whose Encapsulate is defined in terms of ML-KEM's internal encapsulation — and to reproduce the
FIPS 203 §6.2 / ACVP Encaps_internal known-answer vectors. Without it, a hybrid-KEM implementation
has to ship its own ML-KEM in JS instead of using the native one.
Reproduction
const crypto = require('node:crypto');
const { publicKey } = crypto.generateKeyPairSync('ml-kem-768');
crypto.encapsulate(publicKey); // ok — randomized
crypto.encapsulate(publicKey, { entropy: Buffer.alloc(32) }); // there is no options argument today:
// → throws: The "callback" argument must be of type function.
What is the feature you are proposing to solve the problem?
An option on crypto.encapsulate() carrying the 32-byte encapsulation input, mapped to OpenSSL's
ikme KEM parameter:
const { sharedKey, ciphertext } = crypto.encapsulate(key, { entropy }); // entropy: 32-byte view
- The existing
encapsulate(key) / encapsulate(key, callback) forms are unchanged; the option is
additive, in both the sync and async forms.
- For ML-KEM the value must be exactly 32 bytes.
- It should be documented as an advanced, opt-in parameter, not the default path: the value must be
32 fresh, uniformly-random bytes and must never be reused — a low-entropy, reused, or known value
compromises the shared secret. OpenSSL documents the same caveat for ikme.
What alternatives have you considered?
Shipping a separate ML-KEM in JS for the derandomized path, which duplicates the audited primitive
the native API already provides.
What is the problem this feature will solve?
crypto.encapsulate(key[, callback])performs randomized ML-KEM encapsulation only (FIPS 203 §6.1).There is no way to supply the encapsulation randomness, so the derandomized variant —
ML-KEM.Encaps_internal(FIPS 203 §6.2, which takes a 32-byte messagem) — is not reachable fromnode:crypto, even on a Node backed by OpenSSL 3.5.x.OpenSSL already exposes it:
EVP_PKEY_encapsulate_init()accepts anikmeparameter(
EVP_KEM-ML-KEM), and the symmetric half —KeyGen_internal— is already reachable from Node viaraw-seedkey import. Only the encapsulationhalf is missing.
Derandomized encapsulation is needed to build standardized hybrid KEMs on top of native ML-KEM — in
particular X-Wing (
draft-connolly-cfrg-xwing-kem),whose
Encapsulateis defined in terms of ML-KEM's internal encapsulation — and to reproduce theFIPS 203 §6.2 / ACVP
Encaps_internalknown-answer vectors. Without it, a hybrid-KEM implementationhas to ship its own ML-KEM in JS instead of using the native one.
Reproduction
What is the feature you are proposing to solve the problem?
An option on
crypto.encapsulate()carrying the 32-byte encapsulation input, mapped to OpenSSL'sikmeKEM parameter:encapsulate(key)/encapsulate(key, callback)forms are unchanged; the option isadditive, in both the sync and async forms.
32 fresh, uniformly-random bytes and must never be reused — a low-entropy, reused, or known value
compromises the shared secret. OpenSSL documents the same caveat for
ikme.What alternatives have you considered?
Shipping a separate ML-KEM in JS for the derandomized path, which duplicates the audited primitive
the native API already provides.