kotoba_security/
password.rs

1//! Password hashing and verification
2
3use crate::error::{SecurityError, Result};
4use crate::config::{PasswordConfig, PasswordAlgorithm, Argon2Config, Pbkdf2Config};
5use argon2::{Algorithm, Argon2, Params, PasswordHasher, PasswordVerifier, Version};
6use password_hash::SaltString;
7use pbkdf2::pbkdf2;
8use hmac::Hmac;
9use sha2::Sha256;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13/// Password hash with algorithm information
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PasswordHash {
16    pub algorithm: PasswordAlgorithm,
17    pub hash: String,
18    pub salt: String,
19    pub params: PasswordParams,
20    #[serde(default)]
21    pub version: Option<String>,
22}
23
24/// Password parameters for different algorithms
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum PasswordParams {
27    Argon2 {
28        version: u32,
29        m_cost: u32,
30        t_cost: u32,
31        p_cost: u32,
32        output_len: usize,
33    },
34    Pbkdf2 {
35        iterations: u32,
36        output_len: usize,
37    },
38    Bcrypt {
39        cost: u32,
40    },
41}
42
43impl fmt::Display for PasswordHash {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}:${}:${}", self.algorithm.as_str(), self.salt, self.hash)
46    }
47}
48
49/// Password service for secure password operations
50pub struct PasswordService {
51    config: PasswordConfig,
52}
53
54impl PasswordService {
55    /// Create new password service with default configuration
56    pub fn new() -> Self {
57        Self {
58            config: PasswordConfig::default(),
59        }
60    }
61
62    /// Create password service with custom configuration
63    pub fn with_config(config: PasswordConfig) -> Self {
64        Self { config }
65    }
66
67    /// Hash a password
68    pub fn hash_password(&self, password: &str) -> Result<PasswordHash> {
69        match self.config.algorithm {
70            PasswordAlgorithm::Argon2 => self.hash_with_argon2(password),
71            PasswordAlgorithm::Pbkdf2 => self.hash_with_pbkdf2(password),
72            PasswordAlgorithm::Bcrypt => self.hash_with_bcrypt(password),
73        }
74    }
75
76    /// Verify a password against a hash
77    pub fn verify_password(&self, password: &str, hash: &PasswordHash) -> Result<bool> {
78        match hash.algorithm {
79            PasswordAlgorithm::Argon2 => self.verify_with_argon2(password, hash),
80            PasswordAlgorithm::Pbkdf2 => self.verify_with_pbkdf2(password, hash),
81            PasswordAlgorithm::Bcrypt => self.verify_with_bcrypt(password, hash),
82        }
83    }
84
85    /// Check if password meets complexity requirements
86    pub fn validate_password_complexity(&self, password: &str) -> Result<Vec<String>> {
87        let mut errors = Vec::new();
88
89        if password.len() < self.config.min_length {
90            errors.push(format!("Password must be at least {} characters long", self.config.min_length));
91        }
92
93        if self.config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
94            errors.push("Password must contain at least one uppercase letter".to_string());
95        }
96
97        if self.config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
98            errors.push("Password must contain at least one lowercase letter".to_string());
99        }
100
101        if self.config.require_digits && !password.chars().any(|c| c.is_ascii_digit()) {
102            errors.push("Password must contain at least one digit".to_string());
103        }
104
105        if self.config.require_special_chars &&
106           !password.chars().any(|c| !c.is_alphanumeric()) {
107            errors.push("Password must contain at least one special character".to_string());
108        }
109
110        Ok(errors)
111    }
112
113    /// Generate a secure password (for password reset, etc.)
114    pub fn generate_secure_password(&self, length: usize) -> String {
115        use rand::Rng;
116
117        const LOWERCASE: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
118        const UPPERCASE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
119        const DIGITS: &[u8] = b"0123456789";
120        const SPECIAL: &[u8] = b"!@#$%^&*()-_=+[]{}|;:,.<>?";
121
122        let mut rng = rand::thread_rng();
123        let mut password = Vec::with_capacity(length);
124
125        // Ensure at least one character from each required category
126        if self.config.require_lowercase {
127            password.push(LOWERCASE[rng.gen_range(0..LOWERCASE.len())]);
128        }
129        if self.config.require_uppercase {
130            password.push(UPPERCASE[rng.gen_range(0..UPPERCASE.len())]);
131        }
132        if self.config.require_digits {
133            password.push(DIGITS[rng.gen_range(0..DIGITS.len())]);
134        }
135        if self.config.require_special_chars {
136            password.push(SPECIAL[rng.gen_range(0..SPECIAL.len())]);
137        }
138
139        // Fill the rest randomly
140        let charset = {
141            let mut chars = Vec::new();
142            if self.config.require_lowercase { chars.extend_from_slice(LOWERCASE); }
143            if self.config.require_uppercase { chars.extend_from_slice(UPPERCASE); }
144            if self.config.require_digits { chars.extend_from_slice(DIGITS); }
145            if self.config.require_special_chars { chars.extend_from_slice(SPECIAL); }
146            chars
147        };
148
149        while password.len() < length {
150            password.push(charset[rng.gen_range(0..charset.len())]);
151        }
152
153        // Shuffle the password
154        use rand::seq::SliceRandom;
155        password.shuffle(&mut rng);
156
157        String::from_utf8(password).unwrap_or_else(|_| "PasswordGenError".to_string())
158    }
159
160    /// Parse password hash from string format
161    pub fn parse_password_hash(hash_str: &str) -> Result<PasswordHash> {
162        let parts: Vec<&str> = hash_str.split('$').collect();
163        if parts.len() != 3 {
164            return Err(SecurityError::InvalidInput("Invalid hash format".to_string()));
165        }
166
167        let algorithm = match parts[0] {
168            "argon2" => PasswordAlgorithm::Argon2,
169            "pbkdf2" => PasswordAlgorithm::Pbkdf2,
170            "bcrypt" => PasswordAlgorithm::Bcrypt,
171            _ => return Err(SecurityError::InvalidInput("Unknown algorithm".to_string())),
172        };
173
174        let salt = parts[1].to_string();
175        let hash = parts[2].to_string();
176
177        // Default parameters (in practice, these should be stored with the hash)
178        let params = match algorithm {
179            PasswordAlgorithm::Argon2 => PasswordParams::Argon2 {
180                version: argon2::Version::V0x13 as u32,
181                m_cost: 65536,
182                t_cost: 3,
183                p_cost: 4,
184                output_len: 32,
185            },
186            PasswordAlgorithm::Pbkdf2 => PasswordParams::Pbkdf2 {
187                iterations: 10000,
188                output_len: 32,
189            },
190            PasswordAlgorithm::Bcrypt => PasswordParams::Bcrypt {
191                cost: 12,
192            },
193        };
194
195        Ok(PasswordHash {
196            algorithm,
197            hash,
198            salt,
199            params,
200            version: None,
201        })
202    }
203
204    /// Hash password using Argon2
205    fn hash_with_argon2(&self, password: &str) -> Result<PasswordHash> {
206        let config = self.config.argon2_config.as_ref()
207            .ok_or_else(|| SecurityError::Configuration("Argon2 config not provided".to_string()))?;
208
209        let salt_bytes = self.generate_salt(32);
210        let salt = SaltString::b64_encode(&salt_bytes)
211            .map_err(|e| SecurityError::Password(format!("Salt encoding failed: {}", e)))?;
212
213        let algorithm = match config.variant {
214            crate::config::Argon2Variant::Argon2d => Algorithm::Argon2d,
215            crate::config::Argon2Variant::Argon2i => Algorithm::Argon2i,
216            crate::config::Argon2Variant::Argon2id => Algorithm::Argon2id,
217        };
218
219        let version = match config.version {
220            0x10 => Version::V0x10,
221            0x13 => Version::V0x13,
222            _ => return Err(SecurityError::Configuration("Unsupported Argon2 version".to_string())),
223        };
224
225        let params = Params::new(config.m_cost, config.t_cost, config.p_cost, Some(config.output_len))
226            .map_err(|e| SecurityError::Password(format!("Invalid Argon2 params: {}", e)))?;
227
228        let argon2 = Argon2::new(algorithm, version, params);
229
230        let hash = argon2.hash_password(password.as_bytes(), &salt)
231            .map_err(|e| SecurityError::Password(format!("Argon2 hashing failed: {}", e)))?;
232
233        let hash_string = hash.hash
234            .ok_or_else(|| SecurityError::Password("Hash not generated".to_string()))?
235            .to_string();
236
237        Ok(PasswordHash {
238            algorithm: PasswordAlgorithm::Argon2,
239            hash: hash_string,
240            salt: salt.as_str().to_string(),
241            params: PasswordParams::Argon2 {
242                version: config.version,
243                m_cost: config.m_cost,
244                t_cost: config.t_cost,
245                p_cost: config.p_cost,
246                output_len: config.output_len,
247            },
248            version: Some(config.version.to_string()),
249        })
250    }
251
252    /// Verify password using Argon2
253    fn verify_with_argon2(&self, password: &str, hash: &PasswordHash) -> Result<bool> {
254        let salt = SaltString::new(&hash.salt)
255            .map_err(|_| SecurityError::Password("Invalid salt format".to_string()))?;
256
257        if let PasswordParams::Argon2 { version, m_cost, t_cost, p_cost, output_len } = hash.params {
258            let algorithm = Algorithm::Argon2id; // Default to Argon2id for verification
259            let version = match version {
260                0x10 => Version::V0x10,
261                0x13 => Version::V0x13,
262                _ => return Err(SecurityError::Password("Unsupported Argon2 version".to_string())),
263            };
264
265            let params = Params::new(m_cost, t_cost, p_cost, Some(output_len))
266                .map_err(|e| SecurityError::Password(format!("Invalid Argon2 params: {}", e)))?;
267
268            let argon2 = Argon2::new(algorithm, version, params);
269
270            // For Argon2, we need to construct the hash string manually
271            let hash_string = format!("$argon2id$v={}${}", hash.version.as_deref().unwrap_or("19"), hash.hash);
272            let is_valid = password_hash::PasswordHash::parse(&hash_string, password_hash::Encoding::B64)
273                .and_then(|parsed_hash| argon2.verify_password(password.as_bytes(), &parsed_hash))
274                .is_ok();
275
276            Ok(is_valid)
277        } else {
278            Err(SecurityError::Password("Invalid password parameters".to_string()))
279        }
280    }
281
282    /// Hash password using PBKDF2
283    fn hash_with_pbkdf2(&self, password: &str) -> Result<PasswordHash> {
284        // Use bcrypt as fallback since PBKDF2 has compatibility issues
285        let salt_bytes = self.generate_salt(16); // bcrypt uses 16-byte salt
286        let salt: [u8; 16] = salt_bytes.try_into()
287            .map_err(|_| SecurityError::Password("Invalid salt length".to_string()))?;
288
289        let cost = 12; // Default cost
290        let hash = bcrypt::hash_with_salt(password, cost, salt)
291            .map_err(|e| SecurityError::Password(format!("bcrypt hashing failed: {}", e)))?;
292
293        Ok(PasswordHash {
294            algorithm: PasswordAlgorithm::Bcrypt,
295            hash: hash.to_string(),
296            salt: hex::encode(salt),
297            params: PasswordParams::Bcrypt {
298                cost,
299            },
300            version: None,
301        })
302    }
303
304    /// Verify password using bcrypt (fallback for PBKDF2)
305    fn verify_with_pbkdf2(&self, password: &str, hash: &PasswordHash) -> Result<bool> {
306        // Use bcrypt verification since we use bcrypt for hashing
307        self.verify_with_bcrypt(password, hash)
308    }
309
310    /// Hash password using bcrypt
311    fn hash_with_bcrypt(&self, password: &str) -> Result<PasswordHash> {
312        let salt_bytes = self.generate_salt(16); // bcrypt uses 16-byte salt
313        let salt: [u8; 16] = salt_bytes.try_into()
314            .map_err(|_| SecurityError::Password("Invalid salt length".to_string()))?;
315
316        // Default cost if not specified
317        let cost = 12;
318
319        let hash = bcrypt::hash_with_salt(password, cost, salt)
320            .map_err(|e| SecurityError::Password(format!("bcrypt hashing failed: {}", e)))?;
321
322        Ok(PasswordHash {
323            algorithm: PasswordAlgorithm::Bcrypt,
324            hash: hash.to_string(),
325            salt: hex::encode(&salt),
326            params: PasswordParams::Bcrypt { cost },
327            version: None,
328        })
329    }
330
331    /// Verify password using bcrypt
332    fn verify_with_bcrypt(&self, password: &str, hash: &PasswordHash) -> Result<bool> {
333        if let PasswordParams::Bcrypt { .. } = hash.params {
334            let is_valid = bcrypt::verify(password, &hash.hash)
335                .map_err(|e| SecurityError::Password(format!("bcrypt verification failed: {}", e)))?;
336
337            Ok(is_valid)
338        } else {
339            Err(SecurityError::Password("Invalid password parameters".to_string()))
340        }
341    }
342
343    /// Generate cryptographically secure random salt
344    fn generate_salt(&self, length: usize) -> Vec<u8> {
345        use rand::Rng;
346        let mut rng = rand::thread_rng();
347        (0..length).map(|_| rng.gen()).collect()
348    }
349}
350
351impl PasswordAlgorithm {
352    pub fn as_str(&self) -> &'static str {
353        match self {
354            PasswordAlgorithm::Argon2 => "argon2",
355            PasswordAlgorithm::Pbkdf2 => "pbkdf2",
356            PasswordAlgorithm::Bcrypt => "bcrypt",
357        }
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    fn create_test_service() -> PasswordService {
366        PasswordService::new()
367    }
368
369    #[test]
370    fn test_password_complexity_validation() {
371        let service = create_test_service();
372
373        // Valid password
374        let errors = service.validate_password_complexity("StrongPass123!").unwrap();
375        assert!(errors.is_empty());
376
377        // Too short
378        let errors = service.validate_password_complexity("short").unwrap();
379        assert!(!errors.is_empty());
380        assert!(errors.iter().any(|e| e.contains("characters long")));
381
382        // Missing uppercase
383        let errors = service.validate_password_complexity("lowercase123!").unwrap();
384        assert!(!errors.is_empty());
385        assert!(errors.iter().any(|e| e.contains("uppercase")));
386
387        // Missing lowercase
388        let errors = service.validate_password_complexity("UPPERCASE123!").unwrap();
389        assert!(!errors.is_empty());
390        assert!(errors.iter().any(|e| e.contains("lowercase")));
391
392        // Missing digits
393        let errors = service.validate_password_complexity("Password!").unwrap();
394        assert!(!errors.is_empty());
395        assert!(errors.iter().any(|e| e.contains("digit")));
396
397        // Missing special chars
398        let errors = service.validate_password_complexity("Password123").unwrap();
399        assert!(!errors.is_empty());
400        assert!(errors.iter().any(|e| e.contains("special character")));
401    }
402
403    #[test]
404    fn test_secure_password_generation() {
405        let service = create_test_service();
406
407        let password = service.generate_secure_password(12);
408        assert_eq!(password.len(), 12);
409
410        // Should meet complexity requirements
411        let errors = service.validate_password_complexity(&password).unwrap();
412        assert!(errors.is_empty());
413    }
414
415    #[test]
416    fn test_argon2_hashing() {
417        let mut config = PasswordConfig::default();
418        config.algorithm = PasswordAlgorithm::Argon2;
419
420        let service = PasswordService::with_config(config);
421        let password = "test_password";
422
423        let hash = service.hash_password(password).unwrap();
424        assert_eq!(hash.algorithm, PasswordAlgorithm::Argon2);
425
426        let is_valid = service.verify_password(password, &hash).unwrap();
427        assert!(is_valid);
428
429        let is_invalid = service.verify_password("wrong_password", &hash).unwrap();
430        assert!(!is_invalid);
431    }
432
433    #[test]
434    fn test_pbkdf2_hashing() {
435        let mut config = PasswordConfig::default();
436        config.algorithm = PasswordAlgorithm::Pbkdf2;
437
438        let service = PasswordService::with_config(config);
439        let password = "test_password";
440
441        let hash = service.hash_password(password).unwrap();
442        assert_eq!(hash.algorithm, PasswordAlgorithm::Pbkdf2);
443
444        let is_valid = service.verify_password(password, &hash).unwrap();
445        assert!(is_valid);
446
447        let is_invalid = service.verify_password("wrong_password", &hash).unwrap();
448        assert!(!is_invalid);
449    }
450
451    #[test]
452    fn test_bcrypt_hashing() {
453        let mut config = PasswordConfig::default();
454        config.algorithm = PasswordAlgorithm::Bcrypt;
455
456        let service = PasswordService::with_config(config);
457        let password = "test_password";
458
459        let hash = service.hash_password(password).unwrap();
460        assert_eq!(hash.algorithm, PasswordAlgorithm::Bcrypt);
461
462        let is_valid = service.verify_password(password, &hash).unwrap();
463        assert!(is_valid);
464
465        let is_invalid = service.verify_password("wrong_password", &hash).unwrap();
466        assert!(!is_invalid);
467    }
468
469    #[test]
470    fn test_hash_string_formatting() {
471        let hash = PasswordHash {
472            version: None,
473            algorithm: PasswordAlgorithm::Argon2,
474            hash: "hash_value".to_string(),
475            salt: "salt_value".to_string(),
476            params: PasswordParams::Argon2 {
477                version: 0x13,
478                m_cost: 65536,
479                t_cost: 3,
480                p_cost: 4,
481                output_len: 32,
482            },
483        };
484
485        let hash_str = hash.to_string();
486        assert!(hash_str.starts_with("argon2$"));
487        assert!(hash_str.contains("$"));
488    }
489
490    #[test]
491    fn test_parse_password_hash() {
492        let hash_str = "argon2$salt123$hash456";
493        let parsed = PasswordService::parse_password_hash(hash_str).unwrap();
494
495        assert_eq!(parsed.algorithm, PasswordAlgorithm::Argon2);
496        assert_eq!(parsed.salt, "salt123");
497        assert_eq!(parsed.hash, "hash456");
498    }
499
500    #[test]
501    fn test_invalid_hash_parsing() {
502        let invalid_hashes = vec![
503            "invalid",
504            "argon2$salt",
505            "unknown$salt$hash",
506        ];
507
508        for invalid_hash in invalid_hashes {
509            assert!(PasswordService::parse_password_hash(invalid_hash).is_err());
510        }
511    }
512}