MetamorphicCrypto

Hex.pmDocs

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"}
  ]
end

Then:

mix deps.get

That'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.key

Architecture 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 primitives

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

This means you can:

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=true

For 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 test

License

MIT — see LICENSE.

Built by Moss Piglet. Maintained by @f0rest8.