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 {
107 if a.len() != b.len() {
108 return false;
109 }
110 a.ct_eq(b).into()
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use shadow_crypt_core::profile::SecurityProfile;
117
118 #[test]
119 fn test_validate_password_format_empty() {
120 let empty = SecureString::new(String::new());
121 assert!(validate_password_format(&empty).is_err());
122 }
123
124 #[test]
125 fn test_validate_password_format_non_empty() {
126 let password = SecureString::new("test".to_string());
127 assert!(validate_password_format(&password).is_ok());
128 }
129
130 #[test]
131 fn test_validate_password_requirements_test_profile() {
132 let password = SecureString::new("weak".to_string());
133 assert!(validate_password_requirements(&password, &SecurityProfile::Test).is_ok());
134 }
135
136 #[test]
137 fn test_validate_password_requirements_production_weak() {
138 let password = SecureString::new("password".to_string());
139 assert!(validate_password_requirements(&password, &SecurityProfile::Production).is_err());
140 }
141
142 #[test]
143 fn test_validate_password_requirements_production_strong() {
144 let password = SecureString::new("Tr0ub4dour&3!".to_string());
145 assert!(validate_password_requirements(&password, &SecurityProfile::Production).is_ok());
146 }
147
148 #[test]
149 fn test_validate_password_entropy_weak() {
150 let password = SecureString::new("123456".to_string());
151 assert!(validate_password_entropy(&password).is_err());
152 }
153
154 #[test]
155 fn test_validate_password_entropy_strong() {
156 let password = SecureString::new("CorrectHorseBatteryStaple".to_string());
157 assert!(validate_password_entropy(&password).is_ok());
158 }
159
160 #[test]
161 fn test_format_feedback_no_feedback() {
162 let estimate = zxcvbn::zxcvbn("Tr0ub4dour&3!BatteryStaple", &[]);
164 let feedback = format_feedback(&estimate);
165 assert!(!feedback.is_empty());
168 }
169
170 #[test]
171 fn test_constant_time_eq_equal() {
172 let a = b"test";
173 let b = b"test";
174 assert!(constant_time_eq(a, b));
175 }
176
177 #[test]
178 fn test_constant_time_eq_not_equal() {
179 let a = b"test";
180 let b = b"different";
181 assert!(!constant_time_eq(a, b));
182 }
183
184 #[test]
185 fn test_constant_time_eq_different_lengths() {
186 let a = b"test";
187 let b = b"testing";
188 assert!(!constant_time_eq(a, b));
189 }
190}