From 9f7e2803df80e1f6e446c638dca2f884c965a821 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Fri, 10 Apr 2020 03:12:48 -0400 Subject: save sync data to local file --- src/actions.rs | 19 ++++++------- src/bin/agent.rs | 82 ++++++++++++++++++++++--------------------------------- src/bin/rbw.rs | 34 ++++++++++++++++++++++- src/db.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 18 ++++++++++++ src/lib.rs | 2 +- 6 files changed, 176 insertions(+), 62 deletions(-) create mode 100644 src/db.rs diff --git a/src/actions.rs b/src/actions.rs index 14f140e..7d8569c 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -3,12 +3,7 @@ use crate::prelude::*; pub async fn login( email: &str, password: &crate::locked::Password, -) -> Result<( - String, - u32, - crate::cipherstring::CipherString, - crate::locked::Keys, -)> { +) -> Result<(String, String, u32, String, crate::locked::Keys)> { let config = crate::config::Config::load_async().await?; let client = crate::api::Client::new(&config.base_url(), &config.identity_url()); @@ -17,15 +12,15 @@ pub async fn login( let identity = crate::identity::Identity::new(email, password, iterations)?; - let (access_token, _refresh_token, protected_key) = client + let (access_token, refresh_token, protected_key) = client .login(&identity.email, &identity.master_password_hash) .await?; - let protected_key = - crate::cipherstring::CipherString::new(&protected_key)?; - let master_keys = protected_key.decrypt_locked(&identity.keys)?; + let master_keys = crate::cipherstring::CipherString::new(&protected_key)? + .decrypt_locked(&identity.keys)?; Ok(( access_token, + refresh_token, iterations, protected_key, crate::locked::Keys::new(master_keys), @@ -36,11 +31,13 @@ pub async fn unlock( email: &str, password: &crate::locked::Password, iterations: u32, - protected_key: &crate::cipherstring::CipherString, + protected_key: &str, ) -> Result { let identity = crate::identity::Identity::new(email, password, iterations)?; + let protected_key = + crate::cipherstring::CipherString::new(protected_key)?; let master_keys = protected_key.decrypt_locked(&identity.keys)?; Ok(crate::locked::Keys::new(master_keys)) diff --git a/src/bin/agent.rs b/src/bin/agent.rs index 0c65da6..ddcc9fc 100644 --- a/src/bin/agent.rs +++ b/src/bin/agent.rs @@ -22,43 +22,32 @@ async fn send_response( sock.write_all(b"\n").await.unwrap(); } -async fn ensure_login( - sock: &mut tokio::net::UnixStream, - state: std::sync::Arc>, -) { - let rstate = state.read().await; - if rstate.access_token.is_none() { - login(sock, state.clone(), None).await; // tty - } -} - async fn login( sock: &mut tokio::net::UnixStream, state: std::sync::Arc>, tty: Option<&str>, ) { let mut state = state.write().await; + let email = config_email().await; let password = rbw::pinentry::getpin("prompt", "desc", tty).await.unwrap(); - let (access_token, iterations, protected_key, keys) = + + let (access_token, refresh_token, iterations, protected_key, keys) = rbw::actions::login(&email, &password).await.unwrap(); - state.access_token = Some(access_token); - state.iterations = Some(iterations); - state.protected_key = Some(protected_key); + state.priv_key = Some(keys); - send_response(sock, &rbw::agent::Response::Ack).await; -} + let mut db = rbw::db::Db::load_async(&email) + .await + .unwrap_or_else(|_| rbw::db::Db::new()); + db.access_token = Some(access_token); + db.refresh_token = Some(refresh_token); + db.iterations = Some(iterations); + db.protected_key = Some(protected_key); + db.save_async(&email).await.unwrap(); -async fn ensure_unlock( - sock: &mut tokio::net::UnixStream, - state: std::sync::Arc>, -) { - let rstate = state.read().await; - if rstate.priv_key.is_none() { - unlock(sock, state.clone(), None).await; // tty - } + send_response(sock, &rbw::agent::Response::Ack).await; } async fn unlock( @@ -67,36 +56,42 @@ async fn unlock( tty: Option<&str>, ) { let mut state = state.write().await; + let email = config_email().await; let password = rbw::pinentry::getpin("prompt", "desc", tty).await.unwrap(); + + let db = rbw::db::Db::load_async(&email) + .await + .unwrap_or_else(|_| rbw::db::Db::new()); + let keys = rbw::actions::unlock( &email, &password, - state.iterations.unwrap(), - state.protected_key.as_ref().unwrap(), + db.iterations.unwrap(), + db.protected_key.as_deref().unwrap(), ) .await .unwrap(); + state.priv_key = Some(keys); send_response(sock, &rbw::agent::Response::Ack).await; } -async fn sync( - sock: &mut tokio::net::UnixStream, - state: std::sync::Arc>, -) { - ensure_login(sock, state.clone()).await; - let mut state = state.write().await; +async fn sync(sock: &mut tokio::net::UnixStream) { + let email = config_email().await; + let mut db = rbw::db::Db::load_async(&email) + .await + .unwrap_or_else(|_| rbw::db::Db::new()); + let (protected_key, ciphers) = - rbw::actions::sync(state.access_token.as_ref().unwrap()) + rbw::actions::sync(db.access_token.as_ref().unwrap()) .await .unwrap(); - state.protected_key = - Some(rbw::cipherstring::CipherString::new(&protected_key).unwrap()); - println!("{}", serde_json::to_string(&ciphers).unwrap()); - state.ciphers = ciphers; + db.protected_key = Some(protected_key); + db.ciphers = ciphers; + db.save_async(&email).await.unwrap(); send_response(sock, &rbw::agent::Response::Ack).await; } @@ -106,7 +101,6 @@ async fn decrypt( state: std::sync::Arc>, cipherstring: &str, ) { - ensure_unlock(sock, state.clone()).await; let state = state.read().await; let keys = state.priv_key.as_ref().unwrap(); let cipherstring = @@ -133,7 +127,7 @@ async fn handle_sock( rbw::agent::Action::Unlock => { unlock(&mut sock, state.clone(), msg.tty.as_deref()).await } - rbw::agent::Action::Sync => sync(&mut sock, state.clone()).await, + rbw::agent::Action::Sync => sync(&mut sock).await, rbw::agent::Action::Decrypt { cipherstring } => { decrypt(&mut sock, state.clone(), &cipherstring).await } @@ -152,13 +146,7 @@ struct Agent { } struct State { - access_token: Option, priv_key: Option, - - // these should be in a state file - iterations: Option, - protected_key: Option, - ciphers: Vec, } impl Agent { @@ -168,11 +156,7 @@ impl Agent { tokio::time::Duration::from_secs(600), // read from config ), state: std::sync::Arc::new(tokio::sync::RwLock::new(State { - access_token: None, - iterations: None, - protected_key: None, priv_key: None, - ciphers: vec![], })), } } diff --git a/src/bin/rbw.rs b/src/bin/rbw.rs index 3aae676..24ac1ad 100644 --- a/src/bin/rbw.rs +++ b/src/bin/rbw.rs @@ -68,6 +68,7 @@ fn login() { action: rbw::agent::Action::Login, }, ); + let res = recv(&mut sock); match res { rbw::agent::Response::Ack => (), @@ -89,6 +90,7 @@ fn unlock() { action: rbw::agent::Action::Unlock, }, ); + let res = recv(&mut sock); match res { rbw::agent::Response::Ack => (), @@ -110,6 +112,7 @@ fn sync() { action: rbw::agent::Action::Sync, }, ); + let res = recv(&mut sock); match res { rbw::agent::Response::Ack => (), @@ -123,7 +126,31 @@ fn sync() { fn list() { ensure_agent(); - todo!() + let email = config_email(); + let db = rbw::db::Db::load(&email).unwrap_or_else(|_| rbw::db::Db::new()); + for cipher in db.ciphers { + let mut sock = connect(); + send( + &mut sock, + &rbw::agent::Request { + tty: std::env::var("TTY").ok(), + action: rbw::agent::Action::Decrypt { + cipherstring: cipher.name, + }, + }, + ); + + let res = recv(&mut sock); + match res { + rbw::agent::Response::Decrypt { plaintext } => { + println!("{}", plaintext); + } + rbw::agent::Response::Error { error } => { + panic!("failed to login: {}", error) + } + _ => panic!("unexpected message: {:?}", res), + } + } } fn get() { @@ -175,6 +202,11 @@ fn stop_agent() { ); } +fn config_email() -> String { + let config = rbw::config::Config::load().unwrap(); + config.email.unwrap() +} + fn main() { let matches = clap::App::new("rbw") .about("unofficial bitwarden cli") diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..5fa7856 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,83 @@ +use crate::prelude::*; + +use std::io::{Read as _, Write as _}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + +#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] +pub struct Db { + pub access_token: Option, + pub refresh_token: Option, + + pub iterations: Option, + pub protected_key: Option, + + pub ciphers: Vec, +} + +impl Db { + pub fn new() -> Self { + Self::default() + } + + pub fn load(email: &str) -> Result { + let mut fh = std::fs::File::open(Self::filename(email)) + .context(crate::error::LoadDb)?; + let mut json = String::new(); + fh.read_to_string(&mut json).context(crate::error::LoadDb)?; + let slf: Self = + serde_json::from_str(&json).context(crate::error::LoadDbJson)?; + Ok(slf) + } + + pub async fn load_async(email: &str) -> Result { + let mut fh = tokio::fs::File::open(Self::filename(email)) + .await + .context(crate::error::LoadDbAsync)?; + let mut json = String::new(); + fh.read_to_string(&mut json) + .await + .context(crate::error::LoadDbAsync)?; + let slf: Self = + serde_json::from_str(&json).context(crate::error::LoadDbJson)?; + Ok(slf) + } + + // XXX need to make this atomic + pub fn save(&self, email: &str) -> Result<()> { + let filename = Self::filename(email); + std::fs::create_dir_all(filename.parent().unwrap()) + .context(crate::error::SaveDb)?; + let mut fh = + std::fs::File::create(filename).context(crate::error::SaveDb)?; + fh.write_all( + serde_json::to_string(self) + .context(crate::error::SaveDbJson)? + .as_bytes(), + ) + .context(crate::error::SaveDb)?; + Ok(()) + } + + // XXX need to make this atomic + pub async fn save_async(&self, email: &str) -> Result<()> { + let filename = Self::filename(email); + tokio::fs::create_dir_all(filename.parent().unwrap()) + .await + .context(crate::error::SaveDbAsync)?; + let mut fh = tokio::fs::File::create(filename) + .await + .context(crate::error::SaveDbAsync)?; + fh.write_all( + serde_json::to_string(self) + .context(crate::error::SaveDbJson)? + .as_bytes(), + ) + .await + .context(crate::error::SaveDbAsync)?; + Ok(()) + } + + fn filename(email: &str) -> std::path::PathBuf { + crate::dirs::cache_dir().join(format!("{}.json", email)) + } +} diff --git a/src/error.rs b/src/error.rs index 4554b14..de46847 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,15 @@ pub enum Error { #[snafu(display("failed to load config: {}", source))] LoadConfigJson { source: serde_json::Error }, + #[snafu(display("failed to load db: {}", source))] + LoadDb { source: std::io::Error }, + + #[snafu(display("failed to load db: {}", source))] + LoadDbAsync { source: tokio::io::Error }, + + #[snafu(display("failed to load db: {}", source))] + LoadDbJson { source: serde_json::Error }, + #[snafu(display("error reading pinentry output: {}", source))] PinentryReadOutput { source: tokio::io::Error }, @@ -63,6 +72,15 @@ pub enum Error { #[snafu(display("failed to save config: {}", source))] SaveConfigJson { source: serde_json::Error }, + #[snafu(display("failed to save db: {}", source))] + SaveDb { source: std::io::Error }, + + #[snafu(display("failed to save db: {}", source))] + SaveDbAsync { source: tokio::io::Error }, + + #[snafu(display("failed to save db: {}", source))] + SaveDbJson { source: serde_json::Error }, + #[snafu(display("error spawning pinentry: {}", source))] Spawn { source: tokio::io::Error }, diff --git a/src/lib.rs b/src/lib.rs index 62a06c5..d197fbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,10 @@ pub mod agent; pub mod api; pub mod cipherstring; pub mod config; +pub mod db; pub mod dirs; mod error; pub mod identity; pub mod locked; pub mod pinentry; mod prelude; -// pub mod secrets; -- cgit v1.2.3-54-g00ecf