#![allow(clippy::large_enum_variant)] use anyhow::Context as _; mod actions; mod commands; mod sock; fn main() { env_logger::from_env( env_logger::Env::default().default_filter_or("info"), ) .init(); let matches = clap::App::new("rbw") .about("Unofficial Bitwarden CLI") .author(clap::crate_authors!()) .version(clap::crate_version!()) .subcommand( clap::SubCommand::with_name("config") .about("Get or set configuration options") .subcommand( clap::SubCommand::with_name("show").about( "Show the values of all configuration settings", ), ) .subcommand( clap::SubCommand::with_name("set") .about("Set a configuration option") .arg( clap::Arg::with_name("key") .required(true) .help("Configuration key to set"), ) .arg( clap::Arg::with_name("value") .required(true) .help( "Value to set the configuration option to", ), ), ) .subcommand( clap::SubCommand::with_name("unset") .about("Reset a configuration option to its default") .arg( clap::Arg::with_name("key") .required(true) .help("Configuration key to unset"), ), ), ) .subcommand( clap::SubCommand::with_name("login") .about("Log in to the Bitwarden server"), ) .subcommand( clap::SubCommand::with_name("unlock") .about("Unlock the local Bitwarden database"), ) .subcommand( clap::SubCommand::with_name("sync") .about("Update the local copy of the Bitwarden database"), ) .subcommand( clap::SubCommand::with_name("list") .about("List all entries in the local Bitwarden database") .arg( clap::Arg::with_name("fields") .long("fields") .takes_value(true) .use_delimiter(true) .multiple(true) .help( "Fields to display. \ Available options are id, name, user, folder. \ Multiple fields will be separated by tabs.", ), ) .visible_alias("ls"), ) .subcommand( clap::SubCommand::with_name("get") .about("Display the password for a given entry") .arg( clap::Arg::with_name("name") .required(true) .help("Name of the entry to display"), ) .arg( clap::Arg::with_name("user") .help("Username of the entry to display"), ) .arg( clap::Arg::with_name("full").long("full").help( "Display the notes in addition to the password", ), ), ) .subcommand( clap::SubCommand::with_name("add") .about("Add a new password to the database") .long_about( "Add a new password to the database\n\n\ This command will open a text editor to enter \ the password and notes. The editor to use is determined \ by the value of the $EDITOR environment variable. The \ first line will be saved as the password and the \ remainder will be saved as a note.", ) .arg( clap::Arg::with_name("name") .required(true) .help("Name of the password entry"), ) .arg( clap::Arg::with_name("user") .help("Username for the password entry"), ) .arg( clap::Arg::with_name("uri") .long("uri") .takes_value(true) .multiple(true) .number_of_values(1) .use_delimiter(false) .help("URI for the password entry"), ) .arg( clap::Arg::with_name("folder") .long("folder") .takes_value(true) .help("Folder for the password entry"), ), ) .subcommand( clap::SubCommand::with_name("generate") .about("Generate a new password") .long_about( "Generate a new password\n\n\ If given a password entry name, also save the generated \ password to the database.", ) .arg( clap::Arg::with_name("len") .required(true) .help("Length of the password to generate"), ) .arg( clap::Arg::with_name("name") .help("Name of the password entry"), ) .arg( clap::Arg::with_name("user") .help("Username for the password entry"), ) .arg( clap::Arg::with_name("uri") .long("uri") .takes_value(true) .multiple(true) .number_of_values(1) .use_delimiter(false) .help("URI for the password entry"), ) .arg( clap::Arg::with_name("folder") .long("folder") .takes_value(true) .help("Folder for the password entry"), ) .arg( clap::Arg::with_name("no-symbols") .long("no-symbols") .help( "Generate a password with no special characters", ), ) .arg( clap::Arg::with_name("only-numbers") .long("only-numbers") .help( "Generate a password consisting of only numbers", ), ) .arg( clap::Arg::with_name("nonconfusables") .long("nonconfusables") .help( "Generate a password without visually similar \ characters (useful for passwords intended to be \ written down)", ), ) .arg(clap::Arg::with_name("diceware").long("diceware").help( "Generate a password of multiple dictionary \ words chosen from the EFF word list. The len \ parameter for this option will set the number \ of words to generate, rather than characters.", )) .group(clap::ArgGroup::with_name("password-type").args(&[ "no-symbols", "only-numbers", "nonconfusables", "diceware", ])) .visible_alias("gen"), ) .subcommand( clap::SubCommand::with_name("edit") .about("Modify an existing password") .long_about( "Modify an existing password\n\n\ This command will open a text editor with the existing \ password and notes of the given entry for editing. \ The editor to use is determined by the value of the \ $EDITOR environment variable. The first line will be \ saved as the password and the remainder will be saved \ as a note.", ) .arg( clap::Arg::with_name("name") .required(true) .help("Name of the password entry"), ) .arg( clap::Arg::with_name("user") .help("Username for the password entry"), ), ) .subcommand( clap::SubCommand::with_name("remove") .about("Remove a given entry") .arg( clap::Arg::with_name("name") .required(true) .help("Name of the password entry"), ) .arg( clap::Arg::with_name("user") .help("Username for the password entry"), ) .visible_alias("rm"), ) .subcommand( clap::SubCommand::with_name("history") .about("View the password history for a given entry") .arg( clap::Arg::with_name("name") .required(true) .help("Name of the password entry"), ) .arg( clap::Arg::with_name("user") .help("Username for the password entry"), ), ) .subcommand( clap::SubCommand::with_name("lock") .about("Lock the password database"), ) .subcommand( clap::SubCommand::with_name("purge") .about("Remove the local copy of the password database"), ) .subcommand( clap::SubCommand::with_name("stop-agent") .about("Terminate the background agent") .visible_alias("logout"), ) .get_matches(); let res = match matches.subcommand() { ("config", Some(smatches)) => match smatches.subcommand() { ("show", Some(_)) => { commands::config_show().context("config show") } // these unwraps are fine because key and value are both marked // .required(true) ("set", Some(ssmatches)) => commands::config_set( ssmatches.value_of("key").unwrap(), ssmatches.value_of("value").unwrap(), ) .context("config set"), // this unwrap is fine because key is marked .required(true) ("unset", Some(ssmatches)) => { commands::config_unset(ssmatches.value_of("key").unwrap()) .context("config unset") } _ => { eprintln!("{}", smatches.usage()); std::process::exit(1); } }, ("login", Some(_)) => commands::login().context("login"), ("unlock", Some(_)) => commands::unlock().context("unlock"), ("sync", Some(_)) => commands::sync().context("sync"), ("list", Some(smatches)) => commands::list( &smatches .values_of("fields") .map(|it| it.collect()) .unwrap_or_else(|| vec!["name"]), ) .context("list"), // this unwrap is safe because name is marked .required(true) ("get", Some(smatches)) => commands::get( smatches.value_of("name").unwrap(), smatches.value_of("user"), smatches.is_present("full"), ) .context("get"), // this unwrap is safe because name is marked .required(true) ("add", Some(smatches)) => commands::add( smatches.value_of("name").unwrap(), smatches.value_of("user"), smatches .values_of("uri") .map(|it| it.collect()) .unwrap_or_else(|| vec![]), smatches.value_of("folder"), ) .context("add"), ("generate", Some(smatches)) => { let ty = if smatches.is_present("no-symbols") { rbw::pwgen::Type::NoSymbols } else if smatches.is_present("only-numbers") { rbw::pwgen::Type::Numbers } else if smatches.is_present("nonconfusables") { rbw::pwgen::Type::NonConfusables } else if smatches.is_present("diceware") { rbw::pwgen::Type::Diceware } else { rbw::pwgen::Type::AllChars }; // this unwrap is fine because len is marked as .required(true) let len = smatches.value_of("len").unwrap(); match len.parse() { Ok(len) => commands::generate( smatches.value_of("name"), smatches.value_of("user"), smatches .values_of("uri") .map(|it| it.collect()) .unwrap_or_else(|| vec![]), smatches.value_of("folder"), len, ty, ) .context("generate"), Err(e) => Err(e.into()), } } // this unwrap is safe because name is marked .required(true) ("edit", Some(smatches)) => commands::edit( smatches.value_of("name").unwrap(), smatches.value_of("user"), ) .context("edit"), // this unwrap is safe because name is marked .required(true) ("remove", Some(smatches)) => commands::remove( smatches.value_of("name").unwrap(), smatches.value_of("user"), ) .context("remove"), // this unwrap is safe because name is marked .required(true) ("history", Some(smatches)) => commands::history( smatches.value_of("name").unwrap(), smatches.value_of("user"), ) .context("history"), ("lock", Some(_)) => commands::lock().context("lock"), ("purge", Some(_)) => commands::purge().context("purge"), ("stop-agent", Some(_)) => { commands::stop_agent().context("stop-agent") } _ => { eprintln!("{}", matches.usage()); std::process::exit(1); } } .context("rbw"); if let Err(e) = res { eprintln!("{:#}", e); std::process::exit(1); } }