2.1 KiB · text History 8b74183
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::password_hash::SaltString;
use argon2::password_hash::rand_core::OsRng;
use anyhow::{Context, Result};

/// Hash a password using Argon2
pub fn hash_password(password: &str) -> Result<String> {
    // Fresh random salt per hash. The salt is embedded in the returned PHC
    // string, so `verify_password` keeps working against older hashes.
    let salt = SaltString::try_from_rng(&mut OsRng)
        .map_err(|e| anyhow::anyhow!("Failed to generate salt: {}", e))?;
    let argon2 = Argon2::default();
    let password_hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .context("Failed to hash password")?;
    Ok(password_hash.to_string())
}

/// Verify a password against its hash
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
    let parsed_hash = PasswordHash::new(hash).context("Failed to parse password hash")?;
    let argon2 = Argon2::default();
    
    match argon2.verify_password(password.as_bytes(), &parsed_hash) {
        Ok(()) => Ok(true),
        Err(argon2::password_hash::Error::Password) => Ok(false),
        Err(e) => Err(anyhow::anyhow!("Password verification error: {}", e)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_password_hashing() {
        let password = "test_password_123";
        let hash = hash_password(password).expect("Failed to hash password");
        
        // Verify correct password
        assert!(verify_password(password, &hash).unwrap());
        
        // Verify incorrect password
        assert!(!verify_password("wrong_password", &hash).unwrap());
    }

    #[test]
    fn test_hashes_are_salted_per_call() {
        let password = "test_password_123";
        let a = hash_password(password).expect("Failed to hash password");
        let b = hash_password(password).expect("Failed to hash password");

        // Two hashes of the same password must not collide, or the hashes
        // leak which accounts share a password.
        assert_ne!(a, b);
        assert!(verify_password(password, &a).unwrap());
        assert!(verify_password(password, &b).unwrap());
    }
}