use rand::Rng; use rand::rngs::OsRng; use std::fs; use rand::prelude::SliceRandom; use bitflags::bitflags; 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 // 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) } impl Default for PasswordConfig { fn default() -> Self { let charset = CharSet::UPPERCASE | CharSet::LOWERCASE | CharSet::NUMBERS; Self { len: 16, charset } } } fn random_ascii(rng: &mut OsRng, char_set: &[u8]) -> u8 { // NOT IDEAL FOR CRYPTO/password generation // let range_max = 122; // 125 - 33 // 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") } // todo: does not guarantee that a password will contain the necessary 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 fn generate_password(config: &PasswordConfig, rng: &mut OsRng) -> Result { if config.len < 8 { return Err(String::from("Password length not long enough")); } // String is a thin wrapper around Vec so we can expect re-allocation // and copy only when we exceed the capacity of the vector // However, with_capacity will perform only one allocation anyway let mut res = String::with_capacity(config.len); let mut chars : Vec = Vec::new(); if config.charset.contains(CharSet::UPPERCASE) { chars.extend_from_slice(UPPERCASE_CHARS); // 26 chars } 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: we should add symbols, numbers, and Capitalization // // we can take a slice of "words" because we don't care about: // capacity, mutation (push/pop), allocation strategy // // 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 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..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 str], rng: &mut OsRng) -> &'a str { word_list[rng.gen_range(0..word_list.len())] } // Some questions: // todo: “What if I need reproducibility?” fn main() { // todo: not cross-platform // todo: abstract this a bit let content = fs::read_to_string("/usr/share/dict/words") .expect("could not read dictionary"); // filter words here for only ascii // word_list length is 104078 let word_list: Vec<&str> = content.lines().filter(|w| w.is_ascii()).collect(); // we definitely want to use the OS for randomness // 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 // we pass it through the functions to avoid a potential syscall let mut rng = OsRng; // 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()); }