use super::models::{AuthError, AuthResult};
use bcrypt::{hash, verify};
const BCRYPT_COST: u32 = 12;
const MIN_PASSWORD_LEN: usize = 12;
const MAX_PASSWORD_LEN: usize = 72;
const COMMON_PASSWORDS: &[&str] = &[
"123456",
"123456789",
"12345678",
"1234567",
"1234567890",
"12345",
"1234",
"123456789a",
"123456a",
"123321",
"654321",
"666666",
"888888",
"000000",
"111111",
"222222",
"333333",
"444444",
"555555",
"777777",
"999999",
"abc123",
"abcdef",
"abc1234",
"abcab123",
"qwerty",
"qwerty123",
"qwerty1",
"qwerty12",
"qazwsx",
"qweasd",
"q1w2e3r4",
"qwer1234",
"asdfgh",
"zxcvbn",
"zxcvbnm",
"1q2w3e4r",
"1q2w3e",
"1qaz2wsx",
"password",
"password1",
"password12",
"password123",
"passw0rd",
"passw0rd1",
"pass1234",
"passwd",
"p@ssw0rd",
"p@ssword",
"admin",
"admin123",
"administrator",
"root",
"root123",
"toor",
"superuser",
"letmein",
"letmein1",
"welcome",
"welcome1",
"welcome123",
"monkey",
"monkey123",
"monkey1313",
"dragon",
"dragon123",
"master",
"master123",
"login",
"login123",
"princess",
"princess1",
"football",
"football123",
"baseball",
"baseball123",
"soccer",
"soccer123",
"hockey",
"hockey123",
"jordan",
"jordan23",
"jordan123",
"michael",
"michael1",
"daniel",
"daniel123",
"andrew",
"andrew1",
"joshua",
"joshua1",
"harley",
"harley1",
"robert",
"robert1",
"thomas",
"thomas1",
"jennifer",
"jennifer1",
"secret",
"secret1",
"secret123",
"test",
"test123",
"test1234",
"testtest",
"iloveyou",
"iloveyou1",
"iloveyou2",
"trustno1",
"password123!",
"password1!@#",
"welcome123!@",
"admin123!@#",
"qwerty123!@#",
"letmein123!@",
"welcome@1234",
"admin@12345",
"passw0rd!@#",
"password@123",
"abc123!@#$",
"welcome@123",
"admin!@#$%",
"root@12345",
"test@12345",
];
#[derive(Debug, Clone)]
pub struct PasswordPolicy {
pub bcrypt_cost: u32,
pub min_len: usize,
pub max_len: usize,
pub blacklist: Vec<String>,
}
impl Default for PasswordPolicy {
fn default() -> Self {
Self {
bcrypt_cost: BCRYPT_COST,
min_len: MIN_PASSWORD_LEN,
max_len: MAX_PASSWORD_LEN,
blacklist: COMMON_PASSWORDS.iter().map(|s| (*s).to_string()).collect(),
}
}
}
impl PasswordPolicy {
pub fn validate(&self, password: &str) -> AuthResult<()> {
if password.len() < self.min_len {
return Err(AuthError::PasswordHash(format!(
"Password must be at least {} characters",
self.min_len
)));
}
if password.len() > self.max_len {
return Err(AuthError::PasswordHash(format!(
"Password must not exceed {} characters (bcrypt truncation limit)",
self.max_len
)));
}
if !password.chars().any(|c| c.is_uppercase()) {
return Err(AuthError::PasswordHash(
"Password must contain at least one uppercase letter".to_string(),
));
}
if !password.chars().any(|c| c.is_lowercase()) {
return Err(AuthError::PasswordHash(
"Password must contain at least one lowercase letter".to_string(),
));
}
if !password.chars().any(|c| c.is_numeric()) {
return Err(AuthError::PasswordHash(
"Password must contain at least one digit".to_string(),
));
}
if !password.chars().any(|c| !c.is_alphanumeric()) {
return Err(AuthError::PasswordHash(
"Password must contain at least one special character".to_string(),
));
}
let password_lower = password.to_lowercase();
if self.blacklist.iter().any(|p| p == &password_lower) {
return Err(AuthError::PasswordHash(
"Password is too common and easily guessable".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PasswordHasher {
policy: PasswordPolicy,
}
impl PasswordHasher {
pub fn new() -> Self {
Self::with_policy(PasswordPolicy::default())
}
pub fn with_policy(policy: PasswordPolicy) -> Self {
Self { policy }
}
pub fn policy(&self) -> &PasswordPolicy {
&self.policy
}
pub fn hash(&self, password: &str) -> AuthResult<String> {
hash(password, self.policy.bcrypt_cost).map_err(|e| AuthError::PasswordHash(e.to_string()))
}
pub fn verify(&self, password: &str, hash: &str) -> AuthResult<()> {
let is_valid = verify(password, hash).map_err(|_| AuthError::InvalidCredentials)?;
if is_valid {
Ok(())
} else {
Err(AuthError::InvalidCredentials)
}
}
pub fn validate_strength(&self, password: &str) -> AuthResult<()> {
self.policy.validate(password)
}
pub fn validate_strength_with_policy(policy: &PasswordPolicy, password: &str) -> AuthResult<()> {
policy.validate(password)
}
}
impl Default for PasswordHasher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_PASSWORD: &str = "TestPassword123!";
const TEST_WRONG_PASSWORD: &str = "WrongPassword!";
const TEST_VALID_PASSWORD: &str = "ValidPass@123";
const TEST_WEAK_SHORT: &str = "Short1!";
const TEST_WEAK_NO_LETTER: &str = "12345678!@#$";
const TEST_WEAK_NO_DIGIT: &str = "OnlyLetters!@";
const TEST_WEAK_NO_UPPER: &str = "onlylowercase1!";
const TEST_WEAK_NO_LOWER: &str = "ONLYUPPERCASE1!";
const TEST_WEAK_NO_SPECIAL: &str = "Password123456";
const TEST_WEAK_COMMON: &str = "Password123!";
const TEST_PASSWORD_A: &str = "PasswordA@123";
const TEST_PASSWORD_B: &str = "SecurePass@456";
const TEST_PASSWORD_C: &str = "MySecret@789";
#[test]
fn test_hash_password() {
let hasher = PasswordHasher::new();
let password = TEST_PASSWORD;
let hash = hasher.hash(password).unwrap();
assert_ne!(hash, password);
assert!(hash.starts_with("$2b$"));
}
#[test]
fn test_verify_password() {
let hasher = PasswordHasher::new();
let password = TEST_PASSWORD;
let hash = hasher.hash(password).unwrap();
assert!(hasher.verify(password, &hash).is_ok());
assert!(hasher.verify(TEST_WRONG_PASSWORD, &hash).is_err());
}
#[test]
fn test_password_strength() {
let hasher = PasswordHasher::new();
assert!(hasher.validate_strength(TEST_WEAK_SHORT).is_err());
assert!(hasher.validate_strength(TEST_WEAK_NO_DIGIT).is_err());
assert!(hasher.validate_strength(TEST_WEAK_NO_LETTER).is_err());
assert!(hasher.validate_strength(TEST_WEAK_NO_UPPER).is_err());
assert!(hasher.validate_strength(TEST_WEAK_NO_LOWER).is_err());
assert!(hasher.validate_strength(TEST_WEAK_NO_SPECIAL).is_err());
assert!(hasher.validate_strength(TEST_WEAK_COMMON).is_err());
assert!(hasher.validate_strength(TEST_VALID_PASSWORD).is_ok());
}
#[test]
fn test_vuln_0004_password1_rejected() {
let hasher = PasswordHasher::new();
assert!(
hasher.validate_strength("password1").is_err(),
"password1 must be rejected by enhanced policy"
);
}
#[test]
fn test_vuln_0004_bcrypt_cost_12() {
let hasher = PasswordHasher::new();
let hash = hasher.hash(TEST_VALID_PASSWORD).unwrap();
let cost_str = hash.split('$').nth(2).expect("hash should have cost field");
assert_eq!(cost_str, "12", "bcrypt cost factor must be 12");
}
#[test]
fn test_vuln_0004_common_password_blacklist() {
let hasher = PasswordHasher::new();
assert!(
hasher.validate_strength("Password123!").is_err(),
"Password123! is in blacklist and must be rejected"
);
assert!(
hasher.validate_strength("Welcome123!@").is_err(),
"Welcome123!@ is in blacklist and must be rejected"
);
assert!(
hasher.validate_strength("Xy7#kQ9$mL2p").is_ok(),
"Xy7#kQ9$mL2p should pass (not in blacklist)"
);
}
#[test]
fn test_hash_and_verify_consistency() {
let hasher = PasswordHasher::new();
let passwords = vec![TEST_PASSWORD_A, TEST_PASSWORD_B, TEST_PASSWORD_C];
for password in passwords {
let hash = hasher.hash(password).unwrap();
assert!(hasher.verify(password, &hash).is_ok());
assert!(hasher.verify(&password.to_uppercase(), &hash).is_err());
}
}
#[test]
fn test_hd4_default_policy_preserves_original_values() {
let policy = PasswordPolicy::default();
assert_eq!(
policy.bcrypt_cost, BCRYPT_COST,
"default cost must match original const"
);
assert_eq!(
policy.min_len, MIN_PASSWORD_LEN,
"default min_len must match original const"
);
assert_eq!(
policy.max_len, MAX_PASSWORD_LEN,
"default max_len must match original const"
);
assert!(!policy.blacklist.is_empty(), "default blacklist must be non-empty");
assert!(
policy.blacklist.iter().any(|p| p == "password123!"),
"default blacklist must contain 'password123!'"
);
}
#[test]
fn test_hd4_custom_min_len_accepts_shorter_password() {
let custom_policy = PasswordPolicy {
min_len: 8,
..Default::default()
};
let password = "Abcdef1!";
assert!(
PasswordHasher::new().validate_strength(password).is_err(),
"Default policy (min_len=12) must reject 8-char password"
);
assert!(
PasswordHasher::validate_strength_with_policy(&custom_policy, password).is_ok(),
"Custom policy (min_len=8) must accept 8-char password"
);
}
#[test]
fn test_hd4_custom_cost_reflected_in_hash() {
let custom_policy = PasswordPolicy {
bcrypt_cost: 10,
..Default::default()
};
let hasher = PasswordHasher::with_policy(custom_policy);
let hash = hasher.hash(TEST_VALID_PASSWORD).expect("hash should succeed");
let cost_str = hash.split('$').nth(2).expect("hash should have cost field");
assert_eq!(cost_str, "10", "custom policy must use cost=10");
}
#[test]
fn test_hd4_hasher_with_policy_preserves_hash_verify() {
let policy = PasswordPolicy {
bcrypt_cost: 10,
min_len: 8,
..Default::default()
};
let hasher = PasswordHasher::with_policy(policy);
let hash = hasher.hash("TestPass1!").expect("hash should succeed");
assert!(hasher.verify("TestPass1!", &hash).is_ok());
assert!(hasher.verify("wrong-password", &hash).is_err());
}
#[test]
fn test_hd4_custom_max_len_rejects_overlong_password() {
let custom_policy = PasswordPolicy {
max_len: 20,
..Default::default()
};
let password = "Abcdefghijk123456789!"; assert_eq!(password.len(), 21);
assert!(
PasswordHasher::new().validate_strength(password).is_ok(),
"Default policy (max_len=72) must accept 21-char password"
);
assert!(
PasswordHasher::validate_strength_with_policy(&custom_policy, password).is_err(),
"Custom policy (max_len=20) must reject 21-char password"
);
}
#[test]
fn test_hd4_backward_compat_validate_strength_unchanged() {
let hasher = PasswordHasher::new();
assert!(hasher.validate_strength(TEST_WEAK_SHORT).is_err());
assert!(hasher.validate_strength(TEST_WEAK_COMMON).is_err());
assert!(hasher.validate_strength(TEST_VALID_PASSWORD).is_ok());
}
#[test]
fn test_ld3_custom_blacklist_rejects_specified_password() {
let password = "MyCustom@1234";
assert!(
PasswordHasher::new().validate_strength(password).is_ok(),
"Default policy must accept MyCustom@1234 (not in default blacklist)"
);
let custom_policy = PasswordPolicy {
blacklist: vec!["mycustom@1234".to_string()],
..Default::default()
};
assert!(
PasswordHasher::validate_strength_with_policy(&custom_policy, password).is_err(),
"Custom policy with 'mycustom@1234' in blacklist must reject it (case-insensitive)"
);
}
#[test]
fn test_ld3_extend_default_blacklist_with_extra_entries() {
let mut blacklist = PasswordPolicy::default().blacklist;
blacklist.push("company-specific-2024!".to_lowercase());
let custom_policy = PasswordPolicy {
blacklist,
..Default::default()
};
let password = "Company-Specific-2024!";
assert!(
PasswordHasher::validate_strength_with_policy(&custom_policy, password).is_err(),
"Extended blacklist must reject Company-Specific-2024! (case-insensitive)"
);
assert!(
PasswordHasher::validate_strength_with_policy(&custom_policy, "Password123!").is_err(),
"Extended blacklist must still reject original default blacklist entries"
);
}
#[test]
fn test_ld3_empty_blacklist_accepts_former_blacklisted_password() {
let empty_blacklist_policy = PasswordPolicy {
blacklist: vec![],
..Default::default()
};
assert!(
PasswordHasher::validate_strength_with_policy(&empty_blacklist_policy, "Password123!").is_ok(),
"Empty blacklist must accept Password123! (still passes complexity)"
);
}
#[test]
fn test_ld3_blacklist_matching_is_case_insensitive() {
let custom_policy = PasswordPolicy {
blacklist: vec!["forbidden@2024".to_string()],
..Default::default()
};
for variant in &["Forbidden@2024", "FORBIDDEN@2024", "forbidden@2024", "FoRbIdDeN@2024"] {
assert!(
PasswordHasher::validate_strength_with_policy(&custom_policy, variant).is_err(),
"Blacklist must match case-insensitively: {variant}"
);
}
}
}