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 let word_count = self.len();
41
42 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 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 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 if numeric > 0 || (special > 0 && uppercase > 0) {
77 quality += 1;
78 }
79 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}