From c056e91a33c10f2eefb2d2dddb7540f8fbd6b942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Fri, 29 Sep 2023 20:32:54 +0200 Subject: [PATCH 01/13] [PM-3435] Passphrase generator --- .../bitwarden/src/tool/generators/password.rs | 52 +++++++++++++++++-- crates/bw/src/main.rs | 32 ++++++++++-- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/password.rs b/crates/bitwarden/src/tool/generators/password.rs index 0a3874082..8748305cb 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 crate::{error::Result, wordlist::EFF_LONG_WORD_LIST}; +use rand::{seq::SliceRandom, Rng}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -44,6 +45,51 @@ pub(super) fn password(_input: PasswordGeneratorRequest) -> Result { Ok("pa11w0rd".to_string()) } -pub(super) fn passphrase(_input: PassphraseGeneratorRequest) -> Result { - Ok("correct-horse-battery-staple".to_string()) +const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; +const DEFAULT_PASSPHRASE_SEPARATOR: char = ' '; + +pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { + let words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); + let separator = input + .word_separator + .and_then(|s| s.chars().next()) + .unwrap_or(DEFAULT_PASSPHRASE_SEPARATOR); + + let capitalize = input.capitalize.unwrap_or(false); + let include_number = input.include_number.unwrap_or(false); + + fn capitalize_first_letter(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } + } + + let mut rand = rand::thread_rng(); + + let insert_number_idx = include_number.then(|| rand.gen_range(0..words)); + let mut passphrase = String::new(); + + for idx in 0..words { + let word = EFF_LONG_WORD_LIST + .choose(&mut rand) + .expect("slice is not empty"); + + if capitalize { + passphrase.push_str(&capitalize_first_letter(word)); + } else { + passphrase.push_str(word); + } + + if insert_number_idx == Some(idx) { + passphrase.push_str(&rand.gen_range(0..=9).to_string()); + } + + if idx != words - 1 { + passphrase.push(separator) + } + } + + Ok(passphrase) } diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 73e64a4aa..992072d54 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -1,5 +1,7 @@ use bitwarden::{ - auth::RegisterRequest, client::client_settings::ClientSettings, tool::PasswordGeneratorRequest, + auth::RegisterRequest, + client::client_settings::ClientSettings, + tool::{PassphraseGeneratorRequest, PasswordGeneratorRequest}, }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; use clap::{command, Args, CommandFactory, Parser, Subcommand}; @@ -87,7 +89,7 @@ enum ItemCommands { #[derive(Subcommand, Clone)] enum GeneratorCommands { Password(PasswordGeneratorArgs), - Passphrase {}, + Passphrase(PassphraseGeneratorArgs), } #[derive(Args, Clone)] @@ -113,6 +115,18 @@ struct PasswordGeneratorArgs { length: u8, } +#[derive(Args, Clone)] +struct PassphraseGeneratorArgs { + #[arg(long, default_value = "3", help = "Number of words in the passphrase")] + words: u8, + #[arg(long, default_value = " ", help = "Separator between words")] + separator: char, + #[arg(long, action, help = "Capitalize the first letter of each word")] + capitalize: bool, + #[arg(long, action, help = "Include a number in one of the words")] + include_number: bool, +} + #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); @@ -206,7 +220,19 @@ async fn process_commands() -> Result<()> { println!("{}", password); } - GeneratorCommands::Passphrase {} => todo!(), + GeneratorCommands::Passphrase(args) => { + let passphrase = client + .generator() + .passphrase(PassphraseGeneratorRequest { + num_words: Some(args.words), + word_separator: Some(args.separator.to_string()), + capitalize: Some(args.capitalize), + include_number: Some(args.include_number), + }) + .await?; + + println!("{}", passphrase); + } }, }; From 985a9a481d0096e6c305fdd623b1675f62cab0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 9 Oct 2023 12:34:27 +0200 Subject: [PATCH 02/13] Separate passphrase to another file and split word generation --- .../src/tool/generators/client_generator.rs | 5 +- crates/bitwarden/src/tool/generators/mod.rs | 4 +- .../src/tool/generators/passphrase.rs | 82 +++++++++++++++++++ .../bitwarden/src/tool/generators/password.rs | 65 +-------------- 4 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 crates/bitwarden/src/tool/generators/passphrase.rs diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index 0384eec6a..fa1f397c8 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -1,8 +1,7 @@ use crate::{ error::Result, - tool::generators::password::{ - passphrase, password, PassphraseGeneratorRequest, PasswordGeneratorRequest, - }, + tool::generators::passphrase::{passphrase, PassphraseGeneratorRequest}, + tool::generators::password::{password, PasswordGeneratorRequest}, Client, }; diff --git a/crates/bitwarden/src/tool/generators/mod.rs b/crates/bitwarden/src/tool/generators/mod.rs index bdc0fb260..31c7c3e47 100644 --- a/crates/bitwarden/src/tool/generators/mod.rs +++ b/crates/bitwarden/src/tool/generators/mod.rs @@ -1,4 +1,6 @@ mod client_generator; +mod passphrase; mod password; -pub use password::{PassphraseGeneratorRequest, PasswordGeneratorRequest}; +pub use passphrase::PassphraseGeneratorRequest; +pub use password::PasswordGeneratorRequest; diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs new file mode 100644 index 000000000..ad71eec2e --- /dev/null +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -0,0 +1,82 @@ +use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST}; +use rand::{seq::SliceRandom, Rng, RngCore}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Passphrase generator request. +/// +/// The default separator is `-` and default number of words is 3. +#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PassphraseGeneratorRequest { + pub num_words: Option, + pub word_separator: Option, + pub capitalize: Option, + pub include_number: Option, +} + +const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; +const DEFAULT_PASSPHRASE_SEPARATOR: char = ' '; + +pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { + let words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); + let separator = input + .word_separator + .and_then(|s| s.chars().next()) + .unwrap_or(DEFAULT_PASSPHRASE_SEPARATOR); + let capitalize = input.capitalize.unwrap_or(false); + let include_number = input.include_number.unwrap_or(false); + + let mut rand = rand::thread_rng(); + + let mut passphrase_words = gen_words(&mut rand, words); + if include_number { + let number_idx = rand.gen_range(0..words as usize); + passphrase_words[number_idx].push_str(&rand.gen_range(0..=9).to_string()); + } + + if capitalize { + passphrase_words = passphrase_words + .iter() + .map(|w| capitalize_first_letter(w)) + .collect(); + } + + Ok(passphrase_words.join(&separator.to_string())) +} + +fn gen_words(mut rng: impl RngCore, num_words: u8) -> Vec { + (0..num_words) + .map(|_| { + EFF_LONG_WORD_LIST + .choose(&mut rng) + .expect("slice is not empty") + .to_string() + }) + .collect() +} + +fn capitalize_first_letter(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn test_capitalize() { + assert_eq!(super::capitalize_first_letter("hello"), "Hello"); + assert_eq!(super::capitalize_first_letter("1ello"), "1ello"); + assert_eq!(super::capitalize_first_letter("Hello"), "Hello"); + assert_eq!(super::capitalize_first_letter("h"), "H"); + assert_eq!(super::capitalize_first_letter(""), ""); + + // Also supports non-ascii, though the EFF list doesn't have any + assert_eq!(super::capitalize_first_letter("áéíóú"), "Áéíóú"); + } +} diff --git a/crates/bitwarden/src/tool/generators/password.rs b/crates/bitwarden/src/tool/generators/password.rs index 8748305cb..237394f56 100644 --- a/crates/bitwarden/src/tool/generators/password.rs +++ b/crates/bitwarden/src/tool/generators/password.rs @@ -1,5 +1,4 @@ -use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST}; -use rand::{seq::SliceRandom, Rng}; +use crate::error::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -28,68 +27,6 @@ pub struct PasswordGeneratorRequest { pub min_special: Option, } -/// Passphrase generator request. -/// -/// The default separator is `-` and default number of words is 3. -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg_attr(feature = "mobile", derive(uniffi::Record))] -pub struct PassphraseGeneratorRequest { - pub num_words: Option, - pub word_separator: Option, - pub capitalize: Option, - pub include_number: Option, -} - pub(super) fn password(_input: PasswordGeneratorRequest) -> Result { Ok("pa11w0rd".to_string()) } - -const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; -const DEFAULT_PASSPHRASE_SEPARATOR: char = ' '; - -pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { - let words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); - let separator = input - .word_separator - .and_then(|s| s.chars().next()) - .unwrap_or(DEFAULT_PASSPHRASE_SEPARATOR); - - let capitalize = input.capitalize.unwrap_or(false); - let include_number = input.include_number.unwrap_or(false); - - fn capitalize_first_letter(s: &str) -> String { - let mut c = s.chars(); - match c.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::() + c.as_str(), - } - } - - let mut rand = rand::thread_rng(); - - let insert_number_idx = include_number.then(|| rand.gen_range(0..words)); - let mut passphrase = String::new(); - - for idx in 0..words { - let word = EFF_LONG_WORD_LIST - .choose(&mut rand) - .expect("slice is not empty"); - - if capitalize { - passphrase.push_str(&capitalize_first_letter(word)); - } else { - passphrase.push_str(word); - } - - if insert_number_idx == Some(idx) { - passphrase.push_str(&rand.gen_range(0..=9).to_string()); - } - - if idx != words - 1 { - passphrase.push(separator) - } - } - - Ok(passphrase) -} From e4729ac5794881eb6c31a9ab7a05baaa1a1e2bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 9 Oct 2023 13:34:09 +0200 Subject: [PATCH 03/13] Separate functions and add tests --- .../src/tool/generators/passphrase.rs | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index ad71eec2e..0aa60304c 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -20,7 +20,7 @@ const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; const DEFAULT_PASSPHRASE_SEPARATOR: char = ' '; pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { - let words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); + let num_words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); let separator = input .word_separator .and_then(|s| s.chars().next()) @@ -30,19 +30,13 @@ pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { let mut rand = rand::thread_rng(); - let mut passphrase_words = gen_words(&mut rand, words); + let mut passphrase_words = gen_words(&mut rand, num_words); if include_number { - let number_idx = rand.gen_range(0..words as usize); - passphrase_words[number_idx].push_str(&rand.gen_range(0..=9).to_string()); + include_number_in_words(&mut rand, &mut passphrase_words); } - if capitalize { - passphrase_words = passphrase_words - .iter() - .map(|w| capitalize_first_letter(w)) - .collect(); + capitalize_words(&mut passphrase_words); } - Ok(passphrase_words.join(&separator.to_string())) } @@ -57,6 +51,17 @@ fn gen_words(mut rng: impl RngCore, num_words: u8) -> Vec { .collect() } +fn include_number_in_words(mut rng: impl RngCore, words: &mut [String]) { + let number_idx = rng.gen_range(0..words.len()); + words[number_idx].push_str(&rng.gen_range(0..=9).to_string()); +} + +fn capitalize_words(words: &mut [String]) { + words + .iter_mut() + .for_each(|w| *w = capitalize_first_letter(w)); +} + fn capitalize_first_letter(s: &str) -> String { let mut c = s.chars(); match c.next() { @@ -67,16 +72,41 @@ fn capitalize_first_letter(s: &str) -> String { #[cfg(test)] mod tests { + use super::*; #[test] fn test_capitalize() { - assert_eq!(super::capitalize_first_letter("hello"), "Hello"); - assert_eq!(super::capitalize_first_letter("1ello"), "1ello"); - assert_eq!(super::capitalize_first_letter("Hello"), "Hello"); - assert_eq!(super::capitalize_first_letter("h"), "H"); - assert_eq!(super::capitalize_first_letter(""), ""); + assert_eq!(capitalize_first_letter("hello"), "Hello"); + assert_eq!(capitalize_first_letter("1ello"), "1ello"); + assert_eq!(capitalize_first_letter("Hello"), "Hello"); + assert_eq!(capitalize_first_letter("h"), "H"); + assert_eq!(capitalize_first_letter(""), ""); // Also supports non-ascii, though the EFF list doesn't have any - assert_eq!(super::capitalize_first_letter("áéíóú"), "Áéíóú"); + assert_eq!(capitalize_first_letter("áéíóú"), "Áéíóú"); + } + + #[test] + fn test_capitalize_words() { + let mut words = vec!["hello".to_string(), "world".to_string()]; + capitalize_words(&mut words); + assert_eq!(words, &["Hello", "World"]); + } + + #[test] + fn test_include_number() { + let mut rng = rand::thread_rng(); + + fn count_numbers(words: &[String]) -> usize { + words + .iter() + .map(|w| w.chars().filter(|c| c.is_numeric()).count()) + .sum() + } + + let mut words = vec!["hello".to_string(), "world".to_string()]; + assert_eq!(count_numbers(&words), 0); + include_number_in_words(&mut rng, &mut words); + assert_eq!(count_numbers(&words), 1); } } From 071295ab37d10d87c847e2379d7318490e45708b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 10 Oct 2023 13:00:50 +0200 Subject: [PATCH 04/13] Added rand_chacha for some repeatable testing --- Cargo.lock | 1 + crates/bitwarden/Cargo.toml | 1 + .../src/tool/generators/passphrase.rs | 31 ++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58fb5ff26..362288421 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ dependencies = [ "num-traits", "pbkdf2", "rand 0.8.5", + "rand_chacha 0.3.1", "reqwest", "rsa", "schemars", diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 345b1f9cb..1f89c7aae 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -59,5 +59,6 @@ bitwarden-api-identity = { path = "../bitwarden-api-identity", version = "=0.2.1 bitwarden-api-api = { path = "../bitwarden-api-api", version = "=0.2.1" } [dev-dependencies] +rand_chacha = "0.3.1" tokio = { version = "1.28.2", features = ["rt", "macros"] } wiremock = "0.5.18" diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 0aa60304c..82619d7d5 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -72,8 +72,21 @@ fn capitalize_first_letter(s: &str) -> String { #[cfg(test)] mod tests { + use rand::SeedableRng; + use super::*; + #[test] + fn test_gen_words() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + assert_eq!( + &gen_words(&mut rng, 4), + &["subsystem", "undertook", "silenced", "dinginess"] + ); + assert_eq!(&gen_words(&mut rng, 1), &["numbing"]); + assert_eq!(&gen_words(&mut rng, 2), &["catnip", "jokester"]); + } + #[test] fn test_capitalize() { assert_eq!(capitalize_first_letter("hello"), "Hello"); @@ -88,25 +101,21 @@ mod tests { #[test] fn test_capitalize_words() { - let mut words = vec!["hello".to_string(), "world".to_string()]; + let mut words = vec!["hello".into(), "world".into()]; capitalize_words(&mut words); assert_eq!(words, &["Hello", "World"]); } #[test] fn test_include_number() { - let mut rng = rand::thread_rng(); + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); - fn count_numbers(words: &[String]) -> usize { - words - .iter() - .map(|w| w.chars().filter(|c| c.is_numeric()).count()) - .sum() - } + let mut words = vec!["hello".into(), "world".into()]; + include_number_in_words(&mut rng, &mut words); + assert_eq!(words, &["hello", "world7"]); - let mut words = vec!["hello".to_string(), "world".to_string()]; - assert_eq!(count_numbers(&words), 0); + let mut words = vec!["This".into(), "is".into(), "a".into(), "test".into()]; include_number_in_words(&mut rng, &mut words); - assert_eq!(count_numbers(&words), 1); + assert_eq!(words, &["This", "is", "a1", "test"]); } } From c941d34ad805094a8b7a625210fd0edd80ca70d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 10 Oct 2023 16:56:10 +0200 Subject: [PATCH 05/13] Test passphrase --- .../src/tool/generators/passphrase.rs | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 82619d7d5..300b8b02e 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -20,6 +20,10 @@ const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; const DEFAULT_PASSPHRASE_SEPARATOR: char = ' '; pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { + passphrase_with_rng(rand::thread_rng(), input) +} + +fn passphrase_with_rng(mut rng: impl RngCore, input: PassphraseGeneratorRequest) -> Result { let num_words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); let separator = input .word_separator @@ -28,11 +32,9 @@ pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { let capitalize = input.capitalize.unwrap_or(false); let include_number = input.include_number.unwrap_or(false); - let mut rand = rand::thread_rng(); - - let mut passphrase_words = gen_words(&mut rand, num_words); + let mut passphrase_words = gen_words(&mut rng, num_words); if include_number { - include_number_in_words(&mut rand, &mut passphrase_words); + include_number_in_words(&mut rng, &mut passphrase_words); } if capitalize { capitalize_words(&mut passphrase_words); @@ -118,4 +120,42 @@ mod tests { include_number_in_words(&mut rng, &mut words); assert_eq!(words, &["This", "is", "a1", "test"]); } + + #[test] + fn test_passphrase() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let input = PassphraseGeneratorRequest { + num_words: Some(4), + word_separator: Some("-".into()), + capitalize: Some(true), + include_number: Some(true), + }; + assert_eq!( + passphrase_with_rng(&mut rng, input).unwrap(), + "Subsystem4-Undertook-Silenced-Dinginess" + ); + + let input = PassphraseGeneratorRequest { + num_words: Some(2), + word_separator: Some("".into()), + capitalize: Some(false), + include_number: Some(true), + }; + assert_eq!( + passphrase_with_rng(&mut rng, input).unwrap(), + "drew hankering0" + ); + + let input = PassphraseGeneratorRequest { + num_words: Some(5), + word_separator: Some(";".into()), + capitalize: None, + include_number: None, + }; + assert_eq!( + passphrase_with_rng(&mut rng, input).unwrap(), + "sarcasm;duller;backlight;factual;husked" + ); + } } From e5d778f1483ffbd0946da76b2f0e038665efec14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 12 Oct 2023 15:38:12 +0200 Subject: [PATCH 06/13] Document parameters and uppercase function, check num_words in range --- .../src/tool/generators/passphrase.rs | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 300b8b02e..6976576d5 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -1,4 +1,7 @@ -use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST}; +use crate::{ + error::{Error, Result}, + wordlist::EFF_LONG_WORD_LIST, +}; use rand::{seq::SliceRandom, Rng, RngCore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -10,28 +13,51 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PassphraseGeneratorRequest { + /// Number of words in the generated passphrase. + /// This value must be between 3 and 20. + /// The default value when unset is 3 pub num_words: Option, + /// Character separator between words in the generated passphrase. + /// If the value is set, it cannot be empty. + /// The default value when unset is `-` pub word_separator: Option, + /// When set to true, capitalize the first letter of each word in the generated passphrase. + /// The default value when unset is `false` pub capitalize: Option, + /// When set to true, include a number at the end of one of the words in the generated passphrase. + /// The default value when unset is `false` pub include_number: Option, } const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; -const DEFAULT_PASSPHRASE_SEPARATOR: char = ' '; +const DEFAULT_PASSPHRASE_SEPARATOR: &str = " "; + +const MINIMUM_PASSPHRASE_NUM_WORDS: u8 = 3; +const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { passphrase_with_rng(rand::thread_rng(), input) } fn passphrase_with_rng(mut rng: impl RngCore, input: PassphraseGeneratorRequest) -> Result { - let num_words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); - let separator = input - .word_separator - .and_then(|s| s.chars().next()) - .unwrap_or(DEFAULT_PASSPHRASE_SEPARATOR); let capitalize = input.capitalize.unwrap_or(false); let include_number = input.include_number.unwrap_or(false); + let num_words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); + if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&num_words) { + return Err(Error::Internal("'num_words' must be between 3 and 20")); + } + + let Some(separator) = input + .word_separator + .as_deref() + .unwrap_or(DEFAULT_PASSPHRASE_SEPARATOR) + .chars() + .next() + else { + return Err(Error::Internal("'word_separator' cannot be empty")); + }; + let mut passphrase_words = gen_words(&mut rng, num_words); if include_number { include_number_in_words(&mut rng, &mut passphrase_words); @@ -65,6 +91,9 @@ fn capitalize_words(words: &mut [String]) { } fn capitalize_first_letter(s: &str) -> String { + // Unicode case conversion can change the length of the string, so we can't capitalize in place. + // Instead we extract the first character and convert it to uppercase. This returns + // an iterator which we collect into a string, and then append the rest of the input. let mut c = s.chars(); match c.next() { None => String::new(), @@ -137,14 +166,14 @@ mod tests { ); let input = PassphraseGeneratorRequest { - num_words: Some(2), - word_separator: Some("".into()), + num_words: Some(3), + word_separator: Some(" ".into()), capitalize: Some(false), include_number: Some(true), }; assert_eq!( passphrase_with_rng(&mut rng, input).unwrap(), - "drew hankering0" + "drew7 hankering cabana" ); let input = PassphraseGeneratorRequest { @@ -155,7 +184,7 @@ mod tests { }; assert_eq!( passphrase_with_rng(&mut rng, input).unwrap(), - "sarcasm;duller;backlight;factual;husked" + "duller;backlight;factual;husked;remover" ); } } From 8d54b988deda55fd7af5c369db308b7428c0c747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 18 Oct 2023 17:36:06 +0200 Subject: [PATCH 07/13] Document passphrase API --- .../src/tool/generators/client_generator.rs | 21 +++++++++++++++++++ .../src/tool/generators/passphrase.rs | 9 +++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index fa1f397c8..a988b6546 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -14,6 +14,27 @@ impl<'a> ClientGenerator<'a> { password(input) } + /// Generates a random passphrase. + /// A passphrase is a combination of random words separated by a character. + /// An example of passphrase is `correct horse battery staple`. + /// + /// By default, the generated passphrases contain 3 random lowercase words + /// separated by spaces, but this can be customized using the `input` parameter. + /// + /// # Examples + /// + /// ``` + /// use bitwarden::{Client, tool::PassphraseGeneratorRequest, error::Result}; + /// async fn test() -> Result<()> { + /// let input = PassphraseGeneratorRequest { + /// num_words: Some(4), + /// ..Default::default() + /// }; + /// let passphrase = Client::new(None).generator().passphrase(input).await.unwrap(); + /// println!("{}", passphrase); + /// Ok(()) + /// } + /// ``` pub async fn passphrase(&self, input: PassphraseGeneratorRequest) -> Result { passphrase(input) } diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 6976576d5..871db86c9 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -6,9 +6,10 @@ use rand::{seq::SliceRandom, Rng, RngCore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -/// Passphrase generator request. +/// Passphrase generator request options. /// -/// The default separator is `-` and default number of words is 3. +/// By default, the generated passphrases contain 3 random +/// lowercase words separated by spaces, and no digits #[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] @@ -19,7 +20,7 @@ pub struct PassphraseGeneratorRequest { pub num_words: Option, /// Character separator between words in the generated passphrase. /// If the value is set, it cannot be empty. - /// The default value when unset is `-` + /// The default value when unset is ` ` pub word_separator: Option, /// When set to true, capitalize the first letter of each word in the generated passphrase. /// The default value when unset is `false` @@ -35,6 +36,8 @@ const DEFAULT_PASSPHRASE_SEPARATOR: &str = " "; const MINIMUM_PASSPHRASE_NUM_WORDS: u8 = 3; const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; +/// Implementation of the random passphrase generator. This is not accessible to the public API. +/// See [`ClientGenerator::passphrase`](crate::ClientGenerator::passphrase) for the API function. pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { passphrase_with_rng(rand::thread_rng(), input) } From 16207a6127133e70f37ec247ea79a2aaa5472959 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 20 Oct 2023 17:58:19 +0200 Subject: [PATCH 08/13] Remove optional from PassphraseGeneratorRequest --- .../src/tool/generators/passphrase.rs | 82 +++++++++---------- crates/bw/src/main.rs | 8 +- languages/kotlin/doc.md | 16 ++-- 3 files changed, 50 insertions(+), 56 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 871db86c9..4d92b74df 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -7,65 +7,59 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Passphrase generator request options. -/// -/// By default, the generated passphrases contain 3 random -/// lowercase words separated by spaces, and no digits -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PassphraseGeneratorRequest { /// Number of words in the generated passphrase. /// This value must be between 3 and 20. - /// The default value when unset is 3 - pub num_words: Option, + pub num_words: u8, /// Character separator between words in the generated passphrase. /// If the value is set, it cannot be empty. - /// The default value when unset is ` ` - pub word_separator: Option, + pub word_separator: String, /// When set to true, capitalize the first letter of each word in the generated passphrase. - /// The default value when unset is `false` - pub capitalize: Option, + pub capitalize: bool, /// When set to true, include a number at the end of one of the words in the generated passphrase. - /// The default value when unset is `false` - pub include_number: Option, + pub include_number: bool, } -const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3; -const DEFAULT_PASSPHRASE_SEPARATOR: &str = " "; +impl Default for PassphraseGeneratorRequest { + fn default() -> Self { + Self { + num_words: 3, + word_separator: ' '.to_string(), + capitalize: false, + include_number: false, + } + } +} const MINIMUM_PASSPHRASE_NUM_WORDS: u8 = 3; const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; /// Implementation of the random passphrase generator. This is not accessible to the public API. /// See [`ClientGenerator::passphrase`](crate::ClientGenerator::passphrase) for the API function. -pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result { - passphrase_with_rng(rand::thread_rng(), input) +pub(super) fn passphrase(options: PassphraseGeneratorRequest) -> Result { + passphrase_with_rng(rand::thread_rng(), options) } -fn passphrase_with_rng(mut rng: impl RngCore, input: PassphraseGeneratorRequest) -> Result { - let capitalize = input.capitalize.unwrap_or(false); - let include_number = input.include_number.unwrap_or(false); - - let num_words = input.num_words.unwrap_or(DEFAULT_PASSPHRASE_NUM_WORDS); - if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&num_words) { +fn passphrase_with_rng( + mut rng: impl RngCore, + options: PassphraseGeneratorRequest, +) -> Result { + if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&options.num_words) { return Err(Error::Internal("'num_words' must be between 3 and 20")); } - let Some(separator) = input - .word_separator - .as_deref() - .unwrap_or(DEFAULT_PASSPHRASE_SEPARATOR) - .chars() - .next() - else { + let Some(separator) = options.word_separator.chars().next() else { return Err(Error::Internal("'word_separator' cannot be empty")); }; - let mut passphrase_words = gen_words(&mut rng, num_words); - if include_number { + let mut passphrase_words = gen_words(&mut rng, options.num_words); + if options.include_number { include_number_in_words(&mut rng, &mut passphrase_words); } - if capitalize { + if options.capitalize { capitalize_words(&mut passphrase_words); } Ok(passphrase_words.join(&separator.to_string())) @@ -158,10 +152,10 @@ mod tests { let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); let input = PassphraseGeneratorRequest { - num_words: Some(4), - word_separator: Some("-".into()), - capitalize: Some(true), - include_number: Some(true), + num_words: 4, + word_separator: "-".into(), + capitalize: true, + include_number: true, }; assert_eq!( passphrase_with_rng(&mut rng, input).unwrap(), @@ -169,10 +163,10 @@ mod tests { ); let input = PassphraseGeneratorRequest { - num_words: Some(3), - word_separator: Some(" ".into()), - capitalize: Some(false), - include_number: Some(true), + num_words: 3, + word_separator: " ".into(), + capitalize: false, + include_number: true, }; assert_eq!( passphrase_with_rng(&mut rng, input).unwrap(), @@ -180,10 +174,10 @@ mod tests { ); let input = PassphraseGeneratorRequest { - num_words: Some(5), - word_separator: Some(";".into()), - capitalize: None, - include_number: None, + num_words: 5, + word_separator: ";".into(), + capitalize: false, + include_number: false, }; assert_eq!( passphrase_with_rng(&mut rng, input).unwrap(), diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 992072d54..daeec9d9a 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -224,10 +224,10 @@ async fn process_commands() -> Result<()> { let passphrase = client .generator() .passphrase(PassphraseGeneratorRequest { - num_words: Some(args.words), - word_separator: Some(args.separator.to_string()), - capitalize: Some(args.capitalize), - include_number: Some(args.include_number), + num_words: args.words, + word_separator: args.separator.to_string(), + capitalize: args.capitalize, + include_number: args.include_number, }) .await?; diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index 9e8c19929..b083fa123 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -974,23 +974,23 @@ implementations. numWords - integer,null - + integer + Number of words in the generated passphrase. This value must be between 3 and 20. wordSeparator - string,null - + string + Character separator between words in the generated passphrase. If the value is set, it cannot be empty. capitalize - boolean,null - + boolean + When set to true, capitalize the first letter of each word in the generated passphrase. includeNumber - boolean,null - + boolean + When set to true, include a number at the end of one of the words in the generated passphrase. From c8a294116eb8848112468abe309794432ecaa81c Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 20 Oct 2023 18:04:32 +0200 Subject: [PATCH 09/13] Fix doc --- crates/bitwarden/src/tool/generators/client_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index a988b6546..8e9ad258e 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -27,7 +27,7 @@ impl<'a> ClientGenerator<'a> { /// use bitwarden::{Client, tool::PassphraseGeneratorRequest, error::Result}; /// async fn test() -> Result<()> { /// let input = PassphraseGeneratorRequest { - /// num_words: Some(4), + /// num_words: 4, /// ..Default::default() /// }; /// let passphrase = Client::new(None).generator().passphrase(input).await.unwrap(); From db9686abcbbe49512397f388c6bb792e095e1ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 6 Nov 2023 13:06:21 +0100 Subject: [PATCH 10/13] Make passphrase infallible by moving validation outside of it --- .../src/tool/generators/client_generator.rs | 3 +- .../src/tool/generators/passphrase.rs | 68 +++++++++++++------ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index 8e9ad258e..0a3084601 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -36,7 +36,8 @@ impl<'a> ClientGenerator<'a> { /// } /// ``` pub async fn passphrase(&self, input: PassphraseGeneratorRequest) -> Result { - passphrase(input) + let options = input.validate_options()?; + Ok(passphrase(options)) } } diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 4d92b74df..d1f89078e 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -37,24 +37,46 @@ impl Default for PassphraseGeneratorRequest { const MINIMUM_PASSPHRASE_NUM_WORDS: u8 = 3; const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; -/// Implementation of the random passphrase generator. This is not accessible to the public API. -/// See [`ClientGenerator::passphrase`](crate::ClientGenerator::passphrase) for the API function. -pub(super) fn passphrase(options: PassphraseGeneratorRequest) -> Result { - passphrase_with_rng(rand::thread_rng(), options) +// We don't want the validated struct to be accessible, yet at the same time it needs to be public +// to be used as a return type, so we define it in a private module to make it innaccessible. +mod private { + pub struct ValidPassphraseGeneratorOptions { + pub(super) num_words: u8, + pub(super) word_separator: String, + pub(super) capitalize: bool, + pub(super) include_number: bool, + } } +use private::ValidPassphraseGeneratorOptions; + +impl PassphraseGeneratorRequest { + // TODO: Add password generator policy checks + pub fn validate_options(self) -> Result { + if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&self.num_words) + { + return Err(Error::Internal("'num_words' must be between 3 and 20")); + } -fn passphrase_with_rng( - mut rng: impl RngCore, - options: PassphraseGeneratorRequest, -) -> Result { - if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&options.num_words) { - return Err(Error::Internal("'num_words' must be between 3 and 20")); + if self.word_separator.chars().next().is_none() { + return Err(Error::Internal("'word_separator' cannot be empty")); + }; + + Ok(ValidPassphraseGeneratorOptions { + num_words: self.num_words, + word_separator: self.word_separator, + capitalize: self.capitalize, + include_number: self.include_number, + }) } +} - let Some(separator) = options.word_separator.chars().next() else { - return Err(Error::Internal("'word_separator' cannot be empty")); - }; +/// Implementation of the random passphrase generator. This is not accessible to the public API. +/// See [`ClientGenerator::passphrase`](crate::ClientGenerator::passphrase) for the API function. +pub(super) fn passphrase(options: ValidPassphraseGeneratorOptions) -> String { + passphrase_with_rng(rand::thread_rng(), options) +} +fn passphrase_with_rng(mut rng: impl RngCore, options: ValidPassphraseGeneratorOptions) -> String { let mut passphrase_words = gen_words(&mut rng, options.num_words); if options.include_number { include_number_in_words(&mut rng, &mut passphrase_words); @@ -62,7 +84,7 @@ fn passphrase_with_rng( if options.capitalize { capitalize_words(&mut passphrase_words); } - Ok(passphrase_words.join(&separator.to_string())) + passphrase_words.join(&options.word_separator) } fn gen_words(mut rng: impl RngCore, num_words: u8) -> Vec { @@ -156,9 +178,11 @@ mod tests { word_separator: "-".into(), capitalize: true, include_number: true, - }; + } + .validate_options() + .unwrap(); assert_eq!( - passphrase_with_rng(&mut rng, input).unwrap(), + passphrase_with_rng(&mut rng, input), "Subsystem4-Undertook-Silenced-Dinginess" ); @@ -167,9 +191,11 @@ mod tests { word_separator: " ".into(), capitalize: false, include_number: true, - }; + } + .validate_options() + .unwrap(); assert_eq!( - passphrase_with_rng(&mut rng, input).unwrap(), + passphrase_with_rng(&mut rng, input), "drew7 hankering cabana" ); @@ -178,9 +204,11 @@ mod tests { word_separator: ";".into(), capitalize: false, include_number: false, - }; + } + .validate_options() + .unwrap(); assert_eq!( - passphrase_with_rng(&mut rng, input).unwrap(), + passphrase_with_rng(&mut rng, input), "duller;backlight;factual;husked;remover" ); } From 46c2de0319a194168650fd55dffaddee33f3f22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 6 Nov 2023 16:46:40 +0100 Subject: [PATCH 11/13] Test unicode separator and update comments --- .../src/tool/generators/client_generator.rs | 4 +-- .../src/tool/generators/passphrase.rs | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index 0a3084601..5e48ecc22 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -18,8 +18,8 @@ impl<'a> ClientGenerator<'a> { /// A passphrase is a combination of random words separated by a character. /// An example of passphrase is `correct horse battery staple`. /// - /// By default, the generated passphrases contain 3 random lowercase words - /// separated by spaces, but this can be customized using the `input` parameter. + /// The number of words and their case, the word separator, and the inclusion of + /// a number in the passphrase can be customized using the `input` parameter. /// /// # Examples /// diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index d1f89078e..0bbf77510 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -14,8 +14,7 @@ pub struct PassphraseGeneratorRequest { /// Number of words in the generated passphrase. /// This value must be between 3 and 20. pub num_words: u8, - /// Character separator between words in the generated passphrase. - /// If the value is set, it cannot be empty. + /// Character separator between words in the generated passphrase. The value cannot be empty. pub word_separator: String, /// When set to true, capitalize the first letter of each word in the generated passphrase. pub capitalize: bool, @@ -40,6 +39,8 @@ const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; // We don't want the validated struct to be accessible, yet at the same time it needs to be public // to be used as a return type, so we define it in a private module to make it innaccessible. mod private { + /// Represents a set of valid options to generate a passhprase with. + /// To get an instance of it, use [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options) pub struct ValidPassphraseGeneratorOptions { pub(super) num_words: u8, pub(super) word_separator: String, @@ -50,8 +51,10 @@ mod private { use private::ValidPassphraseGeneratorOptions; impl PassphraseGeneratorRequest { - // TODO: Add password generator policy checks + /// Validates the request and returns an immutable struct with valid options to use with [`passphrase`](passphrase). pub fn validate_options(self) -> Result { + // TODO: Add password generator policy checks + if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&self.num_words) { return Err(Error::Internal("'num_words' must be between 3 and 20")); @@ -72,6 +75,10 @@ impl PassphraseGeneratorRequest { /// Implementation of the random passphrase generator. This is not accessible to the public API. /// See [`ClientGenerator::passphrase`](crate::ClientGenerator::passphrase) for the API function. +/// +/// # Arguments: +/// * `options`: Valid parameters used to generate the passphrase. To create it, use +/// [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options). pub(super) fn passphrase(options: ValidPassphraseGeneratorOptions) -> String { passphrase_with_rng(rand::thread_rng(), options) } @@ -169,6 +176,24 @@ mod tests { assert_eq!(words, &["This", "is", "a1", "test"]); } + #[test] + fn test_separator() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let input = PassphraseGeneratorRequest { + num_words: 4, + word_separator: "👨🏻‍❤️‍💋‍👨🏻".into(), // This emoji is 35 bytes long, but represented as a single character + capitalize: false, + include_number: true, + } + .validate_options() + .unwrap(); + assert_eq!( + passphrase_with_rng(&mut rng, input), + "subsystem4👨🏻‍❤️‍💋‍👨🏻undertook👨🏻‍❤️‍💋‍👨🏻silenced👨🏻‍❤️‍💋‍👨🏻dinginess" + ); + } + #[test] fn test_passphrase() { let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); From 231b244f4be7875f92d54c410f63829e1ec72a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 6 Nov 2023 17:58:09 +0100 Subject: [PATCH 12/13] Remove reference to inaccessible struct --- crates/bitwarden/src/tool/generators/passphrase.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 0bbf77510..4c003adf3 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -51,7 +51,7 @@ mod private { use private::ValidPassphraseGeneratorOptions; impl PassphraseGeneratorRequest { - /// Validates the request and returns an immutable struct with valid options to use with [`passphrase`](passphrase). + /// Validates the request and returns an immutable struct with valid options to use with the passphrase generator. pub fn validate_options(self) -> Result { // TODO: Add password generator policy checks From 2a27f5f5e9c16f55ea12b5c02030d39d0babddb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 4 Dec 2023 12:03:19 +0100 Subject: [PATCH 13/13] Change input --- .../src/tool/generators/client_generator.rs | 3 +-- .../src/tool/generators/passphrase.rs | 26 ++++++++----------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index 5e48ecc22..8af8ecd19 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -36,8 +36,7 @@ impl<'a> ClientGenerator<'a> { /// } /// ``` pub async fn passphrase(&self, input: PassphraseGeneratorRequest) -> Result { - let options = input.validate_options()?; - Ok(passphrase(options)) + passphrase(input) } } diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs index 4c003adf3..e62fa8f9a 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -36,23 +36,18 @@ impl Default for PassphraseGeneratorRequest { const MINIMUM_PASSPHRASE_NUM_WORDS: u8 = 3; const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; -// We don't want the validated struct to be accessible, yet at the same time it needs to be public -// to be used as a return type, so we define it in a private module to make it innaccessible. -mod private { - /// Represents a set of valid options to generate a passhprase with. - /// To get an instance of it, use [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options) - pub struct ValidPassphraseGeneratorOptions { - pub(super) num_words: u8, - pub(super) word_separator: String, - pub(super) capitalize: bool, - pub(super) include_number: bool, - } +/// Represents a set of valid options to generate a passhprase with. +/// To get an instance of it, use [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options) +struct ValidPassphraseGeneratorOptions { + pub(super) num_words: u8, + pub(super) word_separator: String, + pub(super) capitalize: bool, + pub(super) include_number: bool, } -use private::ValidPassphraseGeneratorOptions; impl PassphraseGeneratorRequest { /// Validates the request and returns an immutable struct with valid options to use with the passphrase generator. - pub fn validate_options(self) -> Result { + fn validate_options(self) -> Result { // TODO: Add password generator policy checks if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&self.num_words) @@ -79,8 +74,9 @@ impl PassphraseGeneratorRequest { /// # Arguments: /// * `options`: Valid parameters used to generate the passphrase. To create it, use /// [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options). -pub(super) fn passphrase(options: ValidPassphraseGeneratorOptions) -> String { - passphrase_with_rng(rand::thread_rng(), options) +pub(super) fn passphrase(request: PassphraseGeneratorRequest) -> Result { + let options = request.validate_options()?; + Ok(passphrase_with_rng(rand::thread_rng(), options)) } fn passphrase_with_rng(mut rng: impl RngCore, options: ValidPassphraseGeneratorOptions) -> String {