diff options
author | Jesse Luehrs <doy@tozt.net> | 2020-04-06 06:35:30 -0400 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2020-04-06 06:35:30 -0400 |
commit | bc18bca5c67b4a678a31198877e39d57d97b1e0c (patch) | |
tree | 3dc31608586cbe0f7973d7aa730267d57224e6a1 | |
parent | 4ad2f0a0dc3abb4cb10a6b82ca6a1f3a829eb1fb (diff) | |
download | rbw-bc18bca5c67b4a678a31198877e39d57d97b1e0c.tar.gz rbw-bc18bca5c67b4a678a31198877e39d57d97b1e0c.zip |
factor out into an agent
-rw-r--r-- | Cargo.lock | 179 | ||||
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | src/actions.rs | 46 | ||||
-rw-r--r-- | src/api.rs | 31 | ||||
-rw-r--r-- | src/bin/agent.rs | 197 | ||||
-rw-r--r-- | src/bin/rbw.rs (renamed from src/main.rs) | 23 | ||||
-rw-r--r-- | src/dirs.rs | 14 | ||||
-rw-r--r-- | src/lib.rs | 8 | ||||
-rw-r--r-- | src/pinentry.rs | 49 |
9 files changed, 526 insertions, 25 deletions
@@ -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]] @@ -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() +} @@ -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) +} |