From 9b5bc94b5de8c7c1e226887f5e986ecef5967d06 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 21 Feb 2021 21:57:38 -0500 Subject: persist uri match type when editing --- CHANGELOG.md | 2 ++ Cargo.lock | 12 +++++++ Cargo.toml | 1 + src/api.rs | 50 ++++++++++++++++++++++++++-- src/bin/rbw/commands.rs | 50 +++++++++++++++++++++++----- src/bin/rbw/main.rs | 12 +++++-- src/db.rs | 87 ++++++++++++++++++++++++++++++++++++++++++++++++- src/error.rs | 3 ++ 8 files changed, 202 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd5e7a..a025c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Fixed * Stop hardcoding /tmp when using the fallback runtime directory (#37, pschmitt) +* Fix `rbw edit` clearing the match detection setting for websites associated + with the edited password (#34, AdmiralNemo) ## [0.5.2] - 2020-12-02 diff --git a/Cargo.lock b/Cargo.lock index 4b0b3c0..fc2939f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1253,6 +1253,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", + "serde_repr", "snafu", "structopt", "tempfile", @@ -1483,6 +1484,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 8db854c..c65dbcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ ring = "0.16" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_path_to_error = "0.1" +serde_repr = "0.1" snafu = "0.6" structopt = { version = "0.3", features = ["paw", "wrap_help"] } tempfile = "3.2" diff --git a/src/api.rs b/src/api.rs index ca92e25..40fe422 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,6 +4,41 @@ use crate::json::{ DeserializeJsonWithPath as _, DeserializeJsonWithPathAsync as _, }; +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Copy, + Clone, + PartialEq, + Eq, +)] +#[repr(u8)] +pub enum UriMatchType { + Domain = 0, + Host = 1, + StartsWith = 2, + Exact = 3, + RegularExpression = 4, + Never = 5, +} + +impl std::fmt::Display for UriMatchType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[allow(clippy::enum_glob_use)] + use UriMatchType::*; + let s = match self { + Domain => "domain", + Host => "host", + StartsWith => "starts_with", + Exact => "exact", + RegularExpression => "regular_expression", + Never => "never", + }; + write!(f, "{}", s) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TwoFactorProviderType { Authenticator = 0, @@ -245,7 +280,12 @@ impl SyncResCipher { std::vec::Vec::new, |uris| { uris.iter() - .filter_map(|uri| uri.uri.clone()) + .filter_map(|uri| { + uri.uri.clone().map(|s| crate::db::Uri { + uri: s, + match_type: uri.match_type, + }) + }) .collect() }, ), @@ -351,6 +391,8 @@ struct CipherLogin { struct CipherLoginUri { #[serde(rename = "Uri")] uri: Option, + #[serde(rename = "Match")] + match_type: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -648,7 +690,8 @@ impl Client { Some( uris.iter() .map(|s| CipherLoginUri { - uri: Some(s.to_string()), + uri: Some(s.uri.to_string()), + match_type: s.match_type, }) .collect(), ) @@ -780,7 +823,8 @@ impl Client { Some( uris.iter() .map(|s| CipherLoginUri { - uri: Some(s.to_string()), + uri: Some(s.uri.to_string()), + match_type: s.match_type, }) .collect(), ) diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 49c44a4..3f11174 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -93,7 +93,14 @@ impl DecryptedCipher { if let Some(uris) = uris { for uri in uris { - displayed |= self.display_field("URI", Some(uri)); + displayed |= + self.display_field("URI", Some(&uri.uri)); + let match_type = + uri.match_type.map(|ty| format!("{}", ty)); + displayed |= self.display_field( + "Match type", + match_type.as_deref(), + ); } } @@ -312,7 +319,7 @@ enum DecryptedData { username: Option, password: Option, totp: Option, - uris: Option>, + uris: Option>, }, Card { cardholder_name: Option, @@ -358,6 +365,13 @@ struct DecryptedHistoryEntry { password: String, } +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +struct DecryptedUri { + uri: String, + match_type: Option, +} + enum ListField { Name, Id, @@ -587,7 +601,7 @@ pub fn code( pub fn add( name: &str, username: Option<&str>, - uris: Vec, + uris: Vec<(String, Option)>, folder: Option<&str>, ) -> anyhow::Result<()> { unlock()?; @@ -613,9 +627,14 @@ pub fn add( let notes = notes .map(|notes| crate::actions::encrypt(¬es, None)) .transpose()?; - let uris: Vec = uris + let uris: Vec<_> = uris .iter() - .map(|uri| crate::actions::encrypt(&uri, None)) + .map(|uri| { + Ok(rbw::db::Uri { + uri: crate::actions::encrypt(&uri.0, None)?, + match_type: uri.1, + }) + }) .collect::>()?; let mut folder_id = None; @@ -679,7 +698,7 @@ pub fn add( pub fn generate( name: Option<&str>, username: Option<&str>, - uris: Vec, + uris: Vec<(String, Option)>, folder: Option<&str>, len: usize, ty: rbw::pwgen::Type, @@ -701,9 +720,14 @@ pub fn generate( .map(|username| crate::actions::encrypt(username, None)) .transpose()?; let password = crate::actions::encrypt(&password, None)?; - let uris: Vec = uris + let uris: Vec<_> = uris .iter() - .map(|uri| crate::actions::encrypt(&uri, None)) + .map(|uri| { + Ok(rbw::db::Uri { + uri: crate::actions::encrypt(&uri.0, None)?, + match_type: uri.1, + }) + }) .collect::>()?; let mut folder_id = None; @@ -1221,7 +1245,15 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result { uris: uris .iter() .map(|s| { - decrypt_field("uri", Some(s), entry.org_id.as_deref()) + decrypt_field( + "uri", + Some(&s.uri), + entry.org_id.as_deref(), + ) + .map(|uri| DecryptedUri { + uri, + match_type: s.match_type, + }) }) .collect(), }, diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index decdeb4..badff67 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -299,7 +299,11 @@ fn main(opt: Opt) { } => commands::add( &name, user.as_deref(), - uri.to_vec(), + uri.iter() + // XXX not sure what the ui for specifying the match type + // should be + .map(|uri| (uri.clone(), None)) + .collect::>(), folder.as_deref(), ), Opt::Generate { @@ -327,7 +331,11 @@ fn main(opt: Opt) { commands::generate( name.as_deref(), user.as_deref(), - uri.to_vec(), + uri.iter() + // XXX not sure what the ui for specifying the match type + // should be + .map(|uri| (uri.clone(), None)) + .collect::>(), folder.as_deref(), *len, ty, diff --git a/src/db.rs b/src/db.rs index d519f00..067f36f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -18,6 +18,91 @@ pub struct Entry { pub history: Vec, } +#[derive(serde::Serialize, Debug, Clone, Eq, PartialEq)] +pub struct Uri { + pub uri: String, + pub match_type: Option, +} + +// backwards compatibility +impl<'de> serde::Deserialize<'de> for Uri { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct StringOrUri; + impl<'de> serde::de::Visitor<'de> for StringOrUri { + type Value = Uri; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter.write_str("uri") + } + + fn visit_str( + self, + value: &str, + ) -> std::result::Result + where + E: serde::de::Error, + { + Ok(Uri { + uri: value.to_string(), + match_type: None, + }) + } + + fn visit_map( + self, + mut map: M, + ) -> std::result::Result + where + M: serde::de::MapAccess<'de>, + { + let mut uri = None; + let mut match_type = None; + while let Some(key) = map.next_key()? { + match key { + "uri" => { + if uri.is_some() { + return Err( + serde::de::Error::duplicate_field("uri"), + ); + } + uri = Some(map.next_value()?); + } + "match_type" => { + if match_type.is_some() { + return Err( + serde::de::Error::duplicate_field( + "match_type", + ), + ); + } + match_type = map.next_value()?; + } + _ => { + return Err(serde::de::Error::unknown_field( + key, + &["uri", "match_type"], + )) + } + } + } + + uri.map_or_else( + || Err(serde::de::Error::missing_field("uri")), + |uri| Ok(Self::Value { uri, match_type }), + ) + } + } + + deserializer.deserialize_any(StringOrUri) + } +} + #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] @@ -26,7 +111,7 @@ pub enum EntryData { username: Option, password: Option, totp: Option, - uris: Vec, + uris: Vec, }, Card { cardholder_name: Option, diff --git a/src/error.rs b/src/error.rs index 7201ce0..6e8e6da 100644 --- a/src/error.rs +++ b/src/error.rs @@ -96,6 +96,9 @@ pub enum Error { #[snafu(display("openssl error"))] OpenSSL { source: openssl::error::ErrorStack }, + #[snafu(display("failed to parse match type {}", s))] + ParseMatchType { s: String }, + #[snafu(display("pbkdf2 requires at least 1 iteration (got 0)"))] Pbkdf2ZeroIterations, -- cgit v1.2.3-54-g00ecf