//! Argon2id password hashing. //! //! `hash_password` returns a PHC-encoded string suitable for storage in //! `users.password_hash`. `verify_password` checks a candidate against a //! stored hash. Default Argon2 params are argon2id with the crate's //! recommended cost (m=19456 KiB, t=2, p=1) — OWASP-aligned. use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::Argon2; use crate::error::{AppError, AppResult}; pub fn hash_password(plain: &str) -> AppResult { let salt = SaltString::generate(&mut OsRng); Argon2::default() .hash_password(plain.as_bytes(), &salt) .map(|h| h.to_string()) .map_err(|e| AppError::Other(anyhow::anyhow!("password hash failed: {e}"))) } pub fn verify_password(plain: &str, phc: &str) -> bool { let Ok(hash) = PasswordHash::new(phc) else { return false; }; Argon2::default() .verify_password(plain.as_bytes(), &hash) .is_ok() } #[cfg(test)] mod tests { use super::*; #[test] fn hash_then_verify_roundtrip() { let phc = hash_password("correct horse battery staple").unwrap(); assert!(phc.starts_with("$argon2id$")); assert!(verify_password("correct horse battery staple", &phc)); } #[test] fn verify_rejects_wrong_password() { let phc = hash_password("hunter2").unwrap(); assert!(!verify_password("hunter3", &phc)); } #[test] fn verify_rejects_malformed_hash() { assert!(!verify_password("anything", "not a real phc string")); } #[test] fn hashes_are_salted() { let a = hash_password("same").unwrap(); let b = hash_password("same").unwrap(); assert_ne!(a, b, "two hashes of the same password must differ (salt)"); } }