diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 9ddd2ad..d06bb98 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -374,6 +374,7 @@ async fn login_success( async fn unlock_state( state: std::sync::Arc>, environment: &rbw::protocol::Environment, + password: Option<&rbw::locked::Password>, ) -> anyhow::Result<()> { if state.lock().await.needs_unlock() { let db = load_db().await?; @@ -404,6 +405,26 @@ async fn unlock_state( let email = config_email().await?; + if let Some(password) = password { + // Password was passed through stdin + match rbw::actions::unlock( + &email, + &password, + kdf, + iterations, + memory, + parallelism, + &protected_key, + &protected_private_key, + &db.protected_org_keys, + ) { + Ok((keys, org_keys)) => { + return unlock_success(state, keys, org_keys).await + } + Err(e) => return Err(e).context("failed to unlock database"), + } + } + let mut err_msg = None; for i in 1_u8..=3 { let err = if i > 1 { @@ -462,8 +483,9 @@ pub async fn unlock( sock: &mut crate::sock::Sock, state: std::sync::Arc>, environment: &rbw::protocol::Environment, + password: Option<&rbw::locked::Password>, ) -> anyhow::Result<()> { - unlock_state(state, environment).await?; + unlock_state(state, environment, password).await?; respond_ack(sock).await?; @@ -858,7 +880,7 @@ pub async fn get_ssh_public_keys( state.set_timeout(); state.last_environment().clone() }; - unlock_state(state.clone(), &environment).await?; + unlock_state(state.clone(), &environment, None).await?; let db = load_db().await?; let mut pubkeys = Vec::new(); @@ -894,7 +916,7 @@ pub async fn find_ssh_private_key( state.set_timeout(); state.last_environment().clone() }; - unlock_state(state.clone(), &environment).await?; + unlock_state(state.clone(), &environment, None).await?; let request_bytes = request_public_key.to_bytes(); diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs index 1691ed5..7ce9774 100644 --- a/src/bin/rbw-agent/agent.rs +++ b/src/bin/rbw-agent/agent.rs @@ -118,7 +118,7 @@ async fn handle_request( } }; let (action, environment) = req.into_parts(); - let set_timeout = match &action { + let set_timeout = match action { rbw::protocol::Action::Register => { crate::actions::register(sock, &environment).await?; true @@ -127,8 +127,25 @@ async fn handle_request( crate::actions::login(sock, state.clone(), &environment).await?; true } - rbw::protocol::Action::Unlock => { - crate::actions::unlock(sock, state.clone(), &environment).await?; + rbw::protocol::Action::Unlock { mut password } => { + // Copy the password into locked memory, then zeroize + // the original String + let locked_password = password.as_deref().map(|p| { + let mut v = rbw::locked::Vec::new(); + v.extend(p.as_bytes().iter().copied()); + rbw::locked::Password::new(v) + }); + if let Some(ref mut p) = password { + zeroize::Zeroize::zeroize(p); + } + + crate::actions::unlock( + sock, + state.clone(), + &environment, + locked_password.as_ref(), + ) + .await?; true } rbw::protocol::Action::CheckLock => { @@ -148,9 +165,6 @@ async fn handle_request( entry_key, org_id, } => { - let cipherstring = cipherstring.clone(); - let entry_key = entry_key.clone(); - let org_id = org_id.clone(); crate::actions::decrypt( sock, state.clone(), @@ -166,14 +180,14 @@ async fn handle_request( crate::actions::encrypt( sock, state.clone(), - plaintext, + &plaintext, org_id.as_deref(), ) .await?; true } rbw::protocol::Action::ClipboardStore { text } => { - crate::actions::clipboard_store(sock, state.clone(), text) + crate::actions::clipboard_store(sock, state.clone(), &text) .await?; true } diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs index a0a34e8..b13adb7 100644 --- a/src/bin/rbw/actions.rs +++ b/src/bin/rbw/actions.rs @@ -10,8 +10,8 @@ pub fn login() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Login) } -pub fn unlock() -> anyhow::Result<()> { - simple_action(rbw::protocol::Action::Unlock) +pub fn unlock(password: Option) -> anyhow::Result<()> { + simple_action(rbw::protocol::Action::Unlock { password }) } pub fn unlocked() -> anyhow::Result<()> { diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index bddf0ef..e7b8c6f 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -1333,10 +1333,10 @@ pub fn login() -> anyhow::Result<()> { Ok(()) } -pub fn unlock() -> anyhow::Result<()> { +pub fn unlock(password: Option) -> anyhow::Result<()> { ensure_agent()?; crate::actions::login()?; - crate::actions::unlock()?; + crate::actions::unlock(password)?; Ok(()) } @@ -1368,7 +1368,7 @@ pub fn list(fields: &[String], raw: bool) -> anyhow::Result<()> { .collect::>()? }; - unlock()?; + unlock(None)?; let db = load_db()?; let mut entries: Vec = db @@ -1395,7 +1395,7 @@ pub fn get( ignore_case: bool, list_fields: bool, ) -> anyhow::Result<()> { - unlock()?; + unlock(None)?; let db = load_db()?; @@ -1496,7 +1496,7 @@ pub fn search( .collect::>()? }; - unlock()?; + unlock(None)?; let db = load_db()?; @@ -1526,7 +1526,7 @@ pub fn code( clipboard: bool, ignore_case: bool, ) -> anyhow::Result<()> { - unlock()?; + unlock(None)?; let db = load_db()?; @@ -1561,7 +1561,7 @@ pub fn add( uris: &[(String, Option)], folder: Option<&str>, ) -> anyhow::Result<()> { - unlock()?; + unlock(None)?; let mut db = load_db()?; // unwrap is safe here because the call to unlock above is guaranteed to @@ -1666,7 +1666,7 @@ pub fn generate( println!("{password}"); if let Some(name) = name { - unlock()?; + unlock(None)?; let mut db = load_db()?; // unwrap is safe here because the call to unlock above is guaranteed @@ -1756,7 +1756,7 @@ pub fn edit( folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<()> { - unlock()?; + unlock(None)?; let mut db = load_db()?; let access_token = db.access_token.as_ref().unwrap(); @@ -1881,7 +1881,7 @@ pub fn remove( folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<()> { - unlock()?; + unlock(None)?; let mut db = load_db()?; let access_token = db.access_token.as_ref().unwrap(); @@ -1914,7 +1914,7 @@ pub fn history( folder: Option<&str>, ignore_case: bool, ) -> anyhow::Result<()> { - unlock()?; + unlock(None)?; let db = load_db()?; diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index ff2ec74..4ecb3ad 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -43,7 +43,10 @@ enum Opt { Login, #[command(about = "Unlock the local Bitwarden database")] - Unlock, + Unlock { + #[arg(long, help = "Read the password from standard input")] + stdin: bool, + }, #[command(about = "Check if the local Bitwarden database is unlocked")] Unlocked, @@ -247,7 +250,7 @@ impl Opt { } Self::Register => "register".to_string(), Self::Login => "login".to_string(), - Self::Unlock => "unlock".to_string(), + Self::Unlock { .. } => "unlock".to_string(), Self::Unlocked => "unlocked".to_string(), Self::Sync => "sync".to_string(), Self::List { .. } => "list".to_string(), @@ -334,7 +337,19 @@ fn main() { }, Opt::Register => commands::register(), Opt::Login => commands::login(), - Opt::Unlock => commands::unlock(), + Opt::Unlock { stdin } => { + let password = if stdin { + let mut buf = String::new(); + let _ = std::io::stdin() + .read_line(&mut buf) + .context("failed to read password from stdin"); + Some(buf.trim_end_matches('\n').to_string()) + } else { + None + }; + + commands::unlock(password) + } Opt::Unlocked => commands::unlocked(), Opt::Sync => commands::sync(), Opt::List { fields, raw } => commands::list(&fields, raw), diff --git a/src/protocol.rs b/src/protocol.rs index ec0c06e..451e892 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -170,7 +170,9 @@ impl Environment { pub enum Action { Login, Register, - Unlock, + Unlock { + password: Option, + }, CheckLock, Lock, Sync,