From 6a5b3c26860afe7b658bace0457af9fe70ee902d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Fri, 29 Sep 2023 17:05:24 +0200 Subject: [PATCH] [PM-3434] Password generator --- .../bitwarden/src/tool/generators/password.rs | 90 +++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/password.rs b/crates/bitwarden/src/tool/generators/password.rs index 0a3874082..bbdcac393 100644 --- a/crates/bitwarden/src/tool/generators/password.rs +++ b/crates/bitwarden/src/tool/generators/password.rs @@ -1,4 +1,5 @@ use crate::error::Result; +use rand::seq::SliceRandom; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -21,10 +22,10 @@ pub struct PasswordGeneratorRequest { pub length: Option, pub avoid_ambiguous: Option, // TODO: Should we rename this to include_all_characters? - pub min_lowercase: Option, - pub min_uppercase: Option, - pub min_number: Option, - pub min_special: Option, + pub min_lowercase: Option, + pub min_uppercase: Option, + pub min_number: Option, + pub min_special: Option, } /// Passphrase generator request. @@ -40,8 +41,85 @@ pub struct PassphraseGeneratorRequest { pub include_number: Option, } -pub(super) fn password(_input: PasswordGeneratorRequest) -> Result { - Ok("pa11w0rd".to_string()) +const DEFAULT_PASSWORD_LENGTH: u8 = 16; + +const UPPER_CHARS_AMBIGUOUS: [char; 2] = ['I', 'O']; +const UPPER_CHARS: [char; 24] = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z', +]; +const LOWER_CHARS_AMBIGUOUS: [char; 1] = ['l']; +const LOWER_CHARS: [char; 25] = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', +]; +const NUMBER_CHARS_AMBIGUOUS: [char; 2] = ['0', '1']; +const NUMBER_CHARS: [char; 8] = ['2', '3', '4', '5', '6', '7', '8', '9']; +const SPECIAL_CHARS: [char; 8] = ['!', '@', '#', '$', '%', '^', '&', '*']; + +pub(super) fn password(input: PasswordGeneratorRequest) -> Result { + // Generate all character dictionaries + fn gen_chars(chars: &[char], ambiguous: &[char], avoid_ambiguous: bool) -> Vec { + let mut chars = chars.to_vec(); + if !avoid_ambiguous { + chars.extend_from_slice(ambiguous); + } + chars + } + let avoid_ambiguous = input.avoid_ambiguous.unwrap_or(false); + let lower_chars = gen_chars(&LOWER_CHARS, &LOWER_CHARS_AMBIGUOUS, avoid_ambiguous); + let upper_chars = gen_chars(&UPPER_CHARS, &UPPER_CHARS_AMBIGUOUS, avoid_ambiguous); + let number_chars = gen_chars(&NUMBER_CHARS, &NUMBER_CHARS_AMBIGUOUS, avoid_ambiguous); + let all_chars = lower_chars + .iter() + .chain(&upper_chars) + .chain(&number_chars) + .chain(&SPECIAL_CHARS) + .collect::>(); + + // We always have to have at least one character type + let lowercase = input.lowercase || (!input.uppercase && !input.numbers && !input.special); + + // Sanitize the minimum values + fn get_minimum(min: Option, enabled: bool) -> u8 { + if enabled { + // Make sure there's at least one + u8::max(min.unwrap_or(1), 1) + } else { + 0 + } + } + let min_lowercase = get_minimum(input.min_lowercase, lowercase); + let min_uppercase = get_minimum(input.min_uppercase, input.uppercase); + let min_number = get_minimum(input.min_number, input.numbers); + let min_special = get_minimum(input.min_special, input.special); + + // Sanitize the length value + let min_length = min_lowercase + min_uppercase + min_number + min_special; + let length = u8::max(input.length.unwrap_or(DEFAULT_PASSWORD_LENGTH), min_length); + + // Generate the minimum chars of each type, then generate the rest to fill the expected length + let mut chars = Vec::with_capacity(length as usize); + let mut rand = rand::thread_rng(); + + for _ in 0..min_lowercase { + chars.push(*lower_chars.choose(&mut rand).expect("slice is not empty")); + } + for _ in 0..min_uppercase { + chars.push(*upper_chars.choose(&mut rand).expect("slice is not empty")); + } + for _ in 0..min_number { + chars.push(*number_chars.choose(&mut rand).expect("slice is not empty")); + } + for _ in 0..min_special { + chars.push(*SPECIAL_CHARS.choose(&mut rand).expect("slice is not empty")); + } + for _ in min_length..length { + chars.push(**all_chars.choose(&mut rand).expect("slice is not empty")); + } + + chars.shuffle(&mut rand); + Ok(chars.iter().collect()) } pub(super) fn passphrase(_input: PassphraseGeneratorRequest) -> Result {