//! Password hashing with argon2id. use argon2::Argon2; use argon2::password_hash::phc::PasswordHash; use argon2::password_hash::{PasswordHasher, PasswordVerifier}; use crate::error::{Error, Result}; /// Hashes `pw` with argon2id (default parameters) and a random salt. Returns a PHC string. pub fn hash_password(pw: &str) -> Result { // The trait method draws a fresh salt from the OS RNG. Argon2::default() .hash_password(pw.as_bytes()) .map(|h| h.to_string()) .map_err(|e| Error::Crypto(format!("argon2: {e}"))) } /// Returns true only if `pw` matches the PHC string `hash`. Any error yields false. pub fn verify_password(pw: &str, hash: &str) -> bool { let Ok(parsed) = PasswordHash::new(hash) else { return false; }; Argon2::default() .verify_password(pw.as_bytes(), &parsed) .is_ok() } #[cfg(test)] mod tests { use super::*; #[test] fn hash_then_verify() { let hash = hash_password("correct horse").unwrap(); assert!(hash.starts_with("$argon2id$")); assert!(verify_password("correct horse", &hash)); assert!(!verify_password("wrong", &hash)); } #[test] fn salts_differ_between_hashes() { let a = hash_password("pw").unwrap(); let b = hash_password("pw").unwrap(); assert_ne!(a, b); } #[test] fn verify_is_false_on_malformed_hash() { assert!(!verify_password("pw", "not a phc string")); assert!(!verify_password("pw", "")); } }