added passphrase capitalization, number, and symbols. also added passphrase validator

This commit is contained in:
2026-04-23 15:39:27 -04:00
parent 5b81562650
commit b9ad568c63
+103 -22
View File
@@ -27,12 +27,30 @@ struct PasswordConfig {
charset: CharSet, charset: CharSet,
} }
#[derive(Default)] // aka password policy
struct PasswordPolicy(PasswordConfig); struct PassphraseConfig {
phrases: usize,
separator: char,
capitalize: bool,
number: bool,
symbol: bool
}
// todo: support multiple password policies impl PassphraseConfig {
fn validate_password(password: &str, policy: &PasswordPolicy) -> bool { fn most_secure_passphrase_config() -> Self {
if policy.0.charset.contains(CharSet::UPPERCASE) { 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; let mut found_upper = false;
for c in password.chars() { for c in password.chars() {
if UPPERCASE_CHARS.contains(&(c as u8)) { if UPPERCASE_CHARS.contains(&(c as u8)) {
@@ -44,7 +62,7 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool {
return false; return false;
} }
} }
if policy.0.charset.contains(CharSet::LOWERCASE) { if policy.charset.contains(CharSet::LOWERCASE) {
let mut found_lower = false; let mut found_lower = false;
for c in password.chars() { for c in password.chars() {
if LOWERCASE_CHARS.contains(&(c as u8)) { if LOWERCASE_CHARS.contains(&(c as u8)) {
@@ -56,7 +74,7 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool {
return false; return false;
} }
} }
if policy.0.charset.contains(CharSet::NUMBERS) { if policy.charset.contains(CharSet::NUMBERS) {
let mut found_number = false; let mut found_number = false;
for c in password.chars() { for c in password.chars() {
if NUMBER_CHARS.contains(&(c as u8)) { if NUMBER_CHARS.contains(&(c as u8)) {
@@ -68,7 +86,7 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool {
return false; return false;
} }
} }
if policy.0.charset.contains(CharSet::SYMBOLS) { if policy.charset.contains(CharSet::SYMBOLS) {
let mut found_symbol = false; let mut found_symbol = false;
for c in password.chars() { for c in password.chars() {
if SYMBOL_CHARS.contains(&(c as u8)) { if SYMBOL_CHARS.contains(&(c as u8)) {
@@ -80,10 +98,38 @@ fn validate_password(password: &str, policy: &PasswordPolicy) -> bool {
return false; return false;
} }
} }
// todo: do similar checks here
true 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 { impl Default for PasswordConfig {
fn default() -> Self { fn default() -> Self {
let charset = CharSet::UPPERCASE | 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 // let n = rng.next_u32() % range_max; // slightly modulo biased
// (n + 33) as u8 // (n + 33) as u8
// todo: validate this fact
// this function under the hood creates a zone that divides evenly // this function under the hood creates a zone that divides evenly
// into the range so we don't encounter modulo bias // into the range so we don't encounter modulo bias
*char_set.choose(rng).expect("char_set should not be empty") *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 // log2(possible_symbols^len) yields bits of entropy
// so say len is 20 and we use all 80 chars: // so say len is 20 and we use all 80 chars:
// that's log2(80^20) -> 126.4 bits of entropy per password // that's log2(80^20) -> 126.4 bits of entropy per password
fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> Result<String,String> { fn generate_password(config: &PasswordConfig, rng: &mut OsRng) -> Result<String,String> {
if config.len < 8 { if config.len < 8 {
return Err(String::from("Password length not long enough")); return Err(String::from("Password length not long enough"));
} }
@@ -144,6 +191,9 @@ fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> Result<Stri
for _ in 0..config.len { for _ in 0..config.len {
res.push(random_ascii(rng, &chars) as char); res.push(random_ascii(rng, &chars) as char);
} }
if !validate_password(&res, config) {
return Err("Password does not satisfy supplied config/policy".to_string());
}
Ok(res) Ok(res)
} }
@@ -156,17 +206,31 @@ fn generate_random_chars(config: PasswordConfig, rng: &mut OsRng) -> Result<Stri
// log2(104078) -> 16.6 bits per word derived from word_list size // 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 // log2(18) -> 4.1 bits for separator assuming seporator is one of our SYMBOLs
// total 70.5 bits of entropy // total 70.5 bits of entropy
fn generate_passphrase(phrase_count: usize, fn generate_passphrase(config: &PassphraseConfig,
word_list: &[&str], separator: char, rng: &mut OsRng) -> Result<String,String> { word_list: &[&str], rng: &mut OsRng) -> Result<String,String> {
if phrase_count < 2 { if config.phrases < 2 {
return Err("Phrase count too low".to_string()); return Err("Phrase count too low".to_string());
} }
let mut res = String::new(); // don't bother saving allocations with a bad guess let mut res = String::new(); // don't bother saving allocations with a bad guess
for i in 0..phrase_count { for i in 0..config.phrases {
res.push_str(random_word(word_list, rng)); let word = random_word(word_list, rng);
if i < phrase_count - 1 { if config.capitalize {
res.push(separator); 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) Ok(res)
} }
@@ -196,12 +260,14 @@ fn main() {
// todo: add actual tests // todo: add actual tests
// password with alphnumeric // 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 { if let Err(e) = password_res {
println!("Error during first password gen: {}", e); println!("Error during first password gen: {}", e);
} else { } else {
let password = password_res.unwrap(); let password = password_res.unwrap();
println!("generated random chars (alphanumeric): {}", password.as_str()); 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())); assert!(validate_password(&password, &Default::default()));
} }
@@ -210,9 +276,24 @@ fn main() {
charset: CharSet::SYMBOLS, charset: CharSet::SYMBOLS,
..Default::default() ..Default::default()
}; };
let password_res2 = generate_random_chars(config, &mut rng); let password_res2 = generate_password(&config, &mut rng);
println!("generated random chars (with symbols): {}", password_res2.unwrap().as_str()); // 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); let passphrase_config = PassphraseConfig {
println!("generated passphrase: {}", passphrase_res.unwrap().as_str()); 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());
} }