MetamorphicCrypto.Hybrid (metamorphic_crypto v0.8.0)

Copy Markdown View Source

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

LevelML-KEMNIST CategoryEquivalentVersion Tag
Cat-15121~AES-1280x01
Cat-37683~AES-1920x02
Cat-510245~AES-2560x03

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 ct

Key sizes

ComponentCat-1Cat-3Cat-5
Public key832 bytes1216 bytes1600 bytes
Secret key (seed)32 bytes32 bytes32 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

Types

NIST post-quantum security level.

CNSA 2.0 suite axis (orthogonal to security_level/0).

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).

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.

Types

security_level()

@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).

suite()

@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:

  • :hybriddefault & recommended. Existing ML-KEM + X25519 strict-AND construction. Byte-for-byte unchanged; classical floor is X25519 (~Cat-1) at every level. Wire tags 0x01/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 (tag 0x13), Cat-5 → P-521 ECDH (tag 0x14).
  • :pure_cnsa2 — opt-in, Cat-5 only. Pure ML-KEM-1024 + AES-256-GCM, no classical half — the NSA CNSA 2.0 box (tag 0x10). Standards-compliant but without the classical backstop the default :hybrid keeps 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

generate_keypair(level \\ :cat3)

@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_keypair_512()

@spec generate_keypair_512() :: {String.t(), String.t()}

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_keypair_1024()

@spec generate_keypair_1024() :: {String.t(), String.t()}

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()

generate_keypair_suite(suite, level \\ :cat5)

@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)

hybrid_ciphertext?(ciphertext_b64)

@spec hybrid_ciphertext?(ciphertext_b64 :: String.t()) :: boolean()

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

open(ciphertext_b64, secret_key_b64)

@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)

open(ciphertext_b64, secret_key_b64, context_label)

@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")

open_raw(ciphertext_b64, secret_key_b64)

@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}.

open_raw(ciphertext_b64, secret_key_b64, context_label)

@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}.

seal(plaintext, public_key_b64, level \\ :cat3)

@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)

seal_512(plaintext, public_key_b64)

@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)

seal_1024(plaintext, public_key_b64)

@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)

seal_context_v1()

@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"

seal_raw(plaintext_b64, public_key_b64, level \\ :cat3)

@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}.

seal_raw_512(plaintext_b64, public_key_b64)

@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}.

seal_raw_1024(plaintext_b64, public_key_b64)

@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}.

seal_suite(plaintext, public_key_b64, suite, level, opts \\ [])

@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")

seal_suite_raw(plaintext_b64, public_key_b64, suite, level, opts \\ [])

@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}.