aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-04-06 06:35:30 -0400
committerJesse Luehrs <doy@tozt.net>2020-04-06 06:35:30 -0400
commitbc18bca5c67b4a678a31198877e39d57d97b1e0c (patch)
tree3dc31608586cbe0f7973d7aa730267d57224e6a1 /src
parent4ad2f0a0dc3abb4cb10a6b82ca6a1f3a829eb1fb (diff)
downloadrbw-bc18bca5c67b4a678a31198877e39d57d97b1e0c.tar.gz
rbw-bc18bca5c67b4a678a31198877e39d57d97b1e0c.zip
factor out into an agent
Diffstat (limited to 'src')
-rw-r--r--src/actions.rs46
-rw-r--r--src/api.rs31
-rw-r--r--src/bin/agent.rs197
-rw-r--r--src/bin/rbw.rs (renamed from src/main.rs)23
-rw-r--r--src/dirs.rs14
-rw-r--r--src/lib.rs8
-rw-r--r--src/pinentry.rs49
7 files changed, 344 insertions, 24 deletions
diff --git a/src/actions.rs b/src/actions.rs
new file mode 100644
index 0000000..d7f3103
--- /dev/null
+++ b/src/actions.rs
@@ -0,0 +1,46 @@
+// TODO api needs to be async
+
+pub async fn login(email: &str, password: &str) -> (String, u32, String) {
+ let client =
+ crate::api::Client::new_self_hosted("https://bitwarden.tozt.net");
+
+ let iterations = client.prelogin(&email).await.unwrap();
+ let identity =
+ crate::identity::Identity::new(&email, &password, iterations)
+ .unwrap();
+
+ let (access_token, _refresh_token, protected_key) = client
+ .login(&identity.email, &identity.master_password_hash)
+ .await
+ .unwrap();
+
+ (access_token, iterations, protected_key)
+}
+
+pub async fn unlock(
+ email: &str,
+ password: &str,
+ iterations: u32,
+ protected_key: String,
+) -> (Vec<u8>, Vec<u8>) {
+ let identity =
+ crate::identity::Identity::new(&email, &password, iterations)
+ .unwrap();
+
+ let protected_key =
+ crate::cipherstring::CipherString::new(&protected_key).unwrap();
+ let master_key = protected_key
+ .decrypt(&identity.enc_key, &identity.mac_key)
+ .unwrap();
+
+ let enc_key = &master_key[0..32];
+ let mac_key = &master_key[32..64];
+
+ (enc_key.to_vec(), mac_key.to_vec())
+}
+
+pub async fn sync(access_token: &str) -> (String, Vec<crate::api::Cipher>) {
+ let client =
+ crate::api::Client::new_self_hosted("https://bitwarden.tozt.net");
+ client.sync(access_token).await.unwrap()
+}
diff --git a/src/api.rs b/src/api.rs
index ae2e550..de5a70d 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -54,7 +54,7 @@ struct Profile {
key: String,
}
-#[derive(serde::Deserialize, Debug)]
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Cipher {
#[serde(rename = "Name")]
pub name: String,
@@ -62,7 +62,7 @@ pub struct Cipher {
pub login: Login,
}
-#[derive(serde::Deserialize, Debug)]
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Login {
#[serde(rename = "Username")]
pub username: String,
@@ -92,22 +92,23 @@ impl Client {
}
}
- pub fn prelogin(&self, email: &str) -> Result<u32> {
+ pub async fn prelogin(&self, email: &str) -> Result<u32> {
let prelogin = PreloginReq {
email: email.to_string(),
};
- let client = reqwest::blocking::Client::new();
+ let client = reqwest::Client::new();
let res = client
.post(&self.api_url("/accounts/prelogin"))
.json(&prelogin)
.send()
+ .await
.context(crate::error::Reqwest)?;
let prelogin_res: PreloginRes =
- res.json().context(crate::error::Reqwest)?;
+ res.json().await.context(crate::error::Reqwest)?;
Ok(prelogin_res.kdf_iterations)
}
- pub fn login(
+ pub async fn login(
&self,
email: &str,
master_password_hash: &[u8],
@@ -125,14 +126,15 @@ impl Client {
device_name: "test cli".to_string(),
device_push_token: "".to_string(),
};
- let client = reqwest::blocking::Client::new();
+ let client = reqwest::Client::new();
let res = client
.post(&self.identity_url("/connect/token"))
.form(&connect_req)
.send()
+ .await
.context(crate::error::Reqwest)?;
let connect_res: ConnectRes =
- res.json().context(crate::error::Reqwest)?;
+ res.json().await.context(crate::error::Reqwest)?;
Ok((
connect_res.access_token,
connect_res.refresh_token,
@@ -140,15 +142,20 @@ impl Client {
))
}
- pub fn sync(&self, access_token: &str) -> Result<Vec<Cipher>> {
- let client = reqwest::blocking::Client::new();
+ pub async fn sync(
+ &self,
+ access_token: &str,
+ ) -> Result<(String, Vec<Cipher>)> {
+ let client = reqwest::Client::new();
let res = client
.get(&self.api_url("/sync"))
.header("Authorization", format!("Bearer {}", access_token))
.send()
+ .await
.context(crate::error::Reqwest)?;
- let sync_res: SyncRes = res.json().context(crate::error::Reqwest)?;
- Ok(sync_res.ciphers)
+ let sync_res: SyncRes =
+ res.json().await.context(crate::error::Reqwest)?;
+ Ok((sync_res.profile.key, sync_res.ciphers))
}
fn api_url(&self, path: &str) -> String {
diff --git a/src/bin/agent.rs b/src/bin/agent.rs
new file mode 100644
index 0000000..5a97448
--- /dev/null
+++ b/src/bin/agent.rs
@@ -0,0 +1,197 @@
+extern crate rbw;
+
+use fs2::FileExt as _;
+use std::io::Write as _;
+use tokio::io::AsyncBufReadExt as _;
+use tokio::stream::StreamExt as _;
+
+#[derive(serde::Deserialize, Debug)]
+struct Message {
+ tty: Option<String>,
+ action: Action,
+}
+
+#[derive(serde::Deserialize, Debug)]
+#[serde(tag = "type")]
+enum Action {
+ Login,
+ Unlock,
+ Sync,
+ Decrypt { cipherstring: String },
+}
+
+fn make_pidfile() -> std::fs::File {
+ let runtime_dir = rbw::dirs::runtime_dir();
+ std::fs::create_dir_all(&runtime_dir).unwrap();
+
+ let mut fh = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open(runtime_dir.join("pidfile"))
+ .unwrap();
+
+ fh.try_lock_exclusive().unwrap();
+
+ fh.set_len(0).unwrap();
+ fh.write_all(format!("{}", std::process::id()).as_bytes())
+ .unwrap();
+
+ fh
+}
+
+fn make_socket() -> tokio::net::UnixListener {
+ let runtime_dir = rbw::dirs::runtime_dir();
+ std::fs::create_dir_all(&runtime_dir).unwrap();
+
+ let path = runtime_dir.join("socket");
+ std::fs::remove_file(&path).unwrap();
+
+ tokio::net::UnixListener::bind(path).unwrap()
+}
+
+async fn ensure_login(state: std::sync::Arc<tokio::sync::RwLock<State>>) {
+ let rstate = state.read().await;
+ if rstate.access_token.is_none() {
+ login(state.clone(), None).await; // tty
+ }
+}
+
+async fn login(
+ state: std::sync::Arc<tokio::sync::RwLock<State>>,
+ tty: Option<&str>,
+) {
+ let mut state = state.write().await;
+ let email = "bitwarden@tozt.net"; // XXX read from config
+ let password = rbw::pinentry::pinentry("prompt", "desc", tty).await;
+ let (access_token, iterations, protected_key) =
+ rbw::actions::login(email, &password).await;
+ state.access_token = Some(access_token);
+ state.iterations = Some(iterations);
+ let (enc_key, mac_key) =
+ rbw::actions::unlock(email, &password, iterations, protected_key)
+ .await;
+ state.priv_key = Some((enc_key, mac_key));
+}
+
+async fn ensure_unlock(state: std::sync::Arc<tokio::sync::RwLock<State>>) {
+ let rstate = state.read().await;
+ if rstate.priv_key.is_none() {
+ unlock(state.clone(), None).await; // tty
+ }
+}
+
+async fn unlock(
+ state: std::sync::Arc<tokio::sync::RwLock<State>>,
+ tty: Option<&str>,
+) {
+ let mut state = state.write().await;
+ let email = "bitwarden@tozt.net"; // XXX read from config
+ let password = rbw::pinentry::pinentry("prompt", "desc", tty).await;
+ let (enc_key, mac_key) = rbw::actions::unlock(
+ email,
+ &password,
+ state.iterations.unwrap(),
+ state.protected_key.as_ref().unwrap().to_string(),
+ )
+ .await;
+ state.priv_key = Some((enc_key, mac_key));
+}
+
+async fn sync(state: std::sync::Arc<tokio::sync::RwLock<State>>) {
+ ensure_login(state.clone()).await;
+ let mut state = state.write().await;
+ let (protected_key, ciphers) =
+ rbw::actions::sync(state.access_token.as_ref().unwrap()).await;
+ state.protected_key = Some(protected_key);
+ println!("{}", serde_json::to_string(&ciphers).unwrap());
+ state.ciphers = ciphers;
+}
+
+async fn decrypt(
+ state: std::sync::Arc<tokio::sync::RwLock<State>>,
+ cipherstring: &str,
+) {
+ ensure_unlock(state.clone()).await;
+ let state = state.read().await;
+ let (enc_key, mac_key) = state.priv_key.as_ref().unwrap();
+ let cipherstring =
+ rbw::cipherstring::CipherString::new(cipherstring).unwrap();
+ let plain = cipherstring.decrypt(&enc_key, &mac_key).unwrap();
+ println!("{}", String::from_utf8(plain).unwrap());
+}
+
+async fn handle_sock(
+ sock: tokio::net::UnixStream,
+ state: std::sync::Arc<tokio::sync::RwLock<State>>,
+) {
+ let buf = tokio::io::BufStream::new(sock);
+ let mut lines = buf.lines();
+ while let Some(line) = lines.next().await {
+ let line = line.unwrap();
+ let msg: Message = serde_json::from_str(&line).unwrap();
+ match msg.action {
+ Action::Login => login(state.clone(), msg.tty.as_deref()).await,
+ Action::Unlock => unlock(state.clone(), msg.tty.as_deref()).await,
+ Action::Sync => sync(state.clone()).await,
+ Action::Decrypt { cipherstring } => {
+ decrypt(state.clone(), &cipherstring).await
+ }
+ }
+ }
+}
+
+struct Agent {
+ timeout: tokio::time::Delay,
+ state: std::sync::Arc<tokio::sync::RwLock<State>>,
+}
+
+struct State {
+ access_token: Option<String>,
+ priv_key: Option<(Vec<u8>, Vec<u8>)>,
+
+ // these should be in a state file
+ iterations: Option<u32>,
+ protected_key: Option<String>,
+ ciphers: Vec<rbw::api::Cipher>,
+}
+
+impl Agent {
+ fn new() -> Self {
+ Self {
+ timeout: tokio::time::delay_for(
+ tokio::time::Duration::from_secs(600),
+ ),
+ state: std::sync::Arc::new(tokio::sync::RwLock::new(State {
+ access_token: None,
+ iterations: None,
+ protected_key: None,
+ priv_key: None,
+ ciphers: vec![],
+ })),
+ }
+ }
+
+ async fn run(&mut self, mut listener: tokio::net::UnixListener) {
+ loop {
+ tokio::select! {
+ sock = listener.next() => {
+ let state = self.state.clone();
+ tokio::spawn(async move {
+ handle_sock(sock.unwrap().unwrap(), state).await
+ });
+ }
+ _ = &mut self.timeout => {
+ break;
+ }
+ }
+ }
+ }
+}
+
+#[tokio::main]
+async fn main() {
+ let _lock = make_pidfile();
+ let listener = make_socket();
+ let mut agent = Agent::new();
+ agent.run(listener).await;
+}
diff --git a/src/main.rs b/src/bin/rbw.rs
index 171bb6f..6c9bb11 100644
--- a/src/main.rs
+++ b/src/bin/rbw.rs
@@ -1,25 +1,22 @@
-mod api;
-mod cipherstring;
-mod error;
-mod identity;
-mod prelude;
+extern crate rbw;
fn main() {
- let client = api::Client::new_self_hosted("https://bitwarden.tozt.net");
+ let client =
+ rbw::api::Client::new_self_hosted("https://bitwarden.tozt.net");
let email = rprompt::prompt_reply_stderr("Email: ").unwrap();
let password = rpassword::prompt_password_stderr("Password: ").unwrap();
let iterations = client.prelogin(&email).unwrap();
let identity =
- identity::Identity::new(&email, &password, iterations).unwrap();
+ rbw::identity::Identity::new(&email, &password, iterations).unwrap();
let (access_token, _refresh_token, protected_key) = client
.login(&identity.email, &identity.master_password_hash)
.unwrap();
let protected_key =
- cipherstring::CipherString::new(&protected_key).unwrap();
+ rbw::cipherstring::CipherString::new(&protected_key).unwrap();
let master_key = protected_key
.decrypt(&identity.enc_key, &identity.mac_key)
.unwrap();
@@ -27,16 +24,18 @@ fn main() {
let enc_key = &master_key[0..32];
let mac_key = &master_key[32..64];
- let ciphers = client.sync(&access_token).unwrap();
+ let (_, ciphers) = client.sync(&access_token).unwrap();
for cipher in ciphers {
let secret_name =
- cipherstring::CipherString::new(&cipher.name).unwrap();
+ rbw::cipherstring::CipherString::new(&cipher.name).unwrap();
let name = secret_name.decrypt(enc_key, mac_key).unwrap();
let secret_username =
- cipherstring::CipherString::new(&cipher.login.username).unwrap();
+ rbw::cipherstring::CipherString::new(&cipher.login.username)
+ .unwrap();
let username = secret_username.decrypt(enc_key, mac_key).unwrap();
let secret_password =
- cipherstring::CipherString::new(&cipher.login.password).unwrap();
+ rbw::cipherstring::CipherString::new(&cipher.login.password)
+ .unwrap();
let password = secret_password.decrypt(enc_key, mac_key).unwrap();
println!("{}:", String::from_utf8(name).unwrap());
println!(" Username: {}", String::from_utf8(username).unwrap());
diff --git a/src/dirs.rs b/src/dirs.rs
new file mode 100644
index 0000000..177ea58
--- /dev/null
+++ b/src/dirs.rs
@@ -0,0 +1,14 @@
+pub fn config_dir() -> std::path::PathBuf {
+ let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
+ project_dirs.config_dir().to_path_buf()
+}
+
+pub fn cache_dir() -> std::path::PathBuf {
+ let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
+ project_dirs.cache_dir().to_path_buf()
+}
+
+pub fn runtime_dir() -> std::path::PathBuf {
+ let project_dirs = directories::ProjectDirs::from("", "", "rbw").unwrap();
+ project_dirs.runtime_dir().unwrap().to_path_buf()
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..63052f3
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,8 @@
+pub mod actions;
+pub mod api;
+pub mod cipherstring;
+pub mod dirs;
+mod error;
+pub mod identity;
+pub mod pinentry;
+mod prelude;
diff --git a/src/pinentry.rs b/src/pinentry.rs
new file mode 100644
index 0000000..610f52d
--- /dev/null
+++ b/src/pinentry.rs
@@ -0,0 +1,49 @@
+use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _};
+
+// TODO result
+pub async fn pinentry(prompt: &str, desc: &str, tty: Option<&str>) -> String {
+ let mut opts = tokio::process::Command::new("pinentry");
+ let opts = opts
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped());
+ let opts = if let Some(tty) = tty {
+ opts.args(&["-T", tty])
+ } else {
+ opts
+ };
+ let mut child = opts.spawn().unwrap();
+ {
+ let stdin = child.stdin.as_mut().unwrap();
+ let mut stdout =
+ tokio::io::BufReader::new(child.stdout.as_mut().unwrap());
+ let mut buf = String::new();
+
+ stdin.write_all(b"SETTITLE rbw\n").await.unwrap();
+ stdout.read_line(&mut buf).await.unwrap();
+
+ stdin
+ .write_all(format!("SETPROMPT {}\n", prompt).as_bytes())
+ .await
+ .unwrap();
+ stdout.read_line(&mut buf).await.unwrap();
+
+ stdin
+ .write_all(format!("SETDESC {}\n", desc).as_bytes())
+ .await
+ .unwrap();
+ stdout.read_line(&mut buf).await.unwrap();
+
+ stdin.write_all(b"GETPIN\n").await.unwrap();
+ }
+ let res =
+ String::from_utf8(child.wait_with_output().await.unwrap().stdout)
+ .unwrap();
+ for line in res.lines() {
+ if line.starts_with("OK") {
+ continue;
+ } else if line.starts_with("D ") {
+ return line[2..line.len()].to_string();
+ }
+ }
+ panic!("failed to parse pinentry output: {:?}", res)
+}