aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock179
-rw-r--r--Cargo.toml4
-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
9 files changed, 526 insertions, 25 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1080702..d7c2ae7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -33,6 +33,24 @@ dependencies = [
]
[[package]]
+name = "arc-swap"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825"
+
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
+
+[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -61,6 +79,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
+name = "blake2b_simd"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "constant_time_eq",
+]
+
+[[package]]
name = "block-buffer"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -146,6 +175,12 @@ dependencies = [
]
[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -162,6 +197,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "lazy_static",
+]
+
+[[package]]
name = "crypto-mac"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -181,6 +227,28 @@ dependencies = [
]
[[package]]
+name = "directories"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c"
+dependencies = [
+ "cfg-if",
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_users",
+ "winapi 0.3.8",
+]
+
+[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -229,6 +297,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi 0.3.8",
+]
+
+[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -545,13 +623,36 @@ dependencies = [
"kernel32-sys",
"libc",
"log",
- "miow",
+ "miow 0.2.1",
"net2",
"slab",
"winapi 0.2.8",
]
[[package]]
+name = "mio-named-pipes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3"
+dependencies = [
+ "log",
+ "mio",
+ "miow 0.3.3",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio",
+]
+
+[[package]]
name = "miow"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -564,6 +665,16 @@ dependencies = [
]
[[package]]
+name = "miow"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396aa0f2003d7df8395cb93e09871561ccc3e785f0acb369170e8cc74ddf9226"
+dependencies = [
+ "socket2",
+ "winapi 0.3.8",
+]
+
+[[package]]
name = "native-tls"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -800,6 +911,8 @@ dependencies = [
"aes",
"base64 0.11.0",
"block-modes",
+ "directories",
+ "fs2",
"hkdf",
"hmac",
"pbkdf2",
@@ -807,8 +920,10 @@ dependencies = [
"rpassword",
"rprompt",
"serde",
+ "serde_json",
"sha2",
"snafu",
+ "tokio",
"uuid",
]
@@ -819,6 +934,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
[[package]]
+name = "redox_users"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "rust-argon2",
+]
+
+[[package]]
name = "remove_dir_all"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -880,6 +1006,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b386f4748bdae2aefc96857f5fda07647f851d089420e577831e2a14b45230f8"
[[package]]
+name = "rust-argon2"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017"
+dependencies = [
+ "base64 0.11.0",
+ "blake2b_simd",
+ "constant_time_eq",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "ryu"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -980,6 +1118,16 @@ dependencies = [
]
[[package]]
+name = "signal-hook-registry"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
+dependencies = [
+ "arc-swap",
+ "libc",
+]
+
+[[package]]
name = "slab"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1013,6 +1161,18 @@ dependencies = [
]
[[package]]
+name = "socket2"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi 0.3.8",
+]
+
+[[package]]
name = "subtle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1065,11 +1225,28 @@ dependencies = [
"futures-core",
"iovec",
"lazy_static",
+ "libc",
"memchr",
"mio",
+ "mio-named-pipes",
+ "mio-uds",
"num_cpus",
"pin-project-lite",
+ "signal-hook-registry",
"slab",
+ "tokio-macros",
+ "winapi 0.3.8",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index c9be52f..d4743ff 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,8 @@ edition = "2018"
aes = "*"
base64 = "*"
block-modes = "*"
+directories = "*"
+fs2 = "*"
hkdf = "*"
hmac = "*"
pbkdf2 = "*"
@@ -15,6 +17,8 @@ reqwest = { version = "*", features = ["blocking", "json"] }
rpassword = "*"
rprompt = "*"
serde = { version = "*", features = ["derive"] }
+serde_json = "*"
sha2 = "*"
snafu = "*"
+tokio = { version = "*", features = ["full"] }
uuid = { version = "*", features = ["v4"] }
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)
+}