From b9ad568c63553a13dd02da9e6195f18d749d906e Mon Sep 17 00:00:00 2001 From: Ed Guloien Date: Thu, 23 Apr 2026 15:39:27 -0400 Subject: [PATCH] added passphrase capitalization, number, and symbols. also added passphrase validator --- src/main.rs | 125 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index df06cad..8f0914b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,12 +27,30 @@ struct PasswordConfig { charset: CharSet, } -#[derive(Default)] -struct PasswordPolicy(PasswordConfig); +// aka password policy +struct PassphraseConfig { + phrases: usize, + separator: char, + capitalize: bool, + number: bool, + symbol: bool +} -// todo: support multiple password policies -fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { - if policy.0.charset.contains(CharSet::UPPERCASE) { +impl PassphraseConfig { + fn most_secure_passphrase_config() -> Self { + Self { + phrases: 4, + separator: '-', + capitalize: true, + number: true, + symbol: true + } + } +} + +fn validate_password(password: &str, policy: &PasswordConfig) -> bool { + // todo: perf improvement, iterate through string only once + if policy.charset.contains(CharSet::UPPERCASE) { let mut found_upper = false; for c in password.chars() { if UPPERCASE_CHARS.contains(&(c as u8)) { @@ -44,7 +62,7 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { return false; } } - if policy.0.charset.contains(CharSet::LOWERCASE) { + if policy.charset.contains(CharSet::LOWERCASE) { let mut found_lower = false; for c in password.chars() { if LOWERCASE_CHARS.contains(&(c as u8)) { @@ -56,7 +74,7 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { return false; } } - if policy.0.charset.contains(CharSet::NUMBERS) { + if policy.charset.contains(CharSet::NUMBERS) { let mut found_number = false; for c in password.chars() { if NUMBER_CHARS.contains(&(c as u8)) { @@ -68,7 +86,7 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { return false; } } - if policy.0.charset.contains(CharSet::SYMBOLS) { + if policy.charset.contains(CharSet::SYMBOLS) { let mut found_symbol = false; for c in password.chars() { if SYMBOL_CHARS.contains(&(c as u8)) { @@ -80,10 +98,38 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { return false; } } - // todo: do similar checks here true } +fn validate_passphrase(password: &str, policy: &PassphraseConfig) -> bool { + let mut symbol = false; + let mut number = false; + let mut lower = false; + let mut upper = false; + for c in password.chars() { + if UPPERCASE_CHARS.contains(&(c as u8)) { + upper = true; + continue; + } + if LOWERCASE_CHARS.contains(&(c as u8)) { + lower = true; + continue; + } + if SYMBOL_CHARS.contains(&(c as u8)) { + symbol = true; + continue; + } + if NUMBER_CHARS.contains(&(c as u8)) { + number = true; + continue; + } + } + (policy.capitalize || upper) && + (policy.symbol || symbol) && + (policy.number || number) && + (lower) +} + impl Default for PasswordConfig { fn default() -> Self { let charset = CharSet::UPPERCASE | @@ -102,6 +148,7 @@ fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 { // let n = rng.next_u32() % range_max; // slightly modulo biased // (n + 33) as u8 + // todo: validate this fact // this function under the hood creates a zone that divides evenly // into the range so we don't encounter modulo bias *char_set.choose(rng).expect("char_set should not be empty") @@ -114,7 +161,7 @@ fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 { // 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) -> Result { +fn generate_password(config: &PasswordConfig, rng: &mut OsRng) -> Result { if config.len < 8 { return Err(String::from("Password length not long enough")); } @@ -144,6 +191,9 @@ fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> Result Result 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) -> Result { - if phrase_count < 2 { +fn generate_passphrase(config: &PassphraseConfig, + word_list: &[&str], rng: &mut OsRng) -> Result { + if config.phrases < 2 { return Err("Phrase count too low".to_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); + for i in 0..config.phrases { + let word = random_word(word_list, rng); + if config.capitalize { + let mut word_chars = word.chars(); + let capitalized_word : String = word_chars.next().unwrap() + .to_uppercase().chain(word_chars).collect(); + res.push_str(&capitalized_word); + } else { + res.push_str(word); } + if i < config.phrases - 1 { + res.push(config.separator); + } + } + if config.symbol { + res.push(random_ascii(rng, SYMBOL_CHARS) as char) + } + if config.number { + res.push(random_ascii(rng, NUMBER_CHARS) as char) } Ok(res) } @@ -196,12 +260,14 @@ fn main() { // todo: add actual tests // password with alphnumeric - let password_res = generate_random_chars(Default::default(), &mut rng); + let password_res = generate_password(&Default::default(), &mut rng); if let Err(e) = password_res { println!("Error during first password gen: {}", e); } else { let password = password_res.unwrap(); println!("generated random chars (alphanumeric): {}", password.as_str()); + // this will fail intermittently since we haven't added any guarantees + // that specified charsets are generated. assert!(validate_password(&password, &Default::default())); } @@ -210,9 +276,24 @@ fn main() { charset: CharSet::SYMBOLS, ..Default::default() }; - let password_res2 = generate_random_chars(config, &mut rng); - println!("generated random chars (with symbols): {}", password_res2.unwrap().as_str()); + let password_res2 = generate_password(&config, &mut rng); + // this will fail intermittently since we haven't added any guarantees + // that specified charsets are generated. + // Also note: we just unwrap here. We should handle like above + assert!(validate_password(password_res2.as_ref().unwrap(), &config)); + println!("generated random chars (with symbols): {}", + password_res2.unwrap().as_str()); - let passphrase_res = generate_passphrase(3, &word_list, '-', &mut rng); - println!("generated passphrase: {}", passphrase_res.unwrap().as_str()); + let passphrase_config = PassphraseConfig { + phrases: 3, + separator: '-', + capitalize: true, + number: true, + symbol: true, + }; + let passphrase_res = generate_passphrase(&passphrase_config, &word_list, &mut rng); + // Also note: we just unwrap here. We should handle like above + assert!(validate_passphrase(passphrase_res.as_ref().unwrap(), + &PassphraseConfig::most_secure_passphrase_config())); + println!("generated passphrase: {}", passphrase_res.as_ref().unwrap()); }