From 5722c200c1eb6b7a82c22b65837c0447b99c5559 Mon Sep 17 00:00:00 2001 From: Conor Schaefer Date: Thu, 8 Jun 2023 06:41:14 -0700 Subject: [PATCH] pcli: import seed phrase from stdin Changes the behavior for `pcli keys import phrase` to prompt for a masked password input interactively. When run via a pipe, reads from stdin instead, which preserves the ability to import seed phrases from other tools. --- Cargo.lock | 23 ++++++++++++++++ crates/bin/pcli/Cargo.toml | 2 ++ crates/bin/pcli/src/command/keys.rs | 28 +++++++++++++++----- crates/bin/pcli/tests/network_integration.rs | 2 +- crates/wallet/src/key_store.rs | 4 ++- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67bac5f0a0..79603437e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3942,6 +3942,7 @@ dependencies = [ "ark-ff", "assert_cmd", "async-stream 0.2.1", + "atty", "base64 0.21.2", "bincode", "blake2b_simd 0.5.11", @@ -3986,6 +3987,7 @@ dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", "regex", + "rpassword", "serde", "serde_json", "serde_with 1.14.0", @@ -5805,6 +5807,27 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "rpassword" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rusqlite" version = "0.29.0" diff --git a/crates/bin/pcli/Cargo.toml b/crates/bin/pcli/Cargo.toml index a45377f458..ca6abc6d0c 100644 --- a/crates/bin/pcli/Cargo.toml +++ b/crates/bin/pcli/Cargo.toml @@ -47,6 +47,7 @@ ibc-types = { git = "https://github.com/penumbra-zone/ibc-types", branch = "main ibc-proto = { version = "0.31.0" } ark-ff = { version = "0.4", default-features = false } +atty = "0.2" ed25519-consensus = "2" futures = "0.3" async-stream = "0.2" @@ -73,6 +74,7 @@ hex = "0.4" rand = "0.8" rand_chacha = "0.3.1" rand_core = { version = "0.6.3", features = ["getrandom"] } +rpassword = "7" indicatif = "0.16" http-body = "0.4.5" clap = { version = "3", features = ["derive", "env"] } diff --git a/crates/bin/pcli/src/command/keys.rs b/crates/bin/pcli/src/command/keys.rs index 83aba5052e..5cf447c97b 100644 --- a/crates/bin/pcli/src/command/keys.rs +++ b/crates/bin/pcli/src/command/keys.rs @@ -1,3 +1,4 @@ +use std::io::Read; use std::str::FromStr; use anyhow::{anyhow, Result}; @@ -24,11 +25,9 @@ pub enum KeysCmd { #[derive(Debug, clap::Subcommand)] pub enum ImportCmd { - /// Import from an existing seed phrase. - Phrase { - /// A 24 word phrase in quotes. - seed_phrase: String, - }, + /// Import wallet from an existing 24-word seed phrase. Will prompt for input interactively. + /// Also accepts input from stdin, for use with pipes. + Phrase, } #[derive(Debug, clap::Subcommand)] @@ -79,8 +78,23 @@ impl KeysCmd { wallet.save(data_dir.join(crate::CUSTODY_FILE_NAME))?; self.archive_wallet(&wallet)?; } - KeysCmd::Import(ImportCmd::Phrase { seed_phrase }) => { - let wallet = KeyStore::from_seed_phrase(SeedPhrase::from_str(seed_phrase)?); + KeysCmd::Import(ImportCmd::Phrase) => { + let mut seed_phrase = String::new(); + // The `rpassword` crate doesn't support reading from stdin, so we check + // for an interactive session. We must support non-interactive use cases, + // for integration with other tooling. + if atty::is(atty::Stream::Stdin) { + seed_phrase = rpassword::prompt_password("Enter seed phrase: ")?; + } else { + while let Ok(n_bytes) = std::io::stdin().lock().read_to_string(&mut seed_phrase) + { + if n_bytes == 0 { + break; + } + seed_phrase = seed_phrase.trim().to_string(); + } + } + let wallet = KeyStore::from_seed_phrase(SeedPhrase::from_str(&seed_phrase)?); wallet.save(data_dir.join(crate::CUSTODY_FILE_NAME))?; self.archive_wallet(&wallet)?; } diff --git a/crates/bin/pcli/tests/network_integration.rs b/crates/bin/pcli/tests/network_integration.rs index 5f74e60bb5..622e3a04db 100644 --- a/crates/bin/pcli/tests/network_integration.rs +++ b/crates/bin/pcli/tests/network_integration.rs @@ -56,8 +56,8 @@ fn load_wallet_into_tmpdir() -> TempDir { "keys", "import", "phrase", - SEED_PHRASE, ]) + .write_stdin(SEED_PHRASE) .timeout(std::time::Duration::from_secs(TIMEOUT_COMMAND_SECONDS)); setup_cmd .assert() diff --git a/crates/wallet/src/key_store.rs b/crates/wallet/src/key_store.rs index 9bceaf993a..d5ea07b5a7 100644 --- a/crates/wallet/src/key_store.rs +++ b/crates/wallet/src/key_store.rs @@ -15,8 +15,10 @@ impl KeyStore { /// Write the wallet data to the provided path. pub fn save(&self, path: impl AsRef) -> anyhow::Result<()> { if path.as_ref().exists() { + let p = path.as_ref().to_string_lossy(); return Err(anyhow::anyhow!( - "Wallet file already exists, refusing to overwrite it" + "Wallet file already exists, refusing to overwrite it: {}", + &p )); } use std::io::Write;