use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
},
Argon2, Algorithm, Params, Version,
};
use super::config::AuthConfig;
use crate::error::ApiError;
pub fn hash_password(password: &str, config: &AuthConfig) -> Result<String, ApiError> {
let salt = SaltString::generate(&mut OsRng);
let params = Params::new(
config.argon2_memory_cost,
config.argon2_time_cost,
config.argon2_parallelism,
None,
).map_err(|e| ApiError::InternalServerError(format!("Invalid Argon2 params: {}", e)))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| ApiError::InternalServerError(format!("Failed to hash password: {}", e)))?
.to_string();
Ok(password_hash)
}
pub fn hash_password_default(password: &str) -> Result<String, ApiError> {
hash_password(password, &AuthConfig::default())
}
pub fn verify_password(password: &str, hash: &str) -> Result<bool, ApiError> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| ApiError::InternalServerError(format!("Invalid password hash: {}", e)))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
pub fn validate_password_strength(password: &str) -> Result<(), ApiError> {
if password.len() < 8 {
return Err(ApiError::ValidationError(
"Password must be at least 8 characters long".to_string(),
));
}
if !password.chars().any(|c| c.is_uppercase()) {
return Err(ApiError::ValidationError(
"Password must contain at least one uppercase letter".to_string(),
));
}
if !password.chars().any(|c| c.is_lowercase()) {
return Err(ApiError::ValidationError(
"Password must contain at least one lowercase letter".to_string(),
));
}
if !password.chars().any(|c| c.is_ascii_digit()) {
return Err(ApiError::ValidationError(
"Password must contain at least one digit".to_string(),
));
}
Ok(())
}
pub struct PasswordValidator {
min_length: usize,
require_uppercase: bool,
require_lowercase: bool,
require_digit: bool,
require_special: bool,
}
impl PasswordValidator {
pub fn new() -> Self {
Self::default()
}
pub fn min_length(mut self, length: usize) -> Self {
self.min_length = length;
self
}
pub fn require_uppercase(mut self, required: bool) -> Self {
self.require_uppercase = required;
self
}
pub fn require_lowercase(mut self, required: bool) -> Self {
self.require_lowercase = required;
self
}
pub fn require_digit(mut self, required: bool) -> Self {
self.require_digit = required;
self
}
pub fn require_special(mut self, required: bool) -> Self {
self.require_special = required;
self
}
pub fn validate(&self, password: &str) -> Result<(), ApiError> {
let mut errors = Vec::new();
if password.len() < self.min_length {
errors.push(format!(
"Password must be at least {} characters long",
self.min_length
));
}
if self.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
errors.push("Password must contain at least one uppercase letter".to_string());
}
if self.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
errors.push("Password must contain at least one lowercase letter".to_string());
}
if self.require_digit && !password.chars().any(|c| c.is_ascii_digit()) {
errors.push("Password must contain at least one digit".to_string());
}
if self.require_special && !password.chars().any(|c| !c.is_alphanumeric()) {
errors.push("Password must contain at least one special character".to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(ApiError::ValidationError(errors.join("; ")))
}
}
}
impl Default for PasswordValidator {
fn default() -> Self {
Self {
min_length: 8,
require_uppercase: true,
require_lowercase: true,
require_digit: true,
require_special: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_and_verify() {
let config = AuthConfig::default();
let password = "SecurePass123";
let hash = hash_password(password, &config).unwrap();
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("wrong-password", &hash).unwrap());
}
#[test]
fn test_password_validation() {
assert!(validate_password_strength("SecurePass1").is_ok());
assert!(validate_password_strength("short").is_err());
assert!(validate_password_strength("nouppercase1").is_err());
assert!(validate_password_strength("NOLOWERCASE1").is_err());
assert!(validate_password_strength("NoDigitsHere").is_err());
}
#[test]
fn test_custom_validator() {
let validator = PasswordValidator::new()
.min_length(12)
.require_special(true);
assert!(validator.validate("SecurePass1!").is_ok());
assert!(validator.validate("SecurePass1").is_err()); }
}