1use crate::types::{AuthError, Result};
15use argon2::{
16 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
17 Argon2,
18};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PasswordPolicy {
24 pub min_length: usize,
26 pub max_length: usize,
28 pub require_uppercase: bool,
30 pub require_lowercase: bool,
32 pub require_digit: bool,
34 pub require_special: bool,
36 pub min_character_classes: usize,
38 pub disallow_common: bool,
40 pub disallow_username: bool,
42 pub min_strength: PasswordStrength,
44}
45
46impl Default for PasswordPolicy {
47 fn default() -> Self {
48 Self {
49 min_length: 8,
50 max_length: 128,
51 require_uppercase: false,
52 require_lowercase: false,
53 require_digit: false,
54 require_special: false,
55 min_character_classes: 3,
56 disallow_common: true,
57 disallow_username: true,
58 min_strength: PasswordStrength::Medium,
59 }
60 }
61}
62
63impl PasswordPolicy {
64 #[must_use]
66 pub fn strict() -> Self {
67 Self {
68 min_length: 12,
69 max_length: 128,
70 require_uppercase: true,
71 require_lowercase: true,
72 require_digit: true,
73 require_special: true,
74 min_character_classes: 4,
75 disallow_common: true,
76 disallow_username: true,
77 min_strength: PasswordStrength::Strong,
78 }
79 }
80
81 #[must_use]
83 pub fn relaxed() -> Self {
84 Self {
85 min_length: 6,
86 max_length: 128,
87 require_uppercase: false,
88 require_lowercase: false,
89 require_digit: false,
90 require_special: false,
91 min_character_classes: 1,
92 disallow_common: false,
93 disallow_username: false,
94 min_strength: PasswordStrength::VeryWeak,
95 }
96 }
97
98 #[must_use]
101 pub fn nist_compliant() -> Self {
102 Self {
103 min_length: 8,
104 max_length: 64,
105 require_uppercase: false,
106 require_lowercase: false,
107 require_digit: false,
108 require_special: false,
109 min_character_classes: 1,
110 disallow_common: true,
111 disallow_username: true,
112 min_strength: PasswordStrength::Weak,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct PolicyValidationResult {
120 pub is_valid: bool,
122 pub violations: Vec<PolicyViolation>,
124 pub strength: PasswordStrength,
126 pub suggestions: Vec<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub enum PolicyViolation {
133 TooShort { min: usize, actual: usize },
135 TooLong { max: usize, actual: usize },
137 MissingUppercase,
139 MissingLowercase,
141 MissingDigit,
143 MissingSpecial,
145 InsufficientCharacterClasses { required: usize, actual: usize },
147 CommonPassword,
149 ContainsUsername,
151 TooWeak {
153 required: PasswordStrength,
154 actual: PasswordStrength,
155 },
156}
157
158pub struct PasswordManager {
160 argon2: Argon2<'static>,
161 policy: PasswordPolicy,
162}
163
164impl PasswordManager {
165 #[must_use]
167 pub fn new() -> Self {
168 Self {
169 argon2: Argon2::default(),
170 policy: PasswordPolicy::default(),
171 }
172 }
173
174 #[must_use]
176 pub fn with_policy(policy: PasswordPolicy) -> Self {
177 Self {
178 argon2: Argon2::default(),
179 policy,
180 }
181 }
182
183 #[must_use]
185 pub fn policy(&self) -> &PasswordPolicy {
186 &self.policy
187 }
188
189 pub fn hash_password(&self, password: &str) -> Result<String> {
191 let salt = SaltString::generate(&mut OsRng);
192 let password_hash = self
193 .argon2
194 .hash_password(password.as_bytes(), &salt)
195 .map_err(|e| AuthError::InternalError(format!("Failed to hash password: {e}")))?;
196
197 Ok(password_hash.to_string())
198 }
199
200 pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
202 let parsed_hash = PasswordHash::new(hash)
203 .map_err(|e| AuthError::InternalError(format!("Invalid password hash: {e}")))?;
204
205 match self
206 .argon2
207 .verify_password(password.as_bytes(), &parsed_hash)
208 {
209 Ok(()) => Ok(true),
210 Err(_) => Ok(false),
211 }
212 }
213
214 pub fn validate_password(
216 &self,
217 password: &str,
218 username: Option<&str>,
219 ) -> PolicyValidationResult {
220 let mut violations = Vec::new();
221 let mut suggestions = Vec::new();
222
223 let length = password.len();
224 let has_uppercase = password.chars().any(char::is_uppercase);
225 let has_lowercase = password.chars().any(char::is_lowercase);
226 let has_digit = password.chars().any(char::is_numeric);
227 let has_special = password.chars().any(|c| !c.is_alphanumeric());
228
229 let character_classes = usize::from(has_uppercase)
230 + usize::from(has_lowercase)
231 + usize::from(has_digit)
232 + usize::from(has_special);
233
234 if length < self.policy.min_length {
236 violations.push(PolicyViolation::TooShort {
237 min: self.policy.min_length,
238 actual: length,
239 });
240 suggestions.push(format!(
241 "Password must be at least {} characters long",
242 self.policy.min_length
243 ));
244 }
245
246 if self.policy.max_length > 0 && length > self.policy.max_length {
248 violations.push(PolicyViolation::TooLong {
249 max: self.policy.max_length,
250 actual: length,
251 });
252 suggestions.push(format!(
253 "Password must be at most {} characters long",
254 self.policy.max_length
255 ));
256 }
257
258 if self.policy.require_uppercase && !has_uppercase {
260 violations.push(PolicyViolation::MissingUppercase);
261 suggestions.push("Add at least one uppercase letter".to_string());
262 }
263
264 if self.policy.require_lowercase && !has_lowercase {
265 violations.push(PolicyViolation::MissingLowercase);
266 suggestions.push("Add at least one lowercase letter".to_string());
267 }
268
269 if self.policy.require_digit && !has_digit {
270 violations.push(PolicyViolation::MissingDigit);
271 suggestions.push("Add at least one digit".to_string());
272 }
273
274 if self.policy.require_special && !has_special {
275 violations.push(PolicyViolation::MissingSpecial);
276 suggestions.push("Add at least one special character (!@#$%^&* etc.)".to_string());
277 }
278
279 if character_classes < self.policy.min_character_classes {
281 violations.push(PolicyViolation::InsufficientCharacterClasses {
282 required: self.policy.min_character_classes,
283 actual: character_classes,
284 });
285 suggestions.push(format!(
286 "Use at least {} of: uppercase, lowercase, digits, special characters",
287 self.policy.min_character_classes
288 ));
289 }
290
291 if self.policy.disallow_common && Self::is_common_password(password) {
293 violations.push(PolicyViolation::CommonPassword);
294 suggestions.push("Choose a less common password".to_string());
295 }
296
297 if self.policy.disallow_username {
299 if let Some(user) = username {
300 if !user.is_empty() && password.to_lowercase().contains(&user.to_lowercase()) {
301 violations.push(PolicyViolation::ContainsUsername);
302 suggestions.push("Password should not contain your username".to_string());
303 }
304 }
305 }
306
307 let strength = self.check_password_strength(password);
309 if strength < self.policy.min_strength {
310 violations.push(PolicyViolation::TooWeak {
311 required: self.policy.min_strength,
312 actual: strength,
313 });
314 suggestions.push(format!(
315 "Password strength must be at least {:?}",
316 self.policy.min_strength
317 ));
318 }
319
320 PolicyValidationResult {
321 is_valid: violations.is_empty(),
322 violations,
323 strength,
324 suggestions,
325 }
326 }
327
328 pub fn check_password_strength(&self, password: &str) -> PasswordStrength {
330 let length = password.len();
331 let has_uppercase = password.chars().any(char::is_uppercase);
332 let has_lowercase = password.chars().any(char::is_lowercase);
333 let has_digit = password.chars().any(char::is_numeric);
334 let has_special = password.chars().any(|c| !c.is_alphanumeric());
335
336 let score = u8::from(length >= 8)
337 + u8::from(length >= 12)
338 + u8::from(length >= 16)
339 + u8::from(has_uppercase)
340 + u8::from(has_lowercase)
341 + u8::from(has_digit)
342 + u8::from(has_special);
343
344 match score {
345 0..=2 => PasswordStrength::VeryWeak,
346 3..=4 => PasswordStrength::Weak,
347 5 => PasswordStrength::Medium,
348 6 => PasswordStrength::Strong,
349 _ => PasswordStrength::VeryStrong,
350 }
351 }
352
353 fn is_common_password(password: &str) -> bool {
355 const COMMON_PASSWORDS: &[&str] = &[
356 "password",
357 "123456",
358 "12345678",
359 "qwerty",
360 "abc123",
361 "monkey",
362 "1234567",
363 "letmein",
364 "trustno1",
365 "dragon",
366 "baseball",
367 "iloveyou",
368 "master",
369 "sunshine",
370 "ashley",
371 "bailey",
372 "shadow",
373 "123123",
374 "654321",
375 "superman",
376 "qazwsx",
377 "michael",
378 "football",
379 "password1",
380 "password123",
381 "welcome",
382 "jesus",
383 "ninja",
384 "mustang",
385 "password!",
386 "admin",
387 "root",
388 "toor",
389 "pass",
390 "test",
391 "guest",
392 "master",
393 "changeme",
394 "default",
395 "hello",
396 ];
397
398 let lower = password.to_lowercase();
399 COMMON_PASSWORDS.contains(&lower.as_str())
400 }
401
402 #[must_use]
404 pub fn generate_password(&self) -> String {
405 use rand::Rng;
406 let mut rng = rand::rng();
407
408 let length = std::cmp::max(self.policy.min_length, 16);
409
410 let uppercase: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
411 let lowercase: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
412 let digits: &[u8] = b"0123456789";
413 let special: &[u8] = b"!@#$%^&*()_+-=[]{}|;:,.<>?";
414
415 let mut password = Vec::with_capacity(length);
416
417 if self.policy.require_uppercase || self.policy.min_character_classes >= 1 {
419 password.push(uppercase[rng.random_range(0..uppercase.len())]);
420 }
421 if self.policy.require_lowercase || self.policy.min_character_classes >= 2 {
422 password.push(lowercase[rng.random_range(0..lowercase.len())]);
423 }
424 if self.policy.require_digit || self.policy.min_character_classes >= 3 {
425 password.push(digits[rng.random_range(0..digits.len())]);
426 }
427 if self.policy.require_special || self.policy.min_character_classes >= 4 {
428 password.push(special[rng.random_range(0..special.len())]);
429 }
430
431 let all_chars: Vec<u8> = [uppercase, lowercase, digits, special].concat();
433 while password.len() < length {
434 password.push(all_chars[rng.random_range(0..all_chars.len())]);
435 }
436
437 for i in (1..password.len()).rev() {
439 let j = rng.random_range(0..=i);
440 password.swap(i, j);
441 }
442
443 String::from_utf8(password).unwrap_or_else(|_| self.generate_password())
444 }
445}
446
447impl Default for PasswordManager {
448 fn default() -> Self {
449 Self::new()
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
455pub enum PasswordStrength {
456 VeryWeak,
457 Weak,
458 Medium,
459 Strong,
460 VeryStrong,
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_password_hashing() {
469 let manager = PasswordManager::new();
470 let password = "my-secure-password-123";
471
472 let hash = manager.hash_password(password).unwrap();
473 assert!(!hash.is_empty());
474
475 let is_valid = manager.verify_password(password, &hash).unwrap();
477 assert!(is_valid);
478
479 let is_valid = manager.verify_password("wrong-password", &hash).unwrap();
481 assert!(!is_valid);
482 }
483
484 #[test]
485 fn test_password_strength() {
486 let manager = PasswordManager::new();
487
488 assert_eq!(
489 manager.check_password_strength("123"),
490 PasswordStrength::VeryWeak
491 );
492 assert_eq!(
493 manager.check_password_strength("password"),
494 PasswordStrength::VeryWeak
495 ); assert_eq!(
497 manager.check_password_strength("Password123"),
498 PasswordStrength::Weak );
500 assert_eq!(
501 manager.check_password_strength("Password123!"),
502 PasswordStrength::Strong );
504 assert_eq!(
505 manager.check_password_strength("MyVerySecureP@ssw0rd2024!"),
506 PasswordStrength::VeryStrong );
508 }
509
510 #[test]
511 fn test_different_hashes() {
512 let manager = PasswordManager::new();
513 let password = "same-password";
514
515 let hash1 = manager.hash_password(password).unwrap();
516 let hash2 = manager.hash_password(password).unwrap();
517
518 assert_ne!(hash1, hash2);
520
521 assert!(manager.verify_password(password, &hash1).unwrap());
523 assert!(manager.verify_password(password, &hash2).unwrap());
524 }
525
526 #[test]
527 fn test_password_policy_default() {
528 let manager = PasswordManager::new();
529
530 let result = manager.validate_password("Short1!", None);
532 assert!(!result.is_valid);
533 assert!(result
534 .violations
535 .iter()
536 .any(|v| matches!(v, PolicyViolation::TooShort { .. })));
537
538 let result = manager.validate_password("MySecureP@ss123", None);
540 assert!(result.is_valid);
541 }
542
543 #[test]
544 fn test_strict_policy() {
545 let manager = PasswordManager::with_policy(PasswordPolicy::strict());
546
547 let result = manager.validate_password("password", None);
549 assert!(!result.is_valid);
550
551 let result = manager.validate_password("MySecureP@ssw0rd!", None);
553 assert!(result.is_valid);
554 }
555
556 #[test]
557 fn test_common_password_detection() {
558 let manager = PasswordManager::new();
559
560 let result = manager.validate_password("password123", None);
561 assert!(result
563 .violations
564 .iter()
565 .any(|v| matches!(v, PolicyViolation::CommonPassword)));
566 }
567
568 #[test]
569 fn test_username_in_password() {
570 let manager = PasswordManager::new();
571
572 let result = manager.validate_password("MyUsernameIsHere123!", Some("username"));
573 assert!(result
574 .violations
575 .iter()
576 .any(|v| matches!(v, PolicyViolation::ContainsUsername)));
577 }
578
579 #[test]
580 fn test_password_generation() {
581 let manager = PasswordManager::with_policy(PasswordPolicy::strict());
582
583 let password = manager.generate_password();
584
585 let result = manager.validate_password(&password, None);
587 assert!(
588 result.is_valid,
589 "Generated password should meet policy: {:?}",
590 result.violations
591 );
592 }
593
594 #[test]
595 fn test_policy_presets() {
596 let strict = PasswordPolicy::strict();
597 assert_eq!(strict.min_length, 12);
598 assert!(strict.require_uppercase);
599
600 let relaxed = PasswordPolicy::relaxed();
601 assert_eq!(relaxed.min_length, 6);
602 assert!(!relaxed.require_uppercase);
603
604 let nist = PasswordPolicy::nist_compliant();
605 assert_eq!(nist.min_length, 8);
606 assert!(nist.disallow_common);
607 }
608
609 #[test]
610 fn test_strength_comparison() {
611 assert!(PasswordStrength::Strong > PasswordStrength::Medium);
612 assert!(PasswordStrength::VeryStrong > PasswordStrength::Strong);
613 assert!(PasswordStrength::Weak < PasswordStrength::Medium);
614 }
615}