use argon2::{
password_hash::{
rand_core::OsRng, PasswordHash, PasswordHasher as Argon2Hasher, PasswordVerifier,
SaltString,
},
Algorithm, Argon2, Params, Version,
};
use crate::auth::config::PasswordConfig;
use crate::error::Error;
#[derive(Clone)]
pub struct PasswordHasher {
params: Params,
min_password_length: usize,
}
impl Default for PasswordHasher {
fn default() -> Self {
Self::new(PasswordConfig::default())
}
}
impl PasswordHasher {
pub fn new(config: PasswordConfig) -> Self {
let params = Params::new(
config.memory_cost_kib,
config.time_cost,
config.parallelism,
None, )
.expect("Invalid Argon2 parameters");
Self {
params,
min_password_length: config.min_password_length,
}
}
pub fn hash(&self, password: &str) -> Result<String, Error> {
if password.len() < self.min_password_length {
return Err(Error::ValidationError(format!(
"Password must be at least {} characters",
self.min_password_length
)));
}
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, self.params.clone());
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| Error::Auth(format!("Failed to hash password: {}", e)))?;
Ok(hash.to_string())
}
pub fn verify(&self, password: &str, hash: &str) -> Result<bool, Error> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| Error::Auth(format!("Invalid password hash format: {}", e)))?;
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(Error::Auth(format!("Password verification failed: {}", e))),
}
}
pub fn needs_rehash(&self, hash: &str) -> bool {
let Ok(parsed_hash) = PasswordHash::new(hash) else {
return true; };
if parsed_hash.algorithm != argon2::Algorithm::Argon2id.ident() {
return true;
}
let Some(version) = parsed_hash.version else {
return true;
};
if version != 19 {
return true;
}
let params = &parsed_hash.params;
let m = params
.iter()
.find(|(k, _)| k.as_str() == "m")
.and_then(|(_, v)| v.decimal().ok());
let t = params
.iter()
.find(|(k, _)| k.as_str() == "t")
.and_then(|(_, v)| v.decimal().ok());
let p = params
.iter()
.find(|(k, _)| k.as_str() == "p")
.and_then(|(_, v)| v.decimal().ok());
if m != Some(self.params.m_cost()) {
return true;
}
if t != Some(self.params.t_cost()) {
return true;
}
if p != Some(self.params.p_cost()) {
return true;
}
false
}
pub fn min_password_length(&self) -> usize {
self.min_password_length
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_and_verify() {
let hasher = PasswordHasher::default();
let password = "test_password_123";
let hash = hasher.hash(password).expect("Failed to hash password");
assert!(hash.starts_with("$argon2id$"));
assert!(hasher.verify(password, &hash).expect("Verification failed"));
assert!(!hasher
.verify("wrong_password", &hash)
.expect("Verification failed"));
}
#[test]
fn test_password_too_short() {
let hasher = PasswordHasher::default();
let result = hasher.hash("short");
assert!(result.is_err());
if let Err(Error::ValidationError(msg)) = result {
assert!(msg.contains("at least 8 characters"));
} else {
panic!("Expected ValidationError");
}
}
#[test]
fn test_custom_min_length() {
let config = PasswordConfig {
min_password_length: 12,
..Default::default()
};
let hasher = PasswordHasher::new(config);
assert!(hasher.hash("0123456789").is_err());
assert!(hasher.hash("012345678901").is_ok());
}
#[test]
fn test_needs_rehash_same_params() {
let hasher = PasswordHasher::default();
let hash = hasher.hash("test_password_123").unwrap();
assert!(!hasher.needs_rehash(&hash));
}
#[test]
fn test_needs_rehash_different_params() {
let hasher1 = PasswordHasher::new(PasswordConfig {
memory_cost_kib: 32768,
..Default::default()
});
let hash = hasher1.hash("test_password_123").unwrap();
let hasher2 = PasswordHasher::new(PasswordConfig {
memory_cost_kib: 65536,
..Default::default()
});
assert!(hasher2.needs_rehash(&hash));
}
#[test]
fn test_invalid_hash_format() {
let hasher = PasswordHasher::default();
let result = hasher.verify("password", "not_a_valid_hash");
assert!(result.is_err());
}
#[test]
fn test_different_hashes_for_same_password() {
let hasher = PasswordHasher::default();
let password = "test_password_123";
let hash1 = hasher.hash(password).unwrap();
let hash2 = hasher.hash(password).unwrap();
assert_ne!(hash1, hash2);
assert!(hasher.verify(password, &hash1).unwrap());
assert!(hasher.verify(password, &hash2).unwrap());
}
}