From 0cb0b54dc4ab3fa2a0623a6b756120fabd10b8b6 Mon Sep 17 00:00:00 2001 From: Ed Guloien Date: Wed, 22 Apr 2026 22:13:40 -0400 Subject: [PATCH] added notes about bits of entropy for each generator --- src/main.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 609a5fa..0f6135c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ 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"!@#$%^&*()?,.-_=+~"; +const SYMBOL_CHARS: &[u8] = b"!@#$%^&*()?,.-_=+~"; // 18 chars // saves some boilerplate manual bitflags and less clunky to use bitflags! { @@ -73,6 +73,12 @@ fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 { } // todo: protect from small len +// +// How many bits of entropy? +// max_chars_available -> 80 +// log2(possible_symbols^len) yields bits of entropy +// so say len is 20 and we use all 80 chars: +// that's log2(80^20) -> 126.4 bits of entropy per password 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 @@ -80,18 +86,23 @@ fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> String { 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); + chars.extend_from_slice(UPPERCASE_CHARS); // 26 chars } if config.charset.contains(CharSet::LOWERCASE) { - chars.extend_from_slice(LOWERCASE_CHARS); + chars.extend_from_slice(LOWERCASE_CHARS); // 26 chars } if config.charset.contains(CharSet::NUMBERS) { - chars.extend_from_slice(NUMBER_CHARS); + chars.extend_from_slice(NUMBER_CHARS); // 10 chars } if config.charset.contains(CharSet::SYMBOLS) { - chars.extend_from_slice(SYMBOL_CHARS); + chars.extend_from_slice(SYMBOL_CHARS); // 18 chars } + // How many bits of entropy? + // max_chars_available -> 80 + // log2(possible_symbols^len) yields bits of entropy + // so say len is 20 and we use all 80 chars + // that's log2(80^20) -> 126.4 bits of entropy per password for _ in 0..config.len { res.push(random_ascii(rng, &chars) as char); } @@ -102,6 +113,11 @@ fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> String { // we can take a slice of "words" because we don't care about: // capacity, mutation (push/pop), allocation strategy +// +// How many bits of entropy? +// log2(104078) -> 16.6 bits per word derived from word_list size +// log2(18) -> 4.1 bits for separator assuming seporator is one of our SYMBOLs +// total 70.5 bits of entropy fn generate_passphrase(phrase_count: usize, word_list: &[&str], separator: char, rng: &mut OsRng) -> String { let mut res = String::new(); // don't bother saving allocations with a bad guess @@ -119,7 +135,6 @@ fn random_word<'a>(word_list: &[&'a str], rng: &mut OsRng) -> &'a str { } // Some questions: -// todo: “How many bits of entropy does your generator produce?” // todo: “What if I need reproducibility?” fn main() { // todo: not cross-platform @@ -128,6 +143,7 @@ fn main() { .expect("could not read dictionary"); // filter words here for only ascii + // word_list length is 104078 let word_list: Vec<&str> = content.lines().filter(|w| w.is_ascii()).collect();