shadow_crypt_shell/
password.rs1use std::path::Path;
2
3use rpassword;
4use shadow_crypt_core::{memory::SecureString, profile::SecurityProfile};
5
6use crate::errors::{WorkflowError, WorkflowResult};
7
8pub fn prompt_for_password() -> WorkflowResult<SecureString> {
10 let password = rpassword::prompt_password("Enter password: ")
11 .map_err(|e| WorkflowError::Password(format!("Failed to read password: {}", e)))
12 .map(SecureString::new)?;
13
14 Ok(password)
15}
16
17pub fn read_password_from_file(path: &Path) -> WorkflowResult<SecureString> {
21 let mut content = std::fs::read_to_string(path).map_err(|e| {
22 WorkflowError::Password(format!(
23 "Failed to read password file '{}': {}",
24 path.display(),
25 e
26 ))
27 })?;
28 if content.ends_with('\n') {
29 content.pop();
30 if content.ends_with('\r') {
31 content.pop();
32 }
33 }
34
35 let password = SecureString::new(content);
36 validate_password_format(&password)?;
37 Ok(password)
38}
39
40pub fn resolve_password(password_file: Option<&Path>) -> WorkflowResult<SecureString> {
43 match password_file {
44 Some(path) => read_password_from_file(path),
45 None => prompt_for_password(),
46 }
47}
48
49pub fn resolve_encryption_password(
53 password_file: Option<&Path>,
54 security_profile: &SecurityProfile,
55) -> WorkflowResult<SecureString> {
56 match password_file {
57 Some(path) => {
58 let password = read_password_from_file(path)?;
59 validate_password_requirements(&password, security_profile)?;
60 Ok(password)
61 }
62 None => prompt_for_password_with_confirmation(security_profile),
63 }
64}
65
66pub fn prompt_for_password_with_confirmation(
68 security_profile: &SecurityProfile,
69) -> WorkflowResult<SecureString> {
70 let password1 = rpassword::prompt_password("Enter password: ")
71 .map_err(|e| WorkflowError::Password(format!("Failed to read password: {}", e)))
72 .map(SecureString::new)?;
73
74 let password2 = rpassword::prompt_password("Confirm password: ")
75 .map_err(|e| WorkflowError::Password(format!("Failed to read password: {}", e)))
76 .map(SecureString::new)?;
77
78 constant_time_eq(password1.as_str().as_bytes(), password2.as_str().as_bytes())
79 .then_some(())
80 .ok_or(WorkflowError::Password(
81 "Passwords do not match".to_string(),
82 ))?;
83
84 validate_password_requirements(&password1, security_profile)
85 .map_err(|e| WorkflowError::Password(e.to_string()))?;
86
87 Ok(password1)
88}
89
90pub fn validate_password_format(password: &SecureString) -> Result<(), WorkflowError> {
91 if password.is_empty() {
92 return Err(WorkflowError::Password(
93 "Password cannot be empty".to_string(),
94 ));
95 }
96
97 Ok(())
98}
99
100fn validate_password_requirements(
101 password: &SecureString,
102 security_profile: &SecurityProfile,
103) -> Result<(), WorkflowError> {
104 validate_password_format(password)?;
106
107 match security_profile {
109 SecurityProfile::Test => Ok(()), SecurityProfile::Standard | SecurityProfile::Paranoid => {
111 validate_password_entropy(password)
112 }
113 }
114}
115
116fn validate_password_entropy(password: &SecureString) -> Result<(), WorkflowError> {
119 let estimate = zxcvbn::zxcvbn(password.as_str(), &[]);
120
121 if estimate.score() < zxcvbn::Score::Three {
123 let feedback_msg = format_feedback(&estimate);
124 return Err(WorkflowError::Password(format!(
125 "Password strength insufficient (score {}/4). {}. Estimated crack time: {}",
126 estimate.score(),
127 feedback_msg,
128 estimate.crack_times().offline_slow_hashing_1e4_per_second()
129 )));
130 }
131
132 Ok(())
133}
134
135fn format_feedback(estimate: &zxcvbn::Entropy) -> String {
137 if let Some(feedback) = estimate.feedback() {
138 let mut suggestions = Vec::new();
139
140 if let Some(warning) = feedback.warning() {
141 suggestions.push(warning.to_string());
142 }
143
144 suggestions.extend(feedback.suggestions().iter().map(|s| s.to_string()));
145
146 if suggestions.is_empty() {
147 "Use a stronger password".to_string()
148 } else {
149 suggestions.join(". ")
150 }
151 } else {
152 "Use a stronger password".to_string()
153 }
154}
155
156use subtle::ConstantTimeEq;
157
158fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
162 if a.len() != b.len() {
163 return false;
164 }
165 a.ct_eq(b).into()
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use shadow_crypt_core::profile::SecurityProfile;
172
173 #[test]
174 fn test_validate_password_format_empty() {
175 let empty = SecureString::new(String::new());
176 assert!(validate_password_format(&empty).is_err());
177 }
178
179 #[test]
180 fn test_validate_password_format_non_empty() {
181 let password = SecureString::new("test".to_string());
182 assert!(validate_password_format(&password).is_ok());
183 }
184
185 #[test]
186 fn test_validate_password_requirements_test_profile() {
187 let password = SecureString::new("weak".to_string());
188 assert!(validate_password_requirements(&password, &SecurityProfile::Test).is_ok());
189 }
190
191 #[test]
192 fn test_validate_password_requirements_production_weak() {
193 let password = SecureString::new("password".to_string());
194 assert!(validate_password_requirements(&password, &SecurityProfile::Standard).is_err());
195 }
196
197 #[test]
198 fn test_validate_password_requirements_production_strong() {
199 let password = SecureString::new("Tr0ub4dour&3!".to_string());
200 assert!(validate_password_requirements(&password, &SecurityProfile::Standard).is_ok());
201 }
202
203 #[test]
204 fn test_validate_password_entropy_weak() {
205 let password = SecureString::new("123456".to_string());
206 assert!(validate_password_entropy(&password).is_err());
207 }
208
209 #[test]
210 fn test_validate_password_entropy_strong() {
211 let password = SecureString::new("CorrectHorseBatteryStaple".to_string());
212 assert!(validate_password_entropy(&password).is_ok());
213 }
214
215 #[test]
216 fn test_format_feedback_no_feedback() {
217 let estimate = zxcvbn::zxcvbn("Tr0ub4dour&3!BatteryStaple", &[]);
219 let feedback = format_feedback(&estimate);
220 assert!(!feedback.is_empty());
223 }
224
225 #[test]
226 fn test_constant_time_eq_equal() {
227 let a = b"test";
228 let b = b"test";
229 assert!(constant_time_eq(a, b));
230 }
231
232 #[test]
233 fn test_constant_time_eq_not_equal() {
234 let a = b"test";
235 let b = b"different";
236 assert!(!constant_time_eq(a, b));
237 }
238
239 #[test]
240 fn test_constant_time_eq_different_lengths() {
241 let a = b"test";
242 let b = b"testing";
243 assert!(!constant_time_eq(a, b));
244 }
245
246 #[test]
247 fn test_read_password_from_file() {
248 let mut file = tempfile::NamedTempFile::new().unwrap();
249 std::io::Write::write_all(&mut file, b"secret-password\n").unwrap();
250
251 let password = read_password_from_file(file.path()).unwrap();
252 assert_eq!(password.as_str(), "secret-password");
253 }
254
255 #[test]
256 fn test_read_password_from_file_strips_crlf() {
257 let mut file = tempfile::NamedTempFile::new().unwrap();
258 std::io::Write::write_all(&mut file, b"secret-password\r\n").unwrap();
259
260 let password = read_password_from_file(file.path()).unwrap();
261 assert_eq!(password.as_str(), "secret-password");
262 }
263
264 #[test]
265 fn test_read_password_from_file_keeps_inner_content_verbatim() {
266 let mut file = tempfile::NamedTempFile::new().unwrap();
267 std::io::Write::write_all(&mut file, b" spaced password ").unwrap();
268
269 let password = read_password_from_file(file.path()).unwrap();
270 assert_eq!(password.as_str(), " spaced password ");
271 }
272
273 #[test]
274 fn test_read_password_from_empty_file_rejected() {
275 let file = tempfile::NamedTempFile::new().unwrap();
276 assert!(read_password_from_file(file.path()).is_err());
277 }
278
279 #[test]
280 fn test_read_password_from_missing_file_rejected() {
281 let result = read_password_from_file(std::path::Path::new("/nonexistent/password"));
282 assert!(result.is_err());
283 }
284}