Skip to content

Commit

Permalink
Separate passphrase to another file and split word generation
Browse files Browse the repository at this point in the history
  • Loading branch information
dani-garcia committed Oct 9, 2023
1 parent c056e91 commit 985a9a4
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 68 deletions.
5 changes: 2 additions & 3 deletions crates/bitwarden/src/tool/generators/client_generator.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden/src/tool/generators/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod client_generator;
mod passphrase;
mod password;

pub use password::{PassphraseGeneratorRequest, PasswordGeneratorRequest};
pub use passphrase::PassphraseGeneratorRequest;
pub use password::PasswordGeneratorRequest;
82 changes: 82 additions & 0 deletions crates/bitwarden/src/tool/generators/passphrase.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
pub word_separator: Option<String>,
pub capitalize: Option<bool>,
pub include_number: Option<bool>,
}

const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3;
const DEFAULT_PASSPHRASE_SEPARATOR: char = ' ';

pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result<String> {
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<String> {
(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::<String>() + 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("áéíóú"), "Áéíóú");
}
}
65 changes: 1 addition & 64 deletions crates/bitwarden/src/tool/generators/password.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -28,68 +27,6 @@ pub struct PasswordGeneratorRequest {
pub min_special: Option<bool>,
}

/// 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<u8>,
pub word_separator: Option<String>,
pub capitalize: Option<bool>,
pub include_number: Option<bool>,
}

pub(super) fn password(_input: PasswordGeneratorRequest) -> Result<String> {
Ok("pa11w0rd".to_string())
}

const DEFAULT_PASSPHRASE_NUM_WORDS: u8 = 3;
const DEFAULT_PASSPHRASE_SEPARATOR: char = ' ';

pub(super) fn passphrase(input: PassphraseGeneratorRequest) -> Result<String> {
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::<String>() + 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)
}

0 comments on commit 985a9a4

Please sign in to comment.