Skip to main content

auth_framework/utils/
password.rs

1//! Password utility functions for the AuthFramework.
2
3use crate::errors::{AuthError, Result};
4use argon2::{
5    Argon2, Params,
6    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
7};
8
9/// Maximum password length to prevent denial-of-service via hashing.
10const MAX_PASSWORD_LENGTH: usize = 128;
11
12/// Password strength levels
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PasswordStrengthLevel {
15    Weak,
16    Medium,
17    Strong,
18    VeryStrong,
19}
20
21/// Password strength result with level and feedback
22#[derive(Debug, Clone)]
23pub struct PasswordStrength {
24    pub level: PasswordStrengthLevel,
25    pub feedback: Vec<String>,
26}
27
28/// Hash a password using Argon2id with OWASP-minimum parameters.
29pub 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    // OWASP minimum: 46 MiB memory, 1 iteration, 1 degree of parallelism
38    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
49/// Verify a password against its hash.
50///
51/// The Argon2 parameters are read from the hash itself, so verification
52/// works for hashes created with any parameter set.
53pub 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    // Argon2::default() extracts algorithm/params from the PHC string
61    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
69/// Check password strength based on various criteria
70pub 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
115/// Returns `true` when `level` satisfies the production minimum (Strong or VeryStrong).
116pub 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}