1use 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#[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#[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
49pub struct PasswordService {
51 config: PasswordConfig,
52}
53
54impl PasswordService {
55 pub fn new() -> Self {
57 Self {
58 config: PasswordConfig::default(),
59 }
60 }
61
62 pub fn with_config(config: PasswordConfig) -> Self {
64 Self { config }
65 }
66
67 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 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 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 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 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 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 use rand::seq::SliceRandom;
155 password.shuffle(&mut rng);
156
157 String::from_utf8(password).unwrap_or_else(|_| "PasswordGenError".to_string())
158 }
159
160 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 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 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 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; 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 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 fn hash_with_pbkdf2(&self, password: &str) -> Result<PasswordHash> {
284 let salt_bytes = self.generate_salt(16); let salt: [u8; 16] = salt_bytes.try_into()
287 .map_err(|_| SecurityError::Password("Invalid salt length".to_string()))?;
288
289 let cost = 12; 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 fn verify_with_pbkdf2(&self, password: &str, hash: &PasswordHash) -> Result<bool> {
306 self.verify_with_bcrypt(password, hash)
308 }
309
310 fn hash_with_bcrypt(&self, password: &str) -> Result<PasswordHash> {
312 let salt_bytes = self.generate_salt(16); let salt: [u8; 16] = salt_bytes.try_into()
314 .map_err(|_| SecurityError::Password("Invalid salt length".to_string()))?;
315
316 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 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 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 let errors = service.validate_password_complexity("StrongPass123!").unwrap();
375 assert!(errors.is_empty());
376
377 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 let errors = service.validate_password_complexity("lowercase123!").unwrap();
384 assert!(!errors.is_empty());
385 assert!(errors.iter().any(|e| e.contains("uppercase")));
386
387 let errors = service.validate_password_complexity("UPPERCASE123!").unwrap();
389 assert!(!errors.is_empty());
390 assert!(errors.iter().any(|e| e.contains("lowercase")));
391
392 let errors = service.validate_password_complexity("Password!").unwrap();
394 assert!(!errors.is_empty());
395 assert!(errors.iter().any(|e| e.contains("digit")));
396
397 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 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}