MetamorphicCrypto
Zero-knowledge end-to-end encryption for Elixir.
NaCl-compatible symmetric and public-key encryption, Argon2id key derivation, ML-KEM-768 + X25519 hybrid post-quantum encryption, and human-readable recovery keys — powered by Rust NIFs with precompiled binaries.
key = MetamorphicCrypto.generate_key()
{:ok, ciphertext} = MetamorphicCrypto.encrypt("hello", key)
{:ok, "hello"} = MetamorphicCrypto.decrypt(ciphertext, key)Installation
Add metamorphic_crypto to your mix.exs:
def deps do
[
{:metamorphic_crypto, "~> 0.1"}
]
endThen:
mix deps.getThat's it. Precompiled NIF binaries are downloaded automatically for your platform. No Rust toolchain required.
Quick Start
Symmetric Encryption (SecretBox)
XSalsa20-Poly1305 authenticated encryption. Same format as libsodium.
key = MetamorphicCrypto.generate_key()
{:ok, ciphertext} = MetamorphicCrypto.encrypt("sensitive data", key)
{:ok, "sensitive data"} = MetamorphicCrypto.decrypt(ciphertext, key)Public-Key Encryption (Sealed Box)
Anonymous encryption to a recipient's public key. Only they can decrypt.
{public_key, private_key} = MetamorphicCrypto.generate_keypair()
{:ok, sealed} = MetamorphicCrypto.seal("for your eyes only", public_key)
{:ok, "for your eyes only"} = MetamorphicCrypto.unseal(sealed, public_key, private_key)Post-Quantum Hybrid Encryption
ML-KEM-768 + X25519 — quantum-resistant, backward-compatible.
{pq_pk, pq_sk} = MetamorphicCrypto.Hybrid.generate_keypair()
{:ok, ciphertext} = MetamorphicCrypto.Hybrid.seal("quantum-safe", pq_pk)
{:ok, "quantum-safe"} = MetamorphicCrypto.Hybrid.open(ciphertext, pq_sk)Unified Seal/Unseal (Auto-Detecting)
Automatically uses PQ when available, falls back to classical. Detects format on decrypt — old and new ciphertexts coexist seamlessly.
{pk, sk} = MetamorphicCrypto.Keys.generate_keypair()
{pq_pk, pq_sk} = MetamorphicCrypto.Hybrid.generate_keypair()
# Encrypts with hybrid PQ
{:ok, ct} = MetamorphicCrypto.Seal.seal_for_user("secret", pk, pq_public_key: pq_pk)
# Decrypts (auto-detects format)
{:ok, "secret"} = MetamorphicCrypto.Seal.unseal_from_user(ct, pk, sk, pq_secret_key: pq_sk)Key Derivation (Argon2id)
Derive a session key from a password. Uses libsodium's interactive parameters (64 MiB, 2 iterations).
salt = MetamorphicCrypto.Keys.generate_salt()
{:ok, session_key} = MetamorphicCrypto.KDF.derive_session_key("user password", salt)Recovery Keys
Human-readable backup keys (like Matrix or Signal recovery codes).
{:ok, recovery_key, secret} = MetamorphicCrypto.Recovery.generate()
# recovery_key => "ABCDE-FGHJK-LMNPQ-RSTUV-..."
# Back up a private key
{:ok, backup} = MetamorphicCrypto.Recovery.encrypt_private_key(private_key, secret)
# Restore later
{:ok, restored_secret} = MetamorphicCrypto.Recovery.key_to_secret(recovery_key)
{:ok, private_key} = MetamorphicCrypto.Recovery.decrypt_private_key(backup, restored_secret)Key Generation (Mix Task)
mix metamorphic_crypto.gen.keyArchitecture Patterns
Pattern 1: Zero-Knowledge with Defense-in-Depth
This is how Metamorphic uses this library. The client encrypts data before it reaches the server. The server stores opaque ciphertext and wraps it with Cloak as an additional layer.
┌─────────────────────────────────────────────────────────────────┐
│ Client (browser/mobile) │
│ │
│ password ──► Argon2id KDF ──► session_key │
│ │ │
│ decrypt private_key │
│ │ │
│ plaintext ──► MetamorphicCrypto.encrypt(key) ──► ciphertext ─┐ │
│ │ │
└───────────────────────────────────────────────────────────────┼─┘
│
───────┼──────
│
┌───────────────────────────────────────────────────────────────┼─┐
│ Server (Phoenix/LiveView) ▼ │
│ │
│ Receives opaque ciphertext (cannot decrypt) │
│ Cloak wraps it in AES-256-GCM before writing to DB │
│ (defense-in-depth against DB-level compromise) │
│ │
│ Uses MetamorphicCrypto server-side for: │
│ • Key generation for new contexts │
│ • Sealing keys to users (key distribution) │
│ • Re-keying operations │
└─────────────────────────────────────────────────────────────────┘In this pattern, use Cloak for the Ecto layer (encrypted types, blind indexes, key rotation) and MetamorphicCrypto for the E2E crypto operations.
Pattern 2: Server-Side Encryption at Rest
If you just need to encrypt fields at rest and the server holds the keys, use Cloak directly. It has built-in key rotation, multiple Ecto types (Binary, Map, Integer, Float), and HMAC blind indexes.
MetamorphicCrypto isn't the right tool for this pattern.
Pattern 3: Transitioning to Zero-Knowledge
Start with Cloak for server-side encryption. When you're ready to move to a ZK architecture, add MetamorphicCrypto for client-side operations. Cloak stays as the defense-in-depth layer.
# mix.exs
{:cloak_ecto, "~> 1.3"}, # Ecto types, blind indexes, key rotation
{:metamorphic_crypto, "~> 0.1"} # E2E crypto primitivesUsing with Cloak
MetamorphicCrypto and Cloak solve different problems and work great together:
| Cloak | MetamorphicCrypto | |
|---|---|---|
| Purpose | Server-side encryption-at-rest | Zero-knowledge E2E encryption |
| Who holds the key | Server (env vars) | User (derived from password) |
| Cipher | AES-256-GCM | XSalsa20-Poly1305, ML-KEM-768 |
| Key rotation | Built-in | N/A (user owns keys) |
| Ecto types | Binary, Map, Integer, HMAC | — |
| Use for | PII at rest, blind indexes | Client-side crypto, key exchange, PQ |
For encrypted Ecto fields and blind indexes, use Cloak. For E2E encryption primitives (key derivation, sealing, post-quantum), use MetamorphicCrypto.
Modules
| Module | Purpose |
|---|---|
MetamorphicCrypto | Top-level convenience API |
MetamorphicCrypto.SecretBox | XSalsa20-Poly1305 symmetric encryption |
MetamorphicCrypto.BoxSeal | X25519 anonymous sealed box |
MetamorphicCrypto.Hybrid | ML-KEM-768 + X25519 post-quantum hybrid |
MetamorphicCrypto.Seal | Unified seal/unseal with auto-detection |
MetamorphicCrypto.KDF | Argon2id key derivation |
MetamorphicCrypto.Keys | Key generation and private key management |
MetamorphicCrypto.Recovery | Human-readable recovery keys |
Wire Format Compatibility
All ciphertext produced by this library is byte-compatible with:
- libsodium / NaCl
- TweetNaCl.js
- enacl (Erlang libsodium bindings)
-
The
metamorphic-cryptoWASM module (browser clients)
This means you can:
- Replace
enaclin existing projects with no data migration - Decrypt on the server what was encrypted in the browser (and vice versa)
- Incrementally adopt post-quantum encryption without breaking existing data
Deployment
No special deployment steps required. Precompiled binaries cover:
| Platform | Architectures |
|---|---|
| Linux (glibc) | x86_64, aarch64 |
| macOS | x86_64, aarch64 (Apple Silicon) |
| Windows | x86_64 |
If you deploy to a platform not listed above, set METAMORPHIC_CRYPTO_BUILD=true
and ensure Rust is available in your build environment:
# In your Dockerfile (only if your platform isn't precompiled)
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
ENV METAMORPHIC_CRYPTO_BUILD=trueFor standard deployments (Fly.io, Gigalixir, Heroku, Docker on linux/amd64 or arm64), precompiled binaries just work — no configuration needed.
Building from Source
If you want to compile the NIF yourself (e.g., for development on this library):
export METAMORPHIC_CRYPTO_BUILD=true
mix deps.get
mix compile
Requires Rust 1.85+ (rustup update).
Contributing
Contributions welcome! Please open an issue first for significant changes.
git clone https://github.com/moss-piglet/metamorphic_crypto.git
cd metamorphic_crypto
export METAMORPHIC_CRYPTO_BUILD=true
mix deps.get
mix testLicense
MIT — see LICENSE.
Built by Moss Piglet. Maintained by @f0rest8.