sppg/
passphrase.rs

1use itertools::Itertools;
2use std::ops::{Index, IndexMut};
3
4const CHAR_COUNT_MIN: usize = 19;
5const WORD_COUNT_MIN: usize = 4;
6const QUALITY_CHAR_COUNT_MIN: usize = 8;
7const QUALITY_WORD_COUNT_MIN: usize = 2;
8
9#[derive(Clone, Debug)]
10pub struct PassPhrase {
11    separator: char,
12    inner: Vec<String>,
13}
14
15impl PassPhrase {
16    pub fn new(sep: Option<char>) -> Self {
17        let separator = sep.unwrap_or(' ');
18        Self {
19            separator,
20            inner: Vec::<String>::new(),
21        }
22    }
23
24    pub fn len(&self) -> usize {
25        self.inner.len()
26    }
27
28    pub fn is_empty(&self) -> bool {
29        self.len() == 0
30    }
31
32    pub fn push(&mut self, word: &str) -> &mut Self {
33        self.inner.push(word.into());
34
35        self
36    }
37
38    pub fn is_insecure(&self) -> bool {
39        // All lowercase with less than 4 words is insecure
40        let word_count = self.len();
41
42        // we should count the spaces between the words
43        let spaces = self.len() - 1;
44        let mut char_length = 0;
45        for word in &self.inner {
46            char_length += word.chars().count();
47        }
48
49        // does the phrase have quality?
50        let mut quality = 0;
51        let mut uppercase = 0;
52        let mut special = 0;
53        let mut numeric = 0;
54        for word in &self.inner {
55            for ch in word.chars() {
56                // keep track of numbers separately because they
57                // appear in the regular and the special char lists
58                if ch.is_numeric() {
59                    numeric += 1;
60                }
61                if ch.is_ascii_punctuation() {
62                    special += 1;
63                }
64                if ch.is_uppercase() {
65                    uppercase += 1;
66                }
67            }
68        }
69        if uppercase > 0 {
70            quality += 1;
71        }
72        // A quality of 3 is necessary because numbers can appear in
73        // both the word list and the special char list. If there is
74        // not a number but there is at least one upper case and one
75        // special char then increase quality.
76        if numeric > 0 || (special > 0 && uppercase > 0) {
77            quality += 1;
78        }
79        // If there is no punctuation but there is at least one
80        // number and one capital letter then increase quality.
81        if special > 0 || (numeric > 0 && uppercase > 0) {
82            quality += 1;
83        }
84
85        if (QUALITY_WORD_COUNT_MIN..WORD_COUNT_MIN).contains(&word_count) {
86            if quality < 3 || (char_length + spaces) < QUALITY_CHAR_COUNT_MIN {
87                return true;
88            } else if quality >= 3 {
89                return false;
90            }
91        }
92
93        word_count < WORD_COUNT_MIN || (char_length + spaces) < CHAR_COUNT_MIN
94    }
95}
96
97impl Default for PassPhrase {
98    fn default() -> Self {
99        Self::new(None)
100    }
101}
102
103impl std::fmt::Display for PassPhrase {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        let separator = &self.separator.to_string();
106        let pp = self.inner.iter().format(separator);
107
108        write!(f, "{}", pp)
109    }
110}
111
112impl Index<usize> for PassPhrase {
113    type Output = String;
114
115    fn index(&self, index: usize) -> &Self::Output {
116        &self.inner[index]
117    }
118}
119
120impl IndexMut<usize> for PassPhrase {
121    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
122        &mut self.inner[index]
123    }
124}
125
126#[cfg(test)]
127mod test {
128    use super::*;
129
130    #[test]
131    fn test_is_insecure19() {
132        let mut passphrase = PassPhrase::new(None);
133        passphrase
134            .push("this")
135            .push("is")
136            .push("19")
137            .push("chars")
138            .push("lo");
139
140        assert!(!passphrase.is_insecure(), "passphrase IS 19 chars long");
141    }
142
143    #[test]
144    fn test_is_insecure18() {
145        let mut passphrase = PassPhrase::new(None);
146        passphrase
147            .push("this")
148            .push("is")
149            .push("19")
150            .push("chars")
151            .push("l");
152
153        assert!(
154            passphrase.is_insecure(),
155            "insecure: passphrase is LESS THAN 19 chars"
156        );
157    }
158
159    #[test]
160    fn test_is_insecure_i18n() {
161        let mut passphrase = PassPhrase::new(None);
162        passphrase
163            .push("1")
164            .push("2")
165            .push("3")
166            .push("ラウトは難しいです!");
167
168        assert!(
169            passphrase.is_insecure(),
170            "insecure: I18N passphrase is LESS THAN 19 chars"
171        );
172    }
173
174    #[test]
175    fn test_is_insecure_words3() {
176        let mut passphrase = PassPhrase::new(None);
177        passphrase
178            .push("this_is_longer_than")
179            .push("19_chars_but_it_is")
180            .push("only_three_words");
181
182        assert!(passphrase.is_insecure(), "passphrase is LESS THAN 4 words");
183    }
184
185    #[test]
186    fn short_with_quality() {
187        let mut passphrase = PassPhrase::new(None);
188        passphrase.push("!a").push("shortA");
189
190        assert!(
191            !passphrase.is_insecure(),
192            "passphrase is short but contains a capital and special char"
193        );
194    }
195
196    #[test]
197    fn default_impl() {
198        #[derive(Default)]
199        struct TestStruct {
200            pp: PassPhrase,
201        }
202        let s = TestStruct {
203            ..Default::default()
204        };
205
206        assert!(s.pp.is_empty());
207    }
208}