use std::collections::HashSet;
use std::sync::OnceLock;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use tokio::task;
use crate::errors::AppError;
const COMMON_PASSWORDS: &[&str] = &[
"password",
"123456",
"12345678",
"1234567890",
"qwerty",
"abc123",
"monkey",
"1234567",
"letmein",
"trustno1",
"dragon",
"baseball",
"iloveyou",
"master",
"sunshine",
"ashley",
"bailey",
"passw0rd",
"shadow",
"123123",
"654321",
"superman",
"qazwsx",
"michael",
"football",
"password1",
"password123",
"batman",
"login",
"welcome",
"admin",
"princess",
"starwars",
"admin123",
"hello",
"charlie",
"donald",
"login",
"loveme",
"mustang",
"access",
"ninja",
"hunter",
"zaq1zaq1",
"qwerty123",
"letmein1",
"welcome1",
"password!",
"passw0rd!",
"qwerty!",
"123456!",
"hello123",
"welcome123",
"secret",
"secret123",
"changeme",
"passpass",
"freedom",
"whatever",
"qwertyuiop",
"123456789",
"12345678!",
"aaaaaa",
"111111",
"000000",
"abcdef",
"abcdefg",
"abcdefgh",
"abc12345",
"123abc",
"1q2w3e4r",
"1qaz2wsx",
"zxcvbnm",
"asdfghjkl",
"1234qwer",
"qwer1234",
"2020",
"2021",
"2022",
"2023",
"2024",
"2025",
"summer",
"winter",
"spring",
"autumn",
"january",
"february",
"monday",
"friday",
"soccer",
"hockey",
"cookie",
"chocolate",
"computer",
"internet",
"flower",
"orange",
"pepper",
"cheese",
"winner",
"loser",
];
static COMMON_PASSWORD_SET: OnceLock<HashSet<&'static str>> = OnceLock::new();
fn common_password_set() -> &'static HashSet<&'static str> {
COMMON_PASSWORD_SET.get_or_init(|| COMMON_PASSWORDS.iter().copied().collect())
}
fn is_common_password(password: &str) -> bool {
let lower = password.to_lowercase();
if common_password_set().contains(lower.as_str()) {
return true;
}
let base: String = lower
.chars()
.take_while(|c| c.is_ascii_alphabetic())
.collect();
if !base.is_empty() && common_password_set().contains(base.as_str()) {
return true;
}
false
}
#[derive(Clone)]
pub struct PasswordRules {
pub min_length: usize,
pub require_uppercase: bool,
pub require_lowercase: bool,
pub require_number: bool,
pub require_special: bool,
pub check_common_passwords: bool,
}
impl Default for PasswordRules {
fn default() -> Self {
Self {
min_length: 10,
require_uppercase: true,
require_lowercase: true,
require_number: true,
require_special: true,
check_common_passwords: true,
}
}
}
fn dummy_hash() -> &'static str {
use std::sync::OnceLock;
static HASH: OnceLock<String> = OnceLock::new();
HASH.get_or_init(|| {
let salt = SaltString::generate(&mut OsRng);
Argon2::default()
.hash_password(b"timing-attack-mitigation-dummy", &salt)
.expect("dummy hash generation must succeed")
.to_string()
})
}
#[derive(Clone)]
pub struct PasswordService {
rules: PasswordRules,
}
impl Default for PasswordService {
fn default() -> Self {
Self::new(PasswordRules::default())
}
}
impl PasswordService {
pub fn new(rules: PasswordRules) -> Self {
Self { rules }
}
pub fn validate(&self, password: &str) -> Result<(), AppError> {
let mut errors = Vec::new();
if password.len() < self.rules.min_length {
errors.push(format!(
"Password must be at least {} characters",
self.rules.min_length
));
}
if self.rules.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
errors.push("Password must contain at least one uppercase letter".to_string());
}
if self.rules.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
errors.push("Password must contain at least one lowercase letter".to_string());
}
if self.rules.require_number && !password.chars().any(|c| c.is_ascii_digit()) {
errors.push("Password must contain at least one number".to_string());
}
if self.rules.require_special {
let special_chars = "@$!%*?&#^()-._";
if !password.chars().any(|c| special_chars.contains(c)) {
errors.push("Password must contain at least one special character".to_string());
}
}
if self.rules.check_common_passwords && is_common_password(password) {
errors.push(
"This password is too common and easily guessable. Please choose a stronger password."
.to_string(),
);
}
if errors.is_empty() {
Ok(())
} else {
Err(AppError::Validation(errors.join("; ")))
}
}
pub async fn hash(&self, password: String) -> Result<String, AppError> {
task::spawn_blocking(move || {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| AppError::Internal(anyhow::anyhow!("Password hashing failed: {}", e)))
})
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("Password hash task failed: {}", e)))?
}
pub async fn verify(&self, password: String, hash: String) -> Result<bool, AppError> {
task::spawn_blocking(move || {
let parsed_hash = PasswordHash::new(&hash)
.map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid password hash: {}", e)))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
})
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("Password verify task failed: {}", e)))?
}
pub async fn verify_dummy(&self, password: String) {
let _ = task::spawn_blocking(move || {
let hash_str = dummy_hash();
match PasswordHash::new(hash_str) {
Ok(parsed_hash) => {
let _ = Argon2::default().verify_password(password.as_bytes(), &parsed_hash);
}
Err(e) => {
tracing::error!(error = %e, "SECURITY: Failed to parse dummy hash - falling back to sleep");
metrics::counter!("security.password.dummy_hash_fallback").increment(1);
use rand::Rng;
let sleep_ms: u64 = OsRng.gen_range(100..300);
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
}
}
})
.await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_validation_valid() {
let service = PasswordService::default();
assert!(service.validate("SecurePass1!").is_ok());
assert!(service.validate("MyP@ssw0rd123").is_ok());
}
#[test]
fn test_password_validation_too_short() {
let service = PasswordService::default();
let result = service.validate("Short1!");
assert!(result.is_err());
}
#[test]
fn test_password_validation_missing_uppercase() {
let service = PasswordService::default();
let result = service.validate("securepass1!");
assert!(result.is_err());
}
#[test]
fn test_password_validation_missing_special() {
let service = PasswordService::default();
let result = service.validate("SecurePass123");
assert!(result.is_err());
}
#[tokio::test]
async fn test_password_hash_and_verify() {
let service = PasswordService::default();
let password = "SecurePass1!";
let hash = service.hash(password.to_string()).await.unwrap();
assert!(service
.verify(password.to_string(), hash.clone())
.await
.unwrap());
assert!(!service
.verify("WrongPassword1!".to_string(), hash)
.await
.unwrap());
}
#[test]
fn test_password_validation_missing_lowercase() {
let service = PasswordService::default();
let result = service.validate("SECUREPASS1!");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("lowercase"));
}
#[test]
fn test_password_validation_missing_digit() {
let service = PasswordService::default();
let result = service.validate("SecurePass!!");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("number"));
}
#[test]
fn test_password_validation_all_special_chars() {
let service = PasswordService::default();
assert!(service.validate("SecurePass1@").is_ok());
assert!(service.validate("SecurePass1$").is_ok());
assert!(service.validate("SecurePass1!").is_ok());
assert!(service.validate("SecurePass1%").is_ok());
assert!(service.validate("SecurePass1*").is_ok());
assert!(service.validate("SecurePass1?").is_ok());
assert!(service.validate("SecurePass1&").is_ok());
assert!(service.validate("SecurePass1#").is_ok());
assert!(service.validate("SecurePass1^").is_ok());
assert!(service.validate("SecurePass1(").is_ok());
assert!(service.validate("SecurePass1)").is_ok());
}
#[tokio::test]
async fn test_password_hash_is_unique() {
let service = PasswordService::default();
let password = "SecurePass1!";
let hash1 = service.hash(password.to_string()).await.unwrap();
let hash2 = service.hash(password.to_string()).await.unwrap();
assert_ne!(hash1, hash2);
assert!(service.verify(password.to_string(), hash1).await.unwrap());
assert!(service.verify(password.to_string(), hash2).await.unwrap());
}
#[test]
fn test_password_exact_minimum_length() {
let service = PasswordService::default();
assert!(service.validate("Secure1!ab").is_ok());
assert!(service.validate("Secure1!a").is_err());
}
#[tokio::test]
async fn test_verify_dummy_does_not_panic() {
let service = PasswordService::default();
service.verify_dummy("any-password".to_string()).await;
service.verify_dummy(String::new()).await;
service.verify_dummy("a".repeat(1000)).await;
}
#[test]
fn test_common_password_rejection() {
let service = PasswordService::default();
let result = service.validate("Password123!");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too common"));
let result = service.validate("Qwerty1234!");
assert!(result.is_err());
let result = service.validate("Letmein123!");
assert!(result.is_err());
}
#[test]
fn test_common_password_case_insensitive() {
let service = PasswordService::default();
let result = service.validate("PASSWORD123!");
assert!(result.is_err());
let result = service.validate("PaSsWoRd123!");
assert!(result.is_err());
}
#[test]
fn test_common_password_with_suffix() {
let service = PasswordService::default();
let result = service.validate("Summer2024!");
assert!(result.is_err());
let result = service.validate("Dragon123!!");
assert!(result.is_err());
}
#[test]
fn test_uncommon_password_accepted() {
let service = PasswordService::default();
assert!(service.validate("Xk9#mP2$vLqW").is_ok());
assert!(service.validate("MyUnique!Pass1").is_ok());
assert!(service.validate("Th1s1sN0tC0mm0n!").is_ok());
}
#[test]
fn test_common_password_check_disabled() {
let rules = PasswordRules {
check_common_passwords: false,
..PasswordRules::default()
};
let service = PasswordService::new(rules);
assert!(service.validate("Password123!").is_ok());
}
#[test]
fn test_is_common_password_helper() {
assert!(is_common_password("password"));
assert!(is_common_password("PASSWORD"));
assert!(is_common_password("Password123"));
assert!(is_common_password("qwerty"));
assert!(is_common_password("summer2024"));
assert!(!is_common_password("xK9mP2vLqW"));
assert!(!is_common_password("notinlist"));
}
}