bitwarden_pam_sealed/
policy.rs1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6pub const GENERATOR_MIN_LENGTH: u32 = 5;
8pub const GENERATOR_MAX_LENGTH: u32 = 128;
10
11#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
13pub enum PasswordPolicyError {
14 #[error("No character classes are enabled")]
16 NoCharacterClasses,
17 #[error("Invalid length bounds")]
19 InvalidLengthBounds,
20}
21
22#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
24pub struct SealedPasswordPolicy {
25 pub min_length: u32,
27 pub max_length: u32,
29 pub include_uppercase: bool,
31 pub include_lowercase: bool,
33 pub include_digits: bool,
35 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 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}