improve API with PasswordConfig struct, added validate_password and made character generation more fine-grained

This commit is contained in:
2026-04-17 00:41:29 -04:00
parent 32371f20d4
commit f8c97f71f8
3 changed files with 124 additions and 22 deletions
Generated
+7
View File
@@ -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",
]
+1
View File
@@ -6,3 +6,4 @@ edition = "2024"
[dependencies]
rand = "0.8"
webster = "0.3.0"
bitflags = "2"
+116 -22
View File
@@ -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<u8> 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<u8> = 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());
}