aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-04-10 03:12:48 -0400
committerJesse Luehrs <doy@tozt.net>2020-04-10 03:12:48 -0400
commit9f7e2803df80e1f6e446c638dca2f884c965a821 (patch)
tree03872e81096fee84dd88bea31338539117d85222
parentb3a04c4a143c34ba92008cf018eed159f87a0c6e (diff)
downloadrbw-9f7e2803df80e1f6e446c638dca2f884c965a821.tar.gz
rbw-9f7e2803df80e1f6e446c638dca2f884c965a821.zip
save sync data to local file
-rw-r--r--src/actions.rs19
-rw-r--r--src/bin/agent.rs82
-rw-r--r--src/bin/rbw.rs34
-rw-r--r--src/db.rs83
-rw-r--r--src/error.rs18
-rw-r--r--src/lib.rs2
6 files changed, 176 insertions, 62 deletions
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<crate::locked::Keys> {
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<tokio::sync::RwLock<State>>,
-) {
- 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<tokio::sync::RwLock<State>>,
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<tokio::sync::RwLock<State>>,
-) {
- 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<tokio::sync::RwLock<State>>,
-) {
- 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<tokio::sync::RwLock<State>>,
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<String>,
priv_key: Option<rbw::locked::Keys>,
-
- // these should be in a state file
- iterations: Option<u32>,
- protected_key: Option<rbw::cipherstring::CipherString>,
- ciphers: Vec<rbw::api::Cipher>,
}
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<String>,
+ pub refresh_token: Option<String>,
+
+ pub iterations: Option<u32>,
+ pub protected_key: Option<String>,
+
+ pub ciphers: Vec<crate::api::Cipher>,
+}
+
+impl Db {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn load(email: &str) -> Result<Self> {
+ 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<Self> {
+ 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;