Hex.pm Docs

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:

CloakMetamorphicCrypto
PurposeServer-side encryption-at-restZero-knowledge E2E encryption
Who holds the keyServer (env vars)User (derived from password)
CipherAES-256-GCMXSalsa20-Poly1305, ML-KEM-768
Key rotationBuilt-inN/A (user owns keys)
Ecto typesBinary, Map, Integer, HMAC
Use forPII at rest, blind indexesClient-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

ModulePurpose
MetamorphicCryptoTop-level convenience API
MetamorphicCrypto.SecretBoxXSalsa20-Poly1305 symmetric encryption
MetamorphicCrypto.BoxSealX25519 anonymous sealed box
MetamorphicCrypto.HybridML-KEM-768 + X25519 post-quantum hybrid
MetamorphicCrypto.SealUnified seal/unseal with auto-detection
MetamorphicCrypto.KDFArgon2id key derivation
MetamorphicCrypto.KeysKey generation and private key management
MetamorphicCrypto.RecoveryHuman-readable recovery keys

Wire Format Compatibility

All ciphertext produced by this library is byte-compatible with:

This means you can:

  • Replace enacl in 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:

PlatformArchitectures
Linux (glibc)x86_64, aarch64
macOSx86_64, aarch64 (Apple Silicon)
Windowsx86_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.