1use crate::traits::PasswordHasher;
4use crate::{AuthError, AuthResult};
5use rand::{distributions::Alphanumeric, thread_rng, Rng};
6use std::collections::HashMap;
7
8#[cfg(feature = "argon2")]
9use argon2::{
10 password_hash::{PasswordHash, PasswordHasher as _, PasswordVerifier, SaltString},
11 Argon2,
12};
13
14#[cfg(feature = "bcrypt")]
15use bcrypt::{hash, verify, DEFAULT_COST};
16
17#[cfg(feature = "argon2")]
19#[derive(Debug, Clone)]
20pub struct Argon2Hasher {
21 memory_cost: u32,
22 time_cost: u32,
23 parallelism: u32,
24}
25
26#[cfg(feature = "argon2")]
27impl Argon2Hasher {
28 pub fn new(memory_cost: u32, time_cost: u32, parallelism: u32) -> Self {
30 Self {
31 memory_cost,
32 time_cost,
33 parallelism,
34 }
35 }
36
37 pub fn default() -> Self {
39 Self {
40 memory_cost: 65536, time_cost: 3, parallelism: 4, }
44 }
45
46 pub fn production() -> Self {
48 Self {
49 memory_cost: 65536, time_cost: 4, parallelism: 4, }
53 }
54
55 pub fn development() -> Self {
57 Self {
58 memory_cost: 4096, time_cost: 2, parallelism: 2, }
62 }
63}
64
65#[cfg(feature = "argon2")]
66impl PasswordHasher for Argon2Hasher {
67 fn hash_password(&self, password: &str) -> AuthResult<String> {
68 let salt = SaltString::generate(&mut thread_rng());
69 let argon2 = Argon2::new(
70 argon2::Algorithm::Argon2id,
71 argon2::Version::V0x13,
72 argon2::Params::new(self.memory_cost, self.time_cost, self.parallelism, None)
73 .map_err(|e| AuthError::crypto_error(e.to_string()))?,
74 );
75
76 let password_hash = argon2
77 .hash_password(password.as_bytes(), &salt)
78 .map_err(|e| AuthError::crypto_error(e.to_string()))?;
79
80 Ok(password_hash.to_string())
81 }
82
83 fn verify_password(&self, password: &str, hash: &str) -> AuthResult<bool> {
84 let parsed_hash =
85 PasswordHash::new(hash).map_err(|e| AuthError::crypto_error(e.to_string()))?;
86
87 let argon2 = Argon2::new(
88 argon2::Algorithm::Argon2id,
89 argon2::Version::V0x13,
90 argon2::Params::new(self.memory_cost, self.time_cost, self.parallelism, None)
91 .map_err(|e| AuthError::crypto_error(e.to_string()))?,
92 );
93
94 match argon2.verify_password(password.as_bytes(), &parsed_hash) {
95 Ok(()) => Ok(true),
96 Err(_) => Ok(false),
97 }
98 }
99
100 fn hasher_name(&self) -> &str {
101 "argon2"
102 }
103}
104
105#[cfg(feature = "bcrypt")]
107#[derive(Debug, Clone)]
108pub struct BcryptHasher {
109 cost: u32,
110}
111
112#[cfg(feature = "bcrypt")]
113impl BcryptHasher {
114 pub fn new(cost: u32) -> Self {
116 Self { cost }
117 }
118
119 pub fn default() -> Self {
121 Self { cost: DEFAULT_COST }
122 }
123
124 pub fn production() -> Self {
126 Self { cost: 12 }
127 }
128
129 pub fn development() -> Self {
131 Self { cost: 4 }
132 }
133}
134
135#[cfg(feature = "bcrypt")]
136impl PasswordHasher for BcryptHasher {
137 fn hash_password(&self, password: &str) -> AuthResult<String> {
138 hash(password, self.cost).map_err(AuthError::from)
139 }
140
141 fn verify_password(&self, password: &str, hash: &str) -> AuthResult<bool> {
142 verify(password, hash).map_err(AuthError::from)
143 }
144
145 fn hasher_name(&self) -> &str {
146 "bcrypt"
147 }
148}
149
150pub struct PasswordHasherFactory;
152
153impl PasswordHasherFactory {
154 pub fn create_hasher(
156 algorithm: &str,
157 config: HashMap<String, serde_json::Value>,
158 ) -> AuthResult<Box<dyn PasswordHasher>> {
159 match algorithm {
160 #[cfg(feature = "argon2")]
161 "argon2" => {
162 let memory_cost = config
163 .get("memory_cost")
164 .and_then(|v| v.as_u64())
165 .unwrap_or(65536) as u32;
166 let time_cost = config
167 .get("time_cost")
168 .and_then(|v| v.as_u64())
169 .unwrap_or(3) as u32;
170 let parallelism = config
171 .get("parallelism")
172 .and_then(|v| v.as_u64())
173 .unwrap_or(4) as u32;
174
175 Ok(Box::new(Argon2Hasher::new(
176 memory_cost,
177 time_cost,
178 parallelism,
179 )))
180 }
181 #[cfg(feature = "bcrypt")]
182 "bcrypt" => {
183 use bcrypt::DEFAULT_COST;
184 let cost = config
185 .get("cost")
186 .and_then(|v| v.as_u64())
187 .unwrap_or(DEFAULT_COST as u64) as u32;
188
189 Ok(Box::new(BcryptHasher::new(cost)))
190 }
191 _ => Err(AuthError::config_error(format!(
192 "Unknown password hashing algorithm: {} (or feature not enabled)",
193 algorithm
194 ))),
195 }
196 }
197
198 pub fn default_hasher() -> Box<dyn PasswordHasher> {
200 #[cfg(feature = "argon2")]
201 return Box::new(Argon2Hasher::default());
202
203 #[cfg(all(not(feature = "argon2"), feature = "bcrypt"))]
204 return Box::new(BcryptHasher::default());
205
206 #[cfg(all(not(feature = "argon2"), not(feature = "bcrypt")))]
207 panic!("No password hasher available. Enable either 'argon2' or 'bcrypt' feature");
208 }
209}
210
211pub struct CryptoUtils;
213
214impl CryptoUtils {
215 pub fn generate_random_string(length: usize) -> String {
217 thread_rng()
218 .sample_iter(&Alphanumeric)
219 .take(length)
220 .map(char::from)
221 .collect()
222 }
223
224 pub fn generate_token(length: usize) -> String {
226 Self::generate_random_string(length)
227 }
228
229 pub fn generate_backup_codes(count: usize, length: usize) -> Vec<String> {
231 (0..count)
232 .map(|_| Self::generate_random_string(length))
233 .collect()
234 }
235
236 pub fn generate_session_id() -> String {
238 Self::generate_token(32)
239 }
240
241 pub fn generate_totp_secret() -> String {
243 Self::generate_token(32)
244 }
245
246 pub fn generate_jwt_secret(length: Option<usize>) -> String {
248 Self::generate_token(length.unwrap_or(64))
249 }
250
251 pub fn validate_password_strength(
253 password: &str,
254 min_length: usize,
255 max_length: usize,
256 require_uppercase: bool,
257 require_lowercase: bool,
258 require_numbers: bool,
259 require_special: bool,
260 ) -> AuthResult<()> {
261 if password.len() < min_length {
262 return Err(AuthError::generic_error(format!(
263 "Password must be at least {} characters long",
264 min_length
265 )));
266 }
267
268 if password.len() > max_length {
269 return Err(AuthError::generic_error(format!(
270 "Password must be at most {} characters long",
271 max_length
272 )));
273 }
274
275 if require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
276 return Err(AuthError::generic_error(
277 "Password must contain at least one uppercase letter".to_string(),
278 ));
279 }
280
281 if require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
282 return Err(AuthError::generic_error(
283 "Password must contain at least one lowercase letter".to_string(),
284 ));
285 }
286
287 if require_numbers && !password.chars().any(|c| c.is_ascii_digit()) {
288 return Err(AuthError::generic_error(
289 "Password must contain at least one number".to_string(),
290 ));
291 }
292
293 if require_special
294 && !password
295 .chars()
296 .any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))
297 {
298 return Err(AuthError::generic_error(
299 "Password must contain at least one special character".to_string(),
300 ));
301 }
302
303 Ok(())
304 }
305
306 pub fn hash_password(password: &str) -> AuthResult<String> {
308 let hasher = PasswordHasherFactory::default_hasher();
309 hasher.hash_password(password)
310 }
311
312 pub fn verify_password(password: &str, hash: &str) -> AuthResult<bool> {
314 let hasher = PasswordHasherFactory::default_hasher();
315 hasher.verify_password(password, hash)
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_argon2_hasher() {
325 let hasher = Argon2Hasher::default();
326 let password = "test_password_123";
327
328 let hash = hasher.hash_password(password).unwrap();
329 assert!(!hash.is_empty());
330 assert_ne!(hash, password);
331
332 assert!(hasher.verify_password(password, &hash).unwrap());
333 assert!(!hasher.verify_password("wrong_password", &hash).unwrap());
334 }
335
336 #[test]
337 fn test_bcrypt_hasher() {
338 let hasher = BcryptHasher::development(); let password = "test_password_123";
340
341 let hash = hasher.hash_password(password).unwrap();
342 assert!(!hash.is_empty());
343 assert_ne!(hash, password);
344
345 assert!(hasher.verify_password(password, &hash).unwrap());
346 assert!(!hasher.verify_password("wrong_password", &hash).unwrap());
347 }
348
349 #[test]
350 fn test_password_hasher_factory() {
351 let mut config = HashMap::new();
352 config.insert(
353 "cost".to_string(),
354 serde_json::Value::Number(serde_json::Number::from(4)),
355 );
356
357 let hasher = PasswordHasherFactory::create_hasher("bcrypt", config).unwrap();
358 assert_eq!(hasher.hasher_name(), "bcrypt");
359
360 let mut config = HashMap::new();
361 config.insert(
362 "memory_cost".to_string(),
363 serde_json::Value::Number(serde_json::Number::from(4096)),
364 );
365
366 let hasher = PasswordHasherFactory::create_hasher("argon2", config).unwrap();
367 assert_eq!(hasher.hasher_name(), "argon2");
368
369 let result = PasswordHasherFactory::create_hasher("invalid", HashMap::new());
370 assert!(result.is_err());
371 }
372
373 #[test]
374 fn test_crypto_utils_random_generation() {
375 let token1 = CryptoUtils::generate_token(16);
376 let token2 = CryptoUtils::generate_token(16);
377
378 assert_eq!(token1.len(), 16);
379 assert_eq!(token2.len(), 16);
380 assert_ne!(token1, token2);
381
382 let session_id = CryptoUtils::generate_session_id();
383 assert_eq!(session_id.len(), 32);
384
385 let backup_codes = CryptoUtils::generate_backup_codes(5, 8);
386 assert_eq!(backup_codes.len(), 5);
387 assert!(backup_codes.iter().all(|code| code.len() == 8));
388 }
389
390 #[test]
391 fn test_password_strength_validation() {
392 let result =
394 CryptoUtils::validate_password_strength("Test123!", 8, 128, true, true, true, true);
395 assert!(result.is_ok());
396
397 let result =
399 CryptoUtils::validate_password_strength("Test1!", 8, 128, true, true, true, true);
400 assert!(result.is_err());
401
402 let result =
404 CryptoUtils::validate_password_strength("test123!", 8, 128, true, true, true, true);
405 assert!(result.is_err());
406
407 let result =
409 CryptoUtils::validate_password_strength("Test1234", 8, 128, true, true, true, true);
410 assert!(result.is_err());
411 }
412
413 #[test]
414 fn test_default_password_operations() {
415 let password = "test_password_123";
416
417 let hash = CryptoUtils::hash_password(password).unwrap();
418 assert!(!hash.is_empty());
419
420 assert!(CryptoUtils::verify_password(password, &hash).unwrap());
421 assert!(!CryptoUtils::verify_password("wrong", &hash).unwrap());
422 }
423}