109 lines
3.1 KiB
Rust
109 lines
3.1 KiB
Rust
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<const LENGTH: usize>(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<R, const LENGTH: usize>(rng: &mut R) -> ArrayString<LENGTH>
|
|
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<const AMOUNT: usize, const LENGTH: usize>(
|
|
) -> [ArrayString<LENGTH>; 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());
|
|
}
|
|
}
|