From dcca2c702999997ebdc2ae7acd1228edf7333014 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 3 May 2020 18:07:40 -0400 Subject: start adding support for different entry types --- src/actions.rs | 37 +-- src/api.rs | 373 +++++++++++++++++++++++------ src/bin/rbw/commands.rs | 620 +++++++++++++++++++++++++++++++++++++++--------- src/bin/rbw/main.rs | 2 + src/db.rs | 41 +++- src/lib.rs | 2 + 6 files changed, 866 insertions(+), 209 deletions(-) (limited to 'src') 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, ())> { 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, ())> { 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, + login: Option, + #[serde(rename = "Card")] + card: Option, + #[serde(rename = "Identity")] + identity: Option, + #[serde(rename = "SecureNote")] + secure_note: Option, #[serde(rename = "Notes")] notes: Option, #[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 { - 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, #[serde(rename = "Password")] password: Option, + uris: Option>, +} + +#[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, + #[serde(rename = "Number")] + number: Option, + #[serde(rename = "Brand")] + brand: Option, + #[serde(rename = "ExpMonth")] + exp_month: Option, + #[serde(rename = "ExpYear")] + exp_year: Option, + #[serde(rename = "Code")] + code: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +struct CipherIdentity { + #[serde(rename = "Title")] + title: Option, + #[serde(rename = "FirstName")] + first_name: Option, + #[serde(rename = "MiddleName")] + middle_name: Option, + #[serde(rename = "LastName")] + last_name: Option, + #[serde(rename = "Address1")] + address1: Option, + #[serde(rename = "Address2")] + address2: Option, + #[serde(rename = "Address3")] + address3: Option, + #[serde(rename = "City")] + city: Option, + #[serde(rename = "State")] + state: Option, + #[serde(rename = "PostalCode")] + postal_code: Option, + #[serde(rename = "Country")] + country: Option, + #[serde(rename = "Phone")] + phone: Option, + #[serde(rename = "Email")] + email: Option, + #[serde(rename = "SSN")] + ssn: Option, + #[serde(rename = "LicenseNumber")] + license_number: Option, + #[serde(rename = "PassportNumber")] + passport_number: Option, + #[serde(rename = "Username")] + username: Option, } +// 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, name: String, notes: Option, - login: CiphersPostReqLogin, -} - -#[derive(serde::Serialize, Debug)] -struct CiphersPostReqLogin { - username: Option, - password: Option, - uris: Vec, -} - -#[derive(serde::Serialize, Debug)] -struct CiphersPostReqLoginUri { - uri: String, + login: Option, + card: Option, + identity: Option, + #[serde(rename = "secureNote")] + secure_note: Option, } #[derive(serde::Serialize, Debug)] @@ -213,7 +309,11 @@ struct CiphersPutReq { ty: u32, // XXX what are the valid types? name: String, notes: Option, - login: CiphersPutReqLogin, + login: Option, + card: Option, + identity: Option, + #[serde(rename = "secureNote")] + secure_note: Option, #[serde(rename = "passwordHistory")] password_history: Vec, } @@ -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, name: String, - username: Option, - password: Option, + data: DecryptedData, notes: Option, history: Vec, } +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, + password: Option, + }, + Card { + cardholder_name: Option, + number: Option, + brand: Option, + exp_month: Option, + exp_year: Option, + code: Option, + }, + Identity { + title: Option, + first_name: Option, + middle_name: Option, + last_name: Option, + address1: Option, + address2: Option, + address3: Option, + city: Option, + state: Option, + postal_code: Option, + country: Option, + phone: Option, + email: Option, + ssn: Option, + license_number: Option, + passport_number: Option, + username: Option, + }, + 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(¬es, 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(¬es, 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 = 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 = 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 { + 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 { // 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 { 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 { }) }) .collect::>()?; + + 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,12 +11,49 @@ pub struct Entry { pub org_id: Option, pub folder: Option, pub name: String, - pub username: Option, - pub password: Option, + pub data: EntryData, pub notes: Option, pub history: Vec, } +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, +)] +pub enum EntryData { + Login { + username: Option, + password: Option, + }, + Card { + cardholder_name: Option, + number: Option, + brand: Option, + exp_month: Option, + exp_year: Option, + code: Option, + }, + Identity { + title: Option, + first_name: Option, + middle_name: Option, + last_name: Option, + address1: Option, + address2: Option, + address3: Option, + city: Option, + state: Option, + postal_code: Option, + country: Option, + phone: Option, + email: Option, + ssn: Option, + license_number: Option, + passport_number: Option, + username: Option, + }, + SecureNote, +} + #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] 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; -- cgit v1.2.3-54-g00ecf