backbone_auth/
password.rs1use anyhow::Result;
4use argon2::{
5 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString},
6 Argon2,
7};
8
9pub struct PasswordService {
11 argon2: Argon2<'static>,
12}
13
14impl PasswordService {
15 pub fn new() -> Self {
16 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 #[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 #[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 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
76pub 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}