//! Argon2 password hashing and verification utilities. //! //! Uses the `argon2` crate (Argon2id variant) with default parameters, //! which provide a good security / performance trade-off for interactive //! authentication. use anyhow::Result; use argon2::{ password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use rand_core::OsRng; use tracing; /// Hash a plaintext password using Argon2id with a random salt. /// /// The returned string is in the PHC string format /// (`$argon2id$v=19$...`) and can be stored directly in the database. /// /// # Errors /// /// Returns an error if the argon2 library fails (extremely rare — /// typically indicates an OOM or system-level crypto failure). pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); // cryptographic random salt let argon2 = Argon2::default(); // Argon2id with default params let hash = argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| anyhow::anyhow!("failed to hash password: {e}"))?; tracing::debug!("password hashed successfully"); Ok(hash.to_string()) } /// Verify a plaintext password against a previously-hashed PHC string. /// /// Returns `Ok(true)` if the password matches, `Ok(false)` if it does not, /// and `Err` if the hash string is malformed. /// /// # Errors /// /// Returns an error if the hash string is not a valid PHC string or if /// the argon2 library encounters an internal failure. pub fn verify_password(password: &str, hash: &str) -> Result { let parsed_hash = PasswordHash::new(hash) .map_err(|e| anyhow::anyhow!("failed to parse password hash: {e}"))?; let argon2 = Argon2::default(); // Argon2id with default params let valid = argon2 .verify_password(password.as_bytes(), &parsed_hash) .is_ok(); tracing::debug!("password verification result: {valid}"); Ok(valid) } #[cfg(test)] mod tests { use super::*; #[test] fn test_hash_and_verify() { let password = "my-secure-password-123!"; let hash = hash_password(password).unwrap(); assert!(verify_password(password, &hash).unwrap()); } #[test] fn test_wrong_password_fails() { let hash = hash_password("correct-password").unwrap(); assert!(!verify_password("wrong-password", &hash).unwrap()); } #[test] fn test_hashes_are_different() { let h1 = hash_password("same-password").unwrap(); let h2 = hash_password("same-password").unwrap(); // Different salts → different hashes. assert_ne!(h1, h2); } #[test] fn test_invalid_hash_returns_error() { let result = verify_password("password", "not-a-valid-hash"); assert!(result.is_err()); } }