aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2024-04-20 12:43:46 -0400
committerJesse Luehrs <doy@tozt.net>2024-04-20 14:03:26 -0400
commitb2246a2afc7799f1fea3bc54f22abf6c9b4b1c0b (patch)
treec34dc623218400dc4caf99200c3b1dac5c54c0db
parent05e2dc56cfb71f1cabfcb2f46997ffd6247450fc (diff)
downloadrbw-b2246a2afc7799f1fea3bc54f22abf6c9b4b1c0b.tar.gz
rbw-b2246a2afc7799f1fea3bc54f22abf6c9b4b1c0b.zip
clean up url matching a bit, and implement different match types
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/rbw/commands.rs192
3 files changed, 105 insertions, 89 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ad05c55..3c1bffe 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1346,6 +1346,7 @@ dependencies = [
"percent-encoding",
"pkcs8",
"rand",
+ "regex",
"region",
"reqwest",
"rmpv",
diff --git a/Cargo.toml b/Cargo.toml
index e1c2ff7..ae50375 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -65,6 +65,7 @@ copypasta = "0.10.1"
rmpv = "1.0.2"
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-native-roots"] }
is-terminal = "0.4.12"
+regex = "1.10.4"
[package.metadata.deb]
depends = "pinentry"
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs
index 7a1b400..a241aec 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -19,7 +19,7 @@ const MISSING_CONFIG_HELP: &str =
pub enum Needle {
Name(String),
Uri(Url),
- Uuid(String),
+ Uuid(uuid::Uuid),
}
impl Display for Needle {
@@ -27,16 +27,16 @@ impl Display for Needle {
let value = match &self {
Self::Name(name) => name.clone(),
Self::Uri(uri) => uri.to_string(),
- Self::Uuid(uuid) => uuid.clone(),
+ Self::Uuid(uuid) => uuid.to_string(),
};
write!(f, "{value}")
}
}
#[allow(clippy::unnecessary_wraps)]
-pub fn parse_needle(arg: &str) -> Result<Needle, std::num::ParseIntError> {
- if uuid::Uuid::parse_str(arg).is_ok() {
- return Ok(Needle::Uuid(String::from(arg)));
+pub fn parse_needle(arg: &str) -> Result<Needle, std::convert::Infallible> {
+ if let Ok(uuid) = uuid::Uuid::parse_str(arg) {
+ return Ok(Needle::Uuid(uuid));
}
if let Ok(url) = Url::parse(arg) {
return Ok(Needle::Uri(url));
@@ -533,27 +533,8 @@ impl DecryptedCipher {
DecryptedData::Login {
uris: Some(uris), ..
} => {
- if !uris.iter().any(|uri| {
- let url = Url::parse(uri.uri.as_str());
- if url.is_err() {
- return false;
- }
- let url = url.unwrap();
- if url.scheme() != given_uri.scheme() {
- // Allow the case where we have a password
- // saved for http://example.com and we want
- // to get a password for https://example.com.
- if url.scheme() != "http"
- || given_uri.scheme() != "https"
- {
- return false;
- }
- }
- // match whole domain (including subdomains) in
- // exact match
- url.domain().is_some()
- && url.domain() == given_uri.domain()
- }) {
+ if !uris.iter().any(|uri| uri.matches_url(given_uri))
+ {
return false;
}
}
@@ -564,7 +545,7 @@ impl DecryptedCipher {
}
}
Needle::Uuid(uuid) => {
- if &self.id != uuid {
+ if uuid::Uuid::parse_str(&self.id) != Ok(*uuid) {
return false;
}
}
@@ -606,56 +587,13 @@ impl DecryptedCipher {
fn partial_match(
&self,
- needle: &Needle,
+ name: &str,
username: Option<&str>,
folder: Option<&str>,
try_match_folder: bool,
) -> bool {
- match needle {
- Needle::Name(name) => {
- if !self.name.contains(name) {
- return false;
- }
- }
- Needle::Uri(given_uri) => {
- match &self.data {
- DecryptedData::Login {
- uris: Some(uris), ..
- } => {
- if !uris.iter().any(|uri| {
- let url = Url::parse(uri.uri.as_str());
- if url.is_err() {
- return false;
- }
- let url = url.unwrap();
- if url.scheme() != given_uri.scheme() {
- // Allow the case where we have a password
- // saved for http://example.com and we want
- // to get a password for https://example.com.
- if url.scheme() != "http"
- || given_uri.scheme() != "https"
- {
- return false;
- }
- }
- // TODO: only match top and 2nd level domains in partial match
- url.domain().is_some()
- && url.domain() == given_uri.domain()
- }) {
- return false;
- }
- }
- _ => {
- // not sure what else to do here, but open to suggestions
- return false;
- }
- }
- }
- Needle::Uuid(uuid) => {
- if &self.id != uuid {
- return false;
- }
- }
+ if !self.name.contains(name) {
+ return false;
}
if let Some(given_username) = username {
@@ -769,6 +707,80 @@ struct DecryptedUri {
match_type: Option<rbw::api::UriMatchType>,
}
+impl DecryptedUri {
+ fn matches_url(&self, url: &Url) -> bool {
+ match self.match_type.unwrap_or(rbw::api::UriMatchType::Domain) {
+ rbw::api::UriMatchType::Domain => {
+ let Some(given_domain_port) = domain_port(url) else {
+ return false;
+ };
+ if let Ok(self_url) = url::Url::parse(&self.uri) {
+ if let Some(self_domain_port) = domain_port(&self_url) {
+ if self_url.scheme() == url.scheme()
+ && (self_domain_port == given_domain_port
+ || given_domain_port.ends_with(&format!(
+ ".{self_domain_port}"
+ )))
+ {
+ return true;
+ }
+ }
+ }
+ self.uri == given_domain_port
+ || given_domain_port.ends_with(&format!(".{}", self.uri))
+ }
+ rbw::api::UriMatchType::Host => {
+ let Some(given_host_port) = host_port(url) else {
+ return false;
+ };
+ if let Ok(self_url) = url::Url::parse(&self.uri) {
+ if let Some(self_host_port) = host_port(&self_url) {
+ if self_url.scheme() == url.scheme()
+ && self_host_port == given_host_port
+ {
+ return true;
+ }
+ }
+ }
+ self.uri == given_host_port
+ }
+ rbw::api::UriMatchType::StartsWith => {
+ url.to_string().starts_with(&self.uri)
+ }
+ rbw::api::UriMatchType::Exact => url.to_string() == self.uri,
+ rbw::api::UriMatchType::RegularExpression => {
+ let Ok(rx) = regex::Regex::new(&self.uri) else {
+ return false;
+ };
+ rx.is_match(url.as_ref())
+ }
+ rbw::api::UriMatchType::Never => false,
+ }
+ }
+}
+
+fn host_port(url: &Url) -> Option<String> {
+ let Some(host) = url.host_str() else {
+ return None;
+ };
+ Some(
+ url.port().map_or_else(
+ || host.to_string(),
+ |port| format!("{host}:{port}"),
+ ),
+ )
+}
+
+fn domain_port(url: &Url) -> Option<String> {
+ let Some(domain) = url.domain() else {
+ return None;
+ };
+ Some(url.port().map_or_else(
+ || domain.to_string(),
+ |port| format!("{domain}:{port}"),
+ ))
+}
+
enum ListField {
Name,
Id,
@@ -1510,7 +1522,7 @@ fn find_entry(
) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> {
if let Needle::Uuid(uuid) = needle {
for cipher in &db.entries {
- if uuid == &cipher.id {
+ if uuid::Uuid::parse_str(&cipher.id) == Ok(*uuid) {
return Ok((cipher.clone(), decrypt_cipher(cipher)?));
}
}
@@ -1560,30 +1572,32 @@ fn find_entry_raw(
}
}
- matches = entries
- .iter()
- .filter(|&(_, decrypted_cipher)| {
- decrypted_cipher.partial_match(needle, username, folder, true)
- })
- .cloned()
- .collect();
-
- if matches.len() == 1 {
- return Ok(matches[0].clone());
- }
-
- if folder.is_none() {
+ if let Needle::Name(name) = needle {
matches = entries
.iter()
.filter(|&(_, decrypted_cipher)| {
- decrypted_cipher
- .partial_match(needle, username, folder, false)
+ decrypted_cipher.partial_match(name, username, folder, true)
})
.cloned()
.collect();
+
if matches.len() == 1 {
return Ok(matches[0].clone());
}
+
+ if folder.is_none() {
+ matches = entries
+ .iter()
+ .filter(|&(_, decrypted_cipher)| {
+ decrypted_cipher
+ .partial_match(name, username, folder, false)
+ })
+ .cloned()
+ .collect();
+ if matches.len() == 1 {
+ return Ok(matches[0].clone());
+ }
+ }
}
if matches.is_empty() {