From f8c97f71f87e62aeafdc34f555674a91131f31f5 Mon Sep 17 00:00:00 2001 From: Ed Guloien Date: Fri, 17 Apr 2026 00:41:29 -0400 Subject: [PATCH] improve API with PasswordConfig struct, added validate_password and made character generation more fine-grained --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/main.rs | 138 +++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 124 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9859b7..a22401f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "cfg-if" version = "1.0.4" @@ -82,6 +88,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" name = "password-generator" version = "0.1.0" dependencies = [ + "bitflags", "rand", "webster", ] diff --git a/Cargo.toml b/Cargo.toml index 296bd7a..3a23e6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] rand = "0.8" webster = "0.3.0" +bitflags = "2" diff --git a/src/main.rs b/src/main.rs index 4277801..39251d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,77 @@ use rand::Rng; use rand::rngs::OsRng; use std::fs; +use rand::prelude::SliceRandom; +use bitflags::bitflags; -// alphanumeric only -const CHARSET: &[u8] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +const UPPERCASE_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const LOWERCASE_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; +const NUMBER_CHARS: &[u8] = b"0123456789"; +// still exclude symbols that can trip up URLs and CLIs +// excludes: backtick(`), quote('), dquote("), slash(/), bslash(\) +// pipe (|), arrows (<), (>), brackets ([), (]), ({), (}) +const SYMBOL_CHARS: &[u8] = b"!@#$%^&*()?,.-_=+~"; -// excludes backtick(`), quote('), dquote("), slash(/), bslash(\) -const CHARSET_WITH_SYMBOLS: &[u8] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()[]{}|?<>,.-_=+~"; +// saves some boilerplate manual bitflags and less clunky to use +bitflags! { + struct CharSet: u8 { + const UPPERCASE = 0b0001; + const LOWERCASE = 0b0010; + const NUMBERS = 0b0100; + const SYMBOLS = 0b1000; + } +} + +struct PasswordConfig { + len: usize, + charset: CharSet, +} + +struct PasswordPolicy(PasswordConfig); + +// todo: support multiple password policies +// todo: add support for Errors +fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { + if policy.0.charset.contains(CharSet::UPPERCASE) { + let mut found_upper = false; + for c in password.chars() { + if UPPERCASE_CHARS.contains(&(c as u8)) { + found_upper = true; + break; + } + } + if !found_upper { + return false; + } + } + // todo: do similar checks here + true +} + +impl Default for PasswordConfig { + fn default() -> Self { + let charset = CharSet::UPPERCASE | + CharSet::LOWERCASE | + CharSet::NUMBERS; + Self { + len: 16, + charset + } + } +} + +impl Default for PasswordPolicy { + fn default() -> Self { + let charset = CharSet::UPPERCASE | + CharSet::LOWERCASE | + CharSet::NUMBERS; + let config = PasswordConfig { + len: 16, + charset + }; + Self { 0: config } + } +} fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 { @@ -19,39 +82,61 @@ fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 { // this function under the hood creates a zone that divides evenly // into the range so we don't encounter modulo bias - let index = rng.gen_range(0..char_set.len()); - char_set[index] + *char_set.choose(rng).expect("char_set should not be empty") } -fn generate_random_chars(len: usize, symbols: bool, rng: &mut OsRng) -> String { +// todo: protect from small len +fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> String { // String is a thin wrapper around Vec so we can expect re-allocation // and copy only when we exceed the capacity of the vector // However, with_capacity will perform only one allocation anyway - let mut res = String::with_capacity(len); - let char_set = if symbols { CHARSET_WITH_SYMBOLS } else { CHARSET }; - for _ in 0..len { - res.push(random_ascii(rng, char_set) as char); + let mut res = String::with_capacity(config.len); + let mut chars : Vec = Vec::new(); + if config.charset.contains(CharSet::UPPERCASE) { + chars.extend_from_slice(UPPERCASE_CHARS); + } + if config.charset.contains(CharSet::LOWERCASE) { + chars.extend_from_slice(LOWERCASE_CHARS); + } + if config.charset.contains(CharSet::NUMBERS) { + chars.extend_from_slice(NUMBER_CHARS); + } + if config.charset.contains(CharSet::SYMBOLS) { + chars.extend_from_slice(SYMBOL_CHARS); + } + + for _ in 0..config.len { + res.push(random_ascii(rng, &chars) as char); } res } +// todo: protect from small phrase_count + // we can take a slice of "words" because we don't care about: // capacity, mutation (push/pop), allocation strategy fn generate_passphrase(phrase_count: usize, - word_list: &[&str], rng: &mut OsRng) -> String { - let mut res = String::with_capacity(phrase_count*2); // save a couple allocations - for _ in 0..phrase_count { + word_list: &[&str], separator: char, rng: &mut OsRng) -> String { + let mut res = String::new(); // don't bother saving allocations with a bad guess + for i in 0..phrase_count { res.push_str(random_word(word_list, rng)); + if i < phrase_count - 1 { + res.push(separator); + } } res } -fn random_word<'a>(word_list: &'a [&'a str], rng: &mut OsRng) -> &'a str { +fn random_word<'a>(word_list: &[&'a str], rng: &mut OsRng) -> &'a str { word_list[rng.gen_range(0..word_list.len())] } - +// Some questions: +// todo: “How many bits of entropy does your generator produce?” +// todo: “What if I need reproducibility?” fn main() { + // todo: not cross-platform + // todo: abstract this a bit let content = fs::read_to_string("/usr/share/dict/words") .expect("could not read dictionary"); @@ -64,11 +149,20 @@ fn main() { // the OS maintains it's own entropy from mouse movement, hardware noise,etc // we pass it through the functions to avoid a potential syscall let mut rng = OsRng; - let chars = generate_random_chars(20, true, &mut rng); - println!("generated random chars: {}", chars.as_str()); - let phrase = generate_passphrase(3, &word_list, &mut rng); + // todo: add actual tests + let chars = generate_random_chars(Default::default(), &mut rng); + println!("generated random chars (alphanumeric): {}", chars.as_str()); + assert!(validate_password(&chars, &Default::default())); + + let config = PasswordConfig { + charset: CharSet::SYMBOLS, + ..Default::default() + }; + let chars = generate_random_chars(config, &mut rng); + println!("generated random chars (with symbols): {}", chars.as_str()); + + let phrase = generate_passphrase(3, &word_list, '-', &mut rng); println!("generated passphrase: {}", phrase.as_str()); - println!("words in dictionary: {}", word_list.len()); }