Skip to main content

backbone_auth/
password.rs

1//! Password hashing and verification
2
3use anyhow::Result;
4use argon2::{
5    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString},
6    Argon2,
7};
8
9/// Password service
10pub struct PasswordService {
11    argon2: Argon2<'static>,
12}
13
14impl PasswordService {
15    pub fn new() -> Self {
16        // Use secure Argon2 parameters
17        let argon2 = Argon2::new(
18            argon2::Algorithm::Argon2id,
19            argon2::Version::V0x13,
20            argon2::Params::new(19456, 2, 1, Some(32)).unwrap(),
21        );
22
23        Self { argon2 }
24    }
25
26    /// Hash password
27    #[tracing::instrument(skip_all)]
28    pub fn hash_password(&self, password: &str) -> Result<String> {
29        let salt = SaltString::generate(&mut OsRng);
30        let password_hash = self.argon2
31            .hash_password(password.as_bytes(), &salt)
32            .map_err(|e| {
33                tracing::error!(event = "auth.password_hash_failed", "Password hashing failed");
34                anyhow::anyhow!("Failed to hash password: {}", e)
35            })?;
36
37        tracing::debug!(event = "auth.password_hashed", "Password hashed successfully");
38        Ok(password_hash.to_string())
39    }
40
41    /// Verify password
42    #[tracing::instrument(skip_all)]
43    pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
44        let parsed_hash = PasswordHash::new(hash)
45            .map_err(|e| anyhow::anyhow!("Failed to parse password hash: {}", e))?;
46
47        let result = argon2::PasswordVerifier::verify_password(&self.argon2, password.as_bytes(), &parsed_hash).is_ok();
48
49        tracing::debug!(
50            event = "auth.password_verified",
51            success = result,
52            "Password verification completed"
53        );
54
55        Ok(result)
56    }
57
58    /// Generate random password
59    pub fn generate_password(&self, length: usize) -> String {
60        use rand::Rng;
61        use rand::distributions::Alphanumeric;
62
63        let mut rng = rand::thread_rng();
64        (0..length)
65            .map(|_| char::from(rng.sample(Alphanumeric)))
66            .collect()
67    }
68}
69
70impl Default for PasswordService {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76/// Password validation rules
77pub struct PasswordValidator {
78    min_length: usize,
79    require_uppercase: bool,
80    require_lowercase: bool,
81    require_numbers: bool,
82    require_symbols: bool,
83}
84
85impl PasswordValidator {
86    pub fn new() -> Self {
87        Self {
88            min_length: 8,
89            require_uppercase: true,
90            require_lowercase: true,
91            require_numbers: true,
92            require_symbols: true,
93        }
94    }
95
96    #[tracing::instrument(skip_all)]
97    pub fn validate(&self, password: &str) -> Result<()> {
98        if password.len() < self.min_length {
99            tracing::warn!(event = "auth.password_validation_failed", reason = "too_short", "Password validation failed: too short");
100            return Err(anyhow::anyhow!(
101                "Password must be at least {} characters long",
102                self.min_length
103            ));
104        }
105
106        if self.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
107            tracing::warn!(event = "auth.password_validation_failed", reason = "no_uppercase", "Password validation failed: no uppercase");
108            return Err(anyhow::anyhow!("Password must contain at least one uppercase letter"));
109        }
110
111        if self.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
112            tracing::warn!(event = "auth.password_validation_failed", reason = "no_lowercase", "Password validation failed: no lowercase");
113            return Err(anyhow::anyhow!("Password must contain at least one lowercase letter"));
114        }
115
116        if self.require_numbers && !password.chars().any(|c| c.is_numeric()) {
117            tracing::warn!(event = "auth.password_validation_failed", reason = "no_number", "Password validation failed: no number");
118            return Err(anyhow::anyhow!("Password must contain at least one number"));
119        }
120
121        if self.require_symbols
122            && !password.chars().any(|c| !c.is_alphanumeric())
123        {
124            tracing::warn!(event = "auth.password_validation_failed", reason = "no_symbol", "Password validation failed: no special character");
125            return Err(anyhow::anyhow!("Password must contain at least one special character"));
126        }
127
128        Ok(())
129    }
130}
131
132impl Default for PasswordValidator {
133    fn default() -> Self {
134        Self::new()
135    }
136}