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
| Variant | Security | Public Key | Secret Key | Ciphertext | Shared Secret |
|---|---|---|---|---|---|
| MLKEM_512 | AES 128 equivalent | 800 bytes | 1,632 bytes | 768 bytes | 32 bytes |
| MLKEM_768 | AES 192 equivalent | 1,184 bytes | 2,400 bytes | 1,088 bytes | 32 bytes |
| MLKEM_1024 | AES 256 equivalent | 1,568 bytes | 3,168 bytes | 1,568 bytes | 32 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)
- Alice and Bob exchange public keys
- Both compute the same shared secret independently
- Symmetric: either party can initiate
KEM (ML KEM)
- Bob publishes his public key
- Alice generates a random message and encapsulates it for Bob
- Alice sends the ciphertext to Bob
- Bob decapsulates to recover the shared secret
- 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.