1use crate::errors::{AuthError, Result};
7use regex::Regex;
8use std::collections::HashSet;
9use std::sync::OnceLock;
10
11static USERNAME_RE: OnceLock<Regex> = OnceLock::new();
14static EMAIL_RE: OnceLock<Regex> = OnceLock::new();
15static API_KEY_RE: OnceLock<Regex> = OnceLock::new();
16
17#[derive(Debug, Clone)]
19pub struct PasswordPolicy {
20 pub min_length: usize,
22 pub max_length: usize,
24 pub require_uppercase: bool,
26 pub require_lowercase: bool,
28 pub require_digit: bool,
30 pub require_special: bool,
32 pub banned_passwords: HashSet<String>,
34 pub min_entropy: f64,
36}
37
38impl Default for PasswordPolicy {
39 fn default() -> Self {
40 let mut banned_passwords = HashSet::new();
41 for password in [
43 "password",
44 "123456",
45 "password123",
46 "admin",
47 "qwerty",
48 "letmein",
49 "welcome",
50 "monkey",
51 "dragon",
52 "password1",
53 "123456789",
54 "1234567890",
55 "abc123",
56 "iloveyou",
57 ] {
58 banned_passwords.insert(password.to_string());
59 }
60
61 Self {
62 min_length: 8,
63 max_length: 128,
64 require_uppercase: true,
65 require_lowercase: true,
66 require_digit: true,
67 require_special: true,
68 banned_passwords,
69 min_entropy: 3.0,
70 }
71 }
72}
73
74impl PasswordPolicy {
75 pub fn nist_800_63b() -> Self {
87 Self {
88 require_uppercase: false,
89 require_lowercase: false,
90 require_digit: false,
91 require_special: false,
92 ..Default::default()
93 }
94 }
95
96 pub fn high_security() -> Self {
107 Self {
108 min_length: 12,
109 min_entropy: 4.0,
110 ..Default::default()
111 }
112 }
113
114 pub fn with_banned_words(mut self, words: &[&str]) -> Self {
118 for word in words {
119 self.banned_passwords.insert(word.to_lowercase());
120 }
121 self
122 }
123
124 pub fn builder() -> PasswordPolicyBuilder {
126 PasswordPolicyBuilder {
127 policy: PasswordPolicy::default(),
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
149pub struct PasswordPolicyBuilder {
150 policy: PasswordPolicy,
151}
152
153impl PasswordPolicyBuilder {
154 pub fn min_length(mut self, len: usize) -> Self {
156 self.policy.min_length = len;
157 self
158 }
159
160 pub fn max_length(mut self, len: usize) -> Self {
162 self.policy.max_length = len;
163 self
164 }
165
166 pub fn require_uppercase(mut self, require: bool) -> Self {
168 self.policy.require_uppercase = require;
169 self
170 }
171
172 pub fn require_lowercase(mut self, require: bool) -> Self {
174 self.policy.require_lowercase = require;
175 self
176 }
177
178 pub fn require_digit(mut self, require: bool) -> Self {
180 self.policy.require_digit = require;
181 self
182 }
183
184 pub fn require_special(mut self, require: bool) -> Self {
186 self.policy.require_special = require;
187 self
188 }
189
190 pub fn min_entropy(mut self, entropy: f64) -> Self {
192 self.policy.min_entropy = entropy;
193 self
194 }
195
196 pub fn build(self) -> PasswordPolicy {
198 self.policy
199 }
200}
201
202pub fn validate_password_enhanced(password: &str, policy: &PasswordPolicy) -> Result<()> {
204 if password.len() < policy.min_length {
206 return Err(AuthError::validation(format!(
207 "Password must be at least {} characters long",
208 policy.min_length
209 )));
210 }
211
212 if password.len() > policy.max_length {
213 return Err(AuthError::validation(format!(
214 "Password must be no more than {} characters long",
215 policy.max_length
216 )));
217 }
218
219 if policy.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
221 return Err(AuthError::validation(
222 "Password must contain at least one uppercase letter".to_string(),
223 ));
224 }
225
226 if policy.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
227 return Err(AuthError::validation(
228 "Password must contain at least one lowercase letter".to_string(),
229 ));
230 }
231
232 if policy.require_digit && !password.chars().any(|c| c.is_numeric()) {
233 return Err(AuthError::validation(
234 "Password must contain at least one digit".to_string(),
235 ));
236 }
237
238 if policy.require_special && !password.chars().any(|c| !c.is_alphanumeric()) {
239 return Err(AuthError::validation(
240 "Password must contain at least one special character".to_string(),
241 ));
242 }
243
244 if policy.banned_passwords.contains(&password.to_lowercase()) {
246 return Err(AuthError::validation(
247 "Password is too common and not allowed".to_string(),
248 ));
249 }
250
251 let entropy = calculate_password_entropy(password);
253 if entropy < policy.min_entropy {
254 return Err(AuthError::validation(format!(
255 "Password entropy ({:.2}) is below minimum requirement ({:.2})",
256 entropy, policy.min_entropy
257 )));
258 }
259
260 if has_sequential_patterns(password) {
264 return Err(AuthError::validation(
265 "Password contains sequential or keyboard-pattern characters that are easily guessed"
266 .to_string(),
267 ));
268 }
269
270 Ok(())
271}
272
273pub fn validate_password(password: &str) -> Result<()> {
275 validate_password_enhanced(password, &PasswordPolicy::default())
276}
277
278fn calculate_password_entropy(password: &str) -> f64 {
280 let mut char_counts = std::collections::HashMap::new();
281
282 for c in password.chars() {
283 *char_counts.entry(c).or_insert(0) += 1;
284 }
285
286 let length = password.len() as f64;
287 let mut entropy = 0.0;
288
289 for &count in char_counts.values() {
290 let probability = count as f64 / length;
291 entropy -= probability * probability.log2();
292 }
293
294 entropy
295}
296
297fn has_sequential_patterns(password: &str) -> bool {
302 if password.len() < 6 {
303 return false; }
305
306 const KEYBOARD_ROWS: &[&str] = &["qwertyuiop", "asdfghjkl", "zxcvbnm", "1234567890"];
308
309 let lower = password.to_lowercase();
310
311 let chars: Vec<char> = lower.chars().collect();
313 let mut sequential_count: usize = 0;
314 let mut run = 1usize;
315 for i in 1..chars.len() {
316 let diff = chars[i] as i32 - chars[i - 1] as i32;
317 if diff == 1 || diff == -1 {
318 run += 1;
319 } else {
320 if run >= 3 {
321 sequential_count += run;
322 }
323 run = 1;
324 }
325 }
326 if run >= 3 {
327 sequential_count += run;
328 }
329
330 let mut walk_count: usize = 0;
332 for row in KEYBOARD_ROWS {
333 let rev: String = row.chars().rev().collect();
334 for window_len in (4..=lower.len()).rev() {
335 for start in 0..=lower.len().saturating_sub(window_len) {
336 let slice = &lower[start..start + window_len];
337 if row.contains(slice) || rev.contains(slice) {
338 walk_count = walk_count.max(window_len);
339 }
340 }
341 }
342 }
343
344 let dominated = sequential_count.max(walk_count);
345 dominated * 2 > password.len()
347}
348
349pub fn validate_username(username: &str) -> Result<()> {
351 if username.is_empty() {
352 return Err(AuthError::validation(
353 "Username cannot be empty".to_string(),
354 ));
355 }
356
357 if username.len() < 3 {
358 return Err(AuthError::validation(
359 "Username must be at least 3 characters long".to_string(),
360 ));
361 }
362
363 if username.len() > 50 {
364 return Err(AuthError::validation(
365 "Username must be no more than 50 characters long".to_string(),
366 ));
367 }
368
369 let username_regex =
372 USERNAME_RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9_-]+$").expect("valid username regex"));
373 if !username_regex.is_match(username) {
374 return Err(AuthError::validation(
375 "Username can only contain letters, numbers, underscores, and hyphens".to_string(),
376 ));
377 }
378
379 if !username.chars().next().is_some_and(|c| c.is_alphabetic()) {
381 return Err(AuthError::validation(
382 "Username must start with a letter".to_string(),
383 ));
384 }
385
386 Ok(())
387}
388
389pub fn validate_email(email: &str) -> Result<()> {
391 if email.is_empty() {
392 return Err(AuthError::validation("Email cannot be empty".to_string()));
393 }
394
395 if email.len() > 254 {
397 return Err(AuthError::validation(
398 "Email address is too long".to_string(),
399 ));
400 }
401
402 let email_regex = EMAIL_RE.get_or_init(|| {
404 Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").expect("valid email regex")
405 });
406 if !email_regex.is_match(email) {
407 return Err(AuthError::validation("Invalid email format".to_string()));
408 }
409
410 Ok(())
411}
412
413pub fn validate_api_key(api_key: &str) -> Result<()> {
415 if api_key.is_empty() {
416 return Err(AuthError::validation("API key cannot be empty".to_string()));
417 }
418
419 if api_key.len() < 32 {
420 return Err(AuthError::validation(
421 "API key must be at least 32 characters long".to_string(),
422 ));
423 }
424
425 if api_key.len() > 128 {
426 return Err(AuthError::validation(
427 "API key must be no more than 128 characters long".to_string(),
428 ));
429 }
430
431 let api_key_regex =
433 API_KEY_RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9]+$").expect("valid api key regex"));
434 if !api_key_regex.is_match(api_key) {
435 return Err(AuthError::validation(
436 "API key can only contain letters and numbers".to_string(),
437 ));
438 }
439
440 Ok(())
441}
442
443pub fn validate_user_input(input: &str) -> bool {
450 if input.is_empty() || input.len() > 1000 {
451 return false;
452 }
453 if !input.chars().all(|c| {
454 if c.is_control() {
455 matches!(c, ' ' | '\t' | '\n' | '\r')
456 } else {
457 !matches!(c, '<' | '>')
458 }
459 }) {
460 return false;
461 }
462 let lower = input.to_ascii_lowercase();
463 if lower.contains("%3c") || lower.contains("%3e") || lower.contains("%00") {
464 return false;
465 }
466 if lower.contains("javascript:")
467 || lower.contains("data:")
468 || lower.contains("file:")
469 || lower.contains("jndi:")
470 {
471 return false;
472 }
473 if input.contains("${") || input.contains("{{") {
474 return false;
475 }
476 if input.contains("../") || input.contains("..\\") {
477 return false;
478 }
479 if input.contains('\0') {
480 return false;
481 }
482 if lower.contains("; drop")
483 || lower.contains(";drop")
484 || lower.contains("' drop")
485 || lower.contains("'; drop")
486 || lower.contains("--")
487 {
488 return false;
489 }
490 true
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_password_validation() {
499 let policy = PasswordPolicy::default();
500
501 assert!(validate_password_enhanced("StrongP@ssw0rd!", &policy).is_ok());
503
504 assert!(validate_password_enhanced("Short1!", &policy).is_err());
506
507 assert!(validate_password_enhanced("lowercase123!", &policy).is_err());
509
510 assert!(validate_password_enhanced("UPPERCASE123!", &policy).is_err());
512
513 assert!(validate_password_enhanced("NoDigitPass!", &policy).is_err());
515
516 assert!(validate_password_enhanced("NoSpecialChar123", &policy).is_err());
518
519 assert!(validate_password_enhanced("password", &policy).is_err());
521 }
522
523 #[test]
524 fn test_username_validation() {
525 assert!(validate_username("validuser").is_ok());
527 assert!(validate_username("user_123").is_ok());
528 assert!(validate_username("test-user").is_ok());
529
530 assert!(validate_username("").is_err()); assert!(validate_username("ab").is_err()); assert!(validate_username("123user").is_err()); assert!(validate_username("user@test").is_err()); }
536
537 #[test]
538 fn test_email_validation() {
539 assert!(validate_email("test@example.com").is_ok());
541 assert!(validate_email("user.name+tag@domain.co.uk").is_ok());
542
543 assert!(validate_email("").is_err()); assert!(validate_email("invalid.email").is_err()); assert!(validate_email("@domain.com").is_err()); assert!(validate_email("test@").is_err()); }
549
550 #[test]
551 fn test_password_policy_nist_preset() {
552 let policy = PasswordPolicy::nist_800_63b();
553 assert_eq!(policy.min_length, 8);
554 assert!(!policy.require_uppercase);
555 assert!(!policy.require_lowercase);
556 assert!(!policy.require_digit);
557 assert!(!policy.require_special);
558 assert!(validate_password_enhanced("alongpasswordthatisonly lowercase", &policy).is_ok());
560 }
561
562 #[test]
563 fn test_password_policy_high_security_preset() {
564 let policy = PasswordPolicy::high_security();
565 assert_eq!(policy.min_length, 12);
566 assert!(policy.require_uppercase);
567 assert!(policy.require_special);
568 assert!(policy.min_entropy > 3.0);
569 }
570
571 #[test]
572 fn test_password_policy_builder() {
573 let policy = PasswordPolicy::builder()
574 .min_length(10)
575 .require_special(false)
576 .min_entropy(3.5)
577 .build();
578 assert_eq!(policy.min_length, 10);
579 assert!(!policy.require_special);
580 assert_eq!(policy.min_entropy, 3.5);
581 assert!(policy.require_uppercase);
583 assert!(policy.require_digit);
584 }
585
586 #[test]
587 fn test_password_policy_with_banned_words() {
588 let policy = PasswordPolicy::default().with_banned_words(&["CompanyName", "SecretWord"]);
589 assert!(policy.banned_passwords.contains("companyname"));
590 assert!(policy.banned_passwords.contains("secretword"));
591 assert!(policy.banned_passwords.contains("password"));
593 }
594}