hefesto 1.1.0

Double envelope encryption — AES-256-GCM + Argon2id for multi-tenant applications
Documentation
use crate::error::{HefestoError, Result};
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, SaltString},
    PasswordHasher, PasswordVerifier,
};

pub(crate) fn hash_password(password: &str) -> Result<String> {
    let salt = SaltString::generate(&mut OsRng);
    crate::kdf::make_argon2()
        .hash_password(password.as_bytes(), &salt)
        .map(|h| h.to_string())
        .map_err(|e| HefestoError::PasswordHashFailed(e.to_string()))
}

pub(crate) fn verify_password(password: &str, hash: &str) -> bool {
    let parsed = match PasswordHash::new(hash) {
        Ok(h) => h,
        Err(_) => return false,
    };
    crate::kdf::make_argon2()
        .verify_password(password.as_bytes(), &parsed)
        .is_ok()
}

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

    #[test]
    fn hash_and_verify() {
        let hash = hash_password("my_password").unwrap();
        assert!(verify_password("my_password", &hash));
    }

    #[test]
    fn wrong_password_fails() {
        let hash = hash_password("correct_password").unwrap();
        assert!(!verify_password("wrong_password", &hash));
    }

    #[test]
    fn hashes_are_unique() {
        let h1 = hash_password("password").unwrap();
        let h2 = hash_password("password").unwrap();
        assert_ne!(h1, h2);
        assert!(verify_password("password", &h1));
        assert!(verify_password("password", &h2));
    }

    #[test]
    fn invalid_hash_returns_false() {
        assert!(!verify_password("password", "not_a_valid_hash"));
        assert!(!verify_password("password", ""));
    }

    #[test]
    fn hash_starts_with_argon2id() {
        let hash = hash_password("test").unwrap();
        assert!(hash.starts_with("$argon2id$"));
    }
}