use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use crate::error::{AuthError, AuthResult}; /// Hash a plaintext password using Argon2 with a random salt. /// /// Returns a PHC-format string suitable for database storage. pub fn hash_password(plain: &str) -> AuthResult { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let hash = argon2 .hash_password(plain.as_bytes(), &salt) .map_err(|e| AuthError::HashError(e.to_string()))?; Ok(hash.to_string()) } /// Verify a plaintext password against a stored PHC-format hash. /// /// Returns `Ok(true)` if the password matches, `Ok(false)` if not. pub fn verify_password(plain: &str, hash: &str) -> AuthResult { let parsed = PasswordHash::new(hash).map_err(|e| AuthError::HashError(e.to_string()))?; Ok(Argon2::default() .verify_password(plain.as_bytes(), &parsed) .is_ok()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_hash_and_verify() { let hash = hash_password("test123").unwrap(); assert!( verify_password("test123", &hash).unwrap(), "Correct password should verify" ); assert!( !verify_password("wrong", &hash).unwrap(), "Wrong password should not verify" ); } #[test] fn test_hash_is_unique() { let hash1 = hash_password("same_password").unwrap(); let hash2 = hash_password("same_password").unwrap(); assert_ne!( hash1, hash2, "Two hashes of the same password should differ (different salts)" ); } }