use arrayvec::ArrayString; use rand::{seq::SliceRandom, thread_rng, CryptoRng, Rng}; use std::array; const CHARS: &[u8] = br##"!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"##; bitflags::bitflags! { struct PasswordFlags: u8 { const LOWERCASE = 0b0001; const UPPERCASE = 0b0010; const NUMBER = 0b0100; const SPECIAL_CHARACTER = 0b1000; } #[derive(PartialEq, Eq, Clone, Copy)] pub struct PasswordValidity: u8 { const NO_LOWERCASE = 0b00001; const NO_UPPERCASE = 0b00010; const NO_NUMBER = 0b00100; const NO_SPECIAL_CHARACTER = 0b01000; const TOO_SHORT = 0b10000; } } /// Returns true if the generated master password is valid. /// It checks that it has at least one lowercase, one uppercase, one number and one punctuation char #[must_use] fn check_generated_password(password: &[u8; LENGTH]) -> bool { let mut flags = PasswordFlags::empty(); for &byte in password { match byte { b'a'..=b'z' => flags |= PasswordFlags::LOWERCASE, b'A'..=b'Z' => flags |= PasswordFlags::UPPERCASE, b'0'..=b'9' => flags |= PasswordFlags::NUMBER, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~' => { flags |= PasswordFlags::SPECIAL_CHARACTER; } _ => (), } if flags.is_all() { return true; } } false } /// Continuously generates the password until it passes the checks #[must_use] fn generate_password(rng: &mut R) -> ArrayString where R: Rng + CryptoRng, { loop { let password = array::from_fn(|_| *CHARS.choose(rng).unwrap()); if check_generated_password(&password) { return ArrayString::from_byte_string(&password).unwrap(); } } } #[must_use] #[allow(clippy::module_name_repetitions)] pub fn generate_passwords( ) -> [ArrayString; AMOUNT] { let mut rng = thread_rng(); array::from_fn(|_| generate_password(&mut rng)) } #[must_use] pub fn check_master_pass(password: &str) -> PasswordValidity { let mut count = 0; let mut chars = password.chars(); let mut flags = PasswordValidity::all(); for char in &mut chars { count += 1; if char.is_lowercase() { flags.remove(PasswordValidity::NO_LOWERCASE); } else if char.is_uppercase() { flags.remove(PasswordValidity::NO_UPPERCASE); } else if char.is_ascii_digit() { flags.remove(PasswordValidity::NO_NUMBER); } else if char.is_ascii_punctuation() { flags.remove(PasswordValidity::NO_SPECIAL_CHARACTER); } if flags == PasswordValidity::TOO_SHORT { count += chars.count(); break; } } if count >= 8 { flags.remove(PasswordValidity::TOO_SHORT); } flags } #[cfg(test)] mod tests { use super::CHARS; #[test] fn chars_must_be_ascii() { assert!(CHARS.is_ascii()); } }