{"id":50147828,"url":"https://github.com/moss-piglet/metamorphic-crypto","last_synced_at":"2026-07-01T05:01:09.459Z","repository":{"id":357612021,"uuid":"1232423642","full_name":"moss-piglet/metamorphic-crypto","owner":"moss-piglet","description":"Zero-knowledge, post-quantum client-side encryption. #![forbid(unsafe_code)]. WASM + UniFFI targets.","archived":false,"fork":false,"pushed_at":"2026-06-23T02:01:03.000Z","size":126,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-23T03:22:20.535Z","etag":null,"topics":["argon2id","cryptography","encryption","ml-kem","open-source","post-quantum","privacy","rust","wasm","webassembly","x25519","zero-knowledge"],"latest_commit_sha":null,"homepage":"https://metamorphic.app/encryption","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/moss-piglet.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["moss-piglet"]}},"created_at":"2026-05-07T23:21:09.000Z","updated_at":"2026-06-23T01:58:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/moss-piglet/metamorphic-crypto","commit_stats":null,"previous_names":["moss-piglet/metamorphic-crypto"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/moss-piglet/metamorphic-crypto","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moss-piglet%2Fmetamorphic-crypto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moss-piglet%2Fmetamorphic-crypto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moss-piglet%2Fmetamorphic-crypto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moss-piglet%2Fmetamorphic-crypto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/moss-piglet","download_url":"https://codeload.github.com/moss-piglet/metamorphic-crypto/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moss-piglet%2Fmetamorphic-crypto/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34993438,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["argon2id","cryptography","encryption","ml-kem","open-source","post-quantum","privacy","rust","wasm","webassembly","x25519","zero-knowledge"],"created_at":"2026-05-24T06:03:57.260Z","updated_at":"2026-07-01T05:01:09.452Z","avatar_url":"https://github.com/moss-piglet.png","language":"Rust","funding_links":["https://github.com/sponsors/moss-piglet"],"categories":[],"sub_categories":[],"readme":"# metamorphic-crypto\n\nZero-knowledge end-to-end encryption library with post-quantum hybrid KEM, hybrid\nPQ signatures, and an opt-in **CNSA 2.0** suite axis (matched-strength hybrid +\npure ML-KEM-1024 / ML-DSA-87 / AES-256-GCM).\n\nBuilt for [Metamorphic](https://metamorphic.app) and [Mosslet](https://mosslet.com) — privacy-first apps by [Moss Piglet Corporation](https://mosspiglet.dev) where all user data is encrypted client-side and the server only stores opaque ciphertext.\n\n## What this provides\n\n- **Secretbox** (XSalsa20-Poly1305) — symmetric authenticated encryption\n- **Sealed box** (X25519) — anonymous public-key encryption (libsodium-compatible)\n- **Hybrid PQ KEM** (ML-KEM-512 + X25519) — NIST Cat-1 post-quantum key encapsulation (opt-in)\n- **Hybrid PQ KEM** (ML-KEM-768 + X25519) — NIST Cat-3 post-quantum key encapsulation (default)\n- **Hybrid PQ KEM** (ML-KEM-1024 + X25519) — NIST Cat-5 post-quantum key encapsulation (opt-in)\n- **Argon2id KDF** — password-based key derivation (libsodium INTERACTIVE parameters)\n- **Hybrid PQ signatures** (ML-DSA + Ed25519) — NIST Cat-2/3/5 composite digital signatures (strict AND)\n- **CNSA 2.0 suite axis** (opt-in) — matched-strength hybrid (X448 / P-521 / Ed448 / ECDSA-P-521) and pure post-quantum (ML-KEM-1024, ML-DSA-87, AES-256-GCM)\n- **Hashing** (SHA3-512/256, SHA-256/512) — public, one-shot digest functions (e.g. for key fingerprints / safety numbers)\n- **HMAC-SHA256** (RFC 2104) — keyed MAC primitive (e.g. the on-spec KEYTRANS commitment)\n- **Verifiable random functions** (ECVRF, RFC 9381) — Edwards25519 (`0x03`) and NIST P-256 (`0x01`), classical VRFs for transparency-log *index privacy* (CONIKS / KEYTRANS)\n- **WASM bindings** — browser-ready via `wasm-pack`\n- **Recovery keys** — human-readable base32 encoding for key backup\n\n## Security levels\n\n| Level | ML-KEM | NIST Category | Equivalent | Version Tag | Default |\n|-------|--------|---------------|------------|-------------|---------|\n| Cat-1 | 512    | 1             | ~AES-128   | `0x01`      | No      |\n| Cat-3 | 768    | 3             | ~AES-192   | `0x02`      | Yes     |\n| Cat-5 | 1024   | 5             | ~AES-256   | `0x03`      | No      |\n\nNIST (FIPS 203) standardizes ML-KEM only at categories 1/3/5 — there is no category-2/4 parameter set, so none is offered. All levels use the same combiner construction. The classical half is **X25519 (~Cat-1 classical) at every tier** — it does not scale up with the ML-KEM parameter set; at Cat-3/Cat-5 the post-quantum half dominates and X25519 is the classical floor (standard hybrid-KEM practice: a break requires defeating *both* halves). `hybrid_open` auto-detects the level from the version tag byte — old and new ciphertext coexist seamlessly.\n\n## Security properties\n\n- `#![forbid(unsafe_code)]` — no unsafe anywhere in the crate\n- All secret key material zeroized after use\n- Constant-time MAC comparison via RustCrypto\n- OS CSPRNG via `getrandom` (no userspace PRNG)\n- Hybrid construction: both ML-KEM AND X25519 must be broken to compromise a sealed key\n\n## Hybrid KEM construction\n\nThe hybrid combiner matches the format used by [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum)'s `ml_kem768_x25519`:\n\n```\nSeed expansion:  SHAKE256(seed_32) → 96 bytes [ML-KEM seed (64) || X25519 sk (32)]\nCombiner:        SHA3-256(ss_mlkem || ss_x25519 || ct_x25519 || pk_x25519 || label)\n```\n\n### Cat-1 (ML-KEM-512, opt-in)\n```\nPublic key:   ML-KEM-512 ek (800 B) || X25519 pk (32 B) = 832 bytes\nCiphertext:   0x01 || ML-KEM-512 ct (768 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct\n```\n\n### Cat-3 (ML-KEM-768, default)\n```\nPublic key:   ML-KEM-768 ek (1184 B) || X25519 pk (32 B) = 1216 bytes\nCiphertext:   0x02 || ML-KEM-768 ct (1088 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct\n```\n\n### Cat-5 (ML-KEM-1024, opt-in)\n```\nPublic key:   ML-KEM-1024 ek (1568 B) || X25519 pk (32 B) = 1600 bytes\nCiphertext:   0x03 || ML-KEM-1024 ct (1568 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct\n```\n\n## Targets\n\n| Target | Build | Use case |\n|--------|-------|----------|\n| Native | `cargo build` | Tests, CLI tools, Elixir NIF (`metamorphic_crypto` Hex package) |\n| WASM | `wasm-pack build --target web` | Browser (Phoenix LiveView, any SPA) |\n| iOS | UniFFI (planned) | Native Swift apps |\n| Android | UniFFI (planned) | Native Kotlin apps |\n\n## Usage\n\n```rust\nuse metamorphic_crypto::{generate_key, encrypt_secretbox_string, decrypt_secretbox_to_string};\nuse metamorphic_crypto::{generate_hybrid_keypair, hybrid_seal, hybrid_open};\nuse metamorphic_crypto::{generate_hybrid_keypair_512, hybrid_seal_512};\nuse metamorphic_crypto::{generate_hybrid_keypair_1024, hybrid_seal_1024};\n\n// Symmetric encryption\nlet key = generate_key();\nlet ciphertext = encrypt_secretbox_string(\"sensitive data\", \u0026key).unwrap();\nlet plaintext = decrypt_secretbox_to_string(\u0026ciphertext, \u0026key).unwrap();\nassert_eq!(plaintext, \"sensitive data\");\n\n// Hybrid PQ seal (Cat-3, default)\nlet kp = generate_hybrid_keypair();\nlet sealed = hybrid_seal(b\"context_key_bytes\", \u0026kp.public_key).unwrap();\nlet opened = hybrid_open(\u0026sealed, \u0026kp.secret_key).unwrap();\n\n// Hybrid PQ seal (Cat-5)\nlet kp5 = generate_hybrid_keypair_1024();\nlet sealed5 = hybrid_seal_1024(b\"context_key_bytes\", \u0026kp5.public_key).unwrap();\nlet opened5 = hybrid_open(\u0026sealed5, \u0026kp5.secret_key).unwrap(); // auto-detects level\n\n// Hybrid PQ seal (Cat-1)\nlet kp1 = generate_hybrid_keypair_512();\nlet sealed1 = hybrid_seal_512(b\"context_key_bytes\", \u0026kp1.public_key).unwrap();\nlet opened1 = hybrid_open(\u0026sealed1, \u0026kp1.secret_key).unwrap(); // auto-detects level\n```\n\n## Hashing\n\nPublic, one-shot digest functions over the already-present, audited `sha3` and\n`sha2` dependencies. These are intended for **public** data only — key\nfingerprints / safety numbers and key-transparency-log entries — where both the\ninput (e.g. a public key) and the output digest are meant to be public.\n\n`sha3_512` is the recommended default (NIST Cat-5, ~256-bit collision\nresistance, consistent with the crate's Keccak-based combiner). `sha3_256`,\n`sha256`, and `sha512` are provided so integrators can match an existing format.\n\n```rust\nuse metamorphic_crypto::{sha3_512, sha3_256, sha256, sha512};\n\n// Take raw bytes, return fixed-size byte arrays.\nlet digest: [u8; 64] = sha3_512(b\"public key bytes\"); // recommended default\nlet d256:   [u8; 32] = sha3_256(b\"...\");\nlet s256:   [u8; 32] = sha256(b\"...\");   // SHA-2 interop\nlet s512:   [u8; 64] = sha512(b\"...\");   // SHA-2 interop\n\n// Encode the digest yourself when needed:\nuse metamorphic_crypto::b64;\nlet fingerprint_b64 = b64::encode(\u0026digest);\n```\n\n### Domain separation (recommended for fingerprints / transparency logs)\n\nFor key fingerprints, safety numbers, and key-transparency-log entries, prefer\n`sha3_512_with_context`, which binds the digest to a versioned context label so\nthe same bytes hashed for different purposes can never collide or be\nreinterpreted across contexts. It is exactly as strong as `sha3_512` — it *is*\nSHA3-512, over an unambiguously framed message — and makes intent explicit:\n\n```rust\nuse metamorphic_crypto::sha3_512_with_context;\n\nlet fp  = sha3_512_with_context(\"mosslet/key-fingerprint/v1\", pubkey_bytes);\nlet log = sha3_512_with_context(\"mosslet/log-entry/v1\", entry_bytes);\n// fp and log are unrelated even if the byte inputs coincide.\n```\n\nStable wire format (reproduce exactly for cross-language parity):\n\n```text\nSHA3-512( u64_be(len(context_utf8)) || context_utf8 || data )\n```\n\nThe 8-byte big-endian length prefix makes the `(context, data)` boundary\nunambiguous (no boundary-confusion collisions). Use a versioned namespace label.\n\nEncoding: the native functions take `\u0026[u8]` and return raw byte arrays — encode\nto base64 or hex at the call site. The WASM bindings take/return base64 to match\nthe rest of the WASM API (see below).\n\n**Do not hash secrets with these.** A bare hash makes no guarantees about its\ninputs, and (consistent with the rest of the crate) the hashing path adds no\nzeroize/constant-time ceremony — wiping a transient copy of already-public data\nwould add cost without protection. If you need to process secret material\n(passwords, private keys), use the right construction instead — this crate's\nArgon2id `derive_session_key` for password-based derivation, or a dedicated\nKDF/MAC. The encryption APIs that handle secrets already zeroize on drop.\n\n## Message authentication (HMAC-SHA256)\n\n`hmac_sha256(key, msg)` is a thin wrapper over the audited RustCrypto `hmac`\ncrate — the generic keyed-MAC primitive, no novel cryptography. Its headline use\nis the **on-spec IETF KEYTRANS commitment** (`HMAC(Kc, CommitmentValue)`); the\nKEYTRANS-specific framing lives in `metamorphic-log`, so this crate stays the\nsingle source of truth for primitives.\n\n```rust\nuse metamorphic_crypto::hmac_sha256;\n\nlet tag: [u8; 32] = hmac_sha256(key_bytes, message_bytes);\n```\n\nAny key length is accepted (RFC 2104). Validated against RFC 4231 test vectors.\nWASM: `hmacSha256(keyB64, msgB64) -\u003e tagB64`.\n\n\u003e **Not an authenticator by default here.** In the KEYTRANS commitment the \"key\"\n\u003e is a fixed, public per-suite constant and hiding comes from a random opening in\n\u003e the message — HMAC is used as a committing PRF, not a secret-keyed\n\u003e authenticator. Use it in a construction whose properties you understand.\n\n## Verifiable random functions (ECVRF, RFC 9381)\n\nThe crate exposes **two** RFC 9381 ECVRF ciphersuites, sharing an identical\nprove/verify/proof-to-hash shape:\n\n| Module | Ciphersuite | Suite octet | Curve + hash | Used by |\n|--------|-------------|-------------|--------------|---------|\n| `vrf` | ECVRF-EDWARDS25519-SHA512-TAI | `0x03` | Edwards25519 + SHA-512 | KEYTRANS private/experimental + `KT_128_SHA256_Ed25519` |\n| `vrf_p256` | ECVRF-P256-SHA256-TAI | `0x01` | NIST P-256 + SHA-256 | on-spec `KT_128_SHA256_P256` |\n\nA VRF lets the key owner compute, for any input `alpha`, a pseudorandom output\n`beta` plus a proof `pi` that `beta` is correct under their public key. Anyone\nwith the public key can verify `pi`, but cannot compute `beta` for a new input\nand cannot learn `alpha` from `(pi, beta)`. This is the primitive behind\n**transparency-log index privacy** (CONIKS / KEYTRANS): a directory maps a\nprivate identity index to a verifiable, pseudorandom tree position without\nrevealing which identities it holds.\n\nBoth are built on curve backends already in-tree (`curve25519-dalek` for\nEdwards25519; `p256` for P-256 — the same `elliptic-curve` generation as the\ncrate's P-521 stack) — no parallel crypto — and are pinned byte-for-byte by\nRFC 9381's own test vectors.\n\n```rust\n// Edwards25519 (suite 0x03)\nuse metamorphic_crypto::{ecvrf_generate_keypair, ecvrf_prove, ecvrf_verify};\n\nlet (sk, pk) = ecvrf_generate_keypair();\nlet alpha = b\"identity index\";\nlet pi = ecvrf_prove(\u0026sk, alpha)?;         // 80-byte proof\nlet beta = ecvrf_verify(\u0026pk, alpha, \u0026pi)?; // Ok(Some(beta)) if valid\nassert!(beta.is_some());\n\n// NIST P-256 (suite 0x01) — identical shape\nuse metamorphic_crypto::{ecvrf_p256_generate_keypair, ecvrf_p256_prove, ecvrf_p256_verify};\n\nlet (sk, pk) = ecvrf_p256_generate_keypair();\nlet pi = ecvrf_p256_prove(\u0026sk, alpha)?;         // 81-byte proof\nlet beta = ecvrf_p256_verify(\u0026pk, alpha, \u0026pi)?; // Ok(Some(beta)) if valid\n```\n\n| Item | Edwards25519 (`vrf`) | P-256 (`vrf_p256`) |\n|------|----------------------|--------------------|\n| secret key | 32 B (seed) | 32 B (big-endian scalar `x`) |\n| public key | 32 B (compressed Edwards `Y`) | 33 B (SEC1 compressed `Y`) |\n| proof `pi` | 80 B: `Gamma(32) \\|\\| c(16) \\|\\| s(32)` | 81 B: `Gamma(33) \\|\\| c(16) \\|\\| s(32)` |\n| output `beta` | 64 B (SHA-512) | 32 B (SHA-256) |\n\nWASM: `ecvrfEd25519{GenerateKeyPair,PublicKey,Prove,Verify,ProofToHash}` and\n`ecvrfP256{...}` (base64 in/out; `verify` returns the output or `null`).\n\n**Honest posture.** Both VRFs are **classical** (elliptic-curve discrete log).\nThey protect exactly one property — *index privacy* — and are the one\nnon-post-quantum piece in the transparency stack; integrity, authenticity,\nconfidentiality, and hash-based commitments are post-quantum independently of\nthem. RFC 9381's Elligator2 / SSWU siblings are designed-in future additions\nthat never invalidate a TAI proof (the suite octet is bound into every hash). A\nhybrid (post-quantum + classical) VRF is intended for when an audited lattice\nVRF exists; none does today, so it is not built. These primitives are not\nFIPS-validated.\n\n## Hybrid PQ signatures\n\nComposite digital signatures: every message is signed by **both** ML-DSA\n(FIPS 204) **and** Ed25519 (RFC 8032), and verification requires **both** to be\nvalid (strict AND). An attacker has to break both a lattice scheme and an\nelliptic-curve scheme to forge, and cannot strip one algorithm to downgrade the\nother. This is the signing counterpart to the hybrid KEM above.\n\n```rust\nuse metamorphic_crypto::{generate_signing_keypair, sign, verify, SIGN_CONTEXT_V1};\n\nlet kp = generate_signing_keypair(); // Cat-3 (ML-DSA-65 + Ed25519), default\nlet sig = sign(b\"transparency log entry\", SIGN_CONTEXT_V1, \u0026kp.secret_key).unwrap();\nassert!(verify(b\"transparency log entry\", SIGN_CONTEXT_V1, \u0026sig, \u0026kp.public_key).unwrap());\n\n// Re-derive the public key from a backed-up secret key:\nuse metamorphic_crypto::derive_public_key;\nassert_eq!(derive_public_key(\u0026kp.secret_key).unwrap(), kp.public_key);\n```\n\nCat-2 (`generate_signing_keypair_44`) and Cat-5 (`generate_signing_keypair_87`)\nare also available; `verify` auto-detects the level from the signature's version\ntag. The `secret_key` field is zeroized on drop.\n\n### Signing levels and mode\n\n| Level | ML-DSA    | NIST Category | Equivalent | Version Tag | Default |\n|-------|-----------|---------------|------------|-------------|---------|\n| Cat-2 | ML-DSA-44 | 2             | ~AES-128   | `0x01`      | No      |\n| Cat-3 | ML-DSA-65 | 3             | ~AES-192   | `0x02`      | Yes     |\n| Cat-5 | ML-DSA-87 | 5             | ~AES-256   | `0x03`      | No      |\n\nML-DSA is signed with the **hedged (randomized)** variant — FIPS 204's default\nand most conservative mode (resilient to RNG failure, hardened against fault /\nside-channel attacks that deterministic lattice signing invites). Ed25519 is\ndeterministic per RFC 8032. As a result signature **bytes are non-reproducible**,\nbut the **wire format is deterministic and pinned**.\n\n### Domain separation and wire format\n\nBoth algorithms sign the same domain-separated message, framed exactly like\n`sha3_512_with_context` (a length-prefixed context):\n\n```text\nsigned_msg = I2OSP(len(context_utf8), 8) || context_utf8 || message\n```\n\nML-DSA signs `signed_msg` with an empty native context, so the framing is\nidentical for both algorithms and across every language binding. Byte layout\n(Ed25519 first, fixed-size, so the ML-DSA tail needs no length prefix):\n\n```text\nsignature  = tag || ed25519_sig (64 B) || ml_dsa_sig (2420 / 3309 / 4627 B)\npublic_key = tag || ed25519_pk  (32 B) || ml_dsa_pk  (1312 / 1952 / 2592 B)\nsecret_key = tag || ed25519_seed(32 B) || ml_dsa_seed(32 B)              = 65 B\n```\n\n### Dependency audit posture\n\n| Dependency      | Version | Audited             | Notes |\n|-----------------|---------|---------------------|-------|\n| `ed25519-dalek` | 2.x     | Yes (mature)        | Widely deployed RFC 8032 implementation. |\n| `ml-dsa`        | 0.1.x   | **No** (RustCrypto) | FIPS 204 (final). New crate, not yet independently audited. Pinned; tracked for the FIPS-mode roadmap. |\n\nML-DSA is defense-in-depth on top of the independently-strong Ed25519: even if a\nflaw were found in the young `ml-dsa` implementation, the composite remains at\nleast as strong as Ed25519. This is stated honestly so integrators can choose\nwhile the post-quantum implementation matures toward audit / FIPS validation.\n\n## CNSA 2.0 suite axis (opt-in)\n\nBy default everything above is **`Suite::Hybrid`** — the classical+PQ strict-AND\nconstructions (ML-KEM + X25519; ML-DSA + Ed25519). If you have no specific\nmandate, that is the recommended choice and you can ignore this section.\n\nFor deployments that must follow the NSA's **Commercial National Security\nAlgorithm Suite 2.0** (CNSA 2.0 / NIST IR 8547), a `Suite` axis lets you raise\nthe posture with a *single extra argument*. It is **orthogonal** to the\n`SecurityLevel` (Cat-1/3/5) you already know, so you really have two independent\nknobs:\n\n```\n            posture (Suite)                  ×   parameter set (SecurityLevel)\n  ┌──────────────────────────────┐               ┌───────────────────────┐\n  │ Hybrid         (default)      │               │ Cat-1 / Cat-3 / Cat-5 │\n  │ HybridMatched  (opt-in)       │               └───────────────────────┘\n  │ PureCnsa2      (opt-in)       │\n  └──────────────────────────────┘\n```\n\n| Suite | What it is | Classical partner | Status |\n|-------|------------|-------------------|--------|\n| `Hybrid` | Existing strict-AND classical+PQ. **Byte-for-byte unchanged.** | X25519 / Ed25519 (every tier) | **Default, recommended** |\n| `HybridMatched` | Classical partner matched to the PQ category so it is never the weak link | KEM: Cat-3→X448, Cat-5→P-521 ECDH · Sign: Cat-3→Ed448, Cat-5→ECDSA-P-521 | Opt-in |\n| `PureCnsa2` | Pure post-quantum, no classical half (the CNSA-2.0 box) | none | Opt-in, **Cat-5 only** |\n\n`HybridMatched` at the lowest rung (KEM Cat-1 / sign Cat-2) is identical to\n`Hybrid` — no new format is produced there, so nothing breaks.\n\n### New wire formats (new suites only)\n\nThe `Hybrid` suite (and `HybridMatched` at the lowest rung) keep their existing\n`0x01/0x02/0x03` ciphertext tags and byte layout untouched. The matched / pure\nsuites use new tags and a CNSA-correct seal envelope:\n\n| Suite + level | KEM | Tag |\n|---------------|-----|-----|\n| `PureCnsa2` Cat-5 | ML-KEM-1024 + AES-256-GCM | `0x10` |\n| `HybridMatched` Cat-3 | ML-KEM-768 + X448 + AES-256-GCM | `0x13` |\n| `HybridMatched` Cat-5 | ML-KEM-1024 + P-521 ECDH + AES-256-GCM | `0x14` |\n\n```text\nikm  = ss_mlkem (PureCnsa2)  |  ss_mlkem || ss_ecc (HybridMatched)\nkey  = HKDF-SHA512(ikm, info = suite_tag || context_label)   -\u003e 32-byte AES-256 key\nout  = AES-256-GCM(key, 96-bit random nonce, AAD = suite_tag || context_label)\nwire = tag(1) || kem_ct || [ecc_eph_pk] || nonce(12) || ct || gcm_tag(16)\n```\n\nEach encapsulation yields a fresh KEM secret, so the derived AES-256 key is\nsingle-use and the random 96-bit nonce can never repeat — SIV-grade misuse\nresistance without leaving the CNSA-approved set (no AES-GCM-SIV). Note the\ndeliberate hash split: **HKDF-SHA512** for *key derivation* here; **SHA3-512**\nstays the choice for *leaf/transcript* hashing (`sha3_512_with_context`).\n\n### Context labels\n\nThe new suites bind a versioned context label into both the HKDF `info` and the\nGCM AAD (and, for signatures, the I2OSP-framed message). Grammar:\n`\"\u003cnamespace\u003e/\u003cpurpose\u003e/v\u003cmajor\u003e\"`. The **namespace** is the one per-tenant knob;\nthe protocol shape stays fixed. Library defaults are `SEAL_CONTEXT_V1`\n(`\"metamorphic/seal/v1\"`) and `SIGN_CONTEXT_V1` (`\"metamorphic/sign/v1\"`); pass\nyour own (e.g. `\"mosslet/seal/v1\"`) to namespace your deployment.\n\n### Usage (Rust)\n\n```rust\nuse metamorphic_crypto::{\n    Suite, SecurityLevel, SignatureLevel, SEAL_CONTEXT_V1, SIGN_CONTEXT_V1,\n    generate_hybrid_keypair_suite, hybrid_seal_suite, hybrid_open_with_context,\n    generate_signing_keypair_suite, sign, verify,\n};\n\n// --- KEM / seal: the pure CNSA-2.0 box (ML-KEM-1024 + AES-256-GCM) ---\nlet kp = generate_hybrid_keypair_suite(Suite::PureCnsa2, SecurityLevel::Cat5).unwrap();\nlet sealed = hybrid_seal_suite(b\"context_key_bytes\", \u0026kp.public_key,\n                               Suite::PureCnsa2, SecurityLevel::Cat5).unwrap();\n// `hybrid_open` auto-detects the tag using the DEFAULT context label; if you\n// sealed with a custom label, open with it explicitly:\nlet opened = hybrid_open_with_context(\u0026sealed, \u0026kp.secret_key, SEAL_CONTEXT_V1).unwrap();\n\n// --- Signatures: ML-DSA-87 only (Cat-5 pure) ---\nlet sk = generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat5).unwrap();\nlet sig = sign(b\"checkpoint\", SIGN_CONTEXT_V1, \u0026sk.secret_key).unwrap();\nassert!(verify(b\"checkpoint\", SIGN_CONTEXT_V1, \u0026sig, \u0026sk.public_key).unwrap());\n// `sign` / `verify` / `derive_public_key` auto-detect the suite from the version\n// tag — no suite argument is needed once the key exists.\n```\n\n`seal_for_user_with_suite` is the user-facing seal that falls back to legacy\nX25519 when no PQ key is present, mirroring `seal_for_user_with_level`.\n\n### Honest claims\n\nClaim: **\"CNSA 2.0 algorithm suite, NCC-audited components, pure-Rust,\nmemory-safe (`forbid-unsafe`).\"** **Not** \"FIPS 140-3 validated.\" `PureCnsa2` is\nmore standards-compliant but leans entirely on the (not-yet-independently-audited\nat our layer) lattice implementation, which is exactly why the strict-AND\n`Hybrid` default stays recommended: it keeps the classical backstop until the PQ\nimplementations are audited / validated.\n\n## WASM (browser)\n\n```bash\nwasm-pack build --target web --release\n```\n\n```js\nimport init, { deriveSessionKey, encryptSecretboxString } from './pkg/metamorphic_crypto.js';\n\nawait init('/path/to/metamorphic_crypto_bg.wasm');\n\nconst key = deriveSessionKey(password, saltBase64);\nconst ciphertext = encryptSecretboxString(\"hello\", key);\n```\n\n### Hashing (WASM)\n\nDigest exports take base64-encoded input and return the digest as base64. Decode\nor re-encode to hex on the JS side if a hex fingerprint is required.\n\n```js\nimport init, { sha3_512, sha3_512WithContext } from './pkg/metamorphic_crypto.js';\nawait init();\n\nconst dataB64 = btoa(\"public key bytes\");\nconst digestB64 = sha3_512(dataB64); // also: sha3_256, sha256, sha512\n\n// Domain-separated (recommended for fingerprints / transparency logs):\nconst fp = sha3_512WithContext(\"mosslet/key-fingerprint/v1\", dataB64);\n```\n\n### Signatures (WASM)\n\nKeys and signatures are base64; the message is base64 and `context` is a UTF-8\nstring. `verify` returns `true` only if both component signatures are valid.\n\n```js\nimport init, { generateSigningKeyPair, sign, verify } from './pkg/metamorphic_crypto.js';\nawait init();\n\nconst kp = generateSigningKeyPair(\"cat3\"); // { publicKey, secretKey }\nconst msg = btoa(\"transparency log entry\");\nconst sig = sign(msg, \"metamorphic/sign/v1\", kp.secretKey);\nconst ok = verify(msg, \"metamorphic/sign/v1\", sig, kp.publicKey); // true\n```\n\n### HMAC \u0026 VRF (WASM)\n\nBase64 in/out throughout. VRF `verify` returns the base64 output on success or\n`null` on a cryptographic rejection, mirroring the native `Option`.\n\n```js\nimport init, {\n  hmacSha256,\n  ecvrfEd25519GenerateKeyPair, ecvrfEd25519Prove, ecvrfEd25519Verify,\n  ecvrfP256GenerateKeyPair, ecvrfP256Prove, ecvrfP256Verify,\n} from './pkg/metamorphic_crypto.js';\nawait init();\n\n// HMAC-SHA256\nconst tag = hmacSha256(btoa(\"key bytes\"), btoa(\"message\")); // base64 tag\n\n// ECVRF-Edwards25519 (suite 0x03)\nconst ed = ecvrfEd25519GenerateKeyPair();     // { secretKey, publicKey }\nconst alpha = btoa(\"identity index\");\nconst piEd = ecvrfEd25519Prove(ed.secretKey, alpha);\nconst betaEd = ecvrfEd25519Verify(ed.publicKey, alpha, piEd); // base64 or null\n\n// ECVRF-P256 (suite 0x01) — identical shape\nconst p = ecvrfP256GenerateKeyPair();\nconst piP = ecvrfP256Prove(p.secretKey, alpha);\nconst betaP = ecvrfP256Verify(p.publicKey, alpha, piP);       // base64 or null\n```\n\n### CNSA 2.0 suites (WASM)\n\nThe `Suite` axis is exposed as a string argument (`\"hybrid\"` (default),\n`\"hybridMatched\"`, or `\"pureCnsa2\"`) alongside the usual `\"cat1\"`/`\"cat3\"`/`\"cat5\"`\nlevel. Decryption / verification auto-detect the suite from the version tag.\n\n```js\nimport init, {\n  generateHybridKeyPairSuite, hybridSealSuite, hybridOpenWithContext,\n  generateSigningKeyPairSuite, sign, verify,\n} from './pkg/metamorphic_crypto.js';\nawait init();\n\n// Pure CNSA-2.0 KEM box (ML-KEM-1024 + AES-256-GCM)\nconst kp = generateHybridKeyPairSuite(\"pureCnsa2\", \"cat5\"); // { publicKey, secretKey }\nconst sealed = hybridSealSuite(btoa(\"key material\"), kp.publicKey, \"pureCnsa2\", \"cat5\");\n// Open with the context label used at seal time (default \"metamorphic/seal/v1\"):\nconst opened = hybridOpenWithContext(sealed, kp.secretKey, \"metamorphic/seal/v1\"); // base64\n\n// Pure ML-DSA-87 signatures\nconst sk = generateSigningKeyPairSuite(\"pureCnsa2\", \"cat5\");\nconst sig = sign(btoa(\"checkpoint\"), \"metamorphic/sign/v1\", sk.secretKey);\nconst ok = verify(btoa(\"checkpoint\"), \"metamorphic/sign/v1\", sig, sk.publicKey); // true\n```\n\nFor a custom per-tenant namespace, use `hybridSealSuiteWithContext(...,\n\"mosslet/seal/v1\")` and open with the same label. `sealForUserWithSuite` mirrors\n`sealForUser` with the suite/level appended.\n\n## Tests\n\n```bash\ncargo test          # unit + integration + cross-level compatibility\ncargo clippy        # zero warnings\ncargo fmt --check   # formatted\n```\n\n## License\n\nDual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at your option.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoss-piglet%2Fmetamorphic-crypto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmoss-piglet%2Fmetamorphic-crypto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoss-piglet%2Fmetamorphic-crypto/lists"}