#![feature(iter_intersperse)]
use std::sync::Arc;
use clap::ValueEnum;
use lazy_static::lazy_static;
use rand::distributions::{Uniform, WeightedIndex};
use rand::prelude::*;
lazy_static! {
static ref WORDS_LIST: Arc<Vec<&'static str>> = {
let words = include_str!("../wordlist.txt")
.lines()
.filter(|l| l.len() >= 4)
.collect::<Vec<&str>>();
Arc::new(words)
};
}
pub fn memorable_password<R: Rng>(
rng: &mut R,
word_count: usize,
separator: Separator,
capitalize: bool,
scramble: bool,
) -> String {
let formatted_words: Vec<String> = get_random_words(rng, word_count)
.iter()
.map(|word| {
let mut word = word.to_string();
if scramble {
let mut bytes = word.to_string().into_bytes();
bytes.shuffle(rng);
word =
String::from_utf8(bytes.to_vec()).expect("random words should be valid UTF-8");
}
if capitalize {
if let Some(first_letter) = word.get_mut(0..1) {
first_letter.make_ascii_uppercase();
}
}
word
})
.collect();
match separator {
Separator::Space => formatted_words.join(" "),
Separator::Comma => formatted_words.join(","),
Separator::Hyphen => formatted_words.join("-"),
Separator::Period => formatted_words.join("."),
Separator::Underscore => formatted_words.join("_"),
Separator::Numbers => formatted_words
.iter()
.map(|s| s.to_string())
.intersperse_with(|| rng.gen_range(0..10).to_string())
.collect(),
Separator::NumbersAndSymbols => {
let numbers_and_symbols: Vec<char> = SYMBOL_CHARS
.iter()
.chain(NUMBER_CHARS.iter())
.cloned()
.collect();
formatted_words
.iter()
.map(|s| s.to_string())
.intersperse_with(|| {
numbers_and_symbols
.choose(rng)
.expect("numbers and symbols should have a length >= 1")
.to_string()
})
.collect()
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum Separator {
Space,
Comma,
Hyphen,
Period,
Underscore,
Numbers,
NumbersAndSymbols,
}
pub fn random_password<R: Rng>(
rng: &mut R,
characters: u32,
numbers: bool,
symbols: bool,
) -> String {
let mut available_sets = vec![LETTER_CHARS];
if numbers {
available_sets.push(NUMBER_CHARS);
}
if symbols {
available_sets.push(SYMBOL_CHARS);
}
let weights: Vec<u32> = match (numbers, symbols) {
(true, true) => vec![7, 2, 1],
(true, false) => vec![8, 2],
(false, true) => vec![8, 2],
(false, false) => vec![10],
};
let dist_set = WeightedIndex::new(&weights).expect("weights should be valid");
let mut password = String::with_capacity(characters as usize);
for _ in 0..characters {
let selected_set = available_sets
.get(dist_set.sample(rng))
.expect("index should be valid");
let dist_char = Uniform::from(0..selected_set.len());
let index = dist_char.sample(rng);
password.push(selected_set[index]);
}
password
}
pub fn pin_password<R: Rng>(rng: &mut R, numbers: u32) -> String {
(0..numbers)
.map(|_| NUMBER_CHARS[rng.gen_range(0..NUMBER_CHARS.len())])
.collect()
}
const LETTER_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
const NUMBER_CHARS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const SYMBOL_CHARS: &[char] = &['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'];
fn get_random_words<R: Rng>(rng: &mut R, n: usize) -> Vec<&'static str> {
WORDS_LIST.choose_multiple(rng, n).cloned().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memorable_password() {
let seed = 42; let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let password = memorable_password(&mut rng, 4, Separator::Space, false, false);
assert_eq!(password, "choking natural dolly ominous");
let password = memorable_password(&mut rng, 4, Separator::Comma, false, false);
assert_eq!(password, "thrive,punctured,wool,hardcover");
let password = memorable_password(&mut rng, 4, Separator::Hyphen, true, false);
assert_eq!(password, "Violate-Applause-Preorder-Headstone");
let password = memorable_password(&mut rng, 4, Separator::Numbers, true, true);
assert_eq!(password, "Nioutfna2Cerslua5Aborrcw4Wtpse");
}
#[test]
fn test_random_password_length() {
let mut rng = StdRng::seed_from_u64(0);
let length = 12;
let password = random_password(&mut rng, length, true, true);
assert_eq!(password.len(), length as usize);
}
#[test]
fn test_random_password_content() {
let mut rng = StdRng::seed_from_u64(0);
let length = 12;
let password_letters = random_password(&mut rng, length, false, false);
assert!(password_letters.chars().all(|c| LETTER_CHARS.contains(&c)));
let password_numbers = random_password(&mut rng, length, true, false);
assert!(password_numbers.chars().any(|c| NUMBER_CHARS.contains(&c)));
let password_symbols = random_password(&mut rng, length, false, true);
assert!(password_symbols.chars().any(|c| SYMBOL_CHARS.contains(&c)));
let password_numbers_symbols = random_password(&mut rng, length, true, true);
assert!(password_numbers_symbols
.chars()
.any(|c| NUMBER_CHARS.contains(&c) || SYMBOL_CHARS.contains(&c)));
}
#[test]
fn test_random_password_different_seeds() {
let mut rng1 = StdRng::seed_from_u64(0);
let mut rng2 = StdRng::seed_from_u64(1);
let length = 12;
let password1 = random_password(&mut rng1, length, true, true);
let password2 = random_password(&mut rng2, length, true, true);
assert_ne!(password1, password2);
}
#[test]
fn test_pin_password_length() {
let mut rng = StdRng::seed_from_u64(0);
let pin_length = 6;
let pin = pin_password(&mut rng, pin_length);
assert_eq!(pin.len(), pin_length as usize);
}
#[test]
fn test_pin_password_content() {
let mut rng = StdRng::seed_from_u64(0);
let pin_length = 6;
let pin = pin_password(&mut rng, pin_length);
assert!(pin.chars().all(|c| NUMBER_CHARS.contains(&c)));
}
#[test]
fn test_pin_password_different_seeds() {
let mut rng1 = StdRng::seed_from_u64(0);
let mut rng2 = StdRng::seed_from_u64(1);
let pin_length = 6;
let pin1 = pin_password(&mut rng1, pin_length);
let pin2 = pin_password(&mut rng2, pin_length);
assert_ne!(pin1, pin2);
}
#[test]
fn test_get_random_words() {
let seed = 42; let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let words = get_random_words(&mut rng, 5);
assert_eq!(
words,
vec!["chokehold", "nativity", "dolly", "ominous", "throat"]
);
}
}