aboutsummaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/rbw-agent/actions.rs5
-rw-r--r--src/bin/rbw-agent/agent.rs7
-rw-r--r--src/bin/rbw-agent/daemon.rs28
-rw-r--r--src/bin/rbw-agent/debugger.rs2
-rw-r--r--src/bin/rbw-agent/main.rs2
-rw-r--r--src/bin/rbw/actions.rs44
-rw-r--r--src/bin/rbw/commands.rs1016
-rw-r--r--src/bin/rbw/main.rs33
8 files changed, 996 insertions, 141 deletions
diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs
index ff39510..8966785 100644
--- a/src/bin/rbw-agent/actions.rs
+++ b/src/bin/rbw-agent/actions.rs
@@ -184,7 +184,9 @@ pub async fn login(
break 'attempts;
}
}
- return Err(anyhow::anyhow!("TODO"));
+ return Err(anyhow::anyhow!(
+ "unsupported two factor methods: {providers:?}"
+ ));
}
Err(rbw::error::Error::IncorrectPassword { message }) => {
if i == 3 {
@@ -474,7 +476,6 @@ pub async fn lock(
pub async fn check_lock(
sock: &mut crate::sock::Sock,
state: std::sync::Arc<tokio::sync::Mutex<crate::agent::State>>,
- _tty: Option<&str>,
) -> anyhow::Result<()> {
if state.lock().await.needs_unlock() {
return Err(anyhow::anyhow!("agent is locked"));
diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs
index 1345967..a3fecb4 100644
--- a/src/bin/rbw-agent/agent.rs
+++ b/src/bin/rbw-agent/agent.rs
@@ -209,12 +209,7 @@ async fn handle_request(
true
}
rbw::protocol::Action::CheckLock => {
- crate::actions::check_lock(
- sock,
- state.clone(),
- req.tty.as_deref(),
- )
- .await?;
+ crate::actions::check_lock(sock, state.clone()).await?;
false
}
rbw::protocol::Action::Lock => {
diff --git a/src/bin/rbw-agent/daemon.rs b/src/bin/rbw-agent/daemon.rs
index 8cb9998..06db891 100644
--- a/src/bin/rbw-agent/daemon.rs
+++ b/src/bin/rbw-agent/daemon.rs
@@ -1,25 +1,15 @@
pub struct StartupAck {
- writer: std::os::unix::io::RawFd,
+ writer: std::os::unix::io::OwnedFd,
}
impl StartupAck {
- pub fn ack(&self) -> anyhow::Result<()> {
- nix::unistd::write(self.writer, &[0])?;
- nix::unistd::close(self.writer)?;
+ pub fn ack(self) -> anyhow::Result<()> {
+ rustix::io::write(&self.writer, &[0])?;
Ok(())
}
}
-impl Drop for StartupAck {
- fn drop(&mut self) {
- // best effort close here, can't do better in a destructor
- let _ = nix::unistd::close(self.writer);
- }
-}
-
pub fn daemonize() -> anyhow::Result<StartupAck> {
- rbw::dirs::make_all()?;
-
let stdout = std::fs::OpenOptions::new()
.append(true)
.create(true)
@@ -29,25 +19,25 @@ pub fn daemonize() -> anyhow::Result<StartupAck> {
.create(true)
.open(rbw::dirs::agent_stderr_file())?;
- let (r, w) = nix::unistd::pipe()?;
+ let (r, w) = rustix::pipe::pipe()?;
let daemonize = daemonize::Daemonize::new()
.pid_file(rbw::dirs::pid_file())
.stdout(stdout)
.stderr(stderr);
let res = match daemonize.execute() {
daemonize::Outcome::Parent(_) => {
+ drop(w);
+ let mut buf = [0; 1];
// unwraps are necessary because not really a good way to handle
// errors here otherwise
- let _ = nix::unistd::close(w);
- let mut buf = [0; 1];
- nix::unistd::read(r, &mut buf).unwrap();
- nix::unistd::close(r).unwrap();
+ rustix::io::read(&r, &mut buf).unwrap();
+ drop(r);
std::process::exit(0);
}
daemonize::Outcome::Child(res) => res,
};
- let _ = nix::unistd::close(r);
+ drop(r);
match res {
Ok(_) => (),
diff --git a/src/bin/rbw-agent/debugger.rs b/src/bin/rbw-agent/debugger.rs
index 59bbe50..be5260c 100644
--- a/src/bin/rbw-agent/debugger.rs
+++ b/src/bin/rbw-agent/debugger.rs
@@ -12,7 +12,7 @@ pub fn disable_tracing() -> anyhow::Result<()> {
if ret == 0 {
Ok(())
} else {
- let e = nix::Error::last();
+ let e = std::io::Error::last_os_error();
Err(anyhow::anyhow!("failed to disable PTRACE_ATTACH, agent memory may be dumpable by other processes: {}", e))
}
}
diff --git a/src/bin/rbw-agent/main.rs b/src/bin/rbw-agent/main.rs
index 8434c72..d470e10 100644
--- a/src/bin/rbw-agent/main.rs
+++ b/src/bin/rbw-agent/main.rs
@@ -50,6 +50,8 @@ fn real_main() -> anyhow::Result<()> {
.nth(1)
.map_or(false, |arg| arg == "--no-daemonize");
+ rbw::dirs::make_all()?;
+
let startup_ack = if no_daemonize {
None
} else {
diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs
index e0e7a2e..c84ccd4 100644
--- a/src/bin/rbw/actions.rs
+++ b/src/bin/rbw/actions.rs
@@ -1,4 +1,4 @@
-use anyhow::Context as _;
+use anyhow::{bail, Context as _};
use std::io::Read as _;
pub fn register() -> anyhow::Result<()> {
@@ -31,11 +31,17 @@ pub fn quit() -> anyhow::Result<()> {
let pidfile = rbw::dirs::pid_file();
let mut pid = String::new();
std::fs::File::open(pidfile)?.read_to_string(&mut pid)?;
- let pid = nix::unistd::Pid::from_raw(pid.trim_end().parse()?);
+ let Some(pid) =
+ rustix::process::Pid::from_raw(pid.trim_end().parse()?)
+ else {
+ bail!("failed to read pid from pidfile");
+ };
sock.send(&rbw::protocol::Request {
- tty: nix::unistd::ttyname(0).ok().and_then(|p| {
- p.to_str().map(std::string::ToString::to_string)
- }),
+ tty: rustix::termios::ttyname(std::io::stdin(), vec![])
+ .ok()
+ .and_then(|p| {
+ p.to_str().map(std::string::ToString::to_string).ok()
+ }),
action: rbw::protocol::Action::Quit,
})?;
wait_for_exit(pid);
@@ -57,9 +63,11 @@ pub fn decrypt(
) -> anyhow::Result<String> {
let mut sock = connect()?;
sock.send(&rbw::protocol::Request {
- tty: nix::unistd::ttyname(0)
+ tty: rustix::termios::ttyname(std::io::stdin(), vec![])
.ok()
- .and_then(|p| p.to_str().map(std::string::ToString::to_string)),
+ .and_then(|p| {
+ p.to_str().map(std::string::ToString::to_string).ok()
+ }),
action: rbw::protocol::Action::Decrypt {
cipherstring: cipherstring.to_string(),
org_id: org_id.map(std::string::ToString::to_string),
@@ -82,9 +90,11 @@ pub fn encrypt(
) -> anyhow::Result<String> {
let mut sock = connect()?;
sock.send(&rbw::protocol::Request {
- tty: nix::unistd::ttyname(0)
+ tty: rustix::termios::ttyname(std::io::stdin(), vec![])
.ok()
- .and_then(|p| p.to_str().map(std::string::ToString::to_string)),
+ .and_then(|p| {
+ p.to_str().map(std::string::ToString::to_string).ok()
+ }),
action: rbw::protocol::Action::Encrypt {
plaintext: plaintext.to_string(),
org_id: org_id.map(std::string::ToString::to_string),
@@ -110,9 +120,11 @@ pub fn clipboard_store(text: &str) -> anyhow::Result<()> {
pub fn version() -> anyhow::Result<u32> {
let mut sock = connect()?;
sock.send(&rbw::protocol::Request {
- tty: nix::unistd::ttyname(0)
+ tty: rustix::termios::ttyname(std::io::stdin(), vec![])
.ok()
- .and_then(|p| p.to_str().map(std::string::ToString::to_string)),
+ .and_then(|p| {
+ p.to_str().map(std::string::ToString::to_string).ok()
+ }),
action: rbw::protocol::Action::Version,
})?;
@@ -130,9 +142,11 @@ fn simple_action(action: rbw::protocol::Action) -> anyhow::Result<()> {
let mut sock = connect()?;
sock.send(&rbw::protocol::Request {
- tty: nix::unistd::ttyname(0)
+ tty: rustix::termios::ttyname(std::io::stdin(), vec![])
.ok()
- .and_then(|p| p.to_str().map(std::string::ToString::to_string)),
+ .and_then(|p| {
+ p.to_str().map(std::string::ToString::to_string).ok()
+ }),
action,
})?;
@@ -158,9 +172,9 @@ fn connect() -> anyhow::Result<crate::sock::Sock> {
})
}
-fn wait_for_exit(pid: nix::unistd::Pid) {
+fn wait_for_exit(pid: rustix::process::Pid) {
loop {
- if nix::sys::signal::kill(pid, None).is_err() {
+ if rustix::process::test_kill_process(pid).is_err() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs
index a4c0a26..b42c6fc 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -1,7 +1,9 @@
use anyhow::Context as _;
use serde::Serialize;
+use std::fmt::{Display, Formatter, Result as FmtResult};
use std::io;
use std::io::prelude::Write;
+use url::Url;
const MISSING_CONFIG_HELP: &str =
"Before using rbw, you must configure the email address you would like to \
@@ -13,6 +15,36 @@ const MISSING_CONFIG_HELP: &str =
and, if your server has a non-default identity url:\n\n \
rbw config set identity_url <url>\n";
+#[derive(Debug, Clone)]
+pub enum Needle {
+ Name(String),
+ Uri(Url),
+ 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.to_string(),
+ };
+ write!(f, "{value}")
+ }
+}
+
+#[allow(clippy::unnecessary_wraps)]
+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));
+ }
+
+ Ok(Needle::Name(arg.to_string()))
+}
+
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
struct DecryptedCipher {
@@ -485,13 +517,38 @@ impl DecryptedCipher {
fn exact_match(
&self,
- name: &str,
+ needle: &Needle,
username: Option<&str>,
folder: Option<&str>,
try_match_folder: bool,
) -> bool {
- if name != self.name {
- return false;
+ match needle {
+ Needle::Name(name) => {
+ if &self.name != name {
+ return false;
+ }
+ }
+ Needle::Uri(given_uri) => {
+ match &self.data {
+ DecryptedData::Login {
+ uris: Some(uris), ..
+ } => {
+ if !uris.iter().any(|uri| uri.matches_url(given_uri))
+ {
+ return false;
+ }
+ }
+ _ => {
+ // not sure what else to do here, but open to suggestions
+ return false;
+ }
+ }
+ }
+ Needle::Uuid(uuid) => {
+ if uuid::Uuid::parse_str(&self.id) != Ok(*uuid) {
+ return false;
+ }
+ }
}
if let Some(given_username) = username {
@@ -650,6 +707,76 @@ 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 host = url.host_str()?;
+ Some(
+ url.port().map_or_else(
+ || host.to_string(),
+ |port| format!("{host}:{port}"),
+ ),
+ )
+}
+
+fn domain_port(url: &Url) -> Option<String> {
+ let domain = url.domain()?;
+ Some(url.port().map_or_else(
+ || domain.to_string(),
+ |port| format!("{domain}:{port}"),
+ ))
+}
+
enum ListField {
Name,
Id,
@@ -857,7 +984,7 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> {
}
pub fn get(
- name: &str,
+ needle: &Needle,
user: Option<&str>,
folder: Option<&str>,
field: Option<&str>,
@@ -872,10 +999,10 @@ pub fn get(
let desc = format!(
"{}{}",
user.map_or_else(String::new, |s| format!("{s}@")),
- name
+ needle
);
- let (_, decrypted) = find_entry(&db, name, user, folder)
+ let (_, decrypted) = find_entry(&db, needle, user, folder)
.with_context(|| format!("couldn't find entry for '{desc}'"))?;
if raw {
decrypted.display_json(&desc)?;
@@ -891,9 +1018,10 @@ pub fn get(
}
pub fn code(
- name: &str,
+ needle: &Needle,
user: Option<&str>,
folder: Option<&str>,
+ clipboard: bool,
) -> anyhow::Result<()> {
unlock()?;
@@ -902,15 +1030,15 @@ pub fn code(
let desc = format!(
"{}{}",
user.map_or_else(String::new, |s| format!("{s}@")),
- name
+ needle
);
- let (_, decrypted) = find_entry(&db, name, user, folder)
+ let (_, decrypted) = find_entry(&db, needle, user, folder)
.with_context(|| format!("couldn't find entry for '{desc}'"))?;
if let DecryptedData::Login { totp, .. } = decrypted.data {
if let Some(totp) = totp {
- println!("{}", generate_totp(&totp)?);
+ val_display_or_store(clipboard, &generate_totp(&totp)?);
} else {
return Err(anyhow::anyhow!(
"entry does not contain a totp secret"
@@ -967,7 +1095,7 @@ pub fn add(
let (new_access_token, folders) =
rbw::actions::list_folders(&access_token, refresh_token)?;
if let Some(new_access_token) = new_access_token {
- access_token = new_access_token.clone();
+ access_token.clone_from(&new_access_token);
db.access_token = Some(new_access_token);
save_db(&db)?;
}
@@ -990,7 +1118,7 @@ pub fn add(
&crate::actions::encrypt(folder_name, None)?,
)?;
if let Some(new_access_token) = new_access_token {
- access_token = new_access_token.clone();
+ access_token.clone_from(&new_access_token);
db.access_token = Some(new_access_token);
save_db(&db)?;
}
@@ -1060,7 +1188,7 @@ pub fn generate(
let (new_access_token, folders) =
rbw::actions::list_folders(&access_token, refresh_token)?;
if let Some(new_access_token) = new_access_token {
- access_token = new_access_token.clone();
+ access_token.clone_from(&new_access_token);
db.access_token = Some(new_access_token);
save_db(&db)?;
}
@@ -1085,7 +1213,7 @@ pub fn generate(
&crate::actions::encrypt(folder_name, None)?,
)?;
if let Some(new_access_token) = new_access_token {
- access_token = new_access_token.clone();
+ access_token.clone_from(&new_access_token);
db.access_token = Some(new_access_token);
save_db(&db)?;
}
@@ -1133,10 +1261,11 @@ pub fn edit(
name
);
- let (entry, decrypted) = find_entry(&db, name, username, folder)
- .with_context(|| format!("couldn't find entry for '{desc}'"))?;
+ let (entry, decrypted) =
+ find_entry(&db, &Needle::Name(name.to_string()), username, folder)
+ .with_context(|| format!("couldn't find entry for '{desc}'"))?;
- let (data, notes, history) = match &decrypted.data {
+ let (data, fields, notes, history) = match &decrypted.data {
DecryptedData::Login { password, .. } => {
let mut contents =
format!("{}\n", password.as_deref().unwrap_or(""));
@@ -1144,7 +1273,7 @@ pub fn edit(
contents.push_str(&format!("\n{notes}\n"));
}
- let contents = rbw::edit::edit(&contents, HELP_NOTES)?;
+ let contents = rbw::edit::edit(&contents, HELP_PW)?;
let (password, notes) = parse_editor(&contents);
let password = password
@@ -1190,7 +1319,7 @@ pub fn edit(
uris: entry_uris.clone(),
totp: entry_totp.clone(),
};
- (data, notes, history)
+ (data, entry.fields, notes, history)
}
DecryptedData::SecureNote {} => {
let data = rbw::db::EntryData::SecureNote {};
@@ -1210,7 +1339,7 @@ pub fn edit(
})
.transpose()?;
- (data, notes, entry.history)
+ (data, entry.fields, notes, entry.history)
}
_ => {
return Err(anyhow::anyhow!(
@@ -1226,6 +1355,7 @@ pub fn edit(
entry.org_id.as_deref(),
&entry.name,
&data,
+ &fields,
notes.as_deref(),
entry.folder_id.as_deref(),
&history,
@@ -1255,8 +1385,9 @@ pub fn remove(
name
);
- let (entry, _) = find_entry(&db, name, username, folder)
- .with_context(|| format!("couldn't find entry for '{desc}'"))?;
+ let (entry, _) =
+ find_entry(&db, &Needle::Name(name.to_string()), username, folder)
+ .with_context(|| format!("couldn't find entry for '{desc}'"))?;
if let (Some(access_token), ()) =
rbw::actions::remove(access_token, refresh_token, &entry.id)?
@@ -1285,8 +1416,9 @@ pub fn history(
name
);
- let (_, decrypted) = find_entry(&db, name, username, folder)
- .with_context(|| format!("couldn't find entry for '{desc}'"))?;
+ let (_, decrypted) =
+ find_entry(&db, &Needle::Name(name.to_string()), username, folder)
+ .with_context(|| format!("couldn't find entry for '{desc}'"))?;
for history in decrypted.history {
println!("{}: {}", history.last_used_date, history.password);
}
@@ -1381,13 +1513,13 @@ fn version_or_quit() -> anyhow::Result<u32> {
fn find_entry(
db: &rbw::db::Db,
- name: &str,
+ needle: &Needle,
username: Option<&str>,
folder: Option<&str>,
) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> {
- if uuid::Uuid::parse_str(name).is_ok() {
+ if let Needle::Uuid(uuid) = needle {
for cipher in &db.entries {
- if name == cipher.id {
+ if uuid::Uuid::parse_str(&cipher.id) == Ok(*uuid) {
return Ok((cipher.clone(), decrypt_cipher(cipher)?));
}
}
@@ -1401,20 +1533,20 @@ fn find_entry(
decrypt_cipher(&entry).map(|decrypted| (entry, decrypted))
})
.collect::<anyhow::Result<_>>()?;
- find_entry_raw(&ciphers, name, username, folder)
+ find_entry_raw(&ciphers, needle, username, folder)
}
}
fn find_entry_raw(
entries: &[(rbw::db::Entry, DecryptedCipher)],
- name: &str,
+ needle: &Needle,
username: Option<&str>,
folder: Option<&str>,
) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> {
let mut matches: Vec<(rbw::db::Entry, DecryptedCipher)> = entries
.iter()
.filter(|&(_, decrypted_cipher)| {
- decrypted_cipher.exact_match(name, username, folder, true)
+ decrypted_cipher.exact_match(needle, username, folder, true)
})
.cloned()
.collect();
@@ -1427,7 +1559,7 @@ fn find_entry_raw(
matches = entries
.iter()
.filter(|&(_, decrypted_cipher)| {
- decrypted_cipher.exact_match(name, username, folder, false)
+ decrypted_cipher.exact_match(needle, username, folder, false)
})
.cloned()
.collect();
@@ -1437,29 +1569,32 @@ fn find_entry_raw(
}
}
- matches = entries
- .iter()
- .filter(|&(_, decrypted_cipher)| {
- decrypted_cipher.partial_match(name, 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(name, 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() {
@@ -1807,8 +1942,23 @@ fn remove_db() -> anyhow::Result<()> {
)
}
-fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> {
- let secret_str = if let Ok(u) = url::Url::parse(secret) {
+struct TotpParams {
+ secret: Vec<u8>,
+ algorithm: String,
+ digits: u32,
+ period: u64,
+}
+
+fn decode_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> {
+ base32::decode(
+ base32::Alphabet::RFC4648 { padding: false },
+ &secret.replace(' ', ""),
+ )
+ .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32"))
+}
+
+fn parse_totp_secret(secret: &str) -> anyhow::Result<TotpParams> {
+ if let Ok(u) = url::Url::parse(secret) {
if u.scheme() != "otpauth" {
return Err(anyhow::anyhow!(
"totp secret url must have otpauth scheme"
@@ -1821,32 +1971,73 @@ fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> {
}
let query: std::collections::HashMap<_, _> =
u.query_pairs().collect();
- query
- .get("secret")
- .ok_or_else(|| {
- anyhow::anyhow!("totp secret url must have secret")
- })?
- .to_string()
+ Ok(TotpParams {
+ secret: decode_totp_secret(query
+ .get("secret")
+ .ok_or_else(|| {
+ anyhow::anyhow!("totp secret url must have secret")
+ })?)?,
+ algorithm:query.get("algorithm").map_or_else(||{String::from("SHA1")},|alg|{alg.to_string()} ),
+ digits: match query.get("digits") {
+ Some(dig) => {
+ dig.parse::<u32>().map_err(|_|{
+ anyhow::anyhow!("digits parameter in totp url must be a valid integer.")
+ })?
+ }
+ None => 6,
+ },
+ period: match query.get("period") {
+ Some(dig) => {
+ dig.parse::<u64>().map_err(|_|{
+ anyhow::anyhow!("period parameter in totp url must be a valid integer.")
+ })?
+ }
+ None => totp_lite::DEFAULT_STEP,
+ }
+ })
} else {
- secret.to_string()
- };
- base32::decode(
- base32::Alphabet::RFC4648 { padding: false },
- &secret_str.replace(' ', ""),
- )
- .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32"))
+ Ok(TotpParams {
+ secret: decode_totp_secret(secret)?,
+ algorithm: String::from("SHA1"),
+ digits: 6,
+ period: totp_lite::DEFAULT_STEP,
+ })
+ }
}
fn generate_totp(secret: &str) -> anyhow::Result<String> {
- let key = parse_totp_secret(secret)?;
- Ok(totp_lite::totp_custom::<totp_lite::Sha1>(
- totp_lite::DEFAULT_STEP,
- 6,
- &key,
- std::time::SystemTime::now()
- .duration_since(std::time::SystemTime::UNIX_EPOCH)?
- .as_secs(),
- ))
+ let totp_params = parse_totp_secret(secret)?;
+ let alg = totp_params.algorithm.as_str();
+ match alg {
+ "SHA1" => Ok(totp_lite::totp_custom::<totp_lite::Sha1>(
+ totp_params.period,
+ totp_params.digits,
+ &totp_params.secret,
+ std::time::SystemTime::now()
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)?
+ .as_secs(),
+ )),
+ "SHA256" => Ok(totp_lite::totp_custom::<totp_lite::Sha256>(
+ totp_params.period,
+ totp_params.digits,
+ &totp_params.secret,
+ std::time::SystemTime::now()
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)?
+ .as_secs(),
+ )),
+ "SHA512" => Ok(totp_lite::totp_custom::<totp_lite::Sha512>(
+ totp_params.period,
+ totp_params.digits,
+ &totp_params.secret,
+ std::time::SystemTime::now()
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)?
+ .as_secs(),
+ )),
+ _ => Err(anyhow::anyhow!(format!(
+ "{} is not a valid totp algorithm",
+ alg
+ ))),
+ }
}
fn display_field(name: &str, field: Option<&str>, clipboard: bool) -> bool {
@@ -1863,15 +2054,15 @@ mod test {
#[test]
fn test_find_entry() {
let entries = &[
- make_entry("github", Some("foo"), None),
- make_entry("gitlab", Some("foo"), None),
- make_entry("gitlab", Some("bar"), None),
- make_entry("gitter", Some("baz"), None),
- make_entry("git", Some("foo"), None),
- make_entry("bitwarden", None, None),
- make_entry("github", Some("foo"), Some("websites")),
- make_entry("github", Some("foo"), Some("ssh")),
- make_entry("github", Some("root"), Some("ssh")),
+ make_entry("github", Some("foo"), None, &[]),
+ make_entry("gitlab", Some("foo"), None, &[]),
+ make_entry("gitlab", Some("bar"), None, &[]),
+ make_entry("gitter", Some("baz"), None, &[]),
+ make_entry("git", Some("foo"), None, &[]),
+ make_entry("bitwarden", None, None, &[]),
+ make_entry("github", Some("foo"), Some("websites"), &[]),
+ make_entry("github", Some("foo"), Some("ssh"), &[]),
+ make_entry("github", Some("root"), Some("ssh"), &[]),
];
assert!(
@@ -1930,26 +2121,653 @@ mod test {
);
}
+ #[test]
+ fn test_find_by_uuid() {
+ let entries = &[
+ make_entry("github", Some("foo"), None, &[]),
+ make_entry("gitlab", Some("foo"), None, &[]),
+ make_entry("gitlab", Some("bar"), None, &[]),
+ ];
+
+ assert!(
+ one_match(entries, &entries[0].0.id, None, None, 0),
+ "foo@github"
+ );
+ assert!(
+ one_match(entries, &entries[1].0.id, None, None, 1),
+ "foo@gitlab"
+ );
+ assert!(
+ one_match(entries, &entries[2].0.id, None, None, 2),
+ "bar@gitlab"
+ );
+
+ assert!(
+ one_match(
+ entries,
+ &entries[0].0.id.to_uppercase(),
+ None,
+ None,
+ 0
+ ),
+ "foo@github"
+ );
+ assert!(
+ one_match(
+ entries,
+ &entries[0].0.id.to_lowercase(),
+ None,
+ None,
+ 0
+ ),
+ "foo@github"
+ );
+ }
+
+ #[test]
+ fn test_find_by_url_default() {
+ let entries = &[
+ make_entry("one", None, None, &[("https://one.com/", None)]),
+ make_entry("two", None, None, &[("https://two.com/login", None)]),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[("https://login.three.com/", None)],
+ ),
+ make_entry("four", None, None, &[("four.com", None)]),
+ make_entry(
+ "five",
+ None,
+ None,
+ &[("https://five.com:8080/", None)],
+ ),
+ make_entry("six", None, None, &[("six.com:8080", None)]),
+ ];
+
+ assert!(one_match(entries, "https://one.com/", None, None, 0), "one");
+ assert!(
+ one_match(entries, "https://login.one.com/", None, None, 0),
+ "one"
+ );
+ assert!(
+ one_match(entries, "https://one.com:443/", None, None, 0),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(one_match(entries, "https://two.com/", None, None, 1), "two");
+ assert!(
+ one_match(entries, "https://two.com/other-page", None, None, 1),
+ "two"
+ );
+
+ assert!(
+ one_match(entries, "https://login.three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://three.com/", None, None),
+ "three"
+ );
+
+ assert!(
+ one_match(entries, "https://four.com/", None, None, 3),
+ "four"
+ );
+
+ assert!(
+ one_match(entries, "https://five.com:8080/", None, None, 4),
+ "five"
+ );
+ assert!(no_matches(entries, "https://five.com/", None, None), "five");
+
+ assert!(
+ one_match(entries, "https://six.com:8080/", None, None, 5),
+ "six"
+ );
+ assert!(no_matches(entries, "https://six.com/", None, None), "six");
+ }
+
+ #[test]
+ fn test_find_by_url_domain() {
+ let entries = &[
+ make_entry(
+ "one",
+ None,
+ None,
+ &[("https://one.com/", Some(rbw::api::UriMatchType::Domain))],
+ ),
+ make_entry(
+ "two",
+ None,
+ None,
+ &[(
+ "https://two.com/login",
+ Some(rbw::api::UriMatchType::Domain),
+ )],
+ ),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[(
+ "https://login.three.com/",
+ Some(rbw::api::UriMatchType::Domain),
+ )],
+ ),
+ make_entry(
+ "four",
+ None,
+ None,
+ &[("four.com", Some(rbw::api::UriMatchType::Domain))],
+ ),
+ make_entry(
+ "five",
+ None,
+ None,
+ &[(
+ "https://five.com:8080/",
+ Some(rbw::api::UriMatchType::Domain),
+ )],
+ ),
+ make_entry(
+ "six",
+ None,
+ None,
+ &[("six.com:8080", Some(rbw::api::UriMatchType::Domain))],
+ ),
+ ];
+
+ assert!(one_match(entries, "https://one.com/", None, None, 0), "one");
+ assert!(
+ one_match(entries, "https://login.one.com/", None, None, 0),
+ "one"
+ );
+ assert!(
+ one_match(entries, "https://one.com:443/", None, None, 0),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(one_match(entries, "https://two.com/", None, None, 1), "two");
+ assert!(
+ one_match(entries, "https://two.com/other-page", None, None, 1),
+ "two"
+ );
+
+ assert!(
+ one_match(entries, "https://login.three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://three.com/", None, None),
+ "three"
+ );
+
+ assert!(
+ one_match(entries, "https://four.com/", None, None, 3),
+ "four"
+ );
+
+ assert!(
+ one_match(entries, "https://five.com:8080/", None, None, 4),
+ "five"
+ );
+ assert!(no_matches(entries, "https://five.com/", None, None), "five");
+
+ assert!(
+ one_match(entries, "https://six.com:8080/", None, None, 5),
+ "six"
+ );
+ assert!(no_matches(entries, "https://six.com/", None, None), "six");
+ }
+
+ #[test]
+ fn test_find_by_url_host() {
+ let entries = &[
+ make_entry(
+ "one",
+ None,
+ None,
+ &[("https://one.com/", Some(rbw::api::UriMatchType::Host))],
+ ),
+ make_entry(
+ "two",
+ None,
+ None,
+ &[(
+ "https://two.com/login",
+ Some(rbw::api::UriMatchType::Host),
+ )],
+ ),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[(
+ "https://login.three.com/",
+ Some(rbw::api::UriMatchType::Host),
+ )],
+ ),
+ make_entry(
+ "four",
+ None,
+ None,
+ &[("four.com", Some(rbw::api::UriMatchType::Host))],
+ ),
+ make_entry(
+ "five",
+ None,
+ None,
+ &[(
+ "https://five.com:8080/",
+ Some(rbw::api::UriMatchType::Host),
+ )],
+ ),
+ make_entry(
+ "six",
+ None,
+ None,
+ &[("six.com:8080", Some(rbw::api::UriMatchType::Host))],
+ ),
+ ];
+
+ assert!(one_match(entries, "https://one.com/", None, None, 0), "one");
+ assert!(
+ no_matches(entries, "https://login.one.com/", None, None),
+ "one"
+ );
+ assert!(
+ one_match(entries, "https://one.com:443/", None, None, 0),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(one_match(entries, "https://two.com/", None, None, 1), "two");
+ assert!(
+ one_match(entries, "https://two.com/other-page", None, None, 1),
+ "two"
+ );
+
+ assert!(
+ one_match(entries, "https://login.three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://three.com/", None, None),
+ "three"
+ );
+
+ assert!(
+ one_match(entries, "https://four.com/", None, None, 3),
+ "four"
+ );
+
+ assert!(
+ one_match(entries, "https://five.com:8080/", None, None, 4),
+ "five"
+ );
+ assert!(no_matches(entries, "https://five.com/", None, None), "five");
+
+ assert!(
+ one_match(entries, "https://six.com:8080/", None, None, 5),
+ "six"
+ );
+ assert!(no_matches(entries, "https://six.com/", None, None), "six");
+ }
+
+ #[test]
+ fn test_find_by_url_starts_with() {
+ let entries = &[
+ make_entry(
+ "one",
+ None,
+ None,
+ &[(
+ "https://one.com/",
+ Some(rbw::api::UriMatchType::StartsWith),
+ )],
+ ),
+ make_entry(
+ "two",
+ None,
+ None,
+ &[(
+ "https://two.com/login",
+ Some(rbw::api::UriMatchType::StartsWith),
+ )],
+ ),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[(
+ "https://login.three.com/",
+ Some(rbw::api::UriMatchType::StartsWith),
+ )],
+ ),
+ ];
+
+ assert!(one_match(entries, "https://one.com/", None, None, 0), "one");
+ assert!(
+ no_matches(entries, "https://login.one.com/", None, None),
+ "one"
+ );
+ assert!(
+ one_match(entries, "https://one.com:443/", None, None, 0),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(
+ one_match(entries, "https://two.com/login", None, None, 1),
+ "two"
+ );
+ assert!(
+ one_match(entries, "https://two.com/login/sso", None, None, 1),
+ "two"
+ );
+ assert!(no_matches(entries, "https://two.com/", None, None), "two");
+ assert!(
+ no_matches(entries, "https://two.com/other-page", None, None),
+ "two"
+ );
+
+ assert!(
+ one_match(entries, "https://login.three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://three.com/", None, None),
+ "three"
+ );
+ }
+
+ #[test]
+ fn test_find_by_url_exact() {
+ let entries = &[
+ make_entry(
+ "one",
+ None,
+ None,
+ &[("https://one.com/", Some(rbw::api::UriMatchType::Exact))],
+ ),
+ make_entry(
+ "two",
+ None,
+ None,
+ &[(
+ "https://two.com/login",
+ Some(rbw::api::UriMatchType::Exact),
+ )],
+ ),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[(
+ "https://login.three.com/",
+ Some(rbw::api::UriMatchType::Exact),
+ )],
+ ),
+ ];
+
+ assert!(one_match(entries, "https://one.com/", None, None, 0), "one");
+ assert!(
+ no_matches(entries, "https://login.one.com/", None, None),
+ "one"
+ );
+ assert!(
+ one_match(entries, "https://one.com:443/", None, None, 0),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(
+ one_match(entries, "https://two.com/login", None, None, 1),
+ "two"
+ );
+ assert!(
+ no_matches(entries, "https://two.com/login/sso", None, None),
+ "two"
+ );
+ assert!(no_matches(entries, "https://two.com/", None, None), "two");
+ assert!(
+ no_matches(entries, "https://two.com/other-page", None, None),
+ "two"
+ );
+
+ assert!(
+ one_match(entries, "https://login.three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://three.com/", None, None),
+ "three"
+ );
+ }
+
+ #[test]
+ fn test_find_by_url_regex() {
+ let entries = &[
+ make_entry(
+ "one",
+ None,
+ None,
+ &[(
+ r"^https://one\.com/$",
+ Some(rbw::api::UriMatchType::RegularExpression),
+ )],
+ ),
+ make_entry(
+ "two",
+ None,
+ None,
+ &[(
+ r"^https://two\.com/(login|start)",
+ Some(rbw::api::UriMatchType::RegularExpression),
+ )],
+ ),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[(
+ r"^https://(login\.)?three\.com/$",
+ Some(rbw::api::UriMatchType::RegularExpression),
+ )],
+ ),
+ ];
+
+ assert!(one_match(entries, "https://one.com/", None, None, 0), "one");
+ assert!(
+ no_matches(entries, "https://login.one.com/", None, None),
+ "one"
+ );
+ assert!(
+ one_match(entries, "https://one.com:443/", None, None, 0),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(
+ one_match(entries, "https://two.com/login", None, None, 1),
+ "two"
+ );
+ assert!(
+ one_match(entries, "https://two.com/start", None, None, 1),
+ "two"
+ );
+ assert!(
+ one_match(entries, "https://two.com/login/sso", None, None, 1),
+ "two"
+ );
+ assert!(no_matches(entries, "https://two.com/", None, None), "two");
+ assert!(
+ no_matches(entries, "https://two.com/other-page", None, None),
+ "two"
+ );
+
+ assert!(
+ one_match(entries, "https://login.three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ one_match(entries, "https://three.com/", None, None, 2),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://www.three.com/", None, None),
+ "three"
+ );
+ }
+
+ #[test]
+ fn test_find_by_url_never() {
+ let entries = &[
+ make_entry(
+ "one",
+ None,
+ None,
+ &[("https://one.com/", Some(rbw::api::UriMatchType::Never))],
+ ),
+ make_entry(
+ "two",
+ None,
+ None,
+ &[(
+ "https://two.com/login",
+ Some(rbw::api::UriMatchType::Never),
+ )],
+ ),
+ make_entry(
+ "three",
+ None,
+ None,
+ &[(
+ "https://login.three.com/",
+ Some(rbw::api::UriMatchType::Never),
+ )],
+ ),
+ make_entry(
+ "four",
+ None,
+ None,
+ &[("four.com", Some(rbw::api::UriMatchType::Never))],
+ ),
+ make_entry(
+ "five",
+ None,
+ None,
+ &[(
+ "https://five.com:8080/",
+ Some(rbw::api::UriMatchType::Never),
+ )],
+ ),
+ make_entry(
+ "six",
+ None,
+ None,
+ &[("six.com:8080", Some(rbw::api::UriMatchType::Never))],
+ ),
+ ];
+
+ assert!(no_matches(entries, "https://one.com/", None, None), "one");
+ assert!(
+ no_matches(entries, "https://login.one.com/", None, None),
+ "one"
+ );
+ assert!(
+ no_matches(entries, "https://one.com:443/", None, None),
+ "one"
+ );
+ assert!(no_matches(entries, "one.com", None, None), "one");
+ assert!(no_matches(entries, "https", None, None), "one");
+ assert!(no_matches(entries, "com", None, None), "one");
+ assert!(no_matches(entries, "https://com/", None, None), "one");
+
+ assert!(no_matches(entries, "https://two.com/", None, None), "two");
+ assert!(
+ no_matches(entries, "https://two.com/other-page", None, None),
+ "two"
+ );
+
+ assert!(
+ no_matches(entries, "https://login.three.com/", None, None),
+ "three"
+ );
+ assert!(
+ no_matches(entries, "https://three.com/", None, None),
+ "three"
+ );
+
+ assert!(no_matches(entries, "https://four.com/", None, None), "four");
+
+ assert!(
+ no_matches(entries, "https://five.com:8080/", None, None),
+ "five"
+ );
+ assert!(no_matches(entries, "https://five.com/", None, None), "five");
+
+ assert!(
+ no_matches(entries, "https://six.com:8080/", None, None),
+ "six"
+ );
+ assert!(no_matches(entries, "https://six.com/", None, None), "six");
+ }
+
+ #[track_caller]
fn one_match(
entries: &[(rbw::db::Entry, DecryptedCipher)],
- name: &str,
+ needle: &str,
username: Option<&str>,
folder: Option<&str>,
idx: usize,
) -> bool {
entries_eq(
- &find_entry_raw(entries, name, username, folder).unwrap(),
+ &find_entry_raw(
+ entries,
+ &parse_needle(needle).unwrap(),
+ username,
+ folder,
+ )
+ .unwrap(),
&entries[idx],
)
}
+ #[track_caller]
fn no_matches(
entries: &[(rbw::db::Entry, DecryptedCipher)],
- name: &str,
+ needle: &str,
username: Option<&str>,
folder: Option<&str>,
) -> bool {
- let res = find_entry_raw(entries, name, username, folder);
+ let res = find_entry_raw(
+ entries,
+ &parse_needle(needle).unwrap(),
+ username,
+ folder,
+ );
if let Err(e) = res {
format!("{e}").contains("no entry found")
} else {
@@ -1957,13 +2775,19 @@ mod test {
}
}
+ #[track_caller]
fn many_matches(
entries: &[(rbw::db::Entry, DecryptedCipher)],
- name: &str,
+ needle: &str,
username: Option<&str>,
folder: Option<&str>,
) -> bool {
- let res = find_entry_raw(entries, name, username, folder);
+ let res = find_entry_raw(
+ entries,
+ &parse_needle(needle).unwrap(),
+ username,
+ folder,
+ );
if let Err(e) = res {
format!("{e}").contains("multiple entries found")
} else {
@@ -1971,6 +2795,7 @@ mod test {
}
}
+ #[track_caller]
fn entries_eq(
a: &(rbw::db::Entry, DecryptedCipher),
b: &(rbw::db::Entry, DecryptedCipher),
@@ -1982,10 +2807,12 @@ mod test {
name: &str,
username: Option<&str>,
folder: Option<&str>,
+ uris: &[(&str, Option<rbw::api::UriMatchType>)],
) -> (rbw::db::Entry, DecryptedCipher) {
+ let id = uuid::Uuid::new_v4();
(
rbw::db::Entry {
- id: "irrelevant".to_string(),
+ id: id.to_string(),
org_id: None,
folder: folder.map(|_| "encrypted folder name".to_string()),
folder_id: None,
@@ -1995,7 +2822,13 @@ mod test {
"this is the encrypted username".to_string()
}),
password: None,
- uris: vec![],
+ uris: uris
+ .iter()
+ .map(|(_, match_type)| rbw::db::Uri {
+ uri: "this is the encrypted uri".to_string(),
+ match_type: *match_type,
+ })
+ .collect(),
totp: None,
},
fields: vec![],
@@ -2003,14 +2836,21 @@ mod test {
history: vec![],
},
DecryptedCipher {
- id: "irrelevant".to_string(),
+ id: id.to_string(),
folder: folder.map(std::string::ToString::to_string),
name: name.to_string(),
data: DecryptedData::Login {
username: username.map(std::string::ToString::to_string),
password: None,
totp: None,
- uris: None,
+ uris: Some(
+ uris.iter()
+ .map(|(uri, match_type)| DecryptedUri {
+ uri: (*uri).to_string(),
+ match_type: *match_type,
+ })
+ .collect(),
+ ),
},
fields: vec![],
notes: None,
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index 72e4220..2fb96bf 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -71,8 +71,8 @@ enum Opt {
#[command(about = "Display the password for a given entry")]
Get {
- #[arg(help = "Name or UUID of the entry to display")]
- name: String,
+ #[arg(help = "Name, URI or UUID of the entry to display", value_parser = commands::parse_needle)]
+ needle: commands::Needle,
#[arg(help = "Username of the entry to display")]
user: Option<String>,
#[arg(long, help = "Folder name to search in")]
@@ -87,14 +87,19 @@ enum Opt {
clipboard: bool,
},
- #[command(about = "Display the authenticator code for a given entry")]
+ #[command(
+ about = "Display the authenticator code for a given entry",
+ visible_alias = "totp"
+ )]
Code {
- #[arg(help = "Name or UUID of the entry to display")]
- name: String,
+ #[arg(help = "Name, URI or UUID of the entry to display", value_parser = commands::parse_needle)]
+ needle: commands::Needle,
#[arg(help = "Username of the entry to display")]
user: Option<String>,
#[arg(long, help = "Folder name to search in")]
folder: Option<String>,
+ #[structopt(long, help = "Copy result to clipboard")]
+ clipboard: bool,
},
#[command(
@@ -318,7 +323,7 @@ fn main() {
Opt::Sync => commands::sync(),
Opt::List { fields } => commands::list(fields),
Opt::Get {
- name,
+ needle,
user,
folder,
field,
@@ -326,7 +331,7 @@ fn main() {
raw,
clipboard,
} => commands::get(
- name,
+ needle,
user.as_deref(),
folder.as_deref(),
field.as_deref(),
@@ -334,9 +339,17 @@ fn main() {
*raw,
*clipboard,
),
- Opt::Code { name, user, folder } => {
- commands::code(name, user.as_deref(), folder.as_deref())
- }
+ Opt::Code {
+ needle,
+ user,
+ folder,
+ clipboard,
+ } => commands::code(
+ needle,
+ user.as_deref(),
+ folder.as_deref(),
+ *clipboard,
+ ),
Opt::Add {
name,
user,