ML-KEM + X25519 hybrid post-quantum encryption.
Provides quantum-resistant key encapsulation by combining:
- ML-KEM (NIST FIPS 203) — lattice-based post-quantum KEM
- X25519 — classical elliptic-curve Diffie-Hellman
Security Levels
| Level | ML-KEM | NIST Category | Equivalent | Version Tag |
|---|---|---|---|---|
| Cat-1 | 512 | 1 | ~AES-128 | 0x01 |
| Cat-3 | 768 | 3 | ~AES-192 | 0x02 |
| Cat-5 | 1024 | 5 | ~AES-256 | 0x03 |
Pass :cat1, :cat3, or :cat5 to choose the security level. The default is :cat3.
NIST defines ML-KEM only at categories 1/3/5
FIPS 203 standardizes ML-KEM at parameter sets 512/768/1024, mapping to NIST security categories 1/3/5. There is intentionally no Cat-2 or Cat-4 KEM — this module never invents one. The classical half of the hybrid is always X25519 (~Cat-1 classical), so X25519 is the classical floor at every tier; the post-quantum half is what grows from Cat-1 → Cat-3 → Cat-5.
Note the per-artifact-type wire-format version tags: on the KEM side 0x01
denotes Cat-1 (ML-KEM-512), whereas on the signature side 0x01 denotes
Cat-2 (ML-DSA-44). The version byte is scoped to its artifact type; the two
schemes agree on the shared Cat-3/Cat-5 rungs.
Ciphertext formats
v1 (Cat-1): 0x01 || ML-KEM-512 ct (768 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct
v2 (Cat-3): 0x02 || ML-KEM-768 ct (1088 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct
v3 (Cat-5): 0x03 || ML-KEM-1024 ct (1568 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ctKey sizes
| Component | Cat-1 | Cat-3 | Cat-5 |
|---|---|---|---|
| Public key | 832 bytes | 1216 bytes | 1600 bytes |
| Secret key (seed) | 32 bytes | 32 bytes | 32 bytes |
Usage
# Cat-3 (default)
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair()
{:ok, ct} = MetamorphicCrypto.Hybrid.seal("quantum-safe secret", pk)
{:ok, "quantum-safe secret"} = MetamorphicCrypto.Hybrid.open(ct, sk)
# Cat-1 (ML-KEM-512, lightest standardized tier)
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair(:cat1)
{:ok, ct} = MetamorphicCrypto.Hybrid.seal("quantum-safe secret", pk, :cat1)
{:ok, "quantum-safe secret"} = MetamorphicCrypto.Hybrid.open(ct, sk)
# Cat-5 (ML-KEM-1024, highest security)
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair(:cat5)
{:ok, ct} = MetamorphicCrypto.Hybrid.seal("top secret", pk, :cat5)
{:ok, "top secret"} = MetamorphicCrypto.Hybrid.open(ct, sk)
Summary
Functions
Generate a hybrid ML-KEM + X25519 keypair.
Generate a hybrid ML-KEM-512 + X25519 keypair (Cat-1, lightest standardized tier).
Generate a hybrid ML-KEM-1024 + X25519 keypair (Cat-5, highest security).
Generate a keypair for the given suite/0 + security_level/0.
Check if a base64 ciphertext uses the hybrid format (v1 Cat-1, v2 Cat-3, or v3 Cat-5).
Open a hybrid-sealed ciphertext, returning the decrypted UTF-8 string.
Open a hybrid-sealed ciphertext using an explicit context_label.
Open a hybrid-sealed ciphertext, returning plaintext as base64.
Open a hybrid-sealed ciphertext with an explicit context_label, returning
plaintext as base64. See open/3.
Seal a UTF-8 plaintext string using hybrid post-quantum encryption.
Seal a UTF-8 plaintext string using Cat-1 hybrid post-quantum encryption (ML-KEM-512).
Seal a UTF-8 plaintext string using Cat-5 hybrid post-quantum encryption (ML-KEM-1024).
The default versioned context label bound into the new CNSA 2.0 seal suites:
"metamorphic/seal/v1".
Seal raw bytes (as base64) using hybrid post-quantum encryption.
Seal raw bytes (as base64) using Cat-1 hybrid post-quantum encryption.
Seal raw bytes (as base64) using Cat-5 hybrid post-quantum encryption.
Seal a UTF-8 plaintext string under the given suite/0 + security_level/0.
Seal raw bytes (as base64) under the given suite/0 + security_level/0.
Types
@type security_level() :: :cat1 | :cat3 | :cat5
NIST post-quantum security level.
:cat1— ML-KEM-512 + X25519 (~AES-128, NIST Category 1).:cat3— ML-KEM-768 + X25519 (~AES-192, NIST Category 3). Default.:cat5— ML-KEM-1024 + X25519 (~AES-256, NIST Category 5).
@type suite() :: :hybrid | :hybrid_matched | :pure_cnsa2
CNSA 2.0 suite axis (orthogonal to security_level/0).
security_level picks the ML-KEM parameter set; suite picks the
composition posture:
:hybrid— default & recommended. Existing ML-KEM + X25519 strict-AND construction. Byte-for-byte unchanged; classical floor is X25519 (~Cat-1) at every level. Wire tags0x01/0x02/0x03.:hybrid_matched— opt-in. The classical partner is matched to the PQ category so it is never the weak link: Cat-1 → X25519 (identical to:hybrid, no new format), Cat-3 → X448 (tag0x13), Cat-5 → P-521 ECDH (tag0x14).:pure_cnsa2— opt-in, Cat-5 only. Pure ML-KEM-1024 + AES-256-GCM, no classical half — the NSA CNSA 2.0 box (tag0x10). Standards-compliant but without the classical backstop the default:hybridkeeps until the lattice implementation is independently audited.
Honest posture
CNSA 2.0 algorithm suite, NCC-audited primitives, pure-Rust and memory-safe
(#![forbid(unsafe_code)] at our layer) — not "FIPS 140-3 validated."
Functions
@spec generate_keypair(security_level()) :: {String.t(), String.t()}
Generate a hybrid ML-KEM + X25519 keypair.
Accepts an optional security_level/0 (defaults to :cat3).
Returns {public_key_b64, secret_key_b64}.
Examples
# Cat-3 (default)
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair()
# Cat-5
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair(:cat5)
Generate a hybrid ML-KEM-512 + X25519 keypair (Cat-1, lightest standardized tier).
Convenience alias for generate_keypair(:cat1).
Returns {public_key_b64, secret_key_b64}.
Example
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair_512()
Generate a hybrid ML-KEM-1024 + X25519 keypair (Cat-5, highest security).
Convenience alias for generate_keypair(:cat5).
Returns {public_key_b64, secret_key_b64}.
Example
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair_1024()
@spec generate_keypair_suite(suite(), security_level()) :: {:ok, {String.t(), String.t()}} | {:error, String.t()}
Generate a keypair for the given suite/0 + security_level/0.
:hybrid (any level) and :hybrid_matched at :cat1 produce the existing
ML-KEM + X25519 keys (byte-identical to generate_keypair/1). The matched
Cat-3/Cat-5 and pure suites produce the new combined-key layouts; the secret
key stays a single 32-byte root seed.
Returns {:ok, {public_key_b64, secret_key_b64}}, or {:error, reason} for
unsupported combinations (e.g. :pure_cnsa2 below :cat5).
Examples
# Pure CNSA 2.0 (ML-KEM-1024, Cat-5 only)
{:ok, {pk, sk}} = MetamorphicCrypto.Hybrid.generate_keypair_suite(:pure_cnsa2, :cat5)
# Matched-strength hybrid (ML-KEM-768 + X448)
{:ok, {pk, sk}} = MetamorphicCrypto.Hybrid.generate_keypair_suite(:hybrid_matched, :cat3)
Check if a base64 ciphertext uses the hybrid format (v1 Cat-1, v2 Cat-3, or v3 Cat-5).
Example
MetamorphicCrypto.Hybrid.hybrid_ciphertext?(ciphertext)
#=> true
@spec open(ciphertext_b64 :: String.t(), secret_key_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Open a hybrid-sealed ciphertext, returning the decrypted UTF-8 string.
Automatically detects the suite + level from the version tag byte: legacy
:hybrid Cat-1/Cat-3/Cat-5 (0x01/0x02/0x03) and the new CNSA 2.0 suites
(0x10 PureCnsa2, 0x13/0x14 HybridMatched). The new suites are opened with
the default context label (seal_context_v1/0); use open/3 to supply a
custom per-tenant label.
Returns {:ok, plaintext} or {:error, reason}.
Example
{pk, sk} = MetamorphicCrypto.Hybrid.generate_keypair()
{:ok, ct} = MetamorphicCrypto.Hybrid.seal("hello PQ", pk)
{:ok, "hello PQ"} = MetamorphicCrypto.Hybrid.open(ct, sk)
@spec open( ciphertext_b64 :: String.t(), secret_key_b64 :: String.t(), context_label :: String.t() ) :: {:ok, String.t()} | {:error, String.t()}
Open a hybrid-sealed ciphertext using an explicit context_label.
Required only for ciphertexts produced by a new CNSA 2.0 suite
(:hybrid_matched Cat-3/Cat-5, :pure_cnsa2) with a non-default context
label. Legacy :hybrid tags (0x01/0x02/0x03) ignore the label. The
label must match the one passed to seal_suite/5 at seal time.
Returns {:ok, plaintext} or {:error, reason}.
Example
{:ok, ct} =
MetamorphicCrypto.Hybrid.seal_suite("secret", pk, :pure_cnsa2, :cat5,
context_label: "mosslet/seal/v1")
{:ok, "secret"} = MetamorphicCrypto.Hybrid.open(ct, sk, "mosslet/seal/v1")
@spec open_raw(ciphertext_b64 :: String.t(), secret_key_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Open a hybrid-sealed ciphertext, returning plaintext as base64.
Automatically detects Cat-1, Cat-3, or Cat-5 from the version tag.
Returns {:ok, plaintext_b64} or {:error, reason}.
@spec open_raw( ciphertext_b64 :: String.t(), secret_key_b64 :: String.t(), context_label :: String.t() ) :: {:ok, String.t()} | {:error, String.t()}
Open a hybrid-sealed ciphertext with an explicit context_label, returning
plaintext as base64. See open/3.
Returns {:ok, plaintext_b64} or {:error, reason}.
@spec seal(plaintext :: String.t(), public_key_b64 :: String.t(), security_level()) :: {:ok, String.t()} | {:error, String.t()}
Seal a UTF-8 plaintext string using hybrid post-quantum encryption.
Accepts an optional security_level/0 (defaults to :cat3).
Returns {:ok, ciphertext_b64} or {:error, reason}.
Examples
# Cat-3 (default)
{pk, _sk} = MetamorphicCrypto.Hybrid.generate_keypair()
{:ok, ct} = MetamorphicCrypto.Hybrid.seal("secret data", pk)
# Cat-5
{pk, _sk} = MetamorphicCrypto.Hybrid.generate_keypair(:cat5)
{:ok, ct} = MetamorphicCrypto.Hybrid.seal("secret data", pk, :cat5)
@spec seal_512(plaintext :: String.t(), public_key_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Seal a UTF-8 plaintext string using Cat-1 hybrid post-quantum encryption (ML-KEM-512).
Convenience alias for seal(plaintext, public_key_b64, :cat1).
Returns {:ok, ciphertext_b64} or {:error, reason}.
Example
{pk, _sk} = MetamorphicCrypto.Hybrid.generate_keypair(:cat1)
{:ok, ct} = MetamorphicCrypto.Hybrid.seal_512("quantum-safe secret", pk)
@spec seal_1024(plaintext :: String.t(), public_key_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Seal a UTF-8 plaintext string using Cat-5 hybrid post-quantum encryption (ML-KEM-1024).
Convenience alias for seal(plaintext, public_key_b64, :cat5).
Returns {:ok, ciphertext_b64} or {:error, reason}.
Example
{pk, _sk} = MetamorphicCrypto.Hybrid.generate_keypair(:cat5)
{:ok, ct} = MetamorphicCrypto.Hybrid.seal_1024("top secret", pk)
@spec seal_context_v1() :: String.t()
The default versioned context label bound into the new CNSA 2.0 seal suites:
"metamorphic/seal/v1".
Grammar: "<namespace>/<purpose>/v<major>". The namespace is the one
per-tenant knob (pass your own, e.g. "mosslet/seal/v1"); the label is bound
into both the HKDF-SHA512 info and the AES-256-GCM AAD. It is ignored by the
legacy :hybrid suite (tags 0x01/0x02/0x03), which binds no context.
Example
iex> MetamorphicCrypto.Hybrid.seal_context_v1()
"metamorphic/seal/v1"
@spec seal_raw( plaintext_b64 :: String.t(), public_key_b64 :: String.t(), security_level() ) :: {:ok, String.t()} | {:error, String.t()}
Seal raw bytes (as base64) using hybrid post-quantum encryption.
Accepts an optional security_level/0 (defaults to :cat3).
Returns {:ok, ciphertext_b64} or {:error, reason}.
@spec seal_raw_512(plaintext_b64 :: String.t(), public_key_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Seal raw bytes (as base64) using Cat-1 hybrid post-quantum encryption.
Convenience alias for seal_raw(plaintext_b64, public_key_b64, :cat1).
Returns {:ok, ciphertext_b64} or {:error, reason}.
@spec seal_raw_1024(plaintext_b64 :: String.t(), public_key_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Seal raw bytes (as base64) using Cat-5 hybrid post-quantum encryption.
Convenience alias for seal_raw(plaintext_b64, public_key_b64, :cat5).
Returns {:ok, ciphertext_b64} or {:error, reason}.
@spec seal_suite( plaintext :: String.t(), public_key_b64 :: String.t(), suite(), security_level(), opts :: keyword() ) :: {:ok, String.t()} | {:error, String.t()}
Seal a UTF-8 plaintext string under the given suite/0 + security_level/0.
Accepts an optional :context_label (defaults to seal_context_v1/0) which is
bound into the HKDF-SHA512 info and AES-256-GCM AAD for the new CNSA 2.0
suites; it is ignored by the legacy :hybrid suite. Open the result with
open/2 (default label) or open/3 (custom label).
Returns {:ok, ciphertext_b64} or {:error, reason}.
Examples
{:ok, {pk, sk}} = MetamorphicCrypto.Hybrid.generate_keypair_suite(:pure_cnsa2, :cat5)
{:ok, ct} = MetamorphicCrypto.Hybrid.seal_suite("top secret", pk, :pure_cnsa2, :cat5)
{:ok, "top secret"} = MetamorphicCrypto.Hybrid.open(ct, sk)
# Custom per-tenant context label
{:ok, ct} =
MetamorphicCrypto.Hybrid.seal_suite("secret", pk, :pure_cnsa2, :cat5,
context_label: "mosslet/seal/v1")
{:ok, "secret"} = MetamorphicCrypto.Hybrid.open(ct, sk, "mosslet/seal/v1")
@spec seal_suite_raw( plaintext_b64 :: String.t(), public_key_b64 :: String.t(), suite(), security_level(), opts :: keyword() ) :: {:ok, String.t()} | {:error, String.t()}
Seal raw bytes (as base64) under the given suite/0 + security_level/0.
Same as seal_suite/5 but accepts pre-encoded base64 plaintext. Supports the
same :context_label option.
Returns {:ok, ciphertext_b64} or {:error, reason}.