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/bin/rbw/commands.rs | 620 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 509 insertions(+), 111 deletions(-) (limited to 'src/bin/rbw/commands.rs') 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![], }, -- cgit v1.2.3-54-g00ecf