Skip to main content

allowthem_core/
password.rs

1use argon2::{Argon2, PasswordVerifier};
2use password_hash::{PasswordHash as PhcHash, PasswordHasher, SaltString, rand_core::OsRng};
3
4use crate::error::AuthError;
5use crate::types::PasswordHash;
6
7/// Hash a plaintext password with Argon2id.
8///
9/// Uses `Argon2::default()` (Argon2id, OWASP-recommended params: m=19456, t=2, p=1).
10/// Returns the PHC string wrapped in the `PasswordHash` newtype.
11pub fn hash_password(plaintext: &str) -> Result<PasswordHash, AuthError> {
12    let salt = SaltString::generate(&mut OsRng);
13    let phc = Argon2::default()
14        .hash_password(plaintext.as_bytes(), &salt)
15        .map_err(|e| AuthError::InvalidPasswordHash(e.to_string()))?;
16    Ok(PasswordHash::new_unchecked(phc.to_string()))
17}
18
19/// Verify a plaintext password against a stored `PasswordHash`.
20///
21/// Returns `Ok(true)` if the password matches, `Ok(false)` if it does not.
22/// Returns `Err` only for structural errors such as a corrupt or unparseable hash string.
23pub fn verify_password(plaintext: &str, hash: &PasswordHash) -> Result<bool, AuthError> {
24    let phc =
25        PhcHash::new(hash.as_str()).map_err(|e| AuthError::InvalidPasswordHash(e.to_string()))?;
26    match Argon2::default().verify_password(plaintext.as_bytes(), &phc) {
27        Ok(()) => Ok(true),
28        Err(password_hash::Error::Password) => Ok(false),
29        Err(e) => Err(AuthError::InvalidPasswordHash(e.to_string())),
30    }
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[test]
38    fn test_hash_and_verify_correct_password() {
39        let hash = hash_password("correct-horse-battery-staple").expect("hash_password");
40        let result =
41            verify_password("correct-horse-battery-staple", &hash).expect("verify_password");
42        assert!(result, "correct password must verify as true");
43    }
44
45    #[test]
46    fn test_verify_wrong_password_returns_false() {
47        let hash = hash_password("the-real-password").expect("hash_password");
48        let result = verify_password("wrong-password", &hash).expect("verify_password");
49        assert!(
50            !result,
51            "wrong password must return Ok(false), not an error"
52        );
53    }
54
55    #[test]
56    fn test_verify_garbage_hash_returns_error() {
57        let garbage = PasswordHash::new_unchecked("not-a-phc-string".to_string());
58        let result = verify_password("anything", &garbage);
59        assert!(result.is_err(), "corrupt hash must return Err");
60    }
61
62    #[test]
63    fn test_two_hashes_of_same_password_differ() {
64        let h1 = hash_password("same-input").expect("hash 1");
65        let h2 = hash_password("same-input").expect("hash 2");
66        assert_ne!(
67            h1.as_str(),
68            h2.as_str(),
69            "each hash must have a unique salt — identical outputs would indicate missing salt"
70        );
71    }
72}