hefesto 1.0.0

Double envelope encryption — AES-256-GCM + Argon2id for multi-tenant applications
Documentation
use crate::error::{HefestoError, Result};
use argon2::Argon2;
use rand::RngCore;
use zeroize::Zeroizing;

const ARGON2_MEMORY_KB: u32 = 64 * 1024;
const ARGON2_ITERATIONS: u32 = 3;
const ARGON2_PARALLELISM: u32 = 1;

pub(crate) fn derive_key(secret: &str, salt: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
    let mut key = Zeroizing::new([0u8; 32]);

    let params = argon2::Params::new(
        ARGON2_MEMORY_KB,
        ARGON2_ITERATIONS,
        ARGON2_PARALLELISM,
        Some(32),
    )
    .map_err(|e| HefestoError::KeyDerivationFailed(e.to_string()))?;

    Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
        .hash_password_into(secret.as_bytes(), salt, &mut *key)
        .map_err(|e| HefestoError::KeyDerivationFailed(e.to_string()))?;

    Ok(key)
}

pub(crate) fn generate_salt() -> [u8; 16] {
    let mut salt = [0u8; 16];
    rand::rngs::OsRng.fill_bytes(&mut salt);
    salt
}

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

    #[test]
    fn derived_key_is_32_bytes() {
        let key = derive_key("my_secret", &[1u8; 16]).unwrap();
        assert_eq!(key.len(), 32);
    }

    #[test]
    fn same_inputs_produce_same_key() {
        let salt = [42u8; 16];
        let k1 = derive_key("password", &salt).unwrap();
        let k2 = derive_key("password", &salt).unwrap();
        assert_eq!(*k1, *k2);
    }

    #[test]
    fn different_salts_produce_different_keys() {
        let k1 = derive_key("password", &[1u8; 16]).unwrap();
        let k2 = derive_key("password", &[2u8; 16]).unwrap();
        assert_ne!(*k1, *k2);
    }

    #[test]
    fn different_secrets_produce_different_keys() {
        let salt = [1u8; 16];
        let k1 = derive_key("secret_a", &salt).unwrap();
        let k2 = derive_key("secret_b", &salt).unwrap();
        assert_ne!(*k1, *k2);
    }

    #[test]
    fn salts_are_unique() {
        assert_ne!(generate_salt(), generate_salt());
    }
}