added passphrase capitalization, number, and symbols. also added passphrase validator
This commit is contained in:
+103
-22
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user