Key encapsulation (KEM)

Key encapsulation mechanisms (KEMs) let two parties establish a shared secret without transmitting the secret itself. The sender encapsulates a random secret using the recipient’s public key; the recipient decapsulates it using their secret key.

Choosing an algorithm

Algorithm

Type

NIST level

Notes

X25519+ML-KEM-768

Hybrid KEM

Recommended default. Classical + PQC.

ML-KEM-512

Pure PQC KEM

1

Smallest key/ciphertext. Use ML-KEM-768 for new deployments.

ML-KEM-768

Pure PQC KEM

3

Recommended pure-PQC choice.

ML-KEM-1024

Pure PQC KEM

5

Maximum security.

X25519+ML-KEM-1024

Hybrid KEM

Higher security margin.

P-256+ML-KEM-768

Hybrid KEM

When P-256 compatibility is required.

HybridKEM

HybridKEM is the high-level hybrid KEM. It combines X25519 with an ML-KEM variant and uses an HKDF-SHA256 combiner.

from quantum_safe import HybridKEM

# Default: X25519 + ML-KEM-768
kem = HybridKEM()

# Generate a keypair for the recipient
kp = kem.generate_keypair()

# Sender: encapsulate — ct goes to recipient, ss is used locally
ct, ss = kem.encapsulate(kp.public)

# Recipient: decapsulate
ss2 = kem.decapsulate(kp.secret, ct)

assert ss == ss2

Choosing a different combination:

kem = HybridKEM(classical="X25519", pqc="ML-KEM-1024")
kem = HybridKEM(classical="P-256",  pqc="ML-KEM-768")

KEM (pure PQC)

KEM uses a single PQC algorithm. Not recommended for new deployments — prefer HybridKEM.

from quantum_safe import KEM

kem = KEM("ML-KEM-768")
kp  = kem.generate_keypair()
ct, ss = kem.encapsulate(kp.public)
ss2    = kem.decapsulate(kp.secret, ct)

SharedSecret

SharedSecret is not a plain bytes object. Call derive_key() to produce symmetric key material:

# Derive separate keys for encryption and MAC
enc_key = ss.derive_key(32, info=b"myapp-enc-v1")
mac_key = ss.derive_key(32, info=b"myapp-mac-v1")

# Or a single key for AES-256-GCM
aes_key = ss.derive_key(32, info=b"myapp-aes-v1")

The info parameter provides domain separation — use a unique value for each derived key in your application.

Envelope (high-level encryption)

Envelope wraps KEM + AES-256-GCM into a single seal / open API. This is the recommended way to encrypt data:

from quantum_safe import HybridKEM
from quantum_safe.protocols import Envelope

kp = HybridKEM().generate_keypair()

# Encrypt
sealed = Envelope.seal(b"secret payload", kp.public)

# Decrypt
plain = Envelope.open(sealed, kp.secret)

# With authenticated additional data (visible but authenticated)
sealed = Envelope.seal(b"payload", kp.public, aad=b"recipient:user-42")
plain  = Envelope.open(sealed, kp.secret, aad=b"recipient:user-42")

# Serialize for transport
wire   = sealed.to_bytes()
sealed = sealed.__class__.from_bytes(wire)

Key serialization

# Serialize
pem  = kp.public.to_pem()
cbor = kp.public.to_cbor()
jwk  = kp.public.to_jwk()
pem_sec = kp.secret.to_pem()

# Deserialize
from quantum_safe.types import PublicKey, SecretKey
pub = PublicKey.from_pem(pem)
sec = SecretKey.from_pem(pem_sec)

Note

SecretKey zeros its memory buffer on deletion using ctypes.memset against the live bytearray — a Python byte-loop is subject to dead-store elimination by the optimizer and is not used here. Python’s garbage collector still makes hard guarantees impossible, but this reduces the window during which secret material is visible in heap dumps. Callers that need to zero a copy immediately after use should call secret_key._raw_bytearray and zero it with ctypes.memset in a try/finally block.