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 |
|---|---|---|---|
|
Hybrid KEM |
— |
Recommended default. Classical + PQC. |
|
Pure PQC KEM |
1 |
Smallest key/ciphertext. Use ML-KEM-768 for new deployments. |
|
Pure PQC KEM |
3 |
Recommended pure-PQC choice. |
|
Pure PQC KEM |
5 |
Maximum security. |
|
Hybrid KEM |
— |
Higher security margin. |
|
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)
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.