1use 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 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 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 if password.len() >= 12 { score += 20; }
131 else if password.len() >= 10 { score += 15; }
132 else if password.len() >= 8 { score += 10; }
133
134 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 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}