Skip to content

Commit

Permalink
clean up url matching a bit, and implement different match types
Browse files Browse the repository at this point in the history
  • Loading branch information
doy committed Apr 20, 2024
1 parent 05e2dc5 commit b2246a2
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 89 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
192 changes: 103 additions & 89 deletions src/bin/rbw/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,24 @@ const MISSING_CONFIG_HELP: &str =
pub enum Needle {
Name(String),
Uri(Url),
Uuid(String),
Uuid(uuid::Uuid),
}

impl Display for Needle {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
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));
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -564,7 +545,7 @@ impl DecryptedCipher {
}
}
Needle::Uuid(uuid) => {
if &self.id != uuid {
if uuid::Uuid::parse_str(&self.id) != Ok(*uuid) {
return false;
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)?));
}
}
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit b2246a2

Please sign in to comment.