shadow_crypt_shell/
password.rs1use rpassword;
2use shadow_crypt_core::{memory::SecureString, profile::SecurityProfile};
3
4use crate::errors::{WorkflowError, WorkflowResult};
5
6pub fn prompt_for_password() -> WorkflowResult<SecureString> {
8 let password = rpassword::prompt_password("Enter password: ")
9 .map_err(|e| WorkflowError::Password(format!("Failed to read password: {}", e)))
10 .map(SecureString::new)?;
11
12 Ok(password)
13}
14
15pub fn prompt_for_password_with_confirmation(
17 security_profile: &SecurityProfile,
18) -> WorkflowResult<SecureString> {
19 let password1 = rpassword::prompt_password("Enter password: ")
20 .map_err(|e| WorkflowError::Password(format!("Failed to read password: {}", e)))
21 .map(SecureString::new)?;
22
23 let password2 = rpassword::prompt_password("Confirm password: ")
24 .map_err(|e| WorkflowError::Password(format!("Failed to read password: {}", e)))
25 .map(SecureString::new)?;
26
27 constant_time_eq(password1.as_str().as_bytes(), password2.as_str().as_bytes())
28 .then_some(())
29 .ok_or(WorkflowError::Password(
30 "Passwords do not match".to_string(),
31 ))?;
32
33 validate_password_requirements(&password1, security_profile)
34 .map_err(|e| WorkflowError::Password(e.to_string()))?;
35
36 Ok(password1)
37}
38
39pub fn validate_password_format(password: &SecureString) -> Result<(), WorkflowError> {
40 if password.is_empty() {
41 return Err(WorkflowError::Password(
42 "Password cannot be empty".to_string(),
43 ));
44 }
45
46 Ok(())
47}
48
49fn validate_password_requirements(
50 password: &SecureString,
51 security_profile: &SecurityProfile,
52) -> Result<(), WorkflowError> {
53 validate_password_format(password)?;
55
56 match security_profile {
58 SecurityProfile::Test => Ok(()), SecurityProfile::Production => validate_password_entropy(password),
60 }
61}
62
63fn validate_password_entropy(password: &SecureString) -> Result<(), WorkflowError> {
66 let estimate = zxcvbn::zxcvbn(password.as_str(), &[]);
67
68 if estimate.score() < zxcvbn::Score::Three {
70 let feedback_msg = format_feedback(&estimate);
71 return Err(WorkflowError::Password(format!(
72 "Password strength insufficient (score {}/4). {}. Estimated crack time: {}",
73 estimate.score(),
74 feedback_msg,
75 estimate.crack_times().offline_slow_hashing_1e4_per_second()
76 )));
77 }
78
79 Ok(())
80}
81
82fn format_feedback(estimate: &zxcvbn::Entropy) -> String {
84 if let Some(feedback) = estimate.feedback() {
85 let mut suggestions = Vec::new();
86
87 if let Some(warning) = feedback.warning() {
88 suggestions.push(warning.to_string());
89 }
90
91 suggestions.extend(feedback.suggestions().iter().map(|s| s.to_string()));
92
93 if suggestions.is_empty() {
94 "Use a stronger password".to_string()
95 } else {
96 suggestions.join(". ")
97 }
98 } else {
99 "Use a stronger password".to_string()
100 }
101}
102
103use subtle::ConstantTimeEq;
104
105fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
109 if a.len() != b.len() {
110 return false;
111 }
112 a.ct_eq(b).into()
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use shadow_crypt_core::profile::SecurityProfile;
119
120 #[test]
121 fn test_validate_password_format_empty() {
122 let empty = SecureString::new(String::new());
123 assert!(validate_password_format(&empty).is_err());
124 }
125
126 #[test]
127 fn test_validate_password_format_non_empty() {
128 let password = SecureString::new("test".to_string());
129 assert!(validate_password_format(&password).is_ok());
130 }
131
132 #[test]
133 fn test_validate_password_requirements_test_profile() {
134 let password = SecureString::new("weak".to_string());
135 assert!(validate_password_requirements(&password, &SecurityProfile::Test).is_ok());
136 }
137
138 #[test]
139 fn test_validate_password_requirements_production_weak() {
140 let password = SecureString::new("password".to_string());
141 assert!(validate_password_requirements(&password, &SecurityProfile::Production).is_err());
142 }
143
144 #[test]
145 fn test_validate_password_requirements_production_strong() {
146 let password = SecureString::new("Tr0ub4dour&3!".to_string());
147 assert!(validate_password_requirements(&password, &SecurityProfile::Production).is_ok());
148 }
149
150 #[test]
151 fn test_validate_password_entropy_weak() {
152 let password = SecureString::new("123456".to_string());
153 assert!(validate_password_entropy(&password).is_err());
154 }
155
156 #[test]
157 fn test_validate_password_entropy_strong() {
158 let password = SecureString::new("CorrectHorseBatteryStaple".to_string());
159 assert!(validate_password_entropy(&password).is_ok());
160 }
161
162 #[test]
163 fn test_format_feedback_no_feedback() {
164 let estimate = zxcvbn::zxcvbn("Tr0ub4dour&3!BatteryStaple", &[]);
166 let feedback = format_feedback(&estimate);
167 assert!(!feedback.is_empty());
170 }
171
172 #[test]
173 fn test_constant_time_eq_equal() {
174 let a = b"test";
175 let b = b"test";
176 assert!(constant_time_eq(a, b));
177 }
178
179 #[test]
180 fn test_constant_time_eq_not_equal() {
181 let a = b"test";
182 let b = b"different";
183 assert!(!constant_time_eq(a, b));
184 }
185
186 #[test]
187 fn test_constant_time_eq_different_lengths() {
188 let a = b"test";
189 let b = b"testing";
190 assert!(!constant_time_eq(a, b));
191 }
192}