diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | src/actions.rs | 48 | ||||
-rw-r--r-- | src/api.rs | 29 | ||||
-rw-r--r-- | src/bin/rbw-agent/actions.rs | 2 | ||||
-rw-r--r-- | src/bin/rbw/commands.rs | 224 | ||||
-rw-r--r-- | src/bin/rbw/main.rs | 11 | ||||
-rw-r--r-- | src/config.rs | 8 | ||||
-rw-r--r-- | src/dirs.rs | 23 |
8 files changed, 302 insertions, 46 deletions
@@ -82,7 +82,8 @@ out by running `rbw purge`, and you can explicitly lock the database by running functionality. Run `rbw get <name>` to get your passwords. If you also want to get the username -or the note associated, you can use the flag `--full`. +or the note associated, you can use the flag `--full`. You can also use the flag +`--field={field}` to get whatever default or custom field you want. *Note to users of the official Bitwarden server (at bitwarden.com)*: The official server has a tendency to detect command line traffic as bot traffic diff --git a/src/actions.rs b/src/actions.rs index f6cef56..df94c29 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,12 +1,17 @@ use crate::prelude::*; +fn api_client() -> Result<(crate::api::Client, crate::config::Config)> { + let config = crate::config::Config::load()?; + let client = + crate::api::Client::new(&config.base_url(), &config.identity_url(), &config.client_cert_path()); + Ok((client, config)) +} + pub async fn register( email: &str, apikey: crate::locked::ApiKey, ) -> Result<()> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, config) = api_client()?; client .register(email, &crate::config::device_id(&config).await?, &apikey) @@ -21,10 +26,7 @@ pub async fn login( two_factor_token: Option<&str>, two_factor_provider: Option<crate::api::TwoFactorProviderType>, ) -> Result<(String, String, u32, String)> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); - + let (client, config) = api_client()?; let iterations = client.prelogin(email).await?; let identity = crate::identity::Identity::new(email, &password, iterations)?; @@ -121,9 +123,7 @@ async fn sync_once( std::collections::HashMap<String, String>, Vec<crate::db::Entry>, )> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.sync(access_token).await } @@ -147,9 +147,7 @@ fn add_once( notes: Option<&str>, folder_id: Option<&str>, ) -> Result<()> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.add(access_token, name, data, notes, folder_id)?; Ok(()) } @@ -189,9 +187,7 @@ fn edit_once( folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], ) -> Result<()> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.edit( access_token, id, @@ -216,9 +212,7 @@ pub fn remove( } fn remove_once(access_token: &str, id: &str) -> Result<()> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.remove(access_token, id)?; Ok(()) } @@ -233,9 +227,7 @@ pub fn list_folders( } fn list_folders_once(access_token: &str) -> Result<Vec<(String, String)>> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.folders(access_token) } @@ -250,9 +242,7 @@ pub fn create_folder( } fn create_folder_once(access_token: &str, name: &str) -> Result<String> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.create_folder(access_token, name) } @@ -302,15 +292,11 @@ where } fn exchange_refresh_token(refresh_token: &str) -> Result<String> { - let config = crate::config::Config::load()?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.exchange_refresh_token(refresh_token) } async fn exchange_refresh_token_async(refresh_token: &str) -> Result<String> { - let config = crate::config::Config::load_async().await?; - let client = - crate::api::Client::new(&config.base_url(), &config.identity_url()); + let (client, _) = api_client()?; client.exchange_refresh_token_async(refresh_token).await } @@ -8,6 +8,9 @@ use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; +use std::fs::File; +use std::io::Read; + #[derive( serde_repr::Serialize_repr, serde_repr::Deserialize_repr, @@ -551,22 +554,36 @@ struct FoldersPostReq { pub struct Client { base_url: String, identity_url: String, + client_cert_path: String, } impl Client { #[must_use] - pub fn new(base_url: &str, identity_url: &str) -> Self { + pub fn new(base_url: &str, identity_url: &str, client_cert_path: &str) -> Self { Self { base_url: base_url.to_string(), identity_url: identity_url.to_string(), + client_cert_path: client_cert_path.to_string(), } } + fn reqwest_client(&self) -> reqwest::Client { + return if self.client_cert_path == "" { + reqwest::Client::new() + } else { + let mut buf = Vec::new(); + let mut f = File::open(self.client_cert_path.to_string()).expect("cert not found"); + f.read_to_end(&mut buf).expect("cert read failed"); + let pem = reqwest::Identity::from_pem(&buf).expect("invalid cert"); + reqwest::Client::builder().identity(pem).build().expect("wtv") + }; + } + pub async fn prelogin(&self, email: &str) -> Result<u32> { let prelogin = PreloginReq { email: email.to_string(), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client(); let res = client .post(&self.api_url("/accounts/prelogin")) .json(&prelogin) @@ -601,7 +618,7 @@ impl Client { two_factor_token: None, two_factor_provider: None, }; - let client = reqwest::Client::new(); + let client = self.reqwest_client(); let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) @@ -642,7 +659,7 @@ impl Client { #[allow(clippy::as_conversions)] two_factor_provider: two_factor_provider.map(|ty| ty as u32), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client(); let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) @@ -676,7 +693,7 @@ impl Client { std::collections::HashMap<String, String>, Vec<crate::db::Entry>, )> { - let client = reqwest::Client::new(); + let client = self.reqwest_client(); let res = client .get(&self.api_url("/sync")) .header("Authorization", format!("Bearer {}", access_token)) @@ -1072,7 +1089,7 @@ impl Client { client_id: "desktop".to_string(), refresh_token: refresh_token.to_string(), }; - let client = reqwest::Client::new(); + let client = self.reqwest_client(); let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 87a8276..61eb74b 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -367,7 +367,7 @@ pub async fn unlock( let password = rbw::pinentry::getpin( &config_pinentry().await?, "Master Password", - "Unlock the local database", + &format!("Unlock the local database for '{}'", rbw::dirs::profile()), err.as_deref(), tty, true, diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 0068efd..fd720cf 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -1,4 +1,6 @@ use anyhow::Context as _; +use std::io; +use std::io::prelude::Write; const MISSING_CONFIG_HELP: &str = "Before using rbw, you must configure the email address you would like to \ @@ -84,6 +86,215 @@ impl DecryptedCipher { } } + fn display_field(&self, desc: &str, field: &str) { + // Convert the field name to lowercase + let field = field.to_lowercase(); + let field = field.as_str(); + match &self.data { + DecryptedData::Login { + username, + totp, + uris, + .. + } => match field { + "notes" => { + if let Some(notes) = &self.notes { + println!("{}", notes); + } + } + "username" | "user" => { + display_field("Username", username.as_deref()); + } + "totp" | "code" => { + if let Some(totp) = totp { + if let Ok(code) = generate_totp(&totp) { + println!("{}", code); + } + } + } + "uris" | "urls" | "sites" => { + if let Some(uris) = uris { + for uri in uris { + display_field("URI", Some(uri.uri.as_str())); + } + } + } + "password" => { + self.display_short(&desc); + } + _ => { + for f in &self.fields { + if f.name + .as_ref() + .unwrap() + .to_lowercase() + .as_str() + .contains(field) + { + display_field( + f.name.as_deref().unwrap_or("(null)"), + Some(f.value.as_deref().unwrap_or("")), + ); + } + } + } + }, + DecryptedData::Card { + cardholder_name, + brand, + exp_month, + exp_year, + code, + .. + } => match field { + "number" | "card" => { + self.display_short(&desc); + } + "exp" => { + if let (Some(month), Some(year)) = (exp_month, exp_year) { + display_field( + "Exp", + Some(format!("{}/{}", month, year).as_str()), + ); + } + } + "exp_month" | "month" => { + display_field("Month", exp_month.as_deref()); + } + "exp_year" | "year" => { + display_field("Year", exp_year.as_deref()); + } + "cvv" => { + display_field("CVV", code.as_deref()); + } + "name" | "cardholder" => { + display_field("Name", cardholder_name.as_deref()); + } + "brand" | "type" => { + display_field("Brand", brand.as_deref()); + } + "notes" => { + if let Some(notes) = &self.notes { + println!("{}", notes); + } + } + _ => { + for f in &self.fields { + if f.name + .as_ref() + .unwrap() + .to_lowercase() + .as_str() + .contains(field) + { + display_field( + f.name.as_deref().unwrap_or("(null)"), + Some(f.value.as_deref().unwrap_or("")), + ); + } + } + } + }, + DecryptedData::Identity { + address1, + address2, + address3, + city, + state, + postal_code, + country, + phone, + email, + ssn, + license_number, + passport_number, + username, + .. + } => match field { + "name" => { + self.display_short(&desc); + } + "email" => { + display_field("Email", email.as_deref()); + } + "address" => { + display_field("Address", address1.as_deref()); + display_field("Address", address2.as_deref()); + display_field("Address", address3.as_deref()); + } + "city" => { + display_field("City", city.as_deref()); + } + "state" => { + display_field("State", state.as_deref()); + } + "postcode" | "zipcode" | "zip" => { + display_field("Zip", postal_code.as_deref()); + } + "country" => { + display_field("Country", country.as_deref()); + } + "phone" => { + display_field("Phone", phone.as_deref()); + } + "ssn" => { + display_field("SSN", ssn.as_deref()); + } + "license" => { + display_field("License", license_number.as_deref()); + } + "passport" => { + display_field("Passport", passport_number.as_deref()); + } + "username" => { + display_field("Username", username.as_deref()); + } + "notes" => { + if let Some(notes) = &self.notes { + println!("{}", notes); + } + } + _ => { + for f in &self.fields { + if f.name + .as_ref() + .unwrap() + .to_lowercase() + .as_str() + .contains(field) + { + display_field( + f.name.as_deref().unwrap_or("(null)"), + Some(f.value.as_deref().unwrap_or("")), + ); + } + } + } + }, + DecryptedData::SecureNote {} => match field { + "note" | "notes" => { + self.display_short(desc); + } + _ => { + for f in &self.fields { + if f.name + .as_ref() + .unwrap() + .to_lowercase() + .as_str() + .contains(field) + { + display_field( + f.name.as_deref().unwrap_or("(null)"), + Some(f.value.as_deref().unwrap_or("")), + ); + } + } + } + }, + } + } + fn display_long(&self, desc: &str) { match &self.data { DecryptedData::Login { @@ -405,6 +616,7 @@ pub fn config_set(key: &str, value: &str) -> anyhow::Result<()> { "email" => config.email = Some(value.to_string()), "base_url" => config.base_url = Some(value.to_string()), "identity_url" => config.identity_url = Some(value.to_string()), + "client_cert_path" => config.client_cert_path = Some(value.to_string()), "lock_timeout" => { let timeout = value .parse() @@ -437,6 +649,7 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> { "email" => config.email = None, "base_url" => config.base_url = None, "identity_url" => config.identity_url = None, + "client_cert_path" => config.client_cert_path = None, "lock_timeout" => { config.lock_timeout = rbw::config::default_lock_timeout(); } @@ -530,7 +743,13 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> { ), }) .collect(); - println!("{}", values.join("\t")); + + // write to stdout but don't panic when pipe get's closed + // this happens when piping stdout in a shell + match writeln!(&mut io::stdout(), "{}", values.join("\t")) { + Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => Ok(()), + res => res, + }?; } Ok(()) @@ -540,6 +759,7 @@ pub fn get( name: &str, user: Option<&str>, folder: Option<&str>, + field: Option<&str>, full: bool, ) -> anyhow::Result<()> { unlock()?; @@ -556,6 +776,8 @@ pub fn get( .with_context(|| format!("couldn't find entry for '{}'", desc))?; if full { decrypted.display_long(&desc); + } else if field != None { + decrypted.display_field(&desc, field.unwrap()); } else { decrypted.display_short(&desc); } diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 5730298..f56a5b1 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -75,6 +75,8 @@ enum Opt { user: Option<String>, #[structopt(long, help = "Folder name to search in")] folder: Option<String>, + #[structopt(short, long, help = "Field to get")] + field: Option<String>, #[structopt( long, help = "Display the notes in addition to the password" @@ -317,8 +319,15 @@ fn main(opt: Opt) { name, user, folder, + field, full, - } => commands::get(name, user.as_deref(), folder.as_deref(), *full), + } => commands::get( + name, + user.as_deref(), + folder.as_deref(), + field.as_deref(), + *full, + ), Opt::Code { name, user, folder } => { commands::code(name, user.as_deref(), folder.as_deref()) } diff --git a/src/config.rs b/src/config.rs index 23ef765..baa855b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ pub struct Config { pub lock_timeout: u64, #[serde(default = "default_pinentry")] pub pinentry: String, + pub client_cert_path: Option<String>, // backcompat, no longer generated in new configs #[serde(skip_serializing)] pub device_id: Option<String>, @@ -25,6 +26,7 @@ impl Default for Config { identity_url: None, lock_timeout: default_lock_timeout(), pinentry: default_pinentry(), + client_cert_path: None, device_id: None, } } @@ -149,6 +151,12 @@ impl Config { } #[must_use] + pub fn client_cert_path(&self) -> String { + self.client_cert_path.clone() + .unwrap_or_else(|| "".to_string()) + } + + #[must_use] pub fn server_name(&self) -> String { self.base_url .clone() diff --git a/src/dirs.rs b/src/dirs.rs index 5ebeaa2..429f8bd 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -79,32 +79,45 @@ pub fn socket_file() -> std::path::PathBuf { #[must_use] fn config_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.config_dir().to_path_buf() } #[must_use] fn cache_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.cache_dir().to_path_buf() } #[must_use] fn data_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); project_dirs.data_dir().to_path_buf() } #[must_use] fn runtime_dir() -> std::path::PathBuf { - let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap(); + let project_dirs = + directories::ProjectDirs::from("", "", &profile()).unwrap(); match project_dirs.runtime_dir() { Some(dir) => dir.to_path_buf(), None => format!( - "{}/rbw-{}", + "{}/{}-{}", std::env::temp_dir().to_string_lossy(), + &profile(), nix::unistd::getuid().as_raw() ) .into(), } } + +#[must_use] +pub fn profile() -> String { + match std::env::var("RBW_PROFILE") { + Ok(profile) if !profile.is_empty() => format!("rbw-{}", profile), + _ => "rbw".to_string(), + } +} |