aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--src/actions.rs48
-rw-r--r--src/api.rs29
-rw-r--r--src/bin/rbw-agent/actions.rs2
-rw-r--r--src/bin/rbw/commands.rs224
-rw-r--r--src/bin/rbw/main.rs11
-rw-r--r--src/config.rs8
-rw-r--r--src/dirs.rs23
8 files changed, 302 insertions, 46 deletions
diff --git a/README.md b/README.md
index 5487d22..386c0f2 100644
--- a/README.md
+++ b/README.md
@@ -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
}
diff --git a/src/api.rs b/src/api.rs
index 75ab80e..5fa45cf 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -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(),
+ }
+}