1use std::collections::BTreeSet;
2
3use bitwarden_error::bitwarden_error;
4use rand::{Rng, RngExt, distr::Distribution, seq::SliceRandom};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8#[cfg(feature = "wasm")]
9use tsify::Tsify;
10
11#[allow(missing_docs)]
12#[bitwarden_error(flat)]
13#[derive(Debug, Error)]
14pub enum PasswordError {
15 #[error("No character set enabled")]
16 NoCharacterSetEnabled,
17 #[error("Invalid password length")]
18 InvalidLength,
19}
20
21#[derive(Serialize, Deserialize, Debug, JsonSchema)]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
25#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
26pub struct PasswordGeneratorRequest {
27 pub lowercase: bool,
29 pub uppercase: bool,
31 pub numbers: bool,
33 pub special: bool,
35
36 pub length: u8,
39
40 pub avoid_ambiguous: bool, pub min_lowercase: Option<u8>,
47 pub min_uppercase: Option<u8>,
50 pub min_number: Option<u8>,
53 pub min_special: Option<u8>,
56}
57
58const DEFAULT_PASSWORD_LENGTH: u8 = 16;
59
60pub const MINIMUM_PASSWORD_LENGTH: u8 = 5;
64pub const MAXIMUM_PASSWORD_LENGTH: u8 = 128;
66pub const MINIMUM_MIN_CHAR_COUNT: u8 = 0;
68pub const MAXIMUM_MIN_CHAR_COUNT: u8 = 9;
70
71impl Default for PasswordGeneratorRequest {
72 fn default() -> Self {
73 Self {
74 lowercase: true,
75 uppercase: true,
76 numbers: true,
77 special: false,
78 length: DEFAULT_PASSWORD_LENGTH,
79 avoid_ambiguous: false,
80 min_lowercase: None,
81 min_uppercase: None,
82 min_number: None,
83 min_special: None,
84 }
85 }
86}
87
88const UPPER_CHARS_AMBIGUOUS: &[char] = &['I', 'O'];
89const LOWER_CHARS_AMBIGUOUS: &[char] = &['l'];
90const NUMBER_CHARS_AMBIGUOUS: &[char] = &['0', '1'];
91const SPECIAL_CHARS: &[char] = &['!', '@', '#', '$', '%', '^', '&', '*'];
92
93#[derive(Clone, Default)]
98struct CharSet(BTreeSet<char>);
99impl CharSet {
100 pub fn include(self, other: impl IntoIterator<Item = char>) -> Self {
102 self.include_if(true, other)
103 }
104
105 pub fn include_if(mut self, predicate: bool, other: impl IntoIterator<Item = char>) -> Self {
108 if predicate {
109 self.0.extend(other);
110 }
111 self
112 }
113
114 pub fn exclude_if<'a>(
116 self,
117 predicate: bool,
118 other: impl IntoIterator<Item = &'a char>,
119 ) -> Self {
120 if predicate {
121 let other: BTreeSet<_> = other.into_iter().copied().collect();
122 Self(self.0.difference(&other).copied().collect())
123 } else {
124 self
125 }
126 }
127}
128impl<'a> IntoIterator for &'a CharSet {
129 type Item = char;
130 type IntoIter = std::iter::Copied<std::collections::btree_set::Iter<'a, char>>;
131 fn into_iter(self) -> Self::IntoIter {
132 self.0.iter().copied()
133 }
134}
135impl Distribution<char> for CharSet {
136 fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> char {
137 let idx = rng.random_range(0..self.0.len());
138 *self.0.iter().nth(idx).expect("Valid index")
139 }
140}
141
142struct PasswordGeneratorOptions {
146 pub(super) lower: (CharSet, usize),
147 pub(super) upper: (CharSet, usize),
148 pub(super) number: (CharSet, usize),
149 pub(super) special: (CharSet, usize),
150 pub(super) all: (CharSet, usize),
151
152 pub(super) length: usize,
153}
154
155impl PasswordGeneratorRequest {
156 fn validate_options(self) -> Result<PasswordGeneratorOptions, PasswordError> {
159 if !self.lowercase && !self.uppercase && !self.numbers && !self.special {
163 return Err(PasswordError::NoCharacterSetEnabled);
164 }
165
166 if self.length < 4 {
167 return Err(PasswordError::InvalidLength);
168 }
169
170 fn get_minimum(min: Option<u8>, enabled: bool) -> usize {
173 if enabled {
174 usize::max(min.unwrap_or(1) as usize, 1)
175 } else {
176 0
177 }
178 }
179
180 let length = self.length as usize;
181 let min_lowercase = get_minimum(self.min_lowercase, self.lowercase);
182 let min_uppercase = get_minimum(self.min_uppercase, self.uppercase);
183 let min_number = get_minimum(self.min_number, self.numbers);
184 let min_special = get_minimum(self.min_special, self.special);
185
186 let minimum_length = min_lowercase + min_uppercase + min_number + min_special;
188 if minimum_length > length {
189 return Err(PasswordError::InvalidLength);
190 }
191
192 let lower = (
193 CharSet::default()
194 .include_if(self.lowercase, 'a'..='z')
195 .exclude_if(self.avoid_ambiguous, LOWER_CHARS_AMBIGUOUS),
196 min_lowercase,
197 );
198
199 let upper = (
200 CharSet::default()
201 .include_if(self.uppercase, 'A'..='Z')
202 .exclude_if(self.avoid_ambiguous, UPPER_CHARS_AMBIGUOUS),
203 min_uppercase,
204 );
205
206 let number = (
207 CharSet::default()
208 .include_if(self.numbers, '0'..='9')
209 .exclude_if(self.avoid_ambiguous, NUMBER_CHARS_AMBIGUOUS),
210 min_number,
211 );
212
213 let special = (
214 CharSet::default().include_if(self.special, SPECIAL_CHARS.iter().copied()),
215 min_special,
216 );
217
218 let all = (
219 CharSet::default()
220 .include(&lower.0)
221 .include(&upper.0)
222 .include(&number.0)
223 .include(&special.0),
224 length - minimum_length,
225 );
226
227 Ok(PasswordGeneratorOptions {
228 lower,
229 upper,
230 number,
231 special,
232 all,
233 length,
234 })
235 }
236}
237
238pub(crate) fn password(input: PasswordGeneratorRequest) -> Result<String, PasswordError> {
240 let options = input.validate_options()?;
241 Ok(password_with_rng(rand::rng(), options))
242}
243
244fn password_with_rng(mut rng: impl Rng, options: PasswordGeneratorOptions) -> String {
245 let mut buf: Vec<char> = Vec::with_capacity(options.length);
246
247 let opts = [
248 &options.all,
249 &options.upper,
250 &options.lower,
251 &options.number,
252 &options.special,
253 ];
254 for (set, qty) in opts {
255 buf.extend(set.sample_iter(&mut rng).take(*qty));
256 }
257
258 buf.shuffle(&mut rng);
259
260 buf.iter().collect()
261}
262
263#[cfg(test)]
264mod test {
265 use std::collections::BTreeSet;
266
267 use rand::SeedableRng;
268
269 use super::*;
270
271 fn ref_to_set<'a>(chars: impl IntoIterator<Item = &'a char>) -> BTreeSet<char> {
273 chars.into_iter().copied().collect()
274 }
275 fn to_set(chars: impl IntoIterator<Item = char>) -> BTreeSet<char> {
276 chars.into_iter().collect()
277 }
278
279 #[test]
280 fn test_password_gen_all_charsets_enabled() {
281 let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
282
283 let options = PasswordGeneratorRequest {
284 lowercase: true,
285 uppercase: true,
286 numbers: true,
287 special: true,
288 avoid_ambiguous: false,
289 ..Default::default()
290 }
291 .validate_options()
292 .unwrap();
293
294 assert_eq!(to_set(&options.lower.0), to_set('a'..='z'));
295 assert_eq!(to_set(&options.upper.0), to_set('A'..='Z'));
296 assert_eq!(to_set(&options.number.0), to_set('0'..='9'));
297 assert_eq!(to_set(&options.special.0), ref_to_set(SPECIAL_CHARS));
298
299 let pass = password_with_rng(&mut rng, options);
300 assert_eq!(pass, "0oA772tQjaUO$a@L");
301 }
302
303 #[test]
304 fn test_password_gen_only_letters_enabled() {
305 let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
306
307 let options = PasswordGeneratorRequest {
308 lowercase: true,
309 uppercase: true,
310 numbers: false,
311 special: false,
312 avoid_ambiguous: false,
313 ..Default::default()
314 }
315 .validate_options()
316 .unwrap();
317
318 assert_eq!(to_set(&options.lower.0), to_set('a'..='z'));
319 assert_eq!(to_set(&options.upper.0), to_set('A'..='Z'));
320 assert_eq!(to_set(&options.number.0), to_set([]));
321 assert_eq!(to_set(&options.special.0), to_set([]));
322
323 let pass = password_with_rng(&mut rng, options);
324 assert_eq!(pass, "FrNSJGvhnAbXggMU");
325 }
326
327 #[test]
328 fn test_password_gen_only_numbers_and_lower_enabled_no_ambiguous() {
329 let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
330
331 let options = PasswordGeneratorRequest {
332 lowercase: true,
333 uppercase: false,
334 numbers: true,
335 special: false,
336 avoid_ambiguous: true,
337 ..Default::default()
338 }
339 .validate_options()
340 .unwrap();
341
342 assert!(to_set(&options.lower.0).is_subset(&to_set('a'..='z')));
343 assert!(to_set(&options.lower.0).is_disjoint(&ref_to_set(LOWER_CHARS_AMBIGUOUS)));
344
345 assert!(to_set(&options.number.0).is_subset(&to_set('0'..='9')));
346 assert!(to_set(&options.number.0).is_disjoint(&ref_to_set(NUMBER_CHARS_AMBIGUOUS)));
347
348 assert_eq!(to_set(&options.upper.0), to_set([]));
349 assert_eq!(to_set(&options.special.0), to_set([]));
350
351 let pass = password_with_rng(&mut rng, options);
352 assert_eq!(pass, "5uat85wos2jg4n9f");
353 }
354
355 #[test]
356 fn test_password_gen_only_upper_and_special_enabled_no_ambiguous() {
357 let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
358
359 let options = PasswordGeneratorRequest {
360 lowercase: false,
361 uppercase: true,
362 numbers: false,
363 special: true,
364 avoid_ambiguous: true,
365 ..Default::default()
366 }
367 .validate_options()
368 .unwrap();
369
370 assert!(to_set(&options.upper.0).is_subset(&to_set('A'..='Z')));
371 assert!(to_set(&options.upper.0).is_disjoint(&ref_to_set(UPPER_CHARS_AMBIGUOUS)));
372
373 assert_eq!(to_set(&options.special.0), ref_to_set(SPECIAL_CHARS));
374
375 assert_eq!(to_set(&options.lower.0), to_set([]));
376 assert_eq!(to_set(&options.number.0), to_set([]));
377
378 let pass = password_with_rng(&mut rng, options);
379 assert_eq!(pass, "%VBT*%YPT!LH$PAF");
380 }
381
382 #[test]
383 fn test_password_gen_minimum_limits() {
384 let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
385
386 let options = PasswordGeneratorRequest {
387 lowercase: true,
388 uppercase: true,
389 numbers: true,
390 special: true,
391 avoid_ambiguous: false,
392 length: 24,
393 min_lowercase: Some(5),
394 min_uppercase: Some(5),
395 min_number: Some(5),
396 min_special: Some(5),
397 }
398 .validate_options()
399 .unwrap();
400
401 assert_eq!(to_set(&options.lower.0), to_set('a'..='z'));
402 assert_eq!(to_set(&options.upper.0), to_set('A'..='Z'));
403 assert_eq!(to_set(&options.number.0), to_set('0'..='9'));
404 assert_eq!(to_set(&options.special.0), ref_to_set(SPECIAL_CHARS));
405
406 assert_eq!(options.lower.1, 5);
407 assert_eq!(options.upper.1, 5);
408 assert_eq!(options.number.1, 5);
409 assert_eq!(options.special.1, 5);
410
411 let pass = password_with_rng(&mut rng, options);
412 assert_eq!(pass, "t&c0L73*D*G%aak7goq!N2T4");
413 }
414}