1use rand::rngs::StdRng;
22use rand::{Rng, SeedableRng};
23
24use super::wordlist::WORDS;
25
26fn csprng() -> StdRng {
34 StdRng::from_os_rng()
35}
36
37const LOWER_LETTERS: &str = "qwertyuiopasdfghjklzxcvbnm";
38const UPPER_LETTERS: &str = "QWERTYUIOPASDFGHJKLZXCVBNM";
39const DIGITS: &str = "1234567890";
40const SPECIAL_SYMBOLS: &str = "!@#$%^&*()_+-=;:,.?~";
41
42const AMBIGUOUS_CHARS: &str = "lIi1oO0B8Ss5Z2";
46
47#[derive(Debug, Clone)]
49pub struct PasswordOptions {
50 pub lowercase: bool,
52 pub uppercase: bool,
54 pub digits: bool,
56 pub special: bool,
58 pub avoid_ambiguous: bool,
60 pub length: usize,
62}
63
64impl Default for PasswordOptions {
65 fn default() -> Self {
66 Self {
67 lowercase: true,
68 uppercase: true,
69 digits: true,
70 special: false,
71 avoid_ambiguous: false,
72 length: 16,
73 }
74 }
75}
76
77pub fn generate_password(options: &PasswordOptions) -> String {
105 let mut rng = csprng();
106 let mut char_pool = String::new();
107
108 if options.lowercase {
110 char_pool.push_str(LOWER_LETTERS);
111 char_pool.push_str(LOWER_LETTERS);
112 char_pool.push_str(LOWER_LETTERS);
113 }
114 if options.uppercase {
115 char_pool.push_str(UPPER_LETTERS);
116 char_pool.push_str(UPPER_LETTERS);
117 char_pool.push_str(UPPER_LETTERS);
118 }
119 if options.digits {
120 char_pool.push_str(DIGITS);
121 char_pool.push_str(DIGITS);
122 }
123 if options.special {
124 char_pool.push_str(SPECIAL_SYMBOLS);
125 }
126
127 if char_pool.is_empty() {
129 char_pool.push_str(LOWER_LETTERS);
130 }
131
132 if options.avoid_ambiguous {
136 char_pool = char_pool
137 .chars()
138 .filter(|c| !AMBIGUOUS_CHARS.contains(*c))
139 .collect();
140 if char_pool.is_empty() {
141 char_pool.push_str(LOWER_LETTERS);
142 }
143 }
144
145 let pool_chars: Vec<char> = char_pool.chars().collect();
146
147 let mut required: Vec<char> = Vec::new();
152 if options.lowercase {
153 if let Some(c) = pick_from_class(LOWER_LETTERS, options.avoid_ambiguous, &mut rng) {
154 required.push(c);
155 }
156 }
157 if options.uppercase {
158 if let Some(c) = pick_from_class(UPPER_LETTERS, options.avoid_ambiguous, &mut rng) {
159 required.push(c);
160 }
161 }
162 if options.digits {
163 if let Some(c) = pick_from_class(DIGITS, options.avoid_ambiguous, &mut rng) {
164 required.push(c);
165 }
166 }
167 if options.special {
168 if let Some(c) = pick_from_class(SPECIAL_SYMBOLS, options.avoid_ambiguous, &mut rng) {
169 required.push(c);
170 }
171 }
172
173 if options.length <= required.len() {
177 shuffle(&mut required, &mut rng);
178 return required.into_iter().take(options.length).collect();
179 }
180
181 let remaining = options.length - required.len();
183 let mut password_chars: Vec<char> = required;
184 for _ in 0..remaining {
185 let idx = rng.random_range(0..pool_chars.len());
186 password_chars.push(pool_chars[idx]);
187 }
188
189 shuffle(&mut password_chars, &mut rng);
191 password_chars.into_iter().collect()
192}
193
194fn pick_from_class(class: &str, avoid_ambiguous: bool, rng: &mut StdRng) -> Option<char> {
199 let candidates: Vec<char> = class
200 .chars()
201 .filter(|c| !avoid_ambiguous || !AMBIGUOUS_CHARS.contains(*c))
202 .collect();
203 if candidates.is_empty() {
204 None
205 } else {
206 Some(candidates[rng.random_range(0..candidates.len())])
207 }
208}
209
210fn shuffle<T>(items: &mut [T], rng: &mut StdRng) {
212 for i in (1..items.len()).rev() {
213 let j = rng.random_range(0..=i);
214 items.swap(i, j);
215 }
216}
217
218pub fn generate_clever_password(pattern: &str) -> String {
244 let mut rng = csprng();
245 let all_symbols = format!("{}{}{}{}", LOWER_LETTERS, UPPER_LETTERS, DIGITS, SPECIAL_SYMBOLS);
246 let all_chars: Vec<char> = all_symbols.chars().collect();
247 let lower_chars: Vec<char> = LOWER_LETTERS.chars().collect();
248 let upper_chars: Vec<char> = UPPER_LETTERS.chars().collect();
249 let digit_chars: Vec<char> = DIGITS.chars().collect();
250 let special_chars: Vec<char> = SPECIAL_SYMBOLS.chars().collect();
251
252 let mut password = String::with_capacity(pattern.len());
253
254 for ch in pattern.chars() {
255 let generated_char = if LOWER_LETTERS.contains(ch) {
256 lower_chars[rng.random_range(0..lower_chars.len())]
257 } else if UPPER_LETTERS.contains(ch) {
258 upper_chars[rng.random_range(0..upper_chars.len())]
259 } else if DIGITS.contains(ch) {
260 digit_chars[rng.random_range(0..digit_chars.len())]
261 } else if SPECIAL_SYMBOLS.contains(ch) {
262 special_chars[rng.random_range(0..special_chars.len())]
263 } else {
264 all_chars[rng.random_range(0..all_chars.len())]
265 };
266 password.push(generated_char);
267 }
268
269 password
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum MemorableCaps {
280 First,
281 Last,
282}
283
284#[derive(Debug, Clone)]
286pub struct MemorableOptions {
287 pub num_words: usize,
290 pub digits_per_word: usize,
292 pub separator: String,
295 pub prefix: String,
299 pub caps: MemorableCaps,
301}
302
303impl Default for MemorableOptions {
304 fn default() -> Self {
305 Self {
306 num_words: 4,
307 digits_per_word: 1,
308 separator: "-".to_string(),
309 prefix: String::new(),
310 caps: MemorableCaps::First,
311 }
312 }
313}
314
315pub fn generate_memorable_password(opts: &MemorableOptions) -> String {
320 if opts.num_words == 0 {
321 return String::new();
322 }
323
324 let mut rng = csprng();
325 let mut segments: Vec<String> = Vec::with_capacity(opts.num_words);
326
327 if !opts.prefix.is_empty() {
328 segments.push(opts.prefix.clone());
329 }
330
331 for _ in 0..opts.num_words {
332 let word = WORDS[rng.random_range(0..WORDS.len())];
333 let mut s = apply_caps(word, opts.caps);
334 for _ in 0..opts.digits_per_word {
335 let digit = rng.random_range(0..10u32);
336 s.push(char::from_digit(digit, 10).unwrap());
337 }
338 segments.push(s);
339 }
340
341 segments.join(&opts.separator)
342}
343
344fn apply_caps(word: &str, caps: MemorableCaps) -> String {
345 let chars: Vec<char> = word.chars().collect();
346 if chars.is_empty() {
347 return String::new();
348 }
349 match caps {
350 MemorableCaps::First => {
351 let mut out = String::with_capacity(word.len());
352 out.push(chars[0].to_ascii_uppercase());
353 for &c in &chars[1..] {
354 out.push(c);
355 }
356 out
357 }
358 MemorableCaps::Last => {
359 let mut out = String::with_capacity(word.len());
360 for &c in &chars[..chars.len() - 1] {
361 out.push(c);
362 }
363 out.push(chars[chars.len() - 1].to_ascii_uppercase());
364 out
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use std::collections::HashSet;
373
374 #[test]
375 fn test_generate_password_default() {
376 let options = PasswordOptions::default();
377 let password = generate_password(&options);
378 assert_eq!(password.len(), 16);
379 }
380
381 #[test]
382 fn test_generate_password_length() {
383 let options = PasswordOptions {
384 length: 32,
385 ..Default::default()
386 };
387 let password = generate_password(&options);
388 assert_eq!(password.len(), 32);
389 }
390
391 #[test]
392 fn test_generate_password_lowercase_only() {
393 let options = PasswordOptions {
394 lowercase: true,
395 uppercase: false,
396 digits: false,
397 special: false,
398 avoid_ambiguous: false,
399 length: 20,
400 };
401 let password = generate_password(&options);
402 assert_eq!(password.len(), 20);
403 assert!(password.chars().all(|c| c.is_ascii_lowercase()));
404 }
405
406 #[test]
407 fn test_generate_password_uppercase_only() {
408 let options = PasswordOptions {
409 lowercase: false,
410 uppercase: true,
411 digits: false,
412 special: false,
413 avoid_ambiguous: false,
414 length: 20,
415 };
416 let password = generate_password(&options);
417 assert_eq!(password.len(), 20);
418 assert!(password.chars().all(|c| c.is_ascii_uppercase()));
419 }
420
421 #[test]
422 fn test_generate_password_digits_only() {
423 let options = PasswordOptions {
424 lowercase: false,
425 uppercase: false,
426 digits: true,
427 special: false,
428 avoid_ambiguous: false,
429 length: 20,
430 };
431 let password = generate_password(&options);
432 assert_eq!(password.len(), 20);
433 assert!(password.chars().all(|c| c.is_ascii_digit()));
434 }
435
436 #[test]
437 fn test_generate_password_special_only() {
438 let options = PasswordOptions {
439 lowercase: false,
440 uppercase: false,
441 digits: false,
442 special: true,
443 avoid_ambiguous: false,
444 length: 20,
445 };
446 let password = generate_password(&options);
447 assert_eq!(password.len(), 20);
448 assert!(password.chars().all(|c| SPECIAL_SYMBOLS.contains(c)));
449 }
450
451 #[test]
452 fn test_generate_password_all_types() {
453 let options = PasswordOptions {
454 lowercase: true,
455 uppercase: true,
456 digits: true,
457 special: true,
458 avoid_ambiguous: false,
459 length: 100,
460 };
461 let password = generate_password(&options);
462 assert_eq!(password.len(), 100);
463 }
465
466 #[test]
467 fn test_generate_password_empty_options_fallback() {
468 let options = PasswordOptions {
469 lowercase: false,
470 uppercase: false,
471 digits: false,
472 special: false,
473 avoid_ambiguous: false,
474 length: 10,
475 };
476 let password = generate_password(&options);
477 assert_eq!(password.len(), 10);
478 assert!(password.chars().all(|c| c.is_ascii_lowercase()));
480 }
481
482 #[test]
483 fn test_generate_password_avoid_ambiguous() {
484 let options = PasswordOptions {
485 lowercase: true,
486 uppercase: true,
487 digits: true,
488 special: false,
489 avoid_ambiguous: true,
490 length: 200,
491 };
492 let password = generate_password(&options);
493 assert_eq!(password.len(), 200);
494 for c in password.chars() {
495 assert!(
496 !AMBIGUOUS_CHARS.contains(c),
497 "ambiguous char {c:?} should be excluded but appeared in {password}",
498 );
499 }
500 }
501
502 #[test]
503 fn test_generate_password_avoid_ambiguous_off_does_not_filter() {
504 let options = PasswordOptions {
508 lowercase: true,
509 uppercase: true,
510 digits: true,
511 special: false,
512 avoid_ambiguous: false,
513 length: 1000,
514 };
515 let password = generate_password(&options);
516 let saw_any_ambiguous =
517 password.chars().any(|c| AMBIGUOUS_CHARS.contains(c));
518 assert!(
519 saw_any_ambiguous,
520 "expected at least one ambiguous char in 1000-char password",
521 );
522 }
523
524 #[test]
525 fn test_generate_password_guarantees_each_selected_class() {
526 let options = PasswordOptions {
529 lowercase: true,
530 uppercase: true,
531 digits: true,
532 special: true,
533 avoid_ambiguous: false,
534 length: 8,
535 };
536 for _ in 0..200 {
537 let pwd = generate_password(&options);
538 assert_eq!(pwd.len(), 8);
539 assert!(pwd.chars().any(|c| c.is_ascii_lowercase()), "{pwd}");
540 assert!(pwd.chars().any(|c| c.is_ascii_uppercase()), "{pwd}");
541 assert!(pwd.chars().any(|c| c.is_ascii_digit()), "{pwd}");
542 assert!(
543 pwd.chars().any(|c| SPECIAL_SYMBOLS.contains(c)),
544 "expected a special symbol in {pwd}",
545 );
546 }
547 }
548
549 #[test]
550 fn test_generate_password_unselected_class_never_appears() {
551 let options = PasswordOptions {
553 lowercase: true,
554 uppercase: false,
555 digits: true,
556 special: false,
557 avoid_ambiguous: false,
558 length: 100,
559 };
560 for _ in 0..50 {
561 let pwd = generate_password(&options);
562 assert!(
563 pwd.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
564 "unexpected char in {pwd}",
565 );
566 }
567 }
568
569 #[test]
570 fn test_generate_password_guarantees_with_avoid_ambiguous() {
571 let options = PasswordOptions {
573 lowercase: true,
574 uppercase: true,
575 digits: true,
576 special: true,
577 avoid_ambiguous: true,
578 length: 8,
579 };
580 for _ in 0..200 {
581 let pwd = generate_password(&options);
582 assert!(pwd.chars().any(|c| c.is_ascii_lowercase()));
583 assert!(pwd.chars().any(|c| c.is_ascii_uppercase()));
584 assert!(pwd.chars().any(|c| c.is_ascii_digit()));
585 assert!(pwd.chars().any(|c| SPECIAL_SYMBOLS.contains(c)));
586 for c in pwd.chars() {
587 assert!(!AMBIGUOUS_CHARS.contains(c), "ambiguous {c} in {pwd}");
588 }
589 }
590 }
591
592 #[test]
593 fn test_generate_password_uniqueness() {
594 let options = PasswordOptions::default();
595 let p1 = generate_password(&options);
596 let p2 = generate_password(&options);
597 assert_ne!(p1, p2);
599 }
600
601 #[test]
602 fn test_generate_clever_password_length() {
603 let password = generate_clever_password("Aaaa0000");
604 assert_eq!(password.len(), 8);
605 }
606
607 #[test]
608 fn test_generate_clever_password_pattern() {
609 let password = generate_clever_password("aaaa");
610 assert!(password.chars().all(|c| c.is_ascii_lowercase()));
611
612 let password = generate_clever_password("AAAA");
613 assert!(password.chars().all(|c| c.is_ascii_uppercase()));
614
615 let password = generate_clever_password("0000");
616 assert!(password.chars().all(|c| c.is_ascii_digit()));
617 }
618
619 #[test]
620 fn test_generate_clever_password_mixed() {
621 let password = generate_clever_password("Aa00");
622 assert_eq!(password.len(), 4);
623 let chars: Vec<char> = password.chars().collect();
624 assert!(chars[0].is_ascii_uppercase());
625 assert!(chars[1].is_ascii_lowercase());
626 assert!(chars[2].is_ascii_digit());
627 assert!(chars[3].is_ascii_digit());
628 }
629
630 #[test]
631 fn test_generate_clever_password_special() {
632 let password = generate_clever_password("!@#$");
633 assert_eq!(password.len(), 4);
634 assert!(password.chars().all(|c| SPECIAL_SYMBOLS.contains(c)));
635 }
636
637 #[test]
638 fn test_generate_clever_password_unknown_chars() {
639 let password = generate_clever_password(" "); assert_eq!(password.len(), 4);
642 }
643
644 #[test]
645 fn test_generate_clever_password_empty() {
646 let password = generate_clever_password("");
647 assert!(password.is_empty());
648 }
649
650 #[test]
651 fn test_generate_clever_password_uniqueness() {
652 let p1 = generate_clever_password("Aaaa0000@@");
653 let p2 = generate_clever_password("Aaaa0000@@");
654 assert_ne!(p1, p2);
656 }
657
658 fn opts(num_words: usize) -> MemorableOptions {
661 MemorableOptions {
662 num_words,
663 ..Default::default()
664 }
665 }
666
667 fn wordlist_set() -> HashSet<&'static str> {
668 WORDS.iter().copied().collect()
669 }
670
671 fn strip_digits_and_lower(seg: &str) -> String {
674 seg.chars()
675 .filter(|c| !c.is_ascii_digit())
676 .collect::<String>()
677 .to_ascii_lowercase()
678 }
679
680 #[test]
681 fn memorable_zero_words_returns_empty() {
682 let p = generate_memorable_password(&opts(0));
683 assert_eq!(p, "");
684 }
685
686 #[test]
687 fn memorable_one_word_no_separator() {
688 let p = generate_memorable_password(&opts(1));
689 assert!(!p.contains('-'), "single-word output should have no separator: {p}");
690 assert!(p.chars().last().unwrap().is_ascii_digit());
692 assert!(p.chars().next().unwrap().is_ascii_uppercase());
693 }
694
695 #[test]
696 fn memorable_default_four_words_three_dashes() {
697 let p = generate_memorable_password(&opts(4));
698 let dashes = p.chars().filter(|&c| c == '-').count();
699 assert_eq!(dashes, 3, "4 words → 3 dashes; got: {p}");
700 for seg in p.split('-') {
701 assert!(seg.chars().next().unwrap().is_ascii_uppercase());
702 assert!(seg.chars().last().unwrap().is_ascii_digit());
703 }
704 }
705
706 #[test]
707 fn memorable_custom_separator() {
708 let mut o = opts(3);
709 o.separator = "_".to_string();
710 let p = generate_memorable_password(&o);
711 assert_eq!(p.matches('_').count(), 2, "3 words → 2 underscores: {p}");
712 assert!(!p.contains('-'));
713 }
714
715 #[test]
716 fn memorable_multi_char_separator() {
717 let mut o = opts(3);
718 o.separator = "--".to_string();
719 let p = generate_memorable_password(&o);
720 assert_eq!(p.matches("--").count(), 2, "3 words joined by '--': {p}");
722 }
723
724 #[test]
725 fn memorable_zero_digits_per_word() {
726 let mut o = opts(3);
727 o.digits_per_word = 0;
728 let p = generate_memorable_password(&o);
729 for seg in p.split('-') {
730 assert!(
731 !seg.chars().any(|c| c.is_ascii_digit()),
732 "segment {seg:?} should have no digits"
733 );
734 }
735 }
736
737 #[test]
738 fn memorable_three_digits_per_word() {
739 let mut o = opts(3);
740 o.digits_per_word = 3;
741 let p = generate_memorable_password(&o);
742 for seg in p.split('-') {
743 let trailing_digits =
744 seg.chars().rev().take_while(|c| c.is_ascii_digit()).count();
745 assert_eq!(trailing_digits, 3, "segment {seg:?} should end with 3 digits");
746 }
747 }
748
749 #[test]
750 fn memorable_caps_last() {
751 let mut o = opts(3);
752 o.caps = MemorableCaps::Last;
753 o.digits_per_word = 1;
754 let p = generate_memorable_password(&o);
755 for seg in p.split('-') {
756 let chars: Vec<char> = seg.chars().collect();
758 assert!(chars[0].is_ascii_lowercase(), "first char should be lowercase: {seg:?}");
759 assert!(chars.last().unwrap().is_ascii_digit());
761 let upper = chars[chars.len() - 2];
762 assert!(upper.is_ascii_uppercase(), "letter before digit should be uppercase: {seg:?}");
763 }
764 }
765
766 #[test]
767 fn memorable_with_prefix_uses_separator() {
768 let mut o = opts(3);
769 o.prefix = "@home".to_string();
770 let p = generate_memorable_password(&o);
771 assert!(p.starts_with("@home-"), "prefix must be joined by separator: {p}");
772 assert_eq!(p.matches('-').count(), 3, "{p}");
774 }
775
776 #[test]
777 fn memorable_words_come_from_wordlist() {
778 let words = wordlist_set();
779 let p = generate_memorable_password(&opts(4));
780 for seg in p.split('-') {
781 let bare = strip_digits_and_lower(seg);
782 assert!(
783 words.contains(bare.as_str()),
784 "segment {seg:?} stripped to {bare:?} not in wordlist"
785 );
786 }
787 }
788
789 #[test]
790 fn memorable_two_calls_produce_different_outputs() {
791 let p1 = generate_memorable_password(&opts(4));
792 let p2 = generate_memorable_password(&opts(4));
793 assert_ne!(p1, p2, "two consecutive calls must differ");
794 }
795
796 #[test]
797 fn memorable_wordlist_length_is_1024() {
798 assert_eq!(WORDS.len(), 1024);
802 }
803}