aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-05-03 18:07:40 -0400
committerJesse Luehrs <doy@tozt.net>2020-05-03 20:46:45 -0400
commitdcca2c702999997ebdc2ae7acd1228edf7333014 (patch)
tree08c54f5ec850739665d19a5deb7f3ad9a6231cf7
parent2feed7e2e9367c935aeb669daab66a63cff2f6c0 (diff)
downloadrbw-dcca2c702999997ebdc2ae7acd1228edf7333014.tar.gz
rbw-dcca2c702999997ebdc2ae7acd1228edf7333014.zip
start adding support for different entry types
-rw-r--r--CHANGELOG.md2
-rw-r--r--src/actions.rs37
-rw-r--r--src/api.rs373
-rw-r--r--src/bin/rbw/commands.rs620
-rw-r--r--src/bin/rbw/main.rs2
-rw-r--r--src/db.rs41
-rw-r--r--src/lib.rs2
7 files changed, 868 insertions, 209 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b76d3e..e2c3837 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@
bitwarden servers with `rbw config set base_url` without needing to
redownload the password database each time.
* `rbw config unset` to reset configuration items back to the default
+* `rbw list` and `rbw get` now support card, identity, and secure note entry
+ types
### Fixed
diff --git a/src/actions.rs b/src/actions.rs
index a4b8021..736f43e 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -113,30 +113,20 @@ pub fn add(
access_token: &str,
refresh_token: &str,
name: &str,
- username: Option<&str>,
- password: Option<&str>,
+ data: &crate::db::EntryData,
notes: Option<&str>,
uris: &[String],
folder_id: Option<&str>,
) -> Result<(Option<String>, ())> {
with_exchange_refresh_token(access_token, refresh_token, |access_token| {
- add_once(
- access_token,
- name,
- username,
- password,
- notes,
- uris,
- folder_id,
- )
+ add_once(access_token, name, data, notes, uris, folder_id)
})
}
fn add_once(
access_token: &str,
name: &str,
- username: Option<&str>,
- password: Option<&str>,
+ data: &crate::db::EntryData,
notes: Option<&str>,
uris: &[String],
folder_id: Option<&str>,
@@ -147,8 +137,7 @@ fn add_once(
client.add(
access_token,
name,
- username,
- password,
+ data,
notes,
uris,
folder_id.as_deref(),
@@ -161,13 +150,12 @@ pub fn edit(
refresh_token: &str,
id: &str,
name: &str,
- username: Option<&str>,
- password: Option<&str>,
+ data: &crate::db::EntryData,
notes: Option<&str>,
history: &[crate::db::HistoryEntry],
) -> Result<(Option<String>, ())> {
with_exchange_refresh_token(access_token, refresh_token, |access_token| {
- edit_once(access_token, id, name, username, password, notes, history)
+ edit_once(access_token, id, name, data, notes, history)
})
}
@@ -175,23 +163,14 @@ fn edit_once(
access_token: &str,
id: &str,
name: &str,
- username: Option<&str>,
- password: Option<&str>,
+ data: &crate::db::EntryData,
notes: 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());
- client.edit(
- access_token,
- id,
- name,
- username,
- password,
- notes,
- history,
- )?;
+ client.edit(access_token, id, name, data, notes, history)?;
Ok(())
}
diff --git a/src/api.rs b/src/api.rs
index 6218f6f..3eef840 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -90,7 +90,13 @@ struct SyncResCipher {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Login")]
- login: Option<SyncResLogin>,
+ login: Option<CipherLogin>,
+ #[serde(rename = "Card")]
+ card: Option<CipherCard>,
+ #[serde(rename = "Identity")]
+ identity: Option<CipherIdentity>,
+ #[serde(rename = "SecureNote")]
+ secure_note: Option<CipherSecureNote>,
#[serde(rename = "Notes")]
notes: Option<String>,
#[serde(rename = "PasswordHistory")]
@@ -98,47 +104,80 @@ struct SyncResCipher {
}
impl SyncResCipher {
- // TODO: handle other kinds of entries other than login
fn to_entry(
&self,
folders: &[SyncResFolder],
) -> Option<crate::db::Entry> {
- if let Some(login) = &self.login {
- let history = if let Some(history) = &self.password_history {
- history
- .iter()
- .map(|entry| crate::db::HistoryEntry {
- last_used_date: entry.last_used_date.clone(),
- password: entry.password.clone(),
- })
- .collect()
- } else {
- vec![]
- };
- let folder = if let Some(folder_id) = &self.folder_id {
- let mut folder_name = None;
- for folder in folders {
- if &folder.id == folder_id {
- folder_name = Some(folder.name.clone());
- }
+ let history = if let Some(history) = &self.password_history {
+ history
+ .iter()
+ .map(|entry| crate::db::HistoryEntry {
+ last_used_date: entry.last_used_date.clone(),
+ password: entry.password.clone(),
+ })
+ .collect()
+ } else {
+ vec![]
+ };
+ let folder = if let Some(folder_id) = &self.folder_id {
+ let mut folder_name = None;
+ for folder in folders {
+ if &folder.id == folder_id {
+ folder_name = Some(folder.name.clone());
}
- folder_name
- } else {
- None
- };
- Some(crate::db::Entry {
- id: self.id.clone(),
- org_id: self.organization_id.clone(),
- folder,
- name: self.name.clone(),
+ }
+ folder_name
+ } else {
+ None
+ };
+ let data = if let Some(login) = &self.login {
+ crate::db::EntryData::Login {
username: login.username.clone(),
password: login.password.clone(),
- notes: self.notes.clone(),
- history,
- })
+ }
+ } else if let Some(card) = &self.card {
+ crate::db::EntryData::Card {
+ cardholder_name: card.cardholder_name.clone(),
+ number: card.number.clone(),
+ brand: card.brand.clone(),
+ exp_month: card.exp_month.clone(),
+ exp_year: card.exp_year.clone(),
+ code: card.code.clone(),
+ }
+ } else if let Some(identity) = &self.identity {
+ crate::db::EntryData::Identity {
+ title: identity.title.clone(),
+ first_name: identity.first_name.clone(),
+ middle_name: identity.middle_name.clone(),
+ last_name: identity.last_name.clone(),
+ address1: identity.address1.clone(),
+ address2: identity.address2.clone(),
+ address3: identity.address3.clone(),
+ city: identity.city.clone(),
+ state: identity.state.clone(),
+ postal_code: identity.postal_code.clone(),
+ country: identity.country.clone(),
+ phone: identity.phone.clone(),
+ email: identity.email.clone(),
+ ssn: identity.ssn.clone(),
+ license_number: identity.license_number.clone(),
+ passport_number: identity.passport_number.clone(),
+ username: identity.username.clone(),
+ }
+ } else if let Some(_secure_note) = &self.secure_note {
+ crate::db::EntryData::SecureNote
} else {
- None
- }
+ return None;
+ };
+ Some(crate::db::Entry {
+ id: self.id.clone(),
+ org_id: self.organization_id.clone(),
+ folder,
+ name: self.name.clone(),
+ data,
+ notes: self.notes.clone(),
+ history,
+ })
}
}
@@ -169,13 +208,78 @@ struct SyncResFolder {
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
-struct SyncResLogin {
+struct CipherLogin {
#[serde(rename = "Username")]
username: Option<String>,
#[serde(rename = "Password")]
password: Option<String>,
+ uris: Option<Vec<CipherLoginUri>>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
+struct CipherLoginUri {
+ uri: String,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
+struct CipherCard {
+ #[serde(rename = "CardholderName")]
+ cardholder_name: Option<String>,
+ #[serde(rename = "Number")]
+ number: Option<String>,
+ #[serde(rename = "Brand")]
+ brand: Option<String>,
+ #[serde(rename = "ExpMonth")]
+ exp_month: Option<String>,
+ #[serde(rename = "ExpYear")]
+ exp_year: Option<String>,
+ #[serde(rename = "Code")]
+ code: Option<String>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
+struct CipherIdentity {
+ #[serde(rename = "Title")]
+ title: Option<String>,
+ #[serde(rename = "FirstName")]
+ first_name: Option<String>,
+ #[serde(rename = "MiddleName")]
+ middle_name: Option<String>,
+ #[serde(rename = "LastName")]
+ last_name: Option<String>,
+ #[serde(rename = "Address1")]
+ address1: Option<String>,
+ #[serde(rename = "Address2")]
+ address2: Option<String>,
+ #[serde(rename = "Address3")]
+ address3: Option<String>,
+ #[serde(rename = "City")]
+ city: Option<String>,
+ #[serde(rename = "State")]
+ state: Option<String>,
+ #[serde(rename = "PostalCode")]
+ postal_code: Option<String>,
+ #[serde(rename = "Country")]
+ country: Option<String>,
+ #[serde(rename = "Phone")]
+ phone: Option<String>,
+ #[serde(rename = "Email")]
+ email: Option<String>,
+ #[serde(rename = "SSN")]
+ ssn: Option<String>,
+ #[serde(rename = "LicenseNumber")]
+ license_number: Option<String>,
+ #[serde(rename = "PassportNumber")]
+ passport_number: Option<String>,
+ #[serde(rename = "Username")]
+ username: Option<String>,
}
+// this is just a name and some notes, both of which are already on the cipher
+// object
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
+struct CipherSecureNote {}
+
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct SyncResPasswordHistory {
#[serde(rename = "LastUsedDate")]
@@ -192,19 +296,11 @@ struct CiphersPostReq {
folder_id: Option<String>,
name: String,
notes: Option<String>,
- login: CiphersPostReqLogin,
-}
-
-#[derive(serde::Serialize, Debug)]
-struct CiphersPostReqLogin {
- username: Option<String>,
- password: Option<String>,
- uris: Vec<CiphersPostReqLoginUri>,
-}
-
-#[derive(serde::Serialize, Debug)]
-struct CiphersPostReqLoginUri {
- uri: String,
+ login: Option<CipherLogin>,
+ card: Option<CipherCard>,
+ identity: Option<CipherIdentity>,
+ #[serde(rename = "secureNote")]
+ secure_note: Option<CipherSecureNote>,
}
#[derive(serde::Serialize, Debug)]
@@ -213,7 +309,11 @@ struct CiphersPutReq {
ty: u32, // XXX what are the valid types?
name: String,
notes: Option<String>,
- login: CiphersPutReqLogin,
+ login: Option<CipherLogin>,
+ card: Option<CipherCard>,
+ identity: Option<CipherIdentity>,
+ #[serde(rename = "secureNote")]
+ secure_note: Option<CipherSecureNote>,
#[serde(rename = "passwordHistory")]
password_history: Vec<CiphersPutReqHistory>,
}
@@ -380,26 +480,98 @@ impl Client {
&self,
access_token: &str,
name: &str,
- username: Option<&str>,
- password: Option<&str>,
+ data: &crate::db::EntryData,
notes: Option<&str>,
uris: &[String],
folder_id: Option<&str>,
) -> Result<()> {
- let req = CiphersPostReq {
+ let mut req = CiphersPostReq {
ty: 1,
folder_id: folder_id.map(std::string::ToString::to_string),
name: name.to_string(),
notes: notes.map(std::string::ToString::to_string),
- login: CiphersPostReqLogin {
- username: username.map(std::string::ToString::to_string),
- password: password.map(std::string::ToString::to_string),
- uris: uris
- .iter()
- .map(|s| CiphersPostReqLoginUri { uri: s.to_string() })
- .collect(),
- },
+ login: None,
+ card: None,
+ identity: None,
+ secure_note: None,
};
+ match data {
+ crate::db::EntryData::Login { username, password } => {
+ let uris = if uris.is_empty() {
+ None
+ } else {
+ Some(
+ uris.iter()
+ .map(|s| CipherLoginUri { uri: s.to_string() })
+ .collect(),
+ )
+ };
+ req.login = Some(CipherLogin {
+ username: username.clone(),
+ password: password.clone(),
+ uris,
+ })
+ }
+ crate::db::EntryData::Card {
+ cardholder_name,
+ number,
+ brand,
+ exp_month,
+ exp_year,
+ code,
+ } => {
+ req.card = Some(CipherCard {
+ cardholder_name: cardholder_name.clone(),
+ number: number.clone(),
+ brand: brand.clone(),
+ exp_month: exp_month.clone(),
+ exp_year: exp_year.clone(),
+ code: code.clone(),
+ });
+ }
+ crate::db::EntryData::Identity {
+ title,
+ first_name,
+ middle_name,
+ last_name,
+ address1,
+ address2,
+ address3,
+ city,
+ state,
+ postal_code,
+ country,
+ phone,
+ email,
+ ssn,
+ license_number,
+ passport_number,
+ username,
+ } => {
+ req.identity = Some(CipherIdentity {
+ title: title.clone(),
+ first_name: first_name.clone(),
+ middle_name: middle_name.clone(),
+ last_name: last_name.clone(),
+ address1: address1.clone(),
+ address2: address2.clone(),
+ address3: address3.clone(),
+ city: city.clone(),
+ state: state.clone(),
+ postal_code: postal_code.clone(),
+ country: country.clone(),
+ phone: phone.clone(),
+ email: email.clone(),
+ ssn: ssn.clone(),
+ license_number: license_number.clone(),
+ passport_number: passport_number.clone(),
+ username: username.clone(),
+ });
+ }
+ crate::db::EntryData::SecureNote {} => {
+ req.secure_note = Some(CipherSecureNote {});
+ }
+ }
let client = reqwest::blocking::Client::new();
let res = client
.post(&self.api_url("/ciphers"))
@@ -423,19 +595,18 @@ impl Client {
access_token: &str,
id: &str,
name: &str,
- username: Option<&str>,
- password: Option<&str>,
+ data: &crate::db::EntryData,
notes: Option<&str>,
history: &[crate::db::HistoryEntry],
) -> Result<()> {
- let req = CiphersPutReq {
+ let mut req = CiphersPutReq {
ty: 1,
name: name.to_string(),
notes: notes.map(std::string::ToString::to_string),
- login: CiphersPutReqLogin {
- username: username.map(std::string::ToString::to_string),
- password: password.map(std::string::ToString::to_string),
- },
+ login: None,
+ card: None,
+ identity: None,
+ secure_note: None,
password_history: history
.iter()
.map(|entry| CiphersPutReqHistory {
@@ -444,6 +615,74 @@ impl Client {
})
.collect(),
};
+ match data {
+ crate::db::EntryData::Login { username, password } => {
+ req.login = Some(CipherLogin {
+ username: username.clone(),
+ password: password.clone(),
+ uris: None,
+ });
+ }
+ crate::db::EntryData::Card {
+ cardholder_name,
+ number,
+ brand,
+ exp_month,
+ exp_year,
+ code,
+ } => {
+ req.card = Some(CipherCard {
+ cardholder_name: cardholder_name.clone(),
+ number: number.clone(),
+ brand: brand.clone(),
+ exp_month: exp_month.clone(),
+ exp_year: exp_year.clone(),
+ code: code.clone(),
+ });
+ }
+ crate::db::EntryData::Identity {
+ title,
+ first_name,
+ middle_name,
+ last_name,
+ address1,
+ address2,
+ address3,
+ city,
+ state,
+ postal_code,
+ country,
+ phone,
+ email,
+ ssn,
+ license_number,
+ passport_number,
+ username,
+ } => {
+ req.identity = Some(CipherIdentity {
+ title: title.clone(),
+ first_name: first_name.clone(),
+ middle_name: middle_name.clone(),
+ last_name: last_name.clone(),
+ address1: address1.clone(),
+ address2: address2.clone(),
+ address3: address3.clone(),
+ city: city.clone(),
+ state: state.clone(),
+ postal_code: postal_code.clone(),
+ country: country.clone(),
+ phone: phone.clone(),
+ email: email.clone(),
+ ssn: ssn.clone(),
+ license_number: license_number.clone(),
+ passport_number: passport_number.clone(),
+ username: username.clone(),
+ });
+ }
+ crate::db::EntryData::SecureNote {} => {
+ req.secure_note = Some(CipherSecureNote {});
+ }
+ }
let client = reqwest::blocking::Client::new();
let res = client
.put(&self.api_url(&format!("/ciphers/{}", id)))
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs
index a2e9f9d..ab0fd21 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -6,12 +6,265 @@ struct DecryptedCipher {
id: String,
folder: Option<String>,
name: String,
- username: Option<String>,
- password: Option<String>,
+ data: DecryptedData,
notes: Option<String>,
history: Vec<DecryptedHistoryEntry>,
}
+impl DecryptedCipher {
+ fn display_short(&self, desc: &str) -> bool {
+ match &self.data {
+ DecryptedData::Login { password, .. } => {
+ if let Some(password) = password {
+ println!("{}", password);
+ true
+ } else {
+ eprintln!("entry for '{}' had no password", desc);
+ false
+ }
+ }
+ DecryptedData::Card { number, .. } => {
+ if let Some(number) = number {
+ println!("{}", number);
+ true
+ } else {
+ eprintln!("entry for '{}' had no card number", desc);
+ false
+ }
+ }
+ DecryptedData::Identity {
+ title,
+ first_name,
+ middle_name,
+ last_name,
+ ..
+ } => {
+ let names: Vec<_> =
+ [title, first_name, middle_name, last_name]
+ .iter()
+ .copied()
+ .cloned()
+ .filter_map(|x| x)
+ .collect();
+ if names.is_empty() {
+ eprintln!("entry for '{}' had no name", desc);
+ false
+ } else {
+ println!("{}", names.join(" "));
+ true
+ }
+ }
+ DecryptedData::SecureNote {} => {
+ if let Some(notes) = &self.notes {
+ println!("{}", notes);
+ true
+ } else {
+ eprintln!("entry for '{}' had no notes", desc);
+ false
+ }
+ }
+ }
+ }
+
+ fn display_long(&self, desc: &str) {
+ match &self.data {
+ DecryptedData::Login { .. } => {
+ let displayed = self.display_short(desc);
+
+ if let Some(notes) = &self.notes {
+ if displayed {
+ println!();
+ }
+ println!("{}", notes);
+ }
+ }
+ DecryptedData::Card {
+ cardholder_name,
+ brand,
+ exp_month,
+ exp_year,
+ code,
+ ..
+ } => {
+ let mut displayed = self.display_short(desc);
+
+ if let (Some(exp_month), Some(exp_year)) =
+ (exp_month, exp_year)
+ {
+ println!("Expiration: {}/{}", exp_month, exp_year);
+ displayed = true;
+ }
+ displayed |= self.display_field("CVV", code.as_deref());
+ displayed |=
+ self.display_field("Name", cardholder_name.as_deref());
+ displayed |= self.display_field("Brand", brand.as_deref());
+
+ if let Some(notes) = &self.notes {
+ if displayed {
+ println!();
+ }
+ println!("{}", notes);
+ }
+ }
+ DecryptedData::Identity {
+ address1,
+ address2,
+ address3,
+ city,
+ state,
+ postal_code,
+ country,
+ phone,
+ email,
+ ssn,
+ license_number,
+ passport_number,
+ username,
+ ..
+ } => {
+ let mut displayed = self.display_short(desc);
+
+ displayed |=
+ self.display_field("Address", address1.as_deref());
+ displayed |=
+ self.display_field("Address", address2.as_deref());
+ displayed |=
+ self.display_field("Address", address3.as_deref());
+ displayed |= self.display_field("City", city.as_deref());
+ displayed |= self.display_field("State", state.as_deref());
+ displayed |=
+ self.display_field("Postcode", postal_code.as_deref());
+ displayed |=
+ self.display_field("Country", country.as_deref());
+ displayed |= self.display_field("Phone", phone.as_deref());
+ displayed |= self.display_field("Email", email.as_deref());
+ displayed |= self.display_field("SSN", ssn.as_deref());
+ displayed |=
+ self.display_field("License", license_number.as_deref());
+ displayed |= self
+ .display_field("Passport", passport_number.as_deref());
+ displayed |=
+ self.display_field("Username", username.as_deref());
+
+ if let Some(notes) = &self.notes {
+ if displayed {
+ println!();
+ }
+ println!("{}", notes);
+ }
+ }
+ DecryptedData::SecureNote {} => {
+ self.display_short(desc);
+ }
+ }
+ }
+
+ fn display_field(&self, name: &str, field: Option<&str>) -> bool {
+ if let Some(field) = field {
+ println!("{}: {}", name, field);
+ true
+ } else {
+ false
+ }
+ }
+
+ fn display_name(&self) -> String {
+ match &self.data {
+ DecryptedData::Login { username, .. } => {
+ if let Some(username) = username {
+ format!("{}@{}", username, self.name)
+ } else {
+ self.name.clone()
+ }
+ }
+ _ => self.name.clone(),
+ }
+ }
+
+ fn exact_match(&self, name: &str, username: Option<&str>) -> bool {
+ if name != self.name {
+ return false;
+ }
+
+ if let Some(given_username) = username {
+ match &self.data {
+ DecryptedData::Login { username, .. } => {
+ if let Some(found_username) = username {
+ return given_username == found_username;
+ }
+ return false;
+ }
+ _ => {
+ // not sure what else to do here, but open to suggestions
+ return false;
+ }
+ }
+ }
+
+ true
+ }
+
+ fn partial_match(&self, name: &str, username: Option<&str>) -> bool {
+ if !self.name.contains(name) {
+ return false;
+ }
+
+ if let Some(given_username) = username {
+ match &self.data {
+ DecryptedData::Login { username, .. } => {
+ if let Some(found_username) = username {
+ return found_username.contains(given_username);
+ }
+ return false;
+ }
+ _ => {
+ // not sure what else to do here, but open to suggestions
+ return false;
+ }
+ }
+ }
+
+ true
+ }
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(Eq, PartialEq))]
+enum DecryptedData {
+ Login {
+ username: Option<String>,
+ password: Option<String>,
+ },
+ Card {
+ cardholder_name: Option<String>,
+ number: Option<String>,
+ brand: Option<String>,
+ exp_month: Option<String>,
+ exp_year: Option<String>,
+ code: Option<String>,
+ },
+ Identity {
+ title: Option<String>,
+ first_name: Option<String>,
+ middle_name: Option<String>,
+ last_name: Option<String>,
+ address1: Option<String>,
+ address2: Option<String>,
+ address3: Option<String>,
+ city: Option<String>,
+ state: Option<String>,
+ postal_code: Option<String>,
+ country: Option<String>,
+ phone: Option<String>,
+ email: Option<String>,
+ ssn: Option<String>,
+ license_number: Option<String>,
+ passport_number: Option<String>,
+ username: Option<String>,
+ },
+ SecureNote,
+}
+
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
struct DecryptedHistoryEntry {
@@ -139,11 +392,13 @@ pub fn list(fields: &[&str]) -> anyhow::Result<()> {
.map(|field| match field {
ListField::Name => cipher.name.clone(),
ListField::Id => cipher.id.clone(),
- ListField::User => cipher
- .username
- .as_ref()
- .map(std::string::ToString::to_string)
- .unwrap_or_else(|| "".to_string()),
+ ListField::User => match &cipher.data {
+ DecryptedData::Login { username, .. } => username
+ .as_ref()
+ .map(std::string::ToString::to_string)
+ .unwrap_or_else(|| "".to_string()),
+ _ => "".to_string(),
+ },
ListField::Folder => cipher
.folder
.as_ref()
@@ -171,16 +426,10 @@ pub fn get(name: &str, user: Option<&str>, full: bool) -> anyhow::Result<()> {
let (_, decrypted) = find_entry(&db, name, user)
.with_context(|| format!("couldn't find entry for '{}'", desc))?;
- if let Some(password) = decrypted.password {
- println!("{}", password);
- } else {
- eprintln!("entry for '{}' had no password", desc);
- }
-
if full {
- if let Some(notes) = decrypted.notes {
- println!("\n{}", notes);
- }
+ decrypted.display_long(&desc);
+ } else {
+ decrypted.display_short(&desc);
}
Ok(())
@@ -260,8 +509,7 @@ pub fn add(
&access_token,
&refresh_token,
&name,
- username.as_deref(),
- password.as_deref(),
+ &rbw::db::EntryData::Login { username, password },
notes.as_deref(),
&uris,
folder_id.as_deref(),
@@ -347,8 +595,10 @@ pub fn generate(
&access_token,
&refresh_token,
&name,
- username.as_deref(),
- Some(&password),
+ &rbw::db::EntryData::Login {
+ username,
+ password: Some(password),
+ },
None,
&uris,
folder_id.as_deref(),
@@ -381,38 +631,57 @@ pub fn edit(name: &str, username: Option<&str>) -> anyhow::Result<()> {
let (entry, decrypted) = find_entry(&db, name, username)
.with_context(|| format!("couldn't find entry for '{}'", desc))?;
- let mut contents =
- format!("{}\n", decrypted.password.unwrap_or_else(String::new));
- if let Some(notes) = decrypted.notes {
- contents.push_str(&format!("\n{}\n", notes));
- }
-
- let contents = rbw::edit::edit(&contents, HELP)?;
+ let (data, notes, history) = match &decrypted.data {
+ DecryptedData::Login { password, .. } => {
+ let mut contents =
+ format!("{}\n", password.as_deref().unwrap_or(""));
+ if let Some(notes) = decrypted.notes {
+ contents.push_str(&format!("\n{}\n", notes));
+ }
- let (password, notes) = parse_editor(&contents);
- let password = password
- .map(|password| crate::actions::encrypt(&password, None))
- .transpose()?;
- let notes = notes
- .map(|notes| crate::actions::encrypt(&notes, None))
- .transpose()?;
- let mut history = entry.history.clone();
- let new_history_entry = rbw::db::HistoryEntry {
- last_used_date: format!(
- "{}",
- humantime::format_rfc3339(std::time::SystemTime::now())
- ),
- password: entry.password.unwrap_or_else(String::new),
+ let contents = rbw::edit::edit(&contents, HELP)?;
+
+ let (password, notes) = parse_editor(&contents);
+ let password = password
+ .map(|password| crate::actions::encrypt(&password, None))
+ .transpose()?;
+ let notes = notes
+ .map(|notes| crate::actions::encrypt(&notes, None))
+ .transpose()?;
+ let mut history = entry.history.clone();
+ let (entry_username, entry_password) = match &entry.data {
+ rbw::db::EntryData::Login { username, password } => {
+ (username, password)
+ }
+ _ => unreachable!(),
+ };
+ let new_history_entry = rbw::db::HistoryEntry {
+ last_used_date: format!(
+ "{}",
+ humantime::format_rfc3339(std::time::SystemTime::now())
+ ),
+ password: entry_password.clone().unwrap_or_else(String::new),
+ };
+ history.insert(0, new_history_entry);
+ let data = rbw::db::EntryData::Login {
+ username: entry_username.clone(),
+ password,
+ };
+ (data, notes, history)
+ }
+ _ => {
+ return Err(anyhow::anyhow!(
+ "modifications are only supported for login entries"
+ ));
+ }
};
- history.insert(0, new_history_entry);
if let (Some(access_token), ()) = rbw::actions::edit(
&access_token,
&refresh_token,
&entry.id,
&entry.name,
- entry.username.as_deref(),
- password.as_deref(),
+ &data,
notes.as_deref(),
&history,
)? {
@@ -596,12 +865,7 @@ fn find_entry_raw(
.iter()
.cloned()
.filter(|(_, decrypted_cipher)| {
- name == decrypted_cipher.name
- && if let Some(username) = username {
- decrypted_cipher.username.as_deref() == Some(username)
- } else {
- true
- }
+ decrypted_cipher.exact_match(name, username)
})
.collect();
@@ -610,18 +874,7 @@ fn find_entry_raw(
.iter()
.cloned()
.filter(|(_, decrypted_cipher)| {
- decrypted_cipher.name.contains(name)
- && if let Some(username) = username {
- if let Some(decrypted_username) =
- &decrypted_cipher.username
- {
- decrypted_username.contains(username)
- } else {
- false
- }
- } else {
- true
- }
+ decrypted_cipher.partial_match(name, username)
})
.collect();
@@ -630,13 +883,7 @@ fn find_entry_raw(
} else if partial_matches.len() > 1 {
let entries: Vec<String> = partial_matches
.iter()
- .map(|(_, decrypted)| {
- if let Some(username) = &decrypted.username {
- format!("{}@{}", username, decrypted.name)
- } else {
- decrypted.name.clone()
- }
- })
+ .map(|(_, decrypted)| decrypted.display_name())
.collect();
let entries = entries.join(", ");
Err(anyhow::anyhow!("multiple entries found: {}", entries))
@@ -646,13 +893,7 @@ fn find_entry_raw(
} else if exact_matches.len() > 1 {
let entries: Vec<String> = exact_matches
.iter()
- .map(|(_, decrypted)| {
- if let Some(username) = &decrypted.username {
- format!("{}@{}", username, decrypted.name)
- } else {
- decrypted.name.clone()
- }
- })
+ .map(|(_, decrypted)| decrypted.display_name())
.collect();
let entries = entries.join(", ");
Err(anyhow::anyhow!("multiple entries found: {}", entries))
@@ -661,6 +902,24 @@ fn find_entry_raw(
}
}
+fn decrypt_field(
+ name: &str,
+ field: Option<&str>,
+ org_id: Option<&str>,
+) -> Option<String> {
+ let field = field
+ .as_ref()
+ .map(|field| crate::actions::decrypt(field, org_id))
+ .transpose();
+ match field {
+ Ok(field) => field,
+ Err(e) => {
+ log::warn!("failed to decrypt {}: {}", name, e);
+ None
+ }
+ }
+}
+
fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> {
// folder name should always be decrypted with the local key because
// folders are local to a specific user's vault, not the organization
@@ -676,34 +935,6 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> {
None
}
};
- let username = entry
- .username
- .as_ref()
- .map(|username| {
- crate::actions::decrypt(username, entry.org_id.as_deref())
- })
- .transpose();
- let username = match username {
- Ok(username) => username,
- Err(e) => {
- log::warn!("failed to decrypt username: {}", e);
- None
- }
- };
- let password = entry
- .password
- .as_ref()
- .map(|password| {
- crate::actions::decrypt(password, entry.org_id.as_deref())
- })
- .transpose();
- let password = match password {
- Ok(password) => password,
- Err(e) => {
- log::warn!("failed to decrypt password: {}", e);
- None
- }
- };
let notes = entry
.notes
.as_ref()
@@ -729,12 +960,174 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> {
})
})
.collect::<anyhow::Result<_>>()?;
+
+ let data = match &entry.data {
+ rbw::db::EntryData::Login { username, password } => {
+ DecryptedData::Login {
+ username: decrypt_field(
+ "username",
+ username.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ password: decrypt_field(
+ "password",
+ password.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ }
+ }
+ rbw::db::EntryData::Card {
+ cardholder_name,
+ number,
+ brand,
+ exp_month,
+ exp_year,
+ code,
+ } => DecryptedData::Card {
+ cardholder_name: decrypt_field(
+ "cardholder_name",
+ cardholder_name.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ number: decrypt_field(
+ "number",
+ number.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ brand: decrypt_field(
+ "brand",
+ brand.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ exp_month: decrypt_field(
+ "exp_month",
+ exp_month.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ exp_year: decrypt_field(
+ "exp_year",
+ exp_year.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ code: decrypt_field(
+ "code",
+ code.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ },
+ rbw::db::EntryData::Identity {
+ title,
+ first_name,
+ middle_name,
+ last_name,
+ address1,
+ address2,
+ address3,
+ city,
+ state,
+ postal_code,
+ country,
+ phone,
+ email,
+ ssn,
+ license_number,
+ passport_number,
+ username,
+ } => DecryptedData::Identity {
+ title: decrypt_field(
+ "title",
+ title.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ first_name: decrypt_field(
+ "first_name",
+ first_name.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ middle_name: decrypt_field(
+ "middle_name",
+ middle_name.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ last_name: decrypt_field(
+ "last_name",
+ last_name.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ address1: decrypt_field(
+ "address1",
+ address1.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ address2: decrypt_field(
+ "address2",
+ address2.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ address3: decrypt_field(
+ "address3",
+ address3.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ city: decrypt_field(
+ "city",
+ city.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ state: decrypt_field(
+ "state",
+ state.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ postal_code: decrypt_field(
+ "postal_code",
+ postal_code.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ country: decrypt_field(
+ "country",
+ country.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ phone: decrypt_field(
+ "phone",
+ phone.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ email: decrypt_field(
+ "email",
+ email.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ ssn: decrypt_field(
+ "ssn",
+ ssn.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ license_number: decrypt_field(
+ "license_number",
+ license_number.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ passport_number: decrypt_field(
+ "passport_number",
+ passport_number.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ username: decrypt_field(
+ "username",
+ username.as_deref(),
+ entry.org_id.as_deref(),
+ ),
+ },
+ rbw::db::EntryData::SecureNote {} => DecryptedData::SecureNote {},
+ };
+
Ok(DecryptedCipher {
id: entry.id.clone(),
folder,
name: crate::actions::decrypt(&entry.name, entry.org_id.as_deref())?,
- username,
- password,
+ data,
notes,
history,
})
@@ -878,9 +1271,12 @@ mod test {
org_id: None,
folder: None,
name: "this is the encrypted name".to_string(),
- username: username
- .map(|_| "this is the encrypted username".to_string()),
- password: None,
+ data: rbw::db::EntryData::Login {
+ username: username.map(|_| {
+ "this is the encrypted username".to_string()
+ }),
+ password: None,
+ },
notes: None,
history: vec![],
},
@@ -888,8 +1284,10 @@ mod test {
id: "irrelevant".to_string(),
folder: None,
name: name.to_string(),
- username: username.map(std::string::ToString::to_string),
- password: None,
+ data: DecryptedData::Login {
+ username: username.map(std::string::ToString::to_string),
+ password: None,
+ },
notes: None,
history: vec![],
},
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index e2f2e85..9c65cb4 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::large_enum_variant)]
+
use anyhow::Context as _;
mod actions;
diff --git a/src/db.rs b/src/db.rs
index e5c7257..8cb27ad 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -11,8 +11,7 @@ pub struct Entry {
pub org_id: Option<String>,
pub folder: Option<String>,
pub name: String,
- pub username: Option<String>,
- pub password: Option<String>,
+ pub data: EntryData,
pub notes: Option<String>,
pub history: Vec<HistoryEntry>,
}
@@ -20,6 +19,44 @@ pub struct Entry {
#[derive(
serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq,
)]
+pub enum EntryData {
+ Login {
+ username: Option<String>,
+ password: Option<String>,
+ },
+ Card {
+ cardholder_name: Option<String>,
+ number: Option<String>,
+ brand: Option<String>,
+ exp_month: Option<String>,
+ exp_year: Option<String>,
+ code: Option<String>,
+ },
+ Identity {
+ title: Option<String>,
+ first_name: Option<String>,
+ middle_name: Option<String>,
+ last_name: Option<String>,
+ address1: Option<String>,
+ address2: Option<String>,
+ address3: Option<String>,
+ city: Option<String>,
+ state: Option<String>,
+ postal_code: Option<String>,
+ country: Option<String>,
+ phone: Option<String>,
+ email: Option<String>,
+ ssn: Option<String>,
+ license_number: Option<String>,
+ passport_number: Option<String>,
+ username: Option<String>,
+ },
+ SecureNote,
+}
+
+#[derive(
+ serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq,
+)]
pub struct HistoryEntry {
pub last_used_date: String,
pub password: String,
diff --git a/src/lib.rs b/src/lib.rs
index ac99032..f010bf0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,11 +1,13 @@
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
+#![allow(clippy::large_enum_variant)]
#![allow(clippy::missing_const_for_fn)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::similar_names)]
#![allow(clippy::single_match)]
#![allow(clippy::too_many_arguments)]
+#![allow(clippy::too_many_lines)]
#![allow(clippy::type_complexity)]
pub mod actions;