Skip to main content

passay_rs/entropy/
mod.rs

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    /// Returns the estimated entropy bits of a password.
11    fn estimate(&self) -> f64;
12}
13
14/// Entropy bits estimate defined in NIST SP-800-63-1 Randomly Selected Passwords.
15/// see <http://csrc.nist.gov/publications/nistpubs/800-63-1/SP-800-63-1.pdf>
16/// A1. "Randomly Selected Passwords"
17///
18/// # Example
19///
20/// ```
21///    use passay_rs::entropy::RandomPasswordEntropy;
22///    use passay_rs::rule::Rule;
23///    use passay_rs::rule::character_characteristics::CharacterCharacteristics;
24///    use passay_rs::rule::character::CharacterRule;
25///    use passay_rs::rule::character_data::EnglishCharacterData;
26///    use passay_rs::rule::allowed_character::AllowedCharacter;
27///    use passay_rs::rule::PasswordData;
28///    use passay_rs::entropy::Entropy;
29///
30///    let allowed_rules = AllowedCharacter::from_chars("abcdefghijklmnopqrstuvwxyzL");
31///    let ch_rules = vec![
32///        CharacterRule::new(Box::new(EnglishCharacterData::UpperCase), 1).unwrap(),
33///        CharacterRule::new(Box::new(EnglishCharacterData::LowerCase), 1).unwrap(),
34///    ];
35///    let char_rule = CharacterCharacteristics::with_rules_and_characteristics(ch_rules, 2).unwrap();
36///
37///    let rules:Vec<Box<dyn Rule>> = vec![Box::new(allowed_rules), Box::new(char_rule)];
38///    let entropy = RandomPasswordEntropy::new(rules.as_slice(), &PasswordData::with_password("heLlo".to_string())).unwrap();
39///    let ent = entropy.estimate();
40///    assert_eq!(28.50219859070546, ent);
41/// ```
42pub 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        // TODO check password data origin
52        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
90/// Array used for determining dictionary entropy "bonus" for calculating the Shannon entropy estimate.
91const SHANNON_DICTIONARY_SIEVE: &[usize] =
92    &[0, 0, 0, 4, 5, 6, 6, 6, 5, 5, 4, 4, 3, 3, 2, 2, 1, 1, 0];
93/// Array used for determining composition "bonus" for calculating the Shannon entropy estimate.
94const SHANNON_COMPOSITION_SIEVE: &[usize] = &[0, 0, 0, 2, 3, 3, 5, 6];
95
96/// Returns the entropy bits of a user selected password. This estimate is based on a 94 Character Alphabet and is a
97/// "ballpark" estimate based on Claude Shannon's observations.
98/// See <http://csrc.nist.gov/publications/nistpubs/800-63-1/SP-800-63-1.pdf>
99/// A1. "User Selected Passwords"
100///
101/// # Example
102///
103/// ```
104///    use passay_rs::entropy::ShannonEntropy;
105///    use passay_rs::rule::Rule;
106///    use passay_rs::rule::character_characteristics::CharacterCharacteristics;
107///    use passay_rs::rule::character::CharacterRule;
108///    use passay_rs::rule::character_data::EnglishCharacterData;
109///    use passay_rs::rule::allowed_character::AllowedCharacter;
110///    use passay_rs::rule::PasswordData;
111///    use passay_rs::entropy::Entropy;
112///
113///    let allowed_rules = AllowedCharacter::from_chars("abcdefghijklmnopqrstuvwxyzL");
114///    let ch_rules = vec![
115///        CharacterRule::new(Box::new(EnglishCharacterData::UpperCase), 1).unwrap(),
116///        CharacterRule::new(Box::new(EnglishCharacterData::LowerCase), 1).unwrap(),
117///    ];
118///    let char_rule = CharacterCharacteristics::with_rules_and_characteristics(ch_rules, 2).unwrap();
119///
120///    let rules:Vec<Box<dyn Rule>> = vec![Box::new(allowed_rules), Box::new(char_rule)];
121///    let entropy = ShannonEntropy::from_rules(rules.as_slice(), &PasswordData::with_password("heLlo".to_string()));
122///    let ent = entropy.estimate();
123///    assert_eq!(12.0, ent);
124/// ```
125pub struct ShannonEntropy {
126    /// Whether a dictionary was used to check the password.
127    has_dictionary_check: bool,
128    /// Whether at least 1 uppercase and special/symbol character is enforced.
129    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        // TODO check password data origin
137        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                    //4th phase bonus is 1 point, so (passwordSize - THIRD_PHASE_LENGTH) will suffice
184                    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    // TODO need more tests for entropy
231    #[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        // there is a bug in java with invalid number of characteristics of 3
259        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}