Skip to main content

bitwarden_pam_sealed/
policy.rs

1//! Generator-validity check for sealed password policies; doesn't assess password strength.
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// Mirrors `bitwarden_generators::MINIMUM_PASSWORD_LENGTH`.
7pub const GENERATOR_MIN_LENGTH: u32 = 5;
8/// Mirrors `bitwarden_generators::MAXIMUM_PASSWORD_LENGTH`.
9pub const GENERATOR_MAX_LENGTH: u32 = 128;
10
11/// Errors returned by [`SealedPasswordPolicy::validate`].
12#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
13pub enum PasswordPolicyError {
14    /// No character class is enabled.
15    #[error("No character classes are enabled")]
16    NoCharacterClasses,
17    /// Lengths out of order or outside the generator's range.
18    #[error("Invalid length bounds")]
19    InvalidLengthBounds,
20}
21
22/// Password policy for a PAM target system's generated credential.
23#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
24pub struct SealedPasswordPolicy {
25    /// Only checked by [`Self::validate`]; doesn't affect generation.
26    pub min_length: u32,
27    /// The length actually generated.
28    pub max_length: u32,
29    /// Include `A`-`Z`.
30    pub include_uppercase: bool,
31    /// Include `a`-`z`.
32    pub include_lowercase: bool,
33    /// Include `0`-`9`.
34    pub include_digits: bool,
35    /// Include `! @ # $ % ^ & *`.
36    pub include_symbols: bool,
37}
38
39impl SealedPasswordPolicy {
40    fn has_character_class(&self) -> bool {
41        self.include_uppercase
42            || self.include_lowercase
43            || self.include_digits
44            || self.include_symbols
45    }
46
47    /// Validates that the generator can produce a password for this policy.
48    pub fn validate(&self) -> Result<(), PasswordPolicyError> {
49        if !self.has_character_class() {
50            return Err(PasswordPolicyError::NoCharacterClasses);
51        }
52        if !(GENERATOR_MIN_LENGTH <= self.min_length
53            && self.min_length <= self.max_length
54            && self.max_length <= GENERATOR_MAX_LENGTH)
55        {
56            return Err(PasswordPolicyError::InvalidLengthBounds);
57        }
58        Ok(())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    fn all_classes(max_length: u32) -> SealedPasswordPolicy {
67        SealedPasswordPolicy {
68            min_length: GENERATOR_MIN_LENGTH,
69            max_length,
70            include_uppercase: true,
71            include_lowercase: true,
72            include_digits: true,
73            include_symbols: true,
74        }
75    }
76
77    #[test]
78    fn generator_consts_match_bitwarden_generators() {
79        assert_eq!(
80            GENERATOR_MIN_LENGTH,
81            u32::from(bitwarden_generators::MINIMUM_PASSWORD_LENGTH)
82        );
83        assert_eq!(
84            GENERATOR_MAX_LENGTH,
85            u32::from(bitwarden_generators::MAXIMUM_PASSWORD_LENGTH)
86        );
87    }
88
89    #[test]
90    fn validate_rejects_no_character_classes() {
91        let policy = SealedPasswordPolicy {
92            min_length: GENERATOR_MIN_LENGTH,
93            max_length: 20,
94            include_uppercase: false,
95            include_lowercase: false,
96            include_digits: false,
97            include_symbols: false,
98        };
99        assert_eq!(
100            policy.validate(),
101            Err(PasswordPolicyError::NoCharacterClasses)
102        );
103    }
104
105    #[test]
106    fn validate_rejects_min_length_above_max_length() {
107        let mut policy = all_classes(14);
108        policy.min_length = 15;
109        assert_eq!(
110            policy.validate(),
111            Err(PasswordPolicyError::InvalidLengthBounds)
112        );
113    }
114
115    #[test]
116    fn validate_rejects_max_length_above_generator_range() {
117        let mut policy = all_classes(GENERATOR_MAX_LENGTH);
118        policy.max_length = GENERATOR_MAX_LENGTH + 1;
119        assert_eq!(
120            policy.validate(),
121            Err(PasswordPolicyError::InvalidLengthBounds)
122        );
123    }
124
125    #[test]
126    fn validate_rejects_min_length_below_generator_range() {
127        let mut policy = all_classes(14);
128        policy.min_length = GENERATOR_MIN_LENGTH - 1;
129        assert_eq!(
130            policy.validate(),
131            Err(PasswordPolicyError::InvalidLengthBounds)
132        );
133    }
134
135    #[test]
136    fn weak_but_generator_valid_policy_passes() {
137        let policy = SealedPasswordPolicy {
138            min_length: GENERATOR_MIN_LENGTH,
139            max_length: GENERATOR_MIN_LENGTH,
140            include_uppercase: false,
141            include_lowercase: false,
142            include_digits: true,
143            include_symbols: false,
144        };
145        assert!(policy.validate().is_ok());
146    }
147}