native/metamorphic_crypto_nif/src/lib.rs

//! Rustler NIF bindings for metamorphic-crypto.
//!
//! Exposes the Rust crypto library to Elixir via NIF functions.
//! All functions accept and return base64-encoded strings matching
//! the wire format used by the JavaScript/WASM client.

use metamorphic_crypto::{
    CryptoError, b64, box_seal, hybrid, kdf, keys, recovery, seal, secretbox,
};
use rustler::{Error, NifResult};

// ─── Helpers ─────────────────────────────────────────────────────────────────

fn to_nif_error(e: CryptoError) -> Error {
    Error::Term(Box::new(e.to_string()))
}

// ─── Key Derivation ──────────────────────────────────────────────────────────

#[rustler::nif(schedule = "DirtyCpu")]
fn nif_derive_session_key(password: &str, salt_b64: &str) -> NifResult<String> {
    kdf::derive_session_key(password, salt_b64).map_err(to_nif_error)
}

// ─── SecretBox (Symmetric Encryption) ────────────────────────────────────────

#[rustler::nif]
fn nif_encrypt_secretbox(plaintext_b64: &str, key_b64: &str) -> NifResult<String> {
    let pt = b64::decode(plaintext_b64).map_err(to_nif_error)?;
    secretbox::encrypt_secretbox(&pt, key_b64).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_decrypt_secretbox(ciphertext_b64: &str, key_b64: &str) -> NifResult<String> {
    let pt = secretbox::decrypt_secretbox(ciphertext_b64, key_b64).map_err(to_nif_error)?;
    Ok(b64::encode(&pt))
}

#[rustler::nif]
fn nif_encrypt_secretbox_string(plaintext: &str, key_b64: &str) -> NifResult<String> {
    secretbox::encrypt_secretbox_string(plaintext, key_b64).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_decrypt_secretbox_to_string(ciphertext_b64: &str, key_b64: &str) -> NifResult<String> {
    secretbox::decrypt_secretbox_to_string(ciphertext_b64, key_b64).map_err(to_nif_error)
}

// ─── BoxSeal (Public-Key Encryption) ─────────────────────────────────────────

#[rustler::nif]
fn nif_box_seal(plaintext_b64: &str, public_key_b64: &str) -> NifResult<String> {
    let pt = b64::decode(plaintext_b64).map_err(to_nif_error)?;
    box_seal::box_seal(&pt, public_key_b64).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_box_seal_open(
    ciphertext_b64: &str,
    public_key_b64: &str,
    private_key_b64: &str,
) -> NifResult<String> {
    box_seal::box_seal_open(ciphertext_b64, public_key_b64, private_key_b64).map_err(to_nif_error)
}

// ─── Unified Seal/Unseal ─────────────────────────────────────────────────────

#[rustler::nif]
fn nif_seal_for_user(
    plaintext_b64: &str,
    public_key_b64: &str,
    pq_public_key_b64: Option<String>,
) -> NifResult<String> {
    let pt = b64::decode(plaintext_b64).map_err(to_nif_error)?;
    seal::seal_for_user(&pt, public_key_b64, pq_public_key_b64.as_deref()).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_unseal_from_user(
    ciphertext_b64: &str,
    public_key_b64: &str,
    private_key_b64: &str,
    pq_secret_key_b64: Option<String>,
) -> NifResult<String> {
    seal::unseal_from_user(
        ciphertext_b64,
        public_key_b64,
        private_key_b64,
        pq_secret_key_b64.as_deref(),
    )
    .map_err(to_nif_error)
}

// ─── Hybrid PQ KEM ──────────────────────────────────────────────────────────

#[rustler::nif]
fn nif_generate_hybrid_keypair() -> (String, String) {
    let kp = hybrid::generate_hybrid_keypair();
    (kp.public_key, kp.secret_key)
}

#[rustler::nif]
fn nif_hybrid_seal(plaintext_b64: &str, combined_pk_b64: &str) -> NifResult<String> {
    let pt = b64::decode(plaintext_b64).map_err(to_nif_error)?;
    hybrid::hybrid_seal(&pt, combined_pk_b64).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_hybrid_open(ciphertext_b64: &str, seed_b64: &str) -> NifResult<String> {
    let pt = hybrid::hybrid_open(ciphertext_b64, seed_b64).map_err(to_nif_error)?;
    Ok(b64::encode(&pt))
}

#[rustler::nif]
fn nif_is_hybrid_ciphertext(ciphertext_b64: &str) -> bool {
    hybrid::is_hybrid_ciphertext(ciphertext_b64)
}

// ─── Key Generation ──────────────────────────────────────────────────────────

#[rustler::nif]
fn nif_generate_key() -> String {
    keys::generate_key()
}

#[rustler::nif]
fn nif_generate_keypair() -> (String, String) {
    let kp = keys::generate_keypair();
    (kp.public_key, kp.private_key)
}

#[rustler::nif]
fn nif_generate_salt() -> String {
    keys::generate_salt()
}

#[rustler::nif]
fn nif_encrypt_private_key(private_key_b64: &str, session_key_b64: &str) -> NifResult<String> {
    keys::encrypt_private_key(private_key_b64, session_key_b64).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_decrypt_private_key(ciphertext_b64: &str, session_key_b64: &str) -> NifResult<String> {
    keys::decrypt_private_key(ciphertext_b64, session_key_b64).map_err(to_nif_error)
}

// ─── Recovery Key ────────────────────────────────────────────────────────────

#[rustler::nif]
fn nif_generate_recovery_key() -> NifResult<(String, String)> {
    let rk = recovery::generate_recovery_key().map_err(to_nif_error)?;
    Ok((rk.recovery_key, rk.recovery_secret_b64))
}

#[rustler::nif]
fn nif_recovery_key_to_secret(recovery_key: &str) -> NifResult<String> {
    recovery::recovery_key_to_secret(recovery_key).map_err(to_nif_error)
}

#[rustler::nif]
fn nif_encrypt_private_key_for_recovery(
    private_key_b64: &str,
    recovery_secret_b64: &str,
) -> NifResult<String> {
    recovery::encrypt_private_key_for_recovery(private_key_b64, recovery_secret_b64)
        .map_err(to_nif_error)
}

#[rustler::nif]
fn nif_decrypt_private_key_with_recovery(
    ciphertext_b64: &str,
    recovery_secret_b64: &str,
) -> NifResult<String> {
    recovery::decrypt_private_key_with_recovery(ciphertext_b64, recovery_secret_b64)
        .map_err(to_nif_error)
}

// ─── Utility ─────────────────────────────────────────────────────────────────

#[rustler::nif]
fn nif_parse_salt_from_key_hash(key_hash: &str) -> NifResult<String> {
    b64::parse_salt_from_key_hash(key_hash)
        .map(|s| s.to_string())
        .map_err(to_nif_error)
}

// ─── NIF Registration ────────────────────────────────────────────────────────

rustler::init!("Elixir.MetamorphicCrypto.Native");