1use crate::rule::PasswordData;
2use crate::rule::Rule;
3use crate::rule::character::CharacterRule;
4use crate::rule::character_characteristics::CharacterCharacteristics;
5use crate::rule::character_data::EnglishCharacterData;
6use std::collections::HashSet;
7use std::f64;
8
9pub trait Entropy {
10 fn estimate(&self) -> f64;
12}
13
14pub struct RandomPasswordEntropy {
43 alphabet_size: usize,
44 password_size: usize,
45}
46impl RandomPasswordEntropy {
47 pub fn new(
48 rules: &[Box<dyn Rule>],
49 password_data: &PasswordData,
50 ) -> Result<Self, &'static str> {
51 let mut unique_chars = HashSet::<char>::new();
53
54 for rule in rules {
55 if let Some(ccc) = rule.as_has_characters() {
56 unique_chars.extend(ccc.characters().chars())
57 }
58 }
59 if unique_chars.is_empty() {
60 return Err(
61 "Password rules must contain at least 1 unique character by CharacterRule definition",
62 );
63 }
64 Ok(RandomPasswordEntropy {
65 alphabet_size: unique_chars.len(),
66 password_size: password_data.password().len(),
67 })
68 }
69}
70impl Entropy for RandomPasswordEntropy {
71 fn estimate(&self) -> f64 {
72 let base = self.alphabet_size as f64;
73 let exponent = self.password_size as f64;
74 let power_result = base.powf(exponent);
75 log2(power_result)
76 }
77}
78
79fn log2(number: f64) -> f64 {
80 number.ln() / f64::consts::LN_2
81}
82
83const FIRST_PHASE_LENGTH: usize = 1;
84const SECOND_PHASE_LENGTH: usize = 8;
85const THIRD_PHASE_LENGTH: usize = 20;
86const FIRST_PHASE_BONUS: f64 = 4.0;
87const SECOND_PHASE_BONUS: f64 = 2.0;
88const THIRD_PHASE_BONUS: f64 = 1.5;
89
90const SHANNON_DICTIONARY_SIEVE: &[usize] =
92 &[0, 0, 0, 4, 5, 6, 6, 6, 5, 5, 4, 4, 3, 3, 2, 2, 1, 1, 0];
93const SHANNON_COMPOSITION_SIEVE: &[usize] = &[0, 0, 0, 2, 3, 3, 5, 6];
95
96pub struct ShannonEntropy {
126 has_dictionary_check: bool,
128 has_composition_check: bool,
130 password_len: usize,
131}
132const COMPOSITION_CHARACTERISTICS_REQUIREMENT: usize = 4;
133
134impl ShannonEntropy {
135 pub fn new(has_dictionary_check: bool, password_data: &PasswordData) -> ShannonEntropy {
136 let has_composition_check = Self::has_composition(password_data);
138 ShannonEntropy {
139 has_dictionary_check,
140 has_composition_check,
141 password_len: password_data.password().len(),
142 }
143 }
144
145 pub fn from_rules(rules: &[Box<dyn Rule>], password_data: &PasswordData) -> ShannonEntropy {
146 let mut has_dict = false;
147 for rule in rules {
148 if let Some(dr) = rule.as_dictionary_rule() {
149 has_dict = !dr.dictionary().is_empty();
150 break;
151 }
152 }
153 Self::new(has_dict, password_data)
154 }
155 fn has_composition(password_data: &PasswordData) -> bool {
156 let crs = vec![
157 CharacterRule::new(Box::new(EnglishCharacterData::Digit), 1).unwrap(),
158 CharacterRule::new(Box::new(EnglishCharacterData::LowerCase), 1).unwrap(),
159 CharacterRule::new(Box::new(EnglishCharacterData::UpperCase), 1).unwrap(),
160 CharacterRule::new(Box::new(EnglishCharacterData::Special), 1).unwrap(),
161 ];
162
163 let composition_validator = CharacterCharacteristics::with_rules_and_characteristics(
164 crs,
165 COMPOSITION_CHARACTERISTICS_REQUIREMENT,
166 )
167 .unwrap();
168
169 composition_validator.validate(password_data).valid()
170 }
171}
172
173impl Entropy for ShannonEntropy {
174 fn estimate(&self) -> f64 {
175 let mut shannon_entropy = 0.0;
176 if self.password_len > 0 {
177 dbg!("first phase");
178 shannon_entropy += FIRST_PHASE_BONUS;
179 if self.password_len > SECOND_PHASE_LENGTH {
180 shannon_entropy +=
181 (SECOND_PHASE_LENGTH - FIRST_PHASE_LENGTH) as f64 * SECOND_PHASE_BONUS;
182 if self.password_len > THIRD_PHASE_LENGTH {
183 shannon_entropy += (THIRD_PHASE_LENGTH - SECOND_PHASE_LENGTH) as f64
185 * THIRD_PHASE_BONUS
186 + (self.password_len - THIRD_PHASE_LENGTH) as f64;
187 } else {
188 shannon_entropy +=
189 (self.password_len - SECOND_PHASE_LENGTH) as f64 * THIRD_PHASE_BONUS;
190 }
191 } else {
192 dbg!("second phase else");
193 shannon_entropy +=
194 (self.password_len - FIRST_PHASE_LENGTH) as f64 * SECOND_PHASE_BONUS;
195 }
196 if self.has_composition_check {
197 dbg!("has_composition_check");
198
199 let idx = if self.password_len > SHANNON_COMPOSITION_SIEVE.len() {
200 SHANNON_COMPOSITION_SIEVE.len() - 1
201 } else {
202 self.password_len - 1
203 };
204 shannon_entropy += SHANNON_COMPOSITION_SIEVE[idx] as f64;
205 }
206 if self.has_dictionary_check {
207 dbg!("has_dictionary_check");
208 let idx = if self.password_len > SHANNON_DICTIONARY_SIEVE.len() {
209 SHANNON_DICTIONARY_SIEVE.len() - 1
210 } else {
211 self.password_len - 1
212 };
213
214 shannon_entropy += SHANNON_DICTIONARY_SIEVE[idx] as f64;
215 }
216 }
217 shannon_entropy
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use crate::entropy::{Entropy, RandomPasswordEntropy, ShannonEntropy};
224 use crate::rule::allowed_character::AllowedCharacter;
225 use crate::rule::character::CharacterRule;
226 use crate::rule::character_characteristics::CharacterCharacteristics;
227 use crate::rule::character_data::EnglishCharacterData;
228 use crate::rule::{PasswordData, Rule};
229
230 #[test]
232 fn test_random_entropy() {
233 let entropy = RandomPasswordEntropy::new(
234 create_rules().as_slice(),
235 &PasswordData::with_password("heLlo".to_string()),
236 )
237 .unwrap();
238 let ent = entropy.estimate();
239 assert_eq!(28.50219859070546, ent);
240 }
241
242 #[test]
243 fn test_shannon_entropy() {
244 let entropy = ShannonEntropy::from_rules(
245 create_rules().as_slice(),
246 &PasswordData::with_password("heLlo".to_string()),
247 );
248 let ent = entropy.estimate();
249 assert_eq!(12.0, ent);
250 }
251
252 fn create_rules() -> Vec<Box<dyn Rule>> {
253 let allowed_rules = AllowedCharacter::from_chars("abcdefghijklmnopqrstuvwxyzL");
254 let ch_rules = vec![
255 CharacterRule::new(Box::new(EnglishCharacterData::UpperCase), 1).unwrap(),
256 CharacterRule::new(Box::new(EnglishCharacterData::LowerCase), 1).unwrap(),
257 ];
258 let char_rule =
260 CharacterCharacteristics::with_rules_and_characteristics(ch_rules, 2).unwrap();
261
262 vec![Box::new(allowed_rules), Box::new(char_rule)]
263 }
264}