avl_auth/
password.rs

1//! Password hashing and validation with Argon2id
2
3use crate::error::{AuthError, Result};
4use argon2::{
5    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
6    Argon2, Params, Version,
7};
8use rand::rngs::OsRng;
9
10pub struct PasswordManager {
11    argon2: Argon2<'static>,
12    policy: PasswordPolicy,
13}
14
15#[derive(Clone)]
16pub struct PasswordPolicy {
17    pub min_length: usize,
18    pub require_uppercase: bool,
19    pub require_lowercase: bool,
20    pub require_numbers: bool,
21    pub require_special: bool,
22    pub password_history: u32,
23}
24
25impl PasswordManager {
26    pub fn new(policy: PasswordPolicy, memory_cost: u32, time_cost: u32, parallelism: u32) -> Result<Self> {
27        let params = Params::new(memory_cost, time_cost, parallelism, None)
28            .map_err(|e| AuthError::ConfigError(e.to_string()))?;
29
30        let argon2 = Argon2::new(
31            argon2::Algorithm::Argon2id,
32            Version::V0x13,
33            params,
34        );
35
36        Ok(Self { argon2, policy })
37    }
38
39    pub fn hash_password(&self, password: &str) -> Result<String> {
40        self.validate_password(password)?;
41
42        let salt = SaltString::generate(&mut OsRng);
43        let password_hash = self.argon2
44            .hash_password(password.as_bytes(), &salt)?
45            .to_string();
46
47        Ok(password_hash)
48    }
49
50    pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
51        let parsed_hash = PasswordHash::new(hash)
52            .map_err(|e| AuthError::CryptoError(e.to_string()))?;
53
54        Ok(self.argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
55    }
56
57    pub fn validate_password(&self, password: &str) -> Result<()> {
58        if password.len() < self.policy.min_length {
59            return Err(AuthError::InvalidPassword(
60                format!("Password must be at least {} characters", self.policy.min_length)
61            ));
62        }
63
64        if self.policy.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
65            return Err(AuthError::InvalidPassword(
66                "Password must contain at least one uppercase letter".to_string()
67            ));
68        }
69
70        if self.policy.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
71            return Err(AuthError::InvalidPassword(
72                "Password must contain at least one lowercase letter".to_string()
73            ));
74        }
75
76        if self.policy.require_numbers && !password.chars().any(|c| c.is_numeric()) {
77            return Err(AuthError::InvalidPassword(
78                "Password must contain at least one number".to_string()
79            ));
80        }
81
82        if self.policy.require_special {
83            let special_chars = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`";
84            if !password.chars().any(|c| special_chars.contains(c)) {
85                return Err(AuthError::InvalidPassword(
86                    "Password must contain at least one special character".to_string()
87                ));
88            }
89        }
90
91        // Check for common weak passwords
92        if self.is_common_password(password) {
93            return Err(AuthError::InvalidPassword(
94                "Password is too common, please choose a stronger password".to_string()
95            ));
96        }
97
98        Ok(())
99    }
100
101    fn is_common_password(&self, password: &str) -> bool {
102        // Top 100 most common passwords (simplified list)
103        const COMMON_PASSWORDS: &[&str] = &[
104            "password", "123456", "123456789", "12345678", "12345",
105            "qwerty", "abc123", "password1", "111111", "iloveyou",
106            "admin", "welcome", "monkey", "dragon", "letmein",
107        ];
108
109        COMMON_PASSWORDS.contains(&password.to_lowercase().as_str())
110    }
111
112    pub fn generate_strong_password(&self, length: usize) -> String {
113        use rand::Rng;
114
115        let mut rng = OsRng;
116        let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=";
117
118        (0..length)
119            .map(|_| {
120                let idx = rng.gen_range(0..charset.len());
121                charset.chars().nth(idx).unwrap()
122            })
123            .collect()
124    }
125
126    pub fn calculate_strength(&self, password: &str) -> PasswordStrength {
127        let mut score = 0;
128
129        // Length
130        if password.len() >= 12 { score += 20; }
131        else if password.len() >= 10 { score += 15; }
132        else if password.len() >= 8 { score += 10; }
133
134        // Character variety
135        if password.chars().any(|c| c.is_uppercase()) { score += 15; }
136        if password.chars().any(|c| c.is_lowercase()) { score += 15; }
137        if password.chars().any(|c| c.is_numeric()) { score += 15; }
138
139        let special_chars = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`";
140        if password.chars().any(|c| special_chars.contains(c)) { score += 20; }
141
142        // Entropy
143        let unique_chars = password.chars().collect::<std::collections::HashSet<_>>().len();
144        score += (unique_chars * 2).min(15);
145
146        match score {
147            0..=40 => PasswordStrength::Weak,
148            41..=70 => PasswordStrength::Medium,
149            71..=85 => PasswordStrength::Strong,
150            _ => PasswordStrength::VeryStrong,
151        }
152    }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PasswordStrength {
157    Weak,
158    Medium,
159    Strong,
160    VeryStrong,
161}