From 236f06736e45c2a70f43589c9d447a0a3ef240b5 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 12 Apr 2020 00:08:03 -0400 Subject: improve error handling and reporting --- src/bin/rbw-agent/actions.rs | 177 +++++++++++++++++++++++++++++++------------ src/bin/rbw-agent/agent.rs | 42 +++++++--- src/bin/rbw-agent/daemon.rs | 27 ++++--- src/bin/rbw-agent/main.rs | 47 ++++++++++-- src/bin/rbw-agent/sock.rs | 36 ++++++--- 5 files changed, 243 insertions(+), 86 deletions(-) (limited to 'src/bin/rbw-agent') diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 66d732f..0eec665 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -1,26 +1,43 @@ +use anyhow::Context as _; + pub async fn login( sock: &mut crate::sock::Sock, state: std::sync::Arc>, tty: Option<&str>, -) { +) -> anyhow::Result<()> { let mut state = state.write().await; - let email = config_email().await; + let email = config_email() + .await + .context("failed to read email from config")?; let mut db = rbw::db::Db::load_async(&email) .await .unwrap_or_else(|_| rbw::db::Db::new()); if db.needs_login() { - let url = config_base_url().await; - let url = reqwest::Url::parse(&url).unwrap(); + let url_str = config_base_url() + .await + .context("failed to read base url from config")?; + let url = reqwest::Url::parse(&url_str) + .context("failed to parse base url")?; + let host = if let Some(host) = url.host_str() { + host + } else { + return Err(anyhow::anyhow!( + "couldn't find host in rbw base url {}", + url_str + )); + }; let password = rbw::pinentry::getpin( "Master Password", - &format!("Log in to {}", url.host_str().unwrap()), + &format!("Log in to {}", host), tty, ) .await - .unwrap(); + .context("failed to read password from pinentry")?; let (access_token, refresh_token, iterations, protected_key, keys) = - rbw::actions::login(&email, &password).await.unwrap(); + rbw::actions::login(&email, &password) + .await + .context("failed to log in to bitwarden instance")?; state.priv_key = Some(keys); @@ -28,108 +45,172 @@ pub async fn login( db.refresh_token = Some(refresh_token); db.iterations = Some(iterations); db.protected_key = Some(protected_key); - db.save_async(&email).await.unwrap(); + db.save_async(&email) + .await + .context("failed to save local database")?; - sync(sock).await; + sync(sock).await?; } else { - respond_ack(sock).await; + respond_ack(sock).await?; } + + Ok(()) } pub async fn unlock( sock: &mut crate::sock::Sock, state: std::sync::Arc>, tty: Option<&str>, -) { +) -> anyhow::Result<()> { let mut state = state.write().await; if state.needs_unlock() { - let email = config_email().await; + let email = config_email() + .await + .context("failed to read email from config")?; let password = rbw::pinentry::getpin( "Master Password", "Unlock the local database", tty, ) .await - .unwrap(); + .context("failed to read password from pinentry")?; let db = rbw::db::Db::load_async(&email) .await - .unwrap_or_else(|_| rbw::db::Db::new()); - + .context("failed to load local database")?; + + let iterations = if let Some(iterations) = db.iterations { + iterations + } else { + return Err(anyhow::anyhow!( + "failed to find number of iterations in db" + )); + }; + let protected_key = if let Some(protected_key) = db.protected_key { + protected_key + } else { + return Err(anyhow::anyhow!( + "failed to find protected key in db" + )); + }; let keys = rbw::actions::unlock( &email, &password, - db.iterations.unwrap(), - db.protected_key.as_deref().unwrap(), + iterations, + &protected_key, ) .await - .unwrap(); + .context("failed to unlock database")?; state.priv_key = Some(keys); } - respond_ack(sock).await; + respond_ack(sock).await?; + + Ok(()) } pub async fn lock( sock: &mut crate::sock::Sock, state: std::sync::Arc>, -) { +) -> anyhow::Result<()> { let mut state = state.write().await; state.priv_key = None; - respond_ack(sock).await; + respond_ack(sock).await?; + + Ok(()) } -pub async fn sync(sock: &mut crate::sock::Sock) { - let email = config_email().await; +pub async fn sync(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { + let email = config_email() + .await + .context("failed to read email from config")?; let mut db = rbw::db::Db::load_async(&email) .await - .unwrap_or_else(|_| rbw::db::Db::new()); + .context("failed to load local database")?; - let (protected_key, ciphers) = - rbw::actions::sync(db.access_token.as_ref().unwrap()) - .await - .unwrap(); + let access_token = if let Some(access_token) = &db.access_token { + access_token + } else { + return Err(anyhow::anyhow!("failed to find access token in db")); + }; + let (protected_key, ciphers) = rbw::actions::sync(&access_token) + .await + .context("failed to sync database from server")?; db.protected_key = Some(protected_key); db.ciphers = ciphers; - db.save_async(&email).await.unwrap(); + db.save_async(&email) + .await + .context("failed to save database")?; + + respond_ack(sock).await?; - respond_ack(sock).await; + Ok(()) } pub async fn decrypt( sock: &mut crate::sock::Sock, state: std::sync::Arc>, cipherstring: &str, -) { +) -> anyhow::Result<()> { let state = state.read().await; - let keys = state.priv_key.as_ref().unwrap(); - let cipherstring = - rbw::cipherstring::CipherString::new(cipherstring).unwrap(); - let plaintext = - String::from_utf8(cipherstring.decrypt(keys).unwrap()).unwrap(); - - respond_decrypt(sock, plaintext).await; + let keys = if let Some(keys) = &state.priv_key { + keys + } else { + return Err(anyhow::anyhow!( + "failed to find decryption keys in in-memory state" + )); + }; + let cipherstring = rbw::cipherstring::CipherString::new(cipherstring) + .context("failed to parse encrypted secret")?; + let plaintext = String::from_utf8( + cipherstring + .decrypt(&keys) + .context("failed to decrypt encrypted secret")?, + ) + .context("failed to parse decrypted secret")?; + + respond_decrypt(sock, plaintext).await?; + + Ok(()) } -async fn respond_ack(sock: &mut crate::sock::Sock) { - sock.send(&rbw::agent::Response::Ack).await; +async fn respond_ack(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { + sock.send(&rbw::agent::Response::Ack) + .await + .context("failed to send response")?; + + Ok(()) } -async fn respond_decrypt(sock: &mut crate::sock::Sock, plaintext: String) { +async fn respond_decrypt( + sock: &mut crate::sock::Sock, + plaintext: String, +) -> anyhow::Result<()> { sock.send(&rbw::agent::Response::Decrypt { plaintext }) - .await; + .await + .context("failed to send response")?; + + Ok(()) } -async fn config_email() -> String { - let config = rbw::config::Config::load_async().await.unwrap(); - config.email.unwrap() +async fn config_email() -> anyhow::Result { + let config = rbw::config::Config::load_async() + .await + .context("failed to load config")?; + if let Some(email) = config.email { + Ok(email) + } else { + Err(anyhow::anyhow!("failed to find email address in config")) + } } -async fn config_base_url() -> String { - let config = rbw::config::Config::load_async().await.unwrap(); - config.base_url() +async fn config_base_url() -> anyhow::Result { + let config = rbw::config::Config::load_async() + .await + .context("failed to load config")?; + Ok(config.base_url()) } diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs index 439f458..f092787 100644 --- a/src/bin/rbw-agent/agent.rs +++ b/src/bin/rbw-agent/agent.rs @@ -1,3 +1,4 @@ +use anyhow::Context as _; use tokio::stream::StreamExt as _; pub struct State { @@ -16,28 +17,44 @@ pub struct Agent { } impl Agent { - pub fn new() -> Self { - let config = rbw::config::Config::load().unwrap(); - Self { + pub fn new() -> anyhow::Result { + let config = + rbw::config::Config::load().context("failed to load config")?; + Ok(Self { timeout: tokio::time::delay_for( tokio::time::Duration::from_secs(config.lock_timeout), ), state: std::sync::Arc::new(tokio::sync::RwLock::new(State { priv_key: None, })), - } + }) } - pub async fn run(&mut self, mut listener: tokio::net::UnixListener) { + pub async fn run( + &mut self, + mut listener: tokio::net::UnixListener, + ) -> anyhow::Result<()> { loop { tokio::select! { sock = listener.next() => { - let mut sock - = crate::sock::Sock::new(sock.unwrap().unwrap()); + let sock = if let Some(sock) = sock { + sock + } else { + return Ok(()); + }; + let mut sock = crate::sock::Sock::new( + sock.context("failed to accept incoming connection")? + ); let state = self.state.clone(); tokio::spawn(async move { - let req = sock.recv().await; - handle_request(&req, &mut sock, state.clone()).await; + let res + = handle_request(&mut sock, state.clone()).await; + if let Err(e) = res { + // unwrap is the only option here + sock.send(&rbw::agent::Response::Error { + error: format!("{:#}", e), + }).await.unwrap(); + } }); } _ = &mut self.timeout => { @@ -52,10 +69,13 @@ impl Agent { } async fn handle_request( - req: &rbw::agent::Request, sock: &mut crate::sock::Sock, state: std::sync::Arc>, -) { +) -> anyhow::Result<()> { + let req = sock + .recv() + .await + .context("failed to receive incoming message")?; match &req.action { rbw::agent::Action::Login => { crate::actions::login(sock, state.clone(), req.tty.as_deref()) diff --git a/src/bin/rbw-agent/daemon.rs b/src/bin/rbw-agent/daemon.rs index 64cf298..80c94ad 100644 --- a/src/bin/rbw-agent/daemon.rs +++ b/src/bin/rbw-agent/daemon.rs @@ -1,35 +1,42 @@ +use anyhow::Context as _; + pub struct StartupAck { writer: std::os::unix::io::RawFd, } impl StartupAck { - pub fn ack(&self) { - nix::unistd::write(self.writer, &[0]).unwrap(); - nix::unistd::close(self.writer).unwrap(); + pub fn ack(&self) -> anyhow::Result<()> { + nix::unistd::write(self.writer, &[0])?; + nix::unistd::close(self.writer)?; + Ok(()) } } impl Drop for StartupAck { fn drop(&mut self) { - nix::unistd::close(self.writer).unwrap(); + // best effort close here, can't do better in a destructor + let _ = nix::unistd::close(self.writer); } } -pub fn daemonize() -> StartupAck { +pub fn daemonize() -> anyhow::Result { let runtime_dir = rbw::dirs::runtime_dir(); - std::fs::create_dir_all(&runtime_dir).unwrap(); + std::fs::create_dir_all(&runtime_dir) + .context("failed to create runtime directory")?; - let (r, w) = nix::unistd::pipe().unwrap(); + let (r, w) = nix::unistd::pipe()?; let res = daemonize::Daemonize::new() .pid_file(runtime_dir.join("pidfile")) .exit_action(move || { - nix::unistd::close(w).unwrap(); + // 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(); }) .start(); - nix::unistd::close(r).unwrap(); + let _ = nix::unistd::close(r); match res { Ok(_) => (), @@ -46,5 +53,5 @@ pub fn daemonize() -> StartupAck { } } - StartupAck { writer: w } + Ok(StartupAck { writer: w }) } diff --git a/src/bin/rbw-agent/main.rs b/src/bin/rbw-agent/main.rs index 235c4f2..622778e 100644 --- a/src/bin/rbw-agent/main.rs +++ b/src/bin/rbw-agent/main.rs @@ -1,24 +1,57 @@ +use anyhow::Context as _; + mod actions; mod agent; mod daemon; mod sock; -fn main() { +async fn tokio_main( + startup_ack: crate::daemon::StartupAck, +) -> anyhow::Result<()> { + let listener = + crate::sock::listen().context("failed to listen on socket")?; + + startup_ack.ack()?; + + let mut agent = crate::agent::Agent::new()?; + agent.run(listener).await?; + + Ok(()) +} + +fn real_main() -> anyhow::Result<()> { env_logger::from_env( env_logger::Env::default().default_filter_or("info"), ) .init(); - let startup_ack = daemon::daemonize(); + let startup_ack = daemon::daemonize().context("failed to daemonize")?; + let (w, r) = std::sync::mpsc::channel(); // can't use tokio::main because we need to daemonize before starting the // tokio runloop, or else things break + // unwrap is fine here because there's no good reason that this should + // ever fail tokio::runtime::Runtime::new().unwrap().block_on(async { - let listener = crate::sock::listen(); + if let Err(e) = tokio_main(startup_ack).await { + // this unwrap is fine because it's the only real option here + w.send(e).unwrap(); + } + }); + + if let Ok(e) = r.recv() { + return Err(e); + } - startup_ack.ack(); + Ok(()) +} + +fn main() { + let res = real_main(); - let mut agent = crate::agent::Agent::new(); - agent.run(listener.unwrap()).await; - }) + if let Err(e) = res { + // XXX log file? + eprintln!("{:#}", e); + std::process::exit(1); + } } diff --git a/src/bin/rbw-agent/sock.rs b/src/bin/rbw-agent/sock.rs index 73ad5c2..6931b13 100644 --- a/src/bin/rbw-agent/sock.rs +++ b/src/bin/rbw-agent/sock.rs @@ -1,3 +1,4 @@ +use anyhow::Context as _; use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _}; pub struct Sock(tokio::net::UnixStream); @@ -7,30 +8,45 @@ impl Sock { Self(s) } - pub async fn send(&mut self, res: &rbw::agent::Response) { + pub async fn send( + &mut self, + res: &rbw::agent::Response, + ) -> anyhow::Result<()> { let Self(sock) = self; - sock.write_all(serde_json::to_string(res).unwrap().as_bytes()) + sock.write_all( + serde_json::to_string(res) + .context("failed to serialize message")? + .as_bytes(), + ) + .await + .context("failed to write message to socket")?; + sock.write_all(b"\n") .await - .unwrap(); - sock.write_all(b"\n").await.unwrap(); + .context("failed to write message to socket")?; + Ok(()) } - pub async fn recv(&mut self) -> rbw::agent::Request { + pub async fn recv(&mut self) -> anyhow::Result { let Self(sock) = self; let mut buf = tokio::io::BufStream::new(sock); let mut line = String::new(); - buf.read_line(&mut line).await.unwrap(); - serde_json::from_str(&line).unwrap() + buf.read_line(&mut line) + .await + .context("failed to read message from socket")?; + Ok(serde_json::from_str(&line).context("failed to parse message")?) } } pub fn listen() -> anyhow::Result { let runtime_dir = rbw::dirs::runtime_dir(); - std::fs::create_dir_all(&runtime_dir)?; + std::fs::create_dir_all(&runtime_dir) + .context("failed to create runtime dir")?; let path = runtime_dir.join("socket"); - std::fs::remove_file(&path)?; - let sock = tokio::net::UnixListener::bind(&path)?; + // if the socket already doesn't exist, that's fine + let _ = std::fs::remove_file(&path); + let sock = tokio::net::UnixListener::bind(&path) + .context("failed to listen on socket")?; log::debug!("listening on socket {}", path.to_string_lossy()); Ok(sock) } -- cgit v1.2.3-54-g00ecf