#![allow(clippy::large_enum_variant)]
use anyhow::Context as _;
use std::io::Write as _;
use structopt::StructOpt as _;
mod actions;
mod commands;
mod sock;
#[derive(Debug, structopt::StructOpt)]
#[structopt(about = "Unofficial Bitwarden CLI")]
enum Opt {
#[structopt(about = "Get or set configuration options")]
Config {
#[structopt(subcommand)]
config: Config,
},
#[structopt(about = "Log in to the Bitwarden server")]
Login,
#[structopt(about = "Unlock the local Bitwarden database")]
Unlock,
#[structopt(about = "Check if the local Bitwarden database is unlocked")]
Unlocked,
#[structopt(about = "Update the local copy of the Bitwarden database")]
Sync,
#[structopt(
about = "List all entries in the local Bitwarden database",
visible_alias = "ls"
)]
List {
#[structopt(
long,
help = "Fields to display. \
Available options are id, name, user, folder. \
Multiple fields will be separated by tabs.",
default_value = "name",
use_delimiter = true
)]
fields: Vec<String>,
},
#[structopt(about = "Display the password for a given entry")]
Get {
#[structopt(help = "Name or UUID of the entry to display")]
name: String,
#[structopt(help = "Username of the entry to display")]
user: Option<String>,
#[structopt(long, help = "Folder name to search in")]
folder: Option<String>,
#[structopt(
long,
help = "Display the notes in addition to the password"
)]
full: bool,
},
#[structopt(about = "Display the authenticator code for a given entry")]
Code {
#[structopt(help = "Name or UUID of the entry to display")]
name: String,
#[structopt(help = "Username of the entry to display")]
user: Option<String>,
#[structopt(long, help = "Folder name to search in")]
folder: Option<String>,
},
#[structopt(
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 $VISUAL or $EDITOR environment variables.
The first line will be saved as the password and the \
remainder will be saved as a note."
)]
Add {
#[structopt(help = "Name of the password entry")]
name: String,
#[structopt(help = "Username for the password entry")]
user: Option<String>,
#[structopt(
long,
help = "URI for the password entry",
multiple = true,
number_of_values = 1
)]
uri: Vec<String>,
#[structopt(long, help = "Folder for the password entry")]
folder: Option<String>,
},
#[structopt(
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.",
visible_alias = "gen",
group = structopt::clap::ArgGroup::with_name("password-type").args(&[
"no-symbols",
"only-numbers",
"nonconfusables",
"diceware",
])
)]
Generate {
#[structopt(help = "Length of the password to generate")]
len: usize,
#[structopt(help = "Name of the password entry")]
name: Option<String>,
#[structopt(help = "Username for the password entry")]
user: Option<String>,
#[structopt(
long,
help = "URI for the password entry",
multiple = true,
number_of_values = 1
)]
uri: Vec<String>,
#[structopt(long, help = "Folder for the password entry")]
folder: Option<String>,
#[structopt(
long = "no-symbols",
help = "Generate a password with no special characters"
)]
no_symbols: bool,
#[structopt(
long = "only-numbers",
help = "Generate a password consisting of only numbers"
)]
only_numbers: bool,
#[structopt(
long,
help = "Generate a password without visually similar \
characters (useful for passwords intended to be \
written down)"
)]
nonconfusables: bool,
#[structopt(
long,
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."
)]
diceware: bool,
},
#[structopt(
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 \
$VISUAL or $EDITOR environment variables. The first line \
will be saved as the password and the remainder will be saved \
as a note."
)]
Edit {
#[structopt(help = "Name or UUID of the password entry")]
name: String,
#[structopt(help = "Username for the password entry")]
user: Option<String>,
#[structopt(long, help = "Folder name to search in")]
folder: Option<String>,
},
#[structopt(about = "Remove a given entry", visible_alias = "rm")]
Remove {
#[structopt(help = "Name or UUID of the password entry")]
name: String,
#[structopt(help = "Username for the password entry")]
user: Option<String>,
#[structopt(long, help = "Folder name to search in")]
folder: Option<String>,
},
#[structopt(about = "View the password history for a given entry")]
History {
#[structopt(help = "Name or UUID of the password entry")]
name: String,
#[structopt(help = "Username for the password entry")]
user: Option<String>,
#[structopt(long, help = "Folder name to search in")]
folder: Option<String>,
},
#[structopt(about = "Lock the password database")]
Lock,
#[structopt(about = "Remove the local copy of the password database")]
Purge,
#[structopt(
name = "stop-agent",
about = "Terminate the background agent"
)]
StopAgent,
#[structopt(
name = "gen-completions",
about = "Generate completion script for the given shell"
)]
GenCompletions { shell: String },
}
impl Opt {
fn subcommand_name(&self) -> String {
match self {
Self::Config { config } => {
format!("config {}", config.subcommand_name())
}
Self::Login => "login".to_string(),
Self::Unlock => "unlock".to_string(),
Self::Unlocked => "unlocked".to_string(),
Self::Sync => "sync".to_string(),
Self::List { .. } => "list".to_string(),
Self::Get { .. } => "get".to_string(),
Self::Code { .. } => "code".to_string(),
Self::Add { .. } => "add".to_string(),
Self::Generate { .. } => "generate".to_string(),
Self::Edit { .. } => "edit".to_string(),
Self::Remove { .. } => "remove".to_string(),
Self::History { .. } => "history".to_string(),
Self::Lock => "lock".to_string(),
Self::Purge => "purge".to_string(),
Self::StopAgent => "stop-agent".to_string(),
Self::GenCompletions { .. } => "gen-completions".to_string(),
}
}
}
#[derive(Debug, structopt::StructOpt)]
enum Config {
#[structopt(about = "Show the values of all configuration settings")]
Show,
#[structopt(about = "Set a configuration option")]
Set {
#[structopt(help = "Configuration key to set")]
key: String,
#[structopt(help = "Value to set the configuration option to")]
value: String,
},
#[structopt(about = "Reset a configuration option to its default")]
Unset {
#[structopt(help = "Configuration key to unset")]
key: String,
},
}
impl Config {
fn subcommand_name(&self) -> String {
match self {
Self::Show => "show",
Self::Set { .. } => "set",
Self::Unset { .. } => "unset",
}
.to_string()
}
}
#[paw::main]
fn main(opt: Opt) {
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info"),
)
.format(|buf, record| {
if let Some((w, _)) = term_size::dimensions() {
let out = format!("{}: {}", record.level(), record.args());
writeln!(buf, "{}", textwrap::fill(&out, w - 1))
} else {
writeln!(buf, "{}: {}", record.level(), record.args())
}
})
.init();
let res = match &opt {
Opt::Config { config } => match config {
Config::Show => commands::config_show(),
Config::Set { key, value } => commands::config_set(&key, &value),
Config::Unset { key } => commands::config_unset(&key),
},
Opt::Login => commands::login(),
Opt::Unlock => commands::unlock(),
Opt::Unlocked => commands::unlocked(),
Opt::Sync => commands::sync(),
Opt::List { fields } => commands::list(&fields),
Opt::Get {
name,
user,
folder,
full,
} => commands::get(&name, user.as_deref(), folder.as_deref(), *full),
Opt::Code { name, user, folder } => {
commands::code(&name, user.as_deref(), folder.as_deref())
}
Opt::Add {
name,
user,
uri,
folder,
} => commands::add(
&name,
user.as_deref(),
uri.iter()
// XXX not sure what the ui for specifying the match type
// should be
.map(|uri| (uri.clone(), None))
.collect::<Vec<_>>(),
folder.as_deref(),
),
Opt::Generate {
len,
name,
user,
uri,
folder,
no_symbols,
only_numbers,
nonconfusables,
diceware,
} => {
let ty = if *no_symbols {
rbw::pwgen::Type::NoSymbols
} else if *only_numbers {
rbw::pwgen::Type::Numbers
} else if *nonconfusables {
rbw::pwgen::Type::NonConfusables
} else if *diceware {
rbw::pwgen::Type::Diceware
} else {
rbw::pwgen::Type::AllChars
};
commands::generate(
name.as_deref(),
user.as_deref(),
uri.iter()
// XXX not sure what the ui for specifying the match type
// should be
.map(|uri| (uri.clone(), None))
.collect::<Vec<_>>(),
folder.as_deref(),
*len,
ty,
)
}
Opt::Edit { name, user, folder } => {
commands::edit(&name, user.as_deref(), folder.as_deref())
}
Opt::Remove { name, user, folder } => {
commands::remove(&name, user.as_deref(), folder.as_deref())
}
Opt::History { name, user, folder } => {
commands::history(&name, user.as_deref(), folder.as_deref())
}
Opt::Lock => commands::lock(),
Opt::Purge => commands::purge(),
Opt::StopAgent => commands::stop_agent(),
Opt::GenCompletions { shell } => gen_completions(&shell),
}
.context(format!("rbw {}", opt.subcommand_name()));
if let Err(e) = res {
eprintln!("{:#}", e);
std::process::exit(1);
}
}
fn gen_completions(shell: &str) -> anyhow::Result<()> {
let shell = match shell {
"bash" => structopt::clap::Shell::Bash,
"zsh" => structopt::clap::Shell::Zsh,
"fish" => structopt::clap::Shell::Fish,
"powershell" => structopt::clap::Shell::PowerShell,
"elvish" => structopt::clap::Shell::Elvish,
_ => return Err(anyhow::anyhow!("unknown shell {}", shell)),
};
Opt::clap().gen_completions_to("rbw", shell, &mut std::io::stdout());
Ok(())
}