diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/actions.rs | 4 | ||||
-rw-r--r-- | src/api.rs | 138 | ||||
-rw-r--r-- | src/bin/rbw-agent/actions.rs | 5 | ||||
-rw-r--r-- | src/bin/rbw-agent/agent.rs | 7 | ||||
-rw-r--r-- | src/bin/rbw-agent/daemon.rs | 28 | ||||
-rw-r--r-- | src/bin/rbw-agent/debugger.rs | 2 | ||||
-rw-r--r-- | src/bin/rbw-agent/main.rs | 2 | ||||
-rw-r--r-- | src/bin/rbw/actions.rs | 44 | ||||
-rw-r--r-- | src/bin/rbw/commands.rs | 1016 | ||||
-rw-r--r-- | src/bin/rbw/main.rs | 33 | ||||
-rw-r--r-- | src/db.rs | 2 | ||||
-rw-r--r-- | src/dirs.rs | 2 | ||||
-rw-r--r-- | src/error.rs | 9 | ||||
-rw-r--r-- | src/identity.rs | 2 |
14 files changed, 1112 insertions, 182 deletions
diff --git a/src/actions.rs b/src/actions.rs index b07cf44..7ee1fa4 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -185,6 +185,7 @@ pub fn edit( org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], @@ -196,6 +197,7 @@ pub fn edit( org_id, name, data, + fields, notes, folder_uuid, history, @@ -209,6 +211,7 @@ fn edit_once( org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], @@ -220,6 +223,7 @@ fn edit_once( org_id, name, data, + fields, notes, folder_uuid, history, @@ -362,7 +362,7 @@ struct SyncResCipher { #[serde(rename = "PasswordHistory", alias = "passwordHistory")] password_history: Option<Vec<SyncResPasswordHistory>>, #[serde(rename = "Fields", alias = "fields")] - fields: Option<Vec<SyncResField>>, + fields: Option<Vec<CipherField>>, #[serde(rename = "DeletedDate", alias = "deletedDate")] deleted_date: Option<String>, } @@ -463,8 +463,10 @@ impl SyncResCipher { fields .iter() .map(|field| crate::db::Field { + ty: field.ty, name: field.name.clone(), value: field.value.clone(), + linked_id: field.linked_id, }) .collect() }); @@ -582,6 +584,75 @@ struct CipherIdentity { username: Option<String>, } +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Clone, + Copy, + PartialEq, + Eq, +)] +#[repr(u16)] +pub enum FieldType { + Text = 0, + Hidden = 1, + Boolean = 2, + Linked = 3, +} + +#[derive( + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, + Debug, + Clone, + Copy, + PartialEq, + Eq, +)] +#[repr(u16)] +pub enum LinkedIdType { + LoginUsername = 100, + LoginPassword = 101, + CardCardholderName = 300, + CardExpMonth = 301, + CardExpYear = 302, + CardCode = 303, + CardBrand = 304, + CardNumber = 305, + IdentityTitle = 400, + IdentityMiddleName = 401, + IdentityAddress1 = 402, + IdentityAddress2 = 403, + IdentityAddress3 = 404, + IdentityCity = 405, + IdentityState = 406, + IdentityPostalCode = 407, + IdentityCountry = 408, + IdentityCompany = 409, + IdentityEmail = 410, + IdentityPhone = 411, + IdentitySsn = 412, + IdentityUsername = 413, + IdentityPassportNumber = 414, + IdentityLicenseNumber = 415, + IdentityFirstName = 416, + IdentityLastName = 417, + IdentityFullName = 418, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +struct CipherField { + #[serde(rename = "Type", alias = "type")] + ty: FieldType, + #[serde(rename = "Name", alias = "name")] + name: Option<String>, + #[serde(rename = "Value", alias = "value")] + value: Option<String>, + #[serde(rename = "LinkedId", alias = "linkedId")] + linked_id: Option<LinkedIdType>, +} + // this is just a name and some notes, both of which are already on the cipher // object #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -595,16 +666,6 @@ struct SyncResPasswordHistory { password: Option<String>, } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -struct SyncResField { - #[serde(rename = "Type", alias = "type")] - ty: u32, - #[serde(rename = "Name", alias = "name")] - name: Option<String>, - #[serde(rename = "Value", alias = "value")] - value: Option<String>, -} - #[derive(serde::Serialize, Debug)] struct CiphersPostReq { #[serde(rename = "type")] @@ -633,6 +694,7 @@ struct CiphersPutReq { login: Option<CipherLogin>, card: Option<CipherCard>, identity: Option<CipherIdentity>, + fields: Vec<CipherField>, #[serde(rename = "secureNote")] secure_note: Option<CipherSecureNote>, #[serde(rename = "passwordHistory")] @@ -640,12 +702,6 @@ struct CiphersPutReq { } #[derive(serde::Serialize, Debug)] -struct CiphersPutReqLogin { - username: Option<String>, - password: Option<String>, -} - -#[derive(serde::Serialize, Debug)] struct CiphersPutReqHistory { #[serde(rename = "LastUsedDate")] last_used_date: String, @@ -709,20 +765,26 @@ impl Client { file: client_cert_path.clone(), } })?; - let pem = reqwest::Identity::from_pem(&buf).map_err(|e| { - Error::LoadClientCertReqwest { - source: e, - file: client_cert_path.clone(), - } - })?; - Ok(reqwest::Client::builder().identity(pem).build().map_err( - |e| Error::LoadClientCertReqwest { - source: e, - file: client_cert_path.clone(), - }, - )?) + let pem = reqwest::Identity::from_pem(&buf) + .map_err(|e| Error::CreateReqwestClient { source: e })?; + Ok(reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .identity(pem) + .build() + .map_err(|e| Error::CreateReqwestClient { source: e })?) } else { - Ok(reqwest::Client::new()) + Ok(reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .build() + .map_err(|e| Error::CreateReqwestClient { source: e })?) } } @@ -827,14 +889,12 @@ impl Client { let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) + .header("Bitwarden-Client-Name", env!("CARGO_PKG_NAME")) + .header("Bitwarden-Client-Version", env!("CARGO_PKG_VERSION")) .header( "auth-email", crate::base64::encode_url_safe_no_pad(email), ) - .header( - "user-agent", - format!("rbw/{}", env!("CARGO_PKG_VERSION")), - ) .send() .await .map_err(|source| Error::Reqwest { source })?; @@ -1040,6 +1100,7 @@ impl Client { org_id: Option<&str>, name: &str, data: &crate::db::EntryData, + fields: &[crate::db::Field], notes: Option<&str>, folder_uuid: Option<&str>, history: &[crate::db::HistoryEntry], @@ -1059,6 +1120,15 @@ impl Client { card: None, identity: None, secure_note: None, + fields: fields + .iter() + .map(|field| CipherField { + ty: field.ty, + name: field.name.clone(), + value: field.value.clone(), + linked_id: field.linked_id, + }) + .collect(), password_history: history .iter() .map(|entry| CiphersPutReqHistory { 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, @@ -147,8 +147,10 @@ pub enum EntryData { serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq, )] pub struct Field { + pub ty: crate::api::FieldType, pub name: Option<String>, pub value: Option<String>, + pub linked_id: Option<crate::api::LinkedIdType>, } #[derive( diff --git a/src/dirs.rs b/src/dirs.rs index e838e12..2fa6e50 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -108,7 +108,7 @@ fn runtime_dir() -> std::path::PathBuf { "{}/{}-{}", std::env::temp_dir().to_string_lossy(), &profile(), - nix::unistd::getuid().as_raw() + rustix::process::getuid().as_raw() ) .into() }, diff --git a/src/error.rs b/src/error.rs index 8a3b5e2..db0503a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,9 @@ pub enum Error { file: std::path::PathBuf, }, + #[error("failed to create reqwest client")] + CreateReqwestClient { source: reqwest::Error }, + #[error("failed to decrypt")] Decrypt { source: block_padding::UnpadError }, @@ -127,12 +130,6 @@ pub enum Error { file: std::path::PathBuf, }, - #[error("failed to load client cert from {}", .file.display())] - LoadClientCertReqwest { - source: reqwest::Error, - file: std::path::PathBuf, - }, - #[error("invalid padding")] Padding, diff --git a/src/identity.rs b/src/identity.rs index 7836a11..fd46b85 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -17,6 +17,8 @@ impl Identity { memory: Option<u32>, parallelism: Option<u32>, ) -> Result<Self> { + let email = email.trim().to_lowercase(); + let iterations = std::num::NonZeroU32::new(iterations) .ok_or(Error::Pbkdf2ZeroIterations)?; |