native/metamorphic_crypto_nif/metamorphic-crypto/src/kdf.rs

//! Argon2id key derivation.
//!
//! Derives a 32-byte session key from a password and 16-byte salt using Argon2id
//! with libsodium's `OPSLIMIT_INTERACTIVE` / `MEMLIMIT_INTERACTIVE` parameters.

use argon2::{Algorithm, Argon2, Params, Version};
use zeroize::Zeroize;

use crate::CryptoError;
use crate::b64;

/// `crypto_pwhash_OPSLIMIT_INTERACTIVE` (time cost).
const T_COST: u32 = 2;
/// `crypto_pwhash_MEMLIMIT_INTERACTIVE` in KiB (64 MiB).
const M_COST_KIB: u32 = 65_536;
/// Output length (`crypto_secretbox_KEYBYTES`).
const KEY_LEN: usize = 32;
/// Required salt length (`crypto_pwhash_SALTBYTES`).
const SALT_LEN: usize = 16;

/// Derive a 32-byte session key from `password` and a base64-encoded 16-byte `salt`.
///
/// Returns the derived key as base64. The intermediate key material is zeroized on drop.
pub fn derive_session_key(password: &str, salt_b64: &str) -> Result<String, CryptoError> {
    let salt = b64::decode(salt_b64)?;
    if salt.len() != SALT_LEN {
        return Err(CryptoError::InvalidLength {
            expected: SALT_LEN,
            got: salt.len(),
        });
    }

    let params = Params::new(M_COST_KIB, T_COST, 1, Some(KEY_LEN))
        .map_err(|e| CryptoError::Kdf(e.to_string()))?;

    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);

    let mut key = vec![0u8; KEY_LEN];
    argon2
        .hash_password_into(password.as_bytes(), &salt, &mut key)
        .map_err(|e| CryptoError::Kdf(e.to_string()))?;

    let encoded = b64::encode(&key);
    key.zeroize();
    Ok(encoded)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn zero_salt() -> String {
        b64::encode(&[0u8; 16])
    }

    #[test]
    fn deterministic() {
        let a = derive_session_key("pw", &zero_salt()).unwrap();
        let b = derive_session_key("pw", &zero_salt()).unwrap();
        assert_eq!(a, b);
    }

    #[test]
    fn output_is_32_bytes() {
        let key = derive_session_key("pw", &zero_salt()).unwrap();
        assert_eq!(b64::decode(&key).unwrap().len(), 32);
    }

    #[test]
    fn different_passwords_differ() {
        let salt = zero_salt();
        let a = derive_session_key("alpha", &salt).unwrap();
        let b = derive_session_key("beta", &salt).unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn different_salts_differ() {
        let s1 = b64::encode(&[1u8; 16]);
        let s2 = b64::encode(&[2u8; 16]);
        let a = derive_session_key("pw", &s1).unwrap();
        let b = derive_session_key("pw", &s2).unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn bad_salt_length() {
        let short = b64::encode(&[0u8; 8]);
        assert!(derive_session_key("pw", &short).is_err());
    }
}