From 70b6fad2cba4e1e54adbc79e36f38e99fe1996fc Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Fri, 15 Dec 2023 15:25:27 +0000 Subject: [PATCH 1/5] Add search command which returns names of found entries Allow get and search to find matches in notes Signed-off-by: Christian McHugh --- src/bin/rbw/commands.rs | 101 +++++++++++++++++++++++++++++++++++++++- src/bin/rbw/main.rs | 24 ++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 4ea3529..4139027 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -535,7 +535,8 @@ impl DecryptedCipher { folder: Option<&str>, try_match_folder: bool, ) -> bool { - if !self.name.contains(name) { + let notes_value = self.notes.as_deref().unwrap_or(""); + if !self.name.contains(name) & !notes_value.contains(name) { return false; } @@ -886,6 +887,36 @@ pub fn get( Ok(()) } +pub fn search( + name: &str, + user: Option<&str>, + folder: Option<&str>, + raw: bool, +) -> anyhow::Result<()> { + unlock()?; + + let db = load_db()?; + + let desc = format!( + "{}{}", + user.map_or_else(String::new, |s| format!("{s}@")), + name + ); + + let found_name = find_entry_names(&db, name, user, folder) + .with_context(|| format!("No entries found for '{desc}'"))?; + + if raw { + println!("{}", serde_json::to_string(&found_name)?); + } else { + for name in found_name { + val_display_or_store(false, &name); + } + } + + Ok(()) +} + pub fn code( name: &str, user: Option<&str>, @@ -1381,6 +1412,74 @@ fn find_entry( } } +fn find_entry_names( + db: &rbw::db::Db, + name: &str, + username: Option<&str>, + folder: Option<&str>, +) -> anyhow::Result> { + let entries: Vec<(rbw::db::Entry, DecryptedCipher)> = db + .entries + .iter() + .cloned() + .map(|entry| { + decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) + }) + .collect::>()?; + let mut matches: Vec = entries + .iter() + .cloned() + .filter(|(_, decrypted_cipher)| { + decrypted_cipher.exact_match(name, username, folder, true) + }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + .collect(); + + if matches.len() >= 1 { + return Ok(matches.clone()); + } + + if folder.is_none() { + matches = entries + .iter() + .cloned() + .filter(|(_, decrypted_cipher)| { + decrypted_cipher.exact_match(name, username, folder, false) + }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + .collect(); + + if matches.len() >= 1 { + return Ok(matches.clone()); + } + } + + matches = entries + .iter() + .cloned() + .filter(|(_, decrypted_cipher)| { + decrypted_cipher.partial_match(name, username, folder, true) + }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + .collect(); + + if matches.len() >= 1 { + return Ok(matches.clone()); + } + + if folder.is_none() { + matches = entries + .iter() + .cloned() + .filter(|(_, decrypted_cipher)| { + decrypted_cipher.partial_match(name, username, folder, false) + }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + .collect(); + if matches.len() >= 1 { + return Ok(matches.clone()); + } + } + + Err(anyhow::anyhow!("no entries found")) +} + fn find_entry_raw( entries: &[(rbw::db::Entry, DecryptedCipher)], name: &str, diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 72e4220..bdcecb3 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -87,6 +87,18 @@ enum Opt { clipboard: bool, }, + #[command(about = "Search for the password for a given entry")] + Search { + #[arg(help = "Name or UUID of the entry to display")] + name: String, + #[arg(help = "Username of the entry to display")] + user: Option, + #[arg(long, help = "Folder name to search in")] + folder: Option, + #[structopt(long, help = "Display output as JSON")] + raw: bool, + }, + #[command(about = "Display the authenticator code for a given entry")] Code { #[arg(help = "Name or UUID of the entry to display")] @@ -244,6 +256,7 @@ impl Opt { Self::Sync => "sync".to_string(), Self::List { .. } => "list".to_string(), Self::Get { .. } => "get".to_string(), + Self::Search { .. } => "search".to_string(), Self::Code { .. } => "code".to_string(), Self::Add { .. } => "add".to_string(), Self::Generate { .. } => "generate".to_string(), @@ -334,6 +347,17 @@ fn main() { *raw, *clipboard, ), + Opt::Search { + name, + user, + folder, + raw, + } => commands::search( + name, + user.as_deref(), + folder.as_deref(), + *raw, + ), Opt::Code { name, user, folder } => { commands::code(name, user.as_deref(), folder.as_deref()) } From dded4508b88c0f3fb2b2d6db504806d8b3bcf723 Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Fri, 15 Dec 2023 15:43:23 +0000 Subject: [PATCH 2/5] Improve wording Signed-off-by: Christian McHugh --- src/bin/rbw/commands.rs | 6 +++--- src/bin/rbw/main.rs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 4139027..28d0dcf 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -888,7 +888,7 @@ pub fn get( } pub fn search( - name: &str, + term: &str, user: Option<&str>, folder: Option<&str>, raw: bool, @@ -900,10 +900,10 @@ pub fn search( let desc = format!( "{}{}", user.map_or_else(String::new, |s| format!("{s}@")), - name + term ); - let found_name = find_entry_names(&db, name, user, folder) + let found_name = find_entry_names(&db, term, user, folder) .with_context(|| format!("No entries found for '{desc}'"))?; if raw { diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index bdcecb3..66b8c51 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -87,10 +87,10 @@ enum Opt { clipboard: bool, }, - #[command(about = "Search for the password for a given entry")] + #[command(about = "Search for entries")] Search { - #[arg(help = "Name or UUID of the entry to display")] - name: String, + #[arg(help = "Search term to locate entries")] + term: String, #[arg(help = "Username of the entry to display")] user: Option, #[arg(long, help = "Folder name to search in")] @@ -348,12 +348,12 @@ fn main() { *clipboard, ), Opt::Search { - name, + term, user, folder, raw, } => commands::search( - name, + term, user.as_deref(), folder.as_deref(), *raw, From e525957ca677dcdb7bad0b51d3a20dc77100103f Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Fri, 15 Dec 2023 21:01:38 +0000 Subject: [PATCH 3/5] support full output for searches Signed-off-by: Christian McHugh --- src/bin/rbw/commands.rs | 39 +++++++++++++++++++++++++++------------ src/bin/rbw/main.rs | 4 ++++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 28d0dcf..2e16241 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -891,6 +891,7 @@ pub fn search( term: &str, user: Option<&str>, folder: Option<&str>, + full: bool, raw: bool, ) -> anyhow::Result<()> { unlock()?; @@ -903,14 +904,28 @@ pub fn search( term ); - let found_name = find_entry_names(&db, term, user, folder) + let mut found_entries = find_entries(&db, term, user, folder) .with_context(|| format!("No entries found for '{desc}'"))?; - if raw { - println!("{}", serde_json::to_string(&found_name)?); + if !full { + let just_names: Vec = found_entries.iter() + .cloned() + .map(|DecryptedCipher| DecryptedCipher.name) + .collect(); + if raw { + println!("{}", serde_json::to_string(&just_names)?); + } else { + for name in just_names { + val_display_or_store(false, &name); + } + } } else { - for name in found_name { - val_display_or_store(false, &name); + if raw { + println!("{}", serde_json::to_string(&found_entries)?); + } else { + for entry in found_entries { + entry.display_long(&term, false); + } } } @@ -1412,12 +1427,12 @@ fn find_entry( } } -fn find_entry_names( +fn find_entries( db: &rbw::db::Db, name: &str, username: Option<&str>, folder: Option<&str>, -) -> anyhow::Result> { +) -> anyhow::Result> { let entries: Vec<(rbw::db::Entry, DecryptedCipher)> = db .entries .iter() @@ -1426,12 +1441,12 @@ fn find_entry_names( decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) }) .collect::>()?; - let mut matches: Vec = entries + let mut matches: Vec = entries .iter() .cloned() .filter(|(_, decrypted_cipher)| { decrypted_cipher.exact_match(name, username, folder, true) - }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + }).map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { @@ -1444,7 +1459,7 @@ fn find_entry_names( .cloned() .filter(|(_, decrypted_cipher)| { decrypted_cipher.exact_match(name, username, folder, false) - }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + }).map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { @@ -1457,7 +1472,7 @@ fn find_entry_names( .cloned() .filter(|(_, decrypted_cipher)| { decrypted_cipher.partial_match(name, username, folder, true) - }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + }).map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { @@ -1470,7 +1485,7 @@ fn find_entry_names( .cloned() .filter(|(_, decrypted_cipher)| { decrypted_cipher.partial_match(name, username, folder, false) - }).map(|(_, DecryptedCipher)| DecryptedCipher.name) + }).map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { return Ok(matches.clone()); diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 66b8c51..febdaa5 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -95,6 +95,8 @@ enum Opt { user: Option, #[arg(long, help = "Folder name to search in")] folder: Option, + #[arg(long, help = "Display the full entry in addition to the name")] + full: bool, #[structopt(long, help = "Display output as JSON")] raw: bool, }, @@ -351,11 +353,13 @@ fn main() { term, user, folder, + full, raw, } => commands::search( term, user.as_deref(), folder.as_deref(), + *full, *raw, ), Opt::Code { name, user, folder } => { From 7c4b21c85fd495db7bf1dc30f8096673a64ebbe2 Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Fri, 15 Dec 2023 22:32:38 +0000 Subject: [PATCH 4/5] Add case insensitive to search Signed-off-by: Christian McHugh --- src/bin/rbw/commands.rs | 47 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 2e16241..1705624 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -534,10 +534,20 @@ impl DecryptedCipher { username: Option<&str>, folder: Option<&str>, try_match_folder: bool, + case_insensitive: bool, ) -> bool { - let notes_value = self.notes.as_deref().unwrap_or(""); - if !self.name.contains(name) & !notes_value.contains(name) { - return false; + if case_insensitive { + let notes_value = self.notes.as_deref().unwrap_or(""); + if !self.name.to_lowercase().contains(&name.to_lowercase()) + && !notes_value.to_lowercase().contains(&name.to_lowercase()) + { + return false; + } + } else { + let notes_value = self.notes.as_deref().unwrap_or(""); + if !self.name.contains(name) & !notes_value.contains(name) { + return false; + } } if let Some(given_username) = username { @@ -905,13 +915,14 @@ pub fn search( ); let mut found_entries = find_entries(&db, term, user, folder) - .with_context(|| format!("No entries found for '{desc}'"))?; + .with_context(|| format!("No entries found for '{desc}'"))?; if !full { - let just_names: Vec = found_entries.iter() - .cloned() - .map(|DecryptedCipher| DecryptedCipher.name) - .collect(); + let just_names: Vec = found_entries + .iter() + .cloned() + .map(|DecryptedCipher| DecryptedCipher.name) + .collect(); if raw { println!("{}", serde_json::to_string(&just_names)?); } else { @@ -1446,7 +1457,8 @@ fn find_entries( .cloned() .filter(|(_, decrypted_cipher)| { decrypted_cipher.exact_match(name, username, folder, true) - }).map(|(_, DecryptedCipher)| DecryptedCipher) + }) + .map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { @@ -1459,7 +1471,8 @@ fn find_entries( .cloned() .filter(|(_, decrypted_cipher)| { decrypted_cipher.exact_match(name, username, folder, false) - }).map(|(_, DecryptedCipher)| DecryptedCipher) + }) + .map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { @@ -1471,8 +1484,9 @@ fn find_entries( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, true) - }).map(|(_, DecryptedCipher)| DecryptedCipher) + decrypted_cipher.partial_match(name, username, folder, true, true) + }) + .map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { @@ -1484,8 +1498,9 @@ fn find_entries( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, false) - }).map(|(_, DecryptedCipher)| DecryptedCipher) + decrypted_cipher.partial_match(name, username, folder, false, true) + }) + .map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); if matches.len() >= 1 { return Ok(matches.clone()); @@ -1531,7 +1546,7 @@ fn find_entry_raw( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, true) + decrypted_cipher.partial_match(name, username, folder, true, false) }) .collect(); @@ -1544,7 +1559,7 @@ fn find_entry_raw( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, false) + decrypted_cipher.partial_match(name, username, folder, false, false) }) .collect(); if matches.len() == 1 { From 9759e7cbd30748879d4a853a5a591946d70c2fb5 Mon Sep 17 00:00:00 2001 From: Christian McHugh Date: Fri, 15 Dec 2023 22:33:32 +0000 Subject: [PATCH 5/5] fix typo and formatting Signed-off-by: Christian McHugh --- src/bin/rbw/commands.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 1705624..464e396 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -545,7 +545,7 @@ impl DecryptedCipher { } } else { let notes_value = self.notes.as_deref().unwrap_or(""); - if !self.name.contains(name) & !notes_value.contains(name) { + if !self.name.contains(name) && !notes_value.contains(name) { return false; } } @@ -1498,7 +1498,8 @@ fn find_entries( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, false, true) + decrypted_cipher + .partial_match(name, username, folder, false, true) }) .map(|(_, DecryptedCipher)| DecryptedCipher) .collect(); @@ -1546,7 +1547,8 @@ fn find_entry_raw( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, true, false) + decrypted_cipher + .partial_match(name, username, folder, true, false) }) .collect(); @@ -1559,7 +1561,8 @@ fn find_entry_raw( .iter() .cloned() .filter(|(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, false, false) + decrypted_cipher + .partial_match(name, username, folder, false, false) }) .collect(); if matches.len() == 1 {