bitwarden_vault/cipher_risk/
password_strength.rs

1//! Password strength calculation using zxcvbn with cipher context.
2//!
3//! This module implements password strength scoring with cipher-specific
4//! context (username/email) to penalize weak passwords.
5
6/// Calculate password strength with cipher-specific context.
7///
8/// Uses zxcvbn to score password strength from 0 (weakest) to 4 (strongest).
9/// Penalizes passwords that contain parts of the username/email.
10pub(super) fn calculate_password_strength(password: &str, username: Option<&str>) -> u8 {
11    let mut user_inputs = Vec::new();
12
13    // Extract meaningful parts from username field
14    if let Some(username) = username {
15        user_inputs.extend(extract_user_inputs(username));
16    }
17
18    // Call zxcvbn with cipher-specific inputs only (no "bitwarden" globals)
19    let inputs_refs: Vec<&str> = user_inputs.iter().map(|s| s.as_str()).collect();
20    zxcvbn::zxcvbn(password, &inputs_refs).score().into()
21}
22
23/// Extract meaningful tokens from username/email for password penalization.
24///
25/// Handles both email addresses and plain usernames:
26/// - For emails: extracts and tokenizes the local part (before @)
27/// - For usernames: tokenizes the entire string
28/// - Splits on non-alphanumeric characters and converts to lowercase
29fn extract_user_inputs(username: &str) -> Vec<String> {
30    username
31        // Check if it's email-like (contains @)
32        .split_once('@')
33        // Email: extract local part tokens
34        .map_or(username, |(local_part, _domain)| local_part)
35        .trim()
36        .to_lowercase()
37        .split(|c: char| !c.is_alphanumeric())
38        .filter(|s| !s.is_empty())
39        .map(str::to_owned)
40        .collect()
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn test_extract_user_inputs_from_email() {
49        let inputs = extract_user_inputs("[email protected]");
50        assert_eq!(inputs, vec!["john", "doe"]);
51    }
52
53    #[test]
54    fn test_extract_user_inputs_from_username() {
55        let inputs = extract_user_inputs("john_doe123");
56        assert_eq!(inputs, vec!["john", "doe123"]);
57    }
58
59    #[test]
60    fn test_extract_user_inputs_lowercase() {
61        let inputs = extract_user_inputs("[email protected]");
62        assert_eq!(inputs, vec!["johndoe"]);
63    }
64
65    #[test]
66    fn test_extract_user_inputs_empty() {
67        let inputs = extract_user_inputs("");
68        assert!(inputs.is_empty());
69    }
70
71    #[test]
72    fn test_calculate_password_strength_penalizes_username() {
73        // Password containing username should be weaker
74        let strength_with_username = calculate_password_strength("johndoe123!", Some("johndoe"));
75        let strength_without_username = calculate_password_strength("johndoe123!", None);
76
77        assert!(
78            strength_with_username <= strength_without_username,
79            "Password should be weaker when it contains username"
80        );
81    }
82}