shadow_crypt_shell/
password.rs

1use rpassword;
2use shadow_crypt_core::{memory::SecureString, profile::SecurityProfile};
3
4use crate::errors::{WorkflowError, WorkflowResult};
5
6/// Prompt user for password
7pub 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
15/// Prompt user for password with confirmation
16pub 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    // Basic format check
54    validate_password_format(password)?;
55
56    // Professional entropy validation (the only security requirement that matters)
57    match security_profile {
58        SecurityProfile::Test => Ok(()), // Skip entropy check in test mode
59        SecurityProfile::Production => validate_password_entropy(password),
60    }
61}
62
63/// Validate password strength using zxcvbn industry-standard algorithm
64/// Pure function - no side effects
65fn validate_password_entropy(password: &SecureString) -> Result<(), WorkflowError> {
66    let estimate = zxcvbn::zxcvbn(password.as_str(), &[]);
67
68    // Score 3 = "Safely unguessable: moderate protection from offline slow-hash scenario"
69    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
82/// Format zxcvbn feedback into user-friendly message
83fn 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
105/// Constant-time equality comparison for security-sensitive data
106fn 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        // Use a very strong password that likely has no feedback
163        let estimate = zxcvbn::zxcvbn("Tr0ub4dour&3!BatteryStaple", &[]);
164        let feedback = format_feedback(&estimate);
165        // If it has feedback, it should be formatted; if not, default message
166        // This test ensures the function doesn't panic and returns a string
167        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}