auth_framework/utils/
password.rs1use crate::errors::{AuthError, Result};
4use argon2::{
5 Argon2, Params,
6 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
7};
8
9const MAX_PASSWORD_LENGTH: usize = 128;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PasswordStrengthLevel {
15 Weak,
16 Medium,
17 Strong,
18 VeryStrong,
19}
20
21#[derive(Debug, Clone)]
23pub struct PasswordStrength {
24 pub level: PasswordStrengthLevel,
25 pub feedback: Vec<String>,
26}
27
28pub fn hash_password(password: &str) -> Result<String> {
30 if password.len() > MAX_PASSWORD_LENGTH {
31 return Err(AuthError::validation(format!(
32 "Password exceeds maximum length of {} bytes",
33 MAX_PASSWORD_LENGTH
34 )));
35 }
36 let salt = SaltString::generate(&mut OsRng);
37 let params = Params::new(46 * 1024, 1, 1, None)
39 .map_err(|e| AuthError::internal(format!("Invalid Argon2 params: {}", e)))?;
40 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
41
42 let password_hash = argon2
43 .hash_password(password.as_bytes(), &salt)
44 .map_err(|e| AuthError::internal(format!("Failed to hash password: {}", e)))?;
45
46 Ok(password_hash.to_string())
47}
48
49pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
54 if password.len() > MAX_PASSWORD_LENGTH {
55 return Ok(false);
56 }
57 let parsed_hash = PasswordHash::new(hash)
58 .map_err(|e| AuthError::internal(format!("Invalid password hash: {}", e)))?;
59
60 let argon2 = Argon2::default();
62
63 match argon2.verify_password(password.as_bytes(), &parsed_hash) {
64 Ok(()) => Ok(true),
65 Err(_) => Ok(false),
66 }
67}
68
69pub fn check_password_strength(password: &str) -> PasswordStrength {
71 let length = password.len();
72 let has_lowercase = password.chars().any(|c| c.is_lowercase());
73 let has_uppercase = password.chars().any(|c| c.is_uppercase());
74 let has_digit = password.chars().any(|c| c.is_numeric());
75 let has_special = password.chars().any(|c| !c.is_alphanumeric());
76
77 let criteria_met = [has_lowercase, has_uppercase, has_digit, has_special]
78 .iter()
79 .map(|&b| if b { 1 } else { 0 })
80 .sum::<i32>();
81
82 let mut feedback = Vec::new();
83
84 if length < 8 {
85 feedback.push("Password should be at least 8 characters long".to_string());
86 }
87 if !has_lowercase {
88 feedback.push("Add lowercase letters".to_string());
89 }
90 if !has_uppercase {
91 feedback.push("Add uppercase letters".to_string());
92 }
93 if !has_digit {
94 feedback.push("Add numbers".to_string());
95 }
96 if !has_special {
97 feedback.push("Add special characters".to_string());
98 }
99
100 let level = match (length, criteria_met) {
101 (0..=6, _) => PasswordStrengthLevel::Weak,
102 (7..=10, 0..=2) => PasswordStrengthLevel::Weak,
103 (7..=10, 3) => PasswordStrengthLevel::Medium,
104 (7..=10, 4) => PasswordStrengthLevel::Medium,
105 (11..=14, 0..=2) => PasswordStrengthLevel::Medium,
106 (11..=14, 3..=4) => PasswordStrengthLevel::Strong,
107 (15.., 0..=2) => PasswordStrengthLevel::Strong,
108 (15.., 3..=4) => PasswordStrengthLevel::VeryStrong,
109 _ => PasswordStrengthLevel::VeryStrong,
110 };
111
112 PasswordStrength { level, feedback }
113}
114
115pub fn meets_production_strength(level: PasswordStrengthLevel) -> bool {
117 matches!(
118 level,
119 PasswordStrengthLevel::Strong | PasswordStrengthLevel::VeryStrong
120 )
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_password_hashing() {
129 let password = "testpassword123";
130 let hash = hash_password(password).unwrap();
131
132 assert!(verify_password(password, &hash).unwrap());
133 assert!(!verify_password("wrongpassword", &hash).unwrap());
134 }
135
136 #[test]
137 fn test_password_strength() {
138 assert_eq!(
139 check_password_strength("weak").level,
140 PasswordStrengthLevel::Weak
141 );
142 assert_eq!(
143 check_password_strength("Medium123").level,
144 PasswordStrengthLevel::Medium
145 );
146 assert_eq!(
147 check_password_strength("Strong123!").level,
148 PasswordStrengthLevel::Medium
149 );
150 assert_eq!(
151 check_password_strength("VeryStrong123!@#").level,
152 PasswordStrengthLevel::VeryStrong
153 );
154 }
155}