diff --git a/crates/bws/src/cli.rs b/crates/bws/src/cli.rs index 9c81e8bfd..36a8bae9a 100644 --- a/crates/bws/src/cli.rs +++ b/crates/bws/src/cli.rs @@ -81,6 +81,49 @@ pub(crate) enum Commands { #[command(long_about = "Generate shell completion files")] Completions { shell: Option }, + /// Generate secrets + Generate { + /// Include lowercase characters + #[arg(long, short = 'l', default_value_t = true, num_args=0..=1)] + include_lowercase: bool, + + /// Include uppercase characters + #[arg(long, short = 'U', default_value_t = true, num_args=0..=1)] + include_uppercase: bool, + + /// Include numeric characters + #[arg(long, short = 'n', default_value_t = true, num_args=0..=1)] + include_numbers: bool, + + /// Length of the secret, up to 255 characters + #[arg(default_value_t = 24, value_parser = clap::value_parser!(u8).range(1..=255))] + length: u8, + + /// Include special characters + #[arg(long, short = 's', default_value_t = true, num_args=0..=1)] + include_special: bool, + + /// Include ambiguous characters (I, O, l, 0, 1) + #[arg(long, default_value_t = false, num_args=0..=1)] + include_ambiguous: bool, + + /// Minimum lowercase characters + #[arg(long)] + min_lowercase: Option, + + /// Minimum uppercase characters + #[arg(long)] + min_uppercase: Option, + + /// Minimum numeric characters + #[arg(long)] + min_number: Option, + + /// Minimum special characters + #[arg(long)] + min_special: Option, + }, + #[command(long_about = "Commands available on Projects")] Project { #[command(subcommand)] diff --git a/crates/bws/src/command/generate.rs b/crates/bws/src/command/generate.rs new file mode 100644 index 000000000..d4a224296 --- /dev/null +++ b/crates/bws/src/command/generate.rs @@ -0,0 +1,37 @@ +use bitwarden::{ + Client, + generators::{GeneratorClientsExt, PasswordGeneratorRequest}, +}; +use color_eyre::eyre::Result; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn generate_secret( + include_lowercase: bool, + include_uppercase: bool, + include_numbers: bool, + length: u8, + include_special: bool, + include_ambiguous: bool, + min_lowercase: Option, + min_uppercase: Option, + min_number: Option, + min_special: Option, +) -> Result<()> { + let input = PasswordGeneratorRequest { + lowercase: include_lowercase, + uppercase: include_uppercase, + numbers: include_numbers, + length, + special: include_special, + avoid_ambiguous: !include_ambiguous, + min_lowercase, + min_uppercase, + min_number, + min_special, + }; + + let generated_secret = Client::new(None).generator().password(input)?; + print!("{generated_secret}"); + + Ok(()) +} diff --git a/crates/bws/src/command/mod.rs b/crates/bws/src/command/mod.rs index 98287e452..5b710caea 100644 --- a/crates/bws/src/command/mod.rs +++ b/crates/bws/src/command/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod generate; pub(crate) mod project; pub(crate) mod run; pub(crate) mod secret; @@ -7,9 +8,9 @@ use std::{path::PathBuf, str::FromStr}; use bitwarden::auth::AccessToken; use clap::CommandFactory; use clap_complete::Shell; -use color_eyre::eyre::{bail, Result}; +use color_eyre::eyre::{Result, bail}; -use crate::{config, util, Cli, ProfileKey}; +use crate::{Cli, ProfileKey, config, util}; pub(crate) fn completions(shell: Option) -> Result<()> { let Some(shell) = shell.or_else(Shell::from_env) else { diff --git a/crates/bws/src/main.rs b/crates/bws/src/main.rs index e77c8fd24..c46a42387 100644 --- a/crates/bws/src/main.rs +++ b/crates/bws/src/main.rs @@ -1,12 +1,12 @@ use std::{path::PathBuf, str::FromStr}; use bitwarden::{ - auth::{login::AccessTokenLoginRequest, AccessToken}, ClientSettings, + auth::{AccessToken, login::AccessTokenLoginRequest}, }; use bitwarden_cli::install_color_eyre; use clap::{CommandFactory, Parser}; -use color_eyre::eyre::{bail, Result}; +use color_eyre::eyre::{Result, bail}; use config::Profile; use log::error; use render::OutputSettings; @@ -59,6 +59,31 @@ async fn process_commands() -> Result<()> { cli.config_file, ); } + Commands::Generate { + include_lowercase, + include_uppercase, + include_numbers, + length, + include_special, + include_ambiguous, + min_lowercase, + min_uppercase, + min_number, + min_special, + } => { + return command::generate::generate_secret( + include_lowercase, + include_uppercase, + include_numbers, + length, + include_special, + include_ambiguous, + min_lowercase, + min_uppercase, + min_number, + min_special, + ); + } _ => (), } @@ -94,7 +119,10 @@ async fn process_commands() -> Result<()> { ) { Ok(state_file) => Some(state_file), Err(e) => { - eprintln!("Warning: {}\nRetrieving the state file failed. Attempting to continue without using state. Please set \"state_dir\" in your config file to avoid authentication limits.", e); + eprintln!( + "Warning: {}\nRetrieving the state file failed. Attempting to continue without using state. Please set \"state_dir\" in your config file to avoid authentication limits.", + e + ); None } }, @@ -153,7 +181,7 @@ async fn process_commands() -> Result<()> { std::process::exit(exit_code); } - Commands::Config { .. } | Commands::Completions { .. } => { + Commands::Config { .. } | Commands::Completions { .. } | Commands::Generate { .. } => { unreachable!() } }