karbon_framework/validation/constraints/security/
password.rs1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
5pub enum PasswordStrength {
6 Weak,
8 Medium,
10 Strong,
12 VeryStrong,
14}
15
16pub struct Password {
20 pub min_strength: PasswordStrength,
21 pub message: String,
22 pub min_length: usize,
23}
24
25impl Default for Password {
26 fn default() -> Self {
27 Self {
28 min_strength: PasswordStrength::Medium,
29 message: "The password strength is too low. Please use a stronger password.".to_string(),
30 min_length: 8,
31 }
32 }
33}
34
35impl Password {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn strength(mut self, strength: PasswordStrength) -> Self {
41 self.min_strength = strength;
42 self.min_length = match strength {
43 PasswordStrength::Weak => 6,
44 PasswordStrength::Medium => 8,
45 PasswordStrength::Strong => 10,
46 PasswordStrength::VeryStrong => 12,
47 };
48 self
49 }
50
51 pub fn with_message(mut self, message: impl Into<String>) -> Self {
52 self.message = message.into();
53 self
54 }
55
56 fn evaluate_strength(value: &str) -> PasswordStrength {
57 let len = value.len();
58 let has_lower = value.chars().any(|c| c.is_ascii_lowercase());
59 let has_upper = value.chars().any(|c| c.is_ascii_uppercase());
60 let has_digit = value.chars().any(|c| c.is_ascii_digit());
61 let has_special = value.chars().any(|c| !c.is_alphanumeric());
62
63 let common_patterns = [
64 "password", "123456", "qwerty", "abc123", "letmein", "admin",
65 "welcome", "monkey", "dragon", "master", "azerty",
66 ];
67 let lower = value.to_lowercase();
68 let has_common = common_patterns.iter().any(|p| lower.contains(p));
69
70 if len >= 12 && has_lower && has_upper && has_digit && has_special && !has_common {
71 PasswordStrength::VeryStrong
72 } else if len >= 10 && has_lower && has_upper && has_digit && has_special {
73 PasswordStrength::Strong
74 } else if len >= 8 && has_lower && has_upper && has_digit {
75 PasswordStrength::Medium
76 } else {
77 PasswordStrength::Weak
78 }
79 }
80}
81
82impl Constraint for Password {
83 fn validate(&self, value: &str) -> ConstraintResult {
84 if value.len() < self.min_length {
85 return Err(ConstraintViolation::new(
86 self.name(),
87 format!(
88 "The password must be at least {} characters long.",
89 self.min_length
90 ),
91 "[REDACTED]",
92 ));
93 }
94
95 let actual_strength = Self::evaluate_strength(value);
96 if actual_strength < self.min_strength {
97 return Err(ConstraintViolation::new(
98 self.name(),
99 &self.message,
100 "[REDACTED]",
101 ));
102 }
103
104 Ok(())
105 }
106
107 fn name(&self) -> &'static str {
108 "Password"
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_weak_password() {
118 let constraint = Password::new().strength(PasswordStrength::Weak);
119 assert!(constraint.validate("abcdef").is_ok());
120 assert!(constraint.validate("12345").is_err()); }
122
123 #[test]
124 fn test_medium_password() {
125 let constraint = Password::new().strength(PasswordStrength::Medium);
126 assert!(constraint.validate("Abcdef1x").is_ok());
127 assert!(constraint.validate("abcdefgh").is_err()); }
129
130 #[test]
131 fn test_strong_password() {
132 let constraint = Password::new().strength(PasswordStrength::Strong);
133 assert!(constraint.validate("Abcdef1x!z").is_ok());
134 assert!(constraint.validate("Abcdef1x").is_err()); }
136
137 #[test]
138 fn test_very_strong_password() {
139 let constraint = Password::new().strength(PasswordStrength::VeryStrong);
140 assert!(constraint.validate("MyStr0ng!Pass").is_ok());
141 assert!(constraint.validate("password123!A").is_err()); }
143
144 #[test]
145 fn test_too_short() {
146 let constraint = Password::new().strength(PasswordStrength::Medium);
147 let err = constraint.validate("Ab1!").unwrap_err();
148 assert!(err.message.contains("at least"));
149 }
150
151 #[test]
152 fn test_password_not_leaked_in_error() {
153 let constraint = Password::new();
154 let err = constraint.validate("weak").unwrap_err();
155 assert_eq!(err.invalid_value, "[REDACTED]");
156 }
157}