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,
}
#[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<String,String> {
fn generate_password(config: &PasswordConfig, rng: &mut OsRng) -> Result<String,String> {
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<Stri
for _ in 0..config.len {
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)
}
@@ -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(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<String,String> {
if phrase_count < 2 {
fn generate_passphrase(config: &PassphraseConfig,
word_list: &[&str], rng: &mut OsRng) -> Result<String,String> {
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());
}