Skip to main content

elif_auth/
utils.rs

1//! Password hashing and cryptographic utilities
2
3use 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/// Argon2 password hasher implementation
18#[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    /// Create a new Argon2 hasher with custom parameters
29    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    /// Create an Argon2 hasher with default parameters
38    pub fn default() -> Self {
39        Self {
40            memory_cost: 65536, // 64 MB
41            time_cost: 3,       // 3 iterations
42            parallelism: 4,     // 4 threads
43        }
44    }
45
46    /// Create an Argon2 hasher optimized for production
47    pub fn production() -> Self {
48        Self {
49            memory_cost: 65536, // 64 MB
50            time_cost: 4,       // 4 iterations
51            parallelism: 4,     // 4 threads
52        }
53    }
54
55    /// Create an Argon2 hasher optimized for development (faster)
56    pub fn development() -> Self {
57        Self {
58            memory_cost: 4096, // 4 MB
59            time_cost: 2,      // 2 iterations
60            parallelism: 2,    // 2 threads
61        }
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/// bcrypt password hasher implementation
106#[cfg(feature = "bcrypt")]
107#[derive(Debug, Clone)]
108pub struct BcryptHasher {
109    cost: u32,
110}
111
112#[cfg(feature = "bcrypt")]
113impl BcryptHasher {
114    /// Create a new bcrypt hasher with custom cost
115    pub fn new(cost: u32) -> Self {
116        Self { cost }
117    }
118
119    /// Create a bcrypt hasher with default cost
120    pub fn default() -> Self {
121        Self { cost: DEFAULT_COST }
122    }
123
124    /// Create a bcrypt hasher optimized for production
125    pub fn production() -> Self {
126        Self { cost: 12 }
127    }
128
129    /// Create a bcrypt hasher optimized for development (faster)
130    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
150/// Password hasher factory for creating different hashers
151pub struct PasswordHasherFactory;
152
153impl PasswordHasherFactory {
154    /// Create a password hasher by name
155    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    /// Create default hasher (Argon2)
199    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
211/// Utility functions for generating random values
212pub struct CryptoUtils;
213
214impl CryptoUtils {
215    /// Generate a random string of specified length using alphanumeric characters
216    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    /// Generate a secure random token
225    pub fn generate_token(length: usize) -> String {
226        Self::generate_random_string(length)
227    }
228
229    /// Generate backup codes for MFA
230    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    /// Generate a cryptographically secure session ID
237    pub fn generate_session_id() -> String {
238        Self::generate_token(32)
239    }
240
241    /// Generate a TOTP secret key
242    pub fn generate_totp_secret() -> String {
243        Self::generate_token(32)
244    }
245
246    /// Generate a JWT secret key
247    pub fn generate_jwt_secret(length: Option<usize>) -> String {
248        Self::generate_token(length.unwrap_or(64))
249    }
250
251    /// Validate password strength based on policy
252    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    /// Hash a password with default settings
307    pub fn hash_password(password: &str) -> AuthResult<String> {
308        let hasher = PasswordHasherFactory::default_hasher();
309        hasher.hash_password(password)
310    }
311
312    /// Verify a password against its hash
313    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(); // Use low cost for tests
339        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        // Valid password
393        let result =
394            CryptoUtils::validate_password_strength("Test123!", 8, 128, true, true, true, true);
395        assert!(result.is_ok());
396
397        // Too short
398        let result =
399            CryptoUtils::validate_password_strength("Test1!", 8, 128, true, true, true, true);
400        assert!(result.is_err());
401
402        // Missing uppercase
403        let result =
404            CryptoUtils::validate_password_strength("test123!", 8, 128, true, true, true, true);
405        assert!(result.is_err());
406
407        // Missing special character
408        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}