Skip to main content

ML KEM (Post Quantum Key Exchange)

Module Lattice Key Encapsulation Mechanism. This is the NIST post quantum key exchange standard, formerly known as Kyber. Use this to establish shared secrets that will remain secure against quantum computers.

local MlKem = Cryptography.Verification.MlKem

Security Levels

VariantSecurityPublic KeySecret KeyCiphertextShared Secret
MLKEM_512AES 128 equivalent800 bytes1,632 bytes768 bytes32 bytes
MLKEM_768AES 192 equivalent1,184 bytes2,400 bytes1,088 bytes32 bytes
MLKEM_1024AES 256 equivalent1,568 bytes3,168 bytes1,568 bytes32 bytes

Compare to X25519's 32 byte keys and no ciphertext (just public key exchange).

Key Generation

MLKEM_512.GenerateKeys() -> (buffer, buffer)

MLKEM_768.GenerateKeys() -> (buffer, buffer)

MLKEM_1024.GenerateKeys() -> (buffer, buffer)

Generates an encapsulation keypair. Returns (PublicKey, SecretKey).

local PublicKey, SecretKey = MlKem.MLKEM_768.GenerateKeys()

MLKEM_512.KeyGen(D: buffer, Z: buffer) -> (buffer, buffer)

MLKEM_768.KeyGen(D: buffer, Z: buffer) -> (buffer, buffer)

MLKEM_1024.KeyGen(D: buffer, Z: buffer) -> (buffer, buffer)

Deterministic key generation from seed values.

local D = CSPRNG.RandomBytes(32)
local Z = CSPRNG.RandomBytes(32)
local PublicKey, SecretKey = MlKem.MLKEM_768.KeyGen(D, Z)

Encapsulation

MLKEM_512.Encapsulate(Message: buffer, PublicKey: buffer) -> (buffer?, buffer?)

MLKEM_768.Encapsulate(Message: buffer, PublicKey: buffer) -> (buffer?, buffer?)

MLKEM_1024.Encapsulate(Message: buffer, PublicKey: buffer) -> (buffer?, buffer?)

Creates a ciphertext and shared secret using the recipient's public key.

local Message = CSPRNG.RandomBytes(32)
local Ciphertext, SharedSecret = MlKem.MLKEM_768.Encapsulate(Message, RecipientPublicKey)
if Ciphertext and SharedSecret then
-- Send Ciphertext to recipient
-- Use SharedSecret for encryption
print(buffer.len(SharedSecret)) -- 32
end

Returns nil, nil if the public key is invalid.

Decapsulation

MLKEM_512.Decapsulate(Ciphertext: buffer, SecretKey: buffer) -> buffer

MLKEM_768.Decapsulate(Ciphertext: buffer, SecretKey: buffer) -> buffer

MLKEM_1024.Decapsulate(Ciphertext: buffer, SecretKey: buffer) -> buffer

Recovers the shared secret using the secret key.

local SharedSecret = MlKem.MLKEM_768.Decapsulate(Ciphertext, SecretKey)
print(buffer.len(SharedSecret)) -- 32

-- SharedSecret matches what Encapsulate produced

KEM vs DH

Key Encapsulation Mechanisms work differently than Diffie Hellman:

Diffie Hellman (X25519)

  1. Alice and Bob exchange public keys
  2. Both compute the same shared secret independently
  3. Symmetric: either party can initiate

KEM (ML KEM)

  1. Bob publishes his public key
  2. Alice generates a random message and encapsulates it for Bob
  3. Alice sends the ciphertext to Bob
  4. Bob decapsulates to recover the shared secret
  5. Asymmetric: Alice initiates, Bob receives

The KEM model maps naturally to how most protocols work anyway (client initiates connection to server).

How It Works

ML KEM is based on the hardness of the Module Learning With Errors (M LWE) problem. The encapsulation process generates a random message, encrypts it using the public key via a lattice based encryption scheme, and derives the final shared secret.

The decapsulation process decrypts the ciphertext using the secret key, re encrypts the decrypted message and checks it matches, and if valid derives the shared secret the same way. If invalid, it derives a pseudorandom value (prevents leaking information about failures).

This re encryption check is an implicit rejection mechanism that provides IND CCA2 security.

Examples

-- Receiver generates keypair
local PublicKey, SecretKey = MlKem.MLKEM_768.GenerateKeys()

-- Sender encapsulates a shared secret
local Message = CSPRNG.RandomBytes(32)
local Ciphertext, SharedSecret = MlKem.MLKEM_768.Encapsulate(Message, PublicKey)

-- Sender encrypts data with shared secret
local Nonce = CSPRNG.RandomBytes(12)
local EncryptedData, Tag = AEAD.Encrypt(Data, SharedSecret, Nonce)

-- Send: Ciphertext, Nonce, EncryptedData, Tag

-- Receiver decapsulates to get same shared secret
local SharedSecret = MlKem.MLKEM_768.Decapsulate(Ciphertext, SecretKey)

-- Receiver decrypts data
local Data = AEAD.Decrypt(EncryptedData, SharedSecret, Nonce, Tag)

When to Use ML KEM

Use ML KEM when:

  • You need key exchange that survives quantum computers
  • Harvest now, decrypt later is a concern
  • Regulatory requirements mandate post quantum crypto
  • You are building long lived infrastructure

Stick with X25519 when:

  • Bandwidth is constrained (ML KEM ciphertexts are 768 to 1568 bytes)
  • Performance is critical
  • You can migrate to post quantum later

Performance Notes

ML KEM is faster than X25519.

The bigger concern is typically bandwidth rather than computation. Make sure your protocol can handle the larger key and ciphertext sizes.