const algorithm = { name: "ML-KEM-768" }; export async function agree() { // Receiver: a key pair; only the encapsulation key is handed out const { publicKey, privateKey } = await crypto.subtle.generateKey(algorithm, true, ["encapsulateBits", "decapsulateBits"]); // Sender: a fresh 32-byte shared secret and the ciphertext that carries it const { sharedKey, ciphertext } = await crypto.subtle.encapsulateBits(algorithm, publicKey); // Receiver: the same secret from the ciphertext const received = await crypto.subtle.decapsulateBits(algorithm, privateKey, ciphertext); const raw = await crypto.subtle.exportKey("raw-public", publicKey); console.log("encapsulation key", raw.byteLength, "bytes, ciphertext", ciphertext.byteLength, "bytes, shared secret", sharedKey.byteLength, "bytes"); const equal = new Uint8Array(received).every((byte, i) => byte === new Uint8Array(sharedKey)[i]); console.log("shared secret agreed:", equal);} // Decapsulate with a PKCS#8 private key (the DER of a "BEGIN PRIVATE KEY" seed file, e.g. from OpenSSL 3.5)export async function decapsulateWithPkcs8(pkcs8, ciphertext) { const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, algorithm, false, ["decapsulateBits"]); return crypto.subtle.decapsulateBits(algorithm, privateKey, ciphertext);} How it works
generateKey({ name: "ML-KEM-768" }, true, ["encapsulateBits", "decapsulateBits"])creates the pair;exportKey("raw-public", publicKey)returns the 1184-byte encapsulation key.encapsulateBits(algorithm, publicKey)resolves to{ sharedKey, ciphertext }— 32 and 1088 bytes asArrayBuffers.decapsulateBits(algorithm, privateKey, ciphertext)resolves to the same 32 bytes.encapsulateKey/decapsulateKeyreturn a readyCryptoKey(for example AES-GCM) instead of raw bits.importKey("pkcs8", …)takes the DER of OpenSSL's seed-only private key file andimportKey("spki", …)its public key; Chromium decapsulates OpenSSL's ciphertext todee35810…30f9.
Gotchas
- Support is not universal yet: Chromium 156 runs every step, Firefox 157 rejects
generateKeywithNotSupportedError, and WebKit 27.2 fails with a bareTypeError: Type error. Feature-detectcrypto.subtle.encapsulateBitsbefore relying on it. - Like every ML-KEM implementation,
decapsulateBitsreturns a different secret for a tampered ciphertext instead of rejecting it; authenticate what you protect with the derived key. - Node.js 24 implements the same methods in
crypto.subtle, marked experimental.
Related recipes
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Node.js
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 with OpenSSL
- How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Python
- How to sign and verify with ML-DSA-65 in WebCrypto
- How to derive a shared key with ECDH and HKDF in WebCrypto