Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcc9100d1b | |||
| 3734306830 | |||
| 383a1a2b8b | |||
| d333f987fe | |||
| b9ad568c63 | |||
| 5b81562650 | |||
| 2e2bb6b89e | |||
| 0cb0b54dc4 | |||
| 10f770e662 | |||
| f8c97f71f8 |
Generated
+14
@@ -8,6 +8,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -23,6 +29,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diceware_wordlists"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f9b52b69d268c7a2bc582e3aec5cdfa43ac91cef4fe6b6751b02da2b43d6166"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -82,6 +94,8 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
name = "password-generator"
|
name = "password-generator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"diceware_wordlists",
|
||||||
"rand",
|
"rand",
|
||||||
"webster",
|
"webster",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
webster = "0.3.0"
|
webster = "0.3.0"
|
||||||
|
bitflags = "2"
|
||||||
|
diceware_wordlists = "1.2.3"
|
||||||
|
|||||||
+300
-39
@@ -1,74 +1,335 @@
|
|||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::RngCore;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use std::fs;
|
use rand::SeedableRng;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
|
||||||
// alphanumeric only
|
// use std::fs;
|
||||||
const CHARSET: &[u8] =
|
use rand::prelude::SliceRandom;
|
||||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
use bitflags::bitflags;
|
||||||
|
|
||||||
// excludes backtick(`), quote('), dquote("), slash(/), bslash(\)
|
use diceware_wordlists::Wordlist;
|
||||||
const CHARSET_WITH_SYMBOLS: &[u8] =
|
|
||||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()[]{}|?<>,.-_=+~";
|
|
||||||
|
|
||||||
|
const UPPERCASE_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const LOWERCASE_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
|
||||||
|
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"!@#$%^&*()?,.-_=+~"; // 18 chars
|
||||||
|
|
||||||
fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 {
|
// saves some boilerplate manual bitflags and less clunky to use
|
||||||
|
bitflags! {
|
||||||
|
struct CharSet: u8 {
|
||||||
|
const UPPERCASE = 0b0001;
|
||||||
|
const LOWERCASE = 0b0010;
|
||||||
|
const NUMBERS = 0b0100;
|
||||||
|
const SYMBOLS = 0b1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PasswordConfig {
|
||||||
|
len: usize,
|
||||||
|
charset: CharSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
// aka password policy
|
||||||
|
struct PassphraseConfig {
|
||||||
|
phrases: usize,
|
||||||
|
separator: char,
|
||||||
|
capitalize: bool,
|
||||||
|
number: bool,
|
||||||
|
symbol: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
found_upper = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found_upper {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if policy.charset.contains(CharSet::LOWERCASE) {
|
||||||
|
let mut found_lower = false;
|
||||||
|
for c in password.chars() {
|
||||||
|
if LOWERCASE_CHARS.contains(&(c as u8)) {
|
||||||
|
found_lower = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found_lower {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if policy.charset.contains(CharSet::NUMBERS) {
|
||||||
|
let mut found_number = false;
|
||||||
|
for c in password.chars() {
|
||||||
|
if NUMBER_CHARS.contains(&(c as u8)) {
|
||||||
|
found_number = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found_number {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if policy.charset.contains(CharSet::SYMBOLS) {
|
||||||
|
let mut found_symbol = false;
|
||||||
|
for c in password.chars() {
|
||||||
|
if SYMBOL_CHARS.contains(&(c as u8)) {
|
||||||
|
found_symbol = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found_symbol {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) // all passwords need lower chars
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PasswordConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
let charset = CharSet::UPPERCASE |
|
||||||
|
CharSet::LOWERCASE |
|
||||||
|
CharSet::NUMBERS;
|
||||||
|
Self {
|
||||||
|
len: 16,
|
||||||
|
charset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_ascii(rng: &mut MyRng, char_set: &[u8]) -> u8 {
|
||||||
// NOT IDEAL FOR CRYPTO/password generation
|
// NOT IDEAL FOR CRYPTO/password generation
|
||||||
// let range_max = 122; // 125 - 33
|
// let range_max = 122; // 125 - 33
|
||||||
// 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
|
||||||
let index = rng.gen_range(0..char_set.len());
|
*char_set.choose(rng.as_rng()).expect("char_set should not be empty")
|
||||||
char_set[index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_random_chars(len: usize, symbols: bool, rng: &mut OsRng) -> String {
|
// returns error if generated password doesn't contain the necessary chars
|
||||||
|
// to satisfy the policy
|
||||||
|
// let the caller decide what to do
|
||||||
|
fn generate_password(config: &PasswordConfig, rng: &mut MyRng) -> Result<String,String> {
|
||||||
|
if config.len < 8 {
|
||||||
|
return Err(String::from("Password length not long enough"));
|
||||||
|
}
|
||||||
// String is a thin wrapper around Vec<u8> so we can expect re-allocation
|
// String is a thin wrapper around Vec<u8> so we can expect re-allocation
|
||||||
// and copy only when we exceed the capacity of the vector
|
// and copy only when we exceed the capacity of the vector
|
||||||
// However, with_capacity will perform only one allocation anyway
|
// However, with_capacity will perform only one allocation anyway
|
||||||
let mut res = String::with_capacity(len);
|
let mut res = String::with_capacity(config.len);
|
||||||
let char_set = if symbols { CHARSET_WITH_SYMBOLS } else { CHARSET };
|
let mut chars : Vec<u8> = Vec::new();
|
||||||
for _ in 0..len {
|
if config.charset.contains(CharSet::UPPERCASE) {
|
||||||
res.push(random_ascii(rng, char_set) as char);
|
chars.extend_from_slice(UPPERCASE_CHARS); // 26 chars
|
||||||
}
|
}
|
||||||
res
|
if config.charset.contains(CharSet::LOWERCASE) {
|
||||||
|
chars.extend_from_slice(LOWERCASE_CHARS); // 26 chars
|
||||||
|
}
|
||||||
|
if config.charset.contains(CharSet::NUMBERS) {
|
||||||
|
chars.extend_from_slice(NUMBER_CHARS); // 10 chars
|
||||||
|
}
|
||||||
|
if config.charset.contains(CharSet::SYMBOLS) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if !validate_password(&res, config) {
|
||||||
|
return Err("Password does not satisfy supplied config/policy".to_string());
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: haven't updated bits of entropy comments since adding capitalization,
|
||||||
|
// symbols, numbers and changing to diceware wordlist
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
//
|
||||||
// we can take a slice of "words" because we don't care about:
|
// we can take a slice of "words" because we don't care about:
|
||||||
// capacity, mutation (push/pop), allocation strategy
|
// capacity, mutation (push/pop), allocation strategy
|
||||||
fn generate_passphrase(phrase_count: usize,
|
fn generate_passphrase(config: &PassphraseConfig,
|
||||||
word_list: &[&str], rng: &mut OsRng) -> String {
|
word_list: &[&str], rng: &mut MyRng) -> Result<String,String> {
|
||||||
let mut res = String::with_capacity(phrase_count*2); // save a couple allocations
|
if config.phrases < 2 {
|
||||||
for _ in 0..phrase_count {
|
return Err("Phrase count too low".to_string());
|
||||||
res.push_str(random_word(word_list, rng));
|
|
||||||
}
|
}
|
||||||
res
|
let mut res = String::new(); // don't bother saving allocations with a bad guess
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_word<'a>(word_list: &'a [&'a str], rng: &mut OsRng) -> &'a str {
|
fn random_word<'a>(word_list: &[&'a str], rng: &mut MyRng) -> &'a str {
|
||||||
word_list[rng.gen_range(0..word_list.len())]
|
word_list[rng.as_rng().gen_range(0..word_list.len())]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_word_list() -> &'static [&'static str] {
|
||||||
fn main() {
|
// platform dependent word list
|
||||||
let content = fs::read_to_string("/usr/share/dict/words")
|
// let content = fs::read_to_string("/usr/share/dict/words")
|
||||||
.expect("could not read dictionary");
|
// .expect("could not read dictionary");
|
||||||
|
|
||||||
// filter words here for only ascii
|
// filter words here for only ascii
|
||||||
let word_list: Vec<&str> = content.lines().filter(|w|
|
// word_list length is 104078
|
||||||
w.is_ascii()).collect();
|
// let word_list: Vec<&str> = content.lines().filter(|w|
|
||||||
|
// w.is_ascii()).collect();
|
||||||
|
|
||||||
|
// platform independent word list
|
||||||
|
// this list doesn't seem to contain non-ascii or even punctuation
|
||||||
|
Wordlist::get_list(&Wordlist::EffLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Determinism {
|
||||||
|
Predictable,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Random,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MyRng { Std(StdRng), Os(OsRng) }
|
||||||
|
|
||||||
|
impl MyRng {
|
||||||
|
// todo: understand RngCore
|
||||||
|
fn as_rng(&mut self) -> &mut dyn RngCore {
|
||||||
|
match self {
|
||||||
|
MyRng::Std(r) => r,
|
||||||
|
MyRng::Os(r) => r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rng(d: &Determinism) -> MyRng {
|
||||||
|
match d {
|
||||||
|
// when we are looking for Reproducability we can use the same seed value
|
||||||
|
// to create our Rng. Because both StdRng and OsRng implement Rng
|
||||||
|
// we make our methods slightly more generic and this works just fine
|
||||||
|
// comment out when we need random version!
|
||||||
|
Determinism::Predictable => MyRng::Std(StdRng::seed_from_u64(42)),
|
||||||
// we definitely want to use the OS for randomness
|
// we definitely want to use the OS for randomness
|
||||||
// doesn't require seeding because it is backed by CSPRNG, backed by OS
|
// doesn't require seeding because it is backed by CSPRNG, backed by OS
|
||||||
// the OS maintains it's own entropy from mouse movement, hardware noise,etc
|
// the OS maintains it's own entropy from mouse movement, hardware noise,etc
|
||||||
// we pass it through the functions to avoid a potential syscall
|
// we pass it through the functions to avoid a potential syscall
|
||||||
let mut rng = OsRng;
|
Determinism::Random => MyRng::Os(OsRng),
|
||||||
let chars = generate_random_chars(20, true, &mut rng);
|
}
|
||||||
println!("generated random chars: {}", chars.as_str());
|
}
|
||||||
|
|
||||||
let phrase = generate_passphrase(3, &word_list, &mut rng);
|
fn main() {
|
||||||
println!("generated passphrase: {}", phrase.as_str());
|
let word_list = get_word_list();
|
||||||
|
|
||||||
println!("words in dictionary: {}", word_list.len());
|
let mut rng = get_rng(&Determinism::Predictable);
|
||||||
|
|
||||||
|
// todo: add actual tests
|
||||||
|
// password with alphnumeric
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// password with alphanumeric + symbols
|
||||||
|
let config = PasswordConfig {
|
||||||
|
charset: CharSet::SYMBOLS,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
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_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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user