From 69b3b5f614e3f7eae14d5a3d11a9a2a473517e77 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 1 May 2025 00:10:16 +0200 Subject: [PATCH 1/7] Add seedstore-tool for creating secret files --- Cargo.toml | 1 + seedstore-tool/Cargo.toml | 8 ++ seedstore-tool/src/main.rs | 9 ++ seedstore/Cargo.toml | 2 + seedstore/src/lib.rs | 5 + seedstore/src/tool.rs | 235 +++++++++++++++++++++++++++++++++++++ 6 files changed, 260 insertions(+) create mode 100644 seedstore-tool/Cargo.toml create mode 100644 seedstore-tool/src/main.rs create mode 100644 seedstore/src/tool.rs diff --git a/Cargo.toml b/Cargo.toml index 9ebbe0a..aa69ea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "secretstore", "seedstore", + "seedstore-tool", ] exclude = [] diff --git a/seedstore-tool/Cargo.toml b/seedstore-tool/Cargo.toml new file mode 100644 index 0000000..6dbc510 --- /dev/null +++ b/seedstore-tool/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "seedstore-tool" +version = "0.3.3" +edition = "2021" + +[dependencies] +bip39 = "2.1.0" +seedstore = { version = "0.3.3", path = "../seedstore", features = ["toolhelper"] } diff --git a/seedstore-tool/src/main.rs b/seedstore-tool/src/main.rs new file mode 100644 index 0000000..8bc052a --- /dev/null +++ b/seedstore-tool/src/main.rs @@ -0,0 +1,9 @@ +//! Executable shell for seedstore-tool + +use seedstore::SeedStoreTool; +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + SeedStoreTool::run(&args); +} diff --git a/seedstore/Cargo.toml b/seedstore/Cargo.toml index 18caec1..1529c44 100644 --- a/seedstore/Cargo.toml +++ b/seedstore/Cargo.toml @@ -9,6 +9,8 @@ edition = "2021" default = [] # Allow direct access to secret material accesssecret = [] +# Helpers for seedstore-tool +toolhelper = [] [dependencies] bip39 = { version = "2.1.0", features = ["zeroize"] } diff --git a/seedstore/src/lib.rs b/seedstore/src/lib.rs index 410da5c..91d2f56 100644 --- a/seedstore/src/lib.rs +++ b/seedstore/src/lib.rs @@ -14,6 +14,9 @@ mod keystore; mod seedstore; +#[cfg(feature = "toolhelper")] +mod tool; + #[cfg(test)] mod compat_backtest; #[cfg(test)] @@ -24,3 +27,5 @@ mod test_seedstore; // re-exports pub use crate::keystore::{KeyStore, KeyStoreCreator}; pub use crate::seedstore::{ChildSpecifier, SeedStore, SeedStoreCreator}; +#[cfg(feature = "toolhelper")] +pub use crate::tool::SeedStoreTool; diff --git a/seedstore/src/tool.rs b/seedstore/src/tool.rs new file mode 100644 index 0000000..26fe03c --- /dev/null +++ b/seedstore/src/tool.rs @@ -0,0 +1,235 @@ +///! Utility tool implementation: tool to create or check an encrypted secret seed file. +use crate::{SeedStore, SeedStoreCreator}; +use bip39::Mnemonic; +use std::{fs, str::FromStr}; + +const DEFAULT_FILE_NAME: &str = "secret.sec"; +const DEFAULT_NETWORK: u8 = 0; + +#[derive(PartialEq)] +enum Mode { + /// Create new file + Set, + /// Check existing file + Check, +} + +struct Config { + mode: Mode, + filename: String, + network: Option, + program_name: String, +} + +/// Utility tool implementation: tool to create or check an encrypted secret seed file. +pub struct SeedStoreTool { + config: Config, +} + +impl Config { + fn default() -> Self { + Self { + mode: Mode::Check, + filename: DEFAULT_FILE_NAME.to_owned(), + network: None, + program_name: "tool".to_owned(), + } + } +} + +impl ToString for Config { + fn to_string(&self) -> std::string::String { + let mut s = String::with_capacity(200); + s += &format!("[{}]: ", self.program_name); + s += "Mode: "; + s += match self.mode { + Mode::Check => "Check only", + Mode::Set => "Set", + }; + s += &format!(" File: {}", self.filename); + if let Some(n) = self.network { + s += &format!(" Network: {}", n); + } + s + } +} + +impl SeedStoreTool { + pub fn new(args: &Vec) -> Result { + // Process cmd line arguments + let config = Self::process_args(args)?; + + Ok(Self { config }) + } + + pub fn print_usage(progname: &Option<&String>) { + let default_progname = "tool".to_owned(); + let progname = progname.unwrap_or(&default_progname); + println!("{}: Set or check secret seed file", progname); + println!(""); + println!( + "{} [--set] [--file ] [--signet] [--net ]", + progname + ); + println!(" --set: If specified, mnemominc is prompted for, and secret is saved. Secret file must not exist."); + println!(" Default is to only check secret file, and print the xpub"); + println!( + " --file Secret file to use, default is {}", + DEFAULT_FILE_NAME + ); + println!( + " --signet If specified, assume Signet network. Default is mainnet ({})", + DEFAULT_NETWORK + ); + println!( + " --net Network byte. Default is mainnet ({})", + DEFAULT_NETWORK + ); + println!(""); + } + + fn process_args(args: &Vec) -> Result { + let mut config = Config::default(); + let len = args.len(); + if len < 1 { + return Err("Internal arg error, progname missing".to_owned()); + } + debug_assert!(len >= 1); + config.program_name = args[0].clone(); + let mut i = 1; + while i < len { + let a = &args[i]; + if *a == "--set" { + config.mode = Mode::Set; + } else if *a == "--file" { + if i + 1 < len { + config.filename = args[i + 1].clone(); + i += 1; + } else { + return Err("--file requires a argument".to_owned()); + } + } else if *a == "--signet" { + config.network = Some(3); + } else if *a == "--net" { + if i + 1 < len { + match args[i + 1].parse::() { + Err(e) => { + return Err(format!("--net requires a numerical argument, {}", e) + .to_string()) + } + Ok(n) => config.network = Some(n), + } + i += 1; + } else { + return Err("--net requires an argument".to_owned()); + } + } else { + return Err(format!("Unknown argument {}", a)); + } + i += 1; + } + + if config.mode == Mode::Check && config.network.is_some() { + return Err("Network should be specified only in Set mode".to_owned()); + } + + Ok(config) + } + + pub fn run(args: &Vec) { + match Self::new(&args) { + Err(err) => { + println!("Error processing arguments! {}", err); + Self::print_usage(&args.get(0)); + } + Ok(mut tool) => match tool.execute() { + Err(err) => println!("ERROR: {}", err), + Ok(_) => { + println!("Done."); + } + }, + } + } + + pub fn execute(&mut self) -> Result<(), String> { + println!("{}", self.config.to_string()); + + match self.config.mode { + Mode::Set => self.do_set(), + Mode::Check => self.do_check(), + } + } + + fn do_set(&self) -> Result<(), String> { + let exists = fs::exists(&self.config.filename).unwrap_or(true); + if exists { + return Err(format!( + "File already exists, won't overwrite, aborting {}", + self.config.filename + )); + } + + let mnemonic_str = self.read_mnemonic()?; + let mnemonic = Mnemonic::from_str(&mnemonic_str) + .map_err(|e| format!("Invalid mnemonic {}", e.to_string()))?; + let entropy = mnemonic.to_entropy(); + + let password = self.read_password()?; + + let seedstore = + SeedStoreCreator::new_from_data(&entropy, self.config.network.unwrap_or_default()) + .map_err(|e| format!("Could not encrypt secret, {}", e))?; + + let _res = SeedStoreCreator::write_to_file(&seedstore, &self.config.filename, &password) + .map_err(|e| format!("Could not write secret file, {}", e))?; + + println!("Seed written to encrypted file: {}", self.config.filename); + + Ok(()) + } + + fn read_password(&self) -> Result { + let password = "password".to_owned(); // TODO read + Ok(password) + } + + fn read_mnemonic(&self) -> Result { + let mnemonic = "oil oil oil oil oil oil oil oil oil oil oil oil".to_owned(); // TODO read + Ok(mnemonic) + } + + fn do_check(&self) -> Result<(), String> { + let exists = fs::exists(&self.config.filename).unwrap_or(false); + if !exists { + return Err(format!("Could not secret file {}", self.config.filename)); + } + + let password = self.read_password()?; + + let seedstore = SeedStore::new_from_encrypted_file(&self.config.filename, &password) + .map_err(|e| format!("Could not read secret file, {}", e))?; + + let xpub = seedstore.get_xpub()?.to_string(); + let child_spec0 = crate::ChildSpecifier::Index4(0); + let address0 = seedstore.get_child_address(&child_spec0)?; + let pubkey0 = seedstore.get_child_public_key(&child_spec0)?.to_string(); + + let network = seedstore.network(); + let derivation = child_spec0.derivation_path(network)?.to_string(); + println!(""); + println!( + "Seed has been read from secret file {}", + self.config.filename + ); + println!( + "XPUB, first address, and public key (network {}, derivation {}):", + network, derivation + ); + println!(" {}", xpub); + println!(" {}", address0); + println!(" {}", pubkey0); + println!(""); + + Ok(()) + } +} From ac3471b9f1218cc96bd7d9181c9ebdfd594f45b5 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 1 May 2025 01:32:30 +0200 Subject: [PATCH 2/7] Add tests for tool --- secretstore/Cargo.toml | 2 +- seedstore-tool/Cargo.toml | 4 +- seedstore/Cargo.toml | 4 +- seedstore/src/tool.rs | 154 ++++++++++++++++++++++++++++++++------ 4 files changed, 135 insertions(+), 29 deletions(-) diff --git a/secretstore/Cargo.toml b/secretstore/Cargo.toml index 40df981..47db8f2 100644 --- a/secretstore/Cargo.toml +++ b/secretstore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secretstore" -version = "0.3.3" +version = "0.3.4" description = "Store a secret (such as a private key) in an encrypted file" license = "MIT" edition = "2021" diff --git a/seedstore-tool/Cargo.toml b/seedstore-tool/Cargo.toml index 6dbc510..4ba3a44 100644 --- a/seedstore-tool/Cargo.toml +++ b/seedstore-tool/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "seedstore-tool" -version = "0.3.3" +version = "0.3.4" edition = "2021" [dependencies] bip39 = "2.1.0" -seedstore = { version = "0.3.3", path = "../seedstore", features = ["toolhelper"] } +seedstore = { version = "0.3.4", path = "../seedstore", features = ["toolhelper"] } diff --git a/seedstore/Cargo.toml b/seedstore/Cargo.toml index 1529c44..4a78b21 100644 --- a/seedstore/Cargo.toml +++ b/seedstore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seedstore" -version = "0.3.3" +version = "0.3.4" description = "Store bitcoin secret material (BIP39 mnemonic entropy, or similar) in an encrypted file" license = "MIT" edition = "2021" @@ -15,7 +15,7 @@ toolhelper = [] [dependencies] bip39 = { version = "2.1.0", features = ["zeroize"] } bitcoin = "0.32.5" -secretstore = { version = "0.3.3", path = "../secretstore" } +secretstore = { version = "0.3.4", path = "../secretstore" } zeroize = "1.8.1" [dev-dependencies] diff --git a/seedstore/src/tool.rs b/seedstore/src/tool.rs index 26fe03c..6f929c7 100644 --- a/seedstore/src/tool.rs +++ b/seedstore/src/tool.rs @@ -59,7 +59,11 @@ impl SeedStoreTool { // Process cmd line arguments let config = Self::process_args(args)?; - Ok(Self { config }) + Ok(Self::new_from_config(config)) + } + + fn new_from_config(config: Config) -> Self { + Self { config } } pub fn print_usage(progname: &Option<&String>) { @@ -142,7 +146,7 @@ impl SeedStoreTool { println!("Error processing arguments! {}", err); Self::print_usage(&args.get(0)); } - Ok(mut tool) => match tool.execute() { + Ok(tool) => match tool.execute() { Err(err) => println!("ERROR: {}", err), Ok(_) => { println!("Done."); @@ -151,7 +155,8 @@ impl SeedStoreTool { } } - pub fn execute(&mut self) -> Result<(), String> { + /// Return XPub (for testing) + pub fn execute(&self) -> Result { println!("{}", self.config.to_string()); match self.config.mode { @@ -160,7 +165,9 @@ impl SeedStoreTool { } } - fn do_set(&self) -> Result<(), String> { + /// Perform Set file operation + /// Return XPub (for testing) + fn do_set(&self) -> Result { let exists = fs::exists(&self.config.filename).unwrap_or(true); if exists { return Err(format!( @@ -176,16 +183,23 @@ impl SeedStoreTool { let password = self.read_password()?; - let seedstore = - SeedStoreCreator::new_from_data(&entropy, self.config.network.unwrap_or_default()) - .map_err(|e| format!("Could not encrypt secret, {}", e))?; + let passphrase = self.read_passphrase()?; + + let seedstore = SeedStoreCreator::new_from_data( + &entropy, + self.config.network.unwrap_or_default(), + Some(&passphrase), + ) + .map_err(|e| format!("Could not encrypt secret, {}", e))?; + + let xpub = self.print_info(&seedstore)?; let _res = SeedStoreCreator::write_to_file(&seedstore, &self.config.filename, &password) .map_err(|e| format!("Could not write secret file, {}", e))?; println!("Seed written to encrypted file: {}", self.config.filename); - Ok(()) + Ok(xpub) } fn read_password(&self) -> Result { @@ -198,17 +212,14 @@ impl SeedStoreTool { Ok(mnemonic) } - fn do_check(&self) -> Result<(), String> { - let exists = fs::exists(&self.config.filename).unwrap_or(false); - if !exists { - return Err(format!("Could not secret file {}", self.config.filename)); - } - - let password = self.read_password()?; - - let seedstore = SeedStore::new_from_encrypted_file(&self.config.filename, &password) - .map_err(|e| format!("Could not read secret file, {}", e))?; + fn read_passphrase(&self) -> Result { + let passphrase = "".to_owned(); // TODO read + Ok(passphrase) + } + /// Print out info from the seedstore + /// Return XPub (for testing) + fn print_info(&self, seedstore: &SeedStore) -> Result { let xpub = seedstore.get_xpub()?.to_string(); let child_spec0 = crate::ChildSpecifier::Index4(0); let address0 = seedstore.get_child_address(&child_spec0)?; @@ -216,11 +227,6 @@ impl SeedStoreTool { let network = seedstore.network(); let derivation = child_spec0.derivation_path(network)?.to_string(); - println!(""); - println!( - "Seed has been read from secret file {}", - self.config.filename - ); println!( "XPUB, first address, and public key (network {}, derivation {}):", network, derivation @@ -230,6 +236,106 @@ impl SeedStoreTool { println!(" {}", pubkey0); println!(""); - Ok(()) + Ok(xpub) + } + + /// Return XPub (for testing) + fn do_check(&self) -> Result { + let exists = fs::exists(&self.config.filename).unwrap_or(false); + if !exists { + return Err(format!( + "Could not find secret file {}", + self.config.filename + )); + } + + let password = self.read_password()?; + + let passphrase = self.read_passphrase()?; + + let seedstore = + SeedStore::new_from_encrypted_file(&self.config.filename, &password, Some(&passphrase)) + .map_err(|e| format!("Could not read secret file, {}", e))?; + + println!(""); + println!( + "Seed has been read from secret file {}", + self.config.filename + ); + + let xpub = self.print_info(&seedstore)?; + + Ok(xpub) + } +} + +#[cfg(test)] +mod tests { + use super::{Config, Mode, SeedStore, SeedStoreTool}; + use hex_conservative::FromHex; + use rand::Rng; + use std::env::temp_dir; + use std::fs; + + const PAYLOAD_V1_EV2_SCRYPT: &str = "53530104002a2b2c020ee3d3970706fbe9f680eb68763af3c849100010907fc4f2740c7613422df300488137c1e6af59"; + const PASSWORD1: &str = "password"; + const XPUB1: &str = "xpub6CDDB17Xj7pDDWedpLsED1JbPPQmyuapHmAzQEEs2P57hciCjwQ3ov7TfGsTZftAM2gVdPzE55L6gUvHguwWjY82518zw1Z3VbDeWgx3Jqs"; + + fn get_temp_file_name() -> String { + format!( + "{}/_seedstore_tempfile_{}_.tmp", + temp_dir().to_str().unwrap(), + rand::rng().random::() + ) + } + + #[test] + fn check() { + let temp_file = get_temp_file_name(); + let payload = Vec::from_hex(PAYLOAD_V1_EV2_SCRYPT).unwrap(); + let _res = fs::write(&temp_file, &payload).unwrap(); + + let config = Config { + mode: Mode::Check, + filename: temp_file.clone(), + network: None, + program_name: "tool".to_owned(), + }; + let tool = SeedStoreTool::new_from_config(config); + + let xpub = tool.execute().unwrap(); + + assert_eq!(xpub.to_string(), XPUB1); + + let _res = fs::remove_file(&temp_file); + } + + #[test] + fn set() { + let temp_file = get_temp_file_name(); + + let network = 3; + let config = Config { + mode: Mode::Set, + filename: temp_file.clone(), + network: Some(network), + program_name: "tool".to_owned(), + }; + let tool = SeedStoreTool::new_from_config(config); + + let xpub = tool.execute().unwrap(); + + assert_eq!(xpub.to_string(), "tpubDCRo9GmRAvEWANJ5iSfMEqPoq3uYvjBPAAjrDj5iQMxAq7DCs5orw7m9xJes8hWYAwKuH3T63WrKfzzw7g9ucbjq4LUu5cgCLUPMN7gUkrL"); + + // Read back the file + { + let store = SeedStore::new_from_encrypted_file(&temp_file, PASSWORD1, None).unwrap(); + + assert_eq!(store.network(), network); + assert_eq!(store.get_xpub().unwrap().to_string(), "tpubDCRo9GmRAvEWANJ5iSfMEqPoq3uYvjBPAAjrDj5iQMxAq7DCs5orw7m9xJes8hWYAwKuH3T63WrKfzzw7g9ucbjq4LUu5cgCLUPMN7gUkrL"); + drop(store); + } + + let _res = fs::remove_file(&temp_file); } } From 77b8336893a59924310d4fda0cf4c00d18212f33 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Thu, 1 May 2025 02:20:45 +0200 Subject: [PATCH 3/7] Proper input from console --- seedstore/Cargo.toml | 3 ++- seedstore/src/tool.rs | 63 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/seedstore/Cargo.toml b/seedstore/Cargo.toml index 4a78b21..3e3d1f9 100644 --- a/seedstore/Cargo.toml +++ b/seedstore/Cargo.toml @@ -10,11 +10,12 @@ default = [] # Allow direct access to secret material accesssecret = [] # Helpers for seedstore-tool -toolhelper = [] +toolhelper = ["rpassword"] [dependencies] bip39 = { version = "2.1.0", features = ["zeroize"] } bitcoin = "0.32.5" +rpassword = { version = "7.4.0", optional = true } secretstore = { version = "0.3.4", path = "../secretstore" } zeroize = "1.8.1" diff --git a/seedstore/src/tool.rs b/seedstore/src/tool.rs index 6f929c7..253fcf1 100644 --- a/seedstore/src/tool.rs +++ b/seedstore/src/tool.rs @@ -2,6 +2,7 @@ use crate::{SeedStore, SeedStoreCreator}; use bip39::Mnemonic; use std::{fs, str::FromStr}; +use std::io::{BufRead, Write, self, stdout}; const DEFAULT_FILE_NAME: &str = "secret.sec"; const DEFAULT_NETWORK: u8 = 0; @@ -180,17 +181,18 @@ impl SeedStoreTool { let mnemonic = Mnemonic::from_str(&mnemonic_str) .map_err(|e| format!("Invalid mnemonic {}", e.to_string()))?; let entropy = mnemonic.to_entropy(); + println!("Seedphrase entered, seems OK"); let password = self.read_password()?; - let passphrase = self.read_passphrase()?; + let passphrase = self.read_passphrase_if_needed(&password)?; let seedstore = SeedStoreCreator::new_from_data( &entropy, self.config.network.unwrap_or_default(), Some(&passphrase), ) - .map_err(|e| format!("Could not encrypt secret, {}", e))?; + .map_err(|e| format!("Could not encrypt secret, {}", e))?; let xpub = self.print_info(&seedstore)?; @@ -203,18 +205,63 @@ impl SeedStoreTool { } fn read_password(&self) -> Result { - let password = "password".to_owned(); // TODO read - Ok(password) + let password1 = rpassword::prompt_password("Enter the encryption password: ") + .map_err(|e| format!("Error reading password, {}", e.to_string()))?; + let password2 = rpassword::prompt_password("Repeat the encryption password: ") + .map_err(|e| format!("Error reading password, {}", e.to_string()))?; + if password1 != password2 { + return Err("The two passwords don't match".to_owned()); + } + println!("Passwords entered, match OK"); + debug_assert_eq!(password1, password2); + Ok(password1) } fn read_mnemonic(&self) -> Result { - let mnemonic = "oil oil oil oil oil oil oil oil oil oil oil oil".to_owned(); // TODO read + let mnemonic = rpassword::prompt_password("Enter the seedphrase (mnemonic) words (input is hidden): ") + .map_err(|e| format!("Error reading mnemonic, {}", e.to_string()))?; Ok(mnemonic) } + fn read_line() -> String { + let stdin = io::stdin(); + let mut iterator = stdin.lock().lines(); + let line1 = iterator.next().unwrap().unwrap_or_default(); + line1 + } + fn read_passphrase(&self) -> Result { - let passphrase = "".to_owned(); // TODO read - Ok(passphrase) + let passphrase1 = rpassword::prompt_password("Enter the seed passphrase: ") + .map_err(|e| format!("Error reading passphrase, {}", e.to_string()))?; + let passphrase2 = rpassword::prompt_password("Repeat the seed passphrase: ") + .map_err(|e| format!("Error reading passphrase, {}", e.to_string()))?; + if passphrase1 != passphrase2 { + return Err("The two passphrase don't match".to_owned()); + } + println!("Passphrases entered, match OK"); + debug_assert_eq!(passphrase1, passphrase2); + Ok(passphrase1) + } + + fn read_passphrase_if_needed(&self, encryption_password: &String) -> Result { + println!("Optionally, a seed passphrase can be used. Please choose:"); + println!(" Enter or 0 : no passphrase"); + println!(" 1 : enter a passphrase"); + println!(" 2 : use the encryption password as passphrase too"); + loop { + print!("Enter your choice: "); + let _res = stdout().flush(); + let resp = Self::read_line(); + match resp.as_str() { + "" | "0" => return Ok("".to_string()), + "1" => { + let passphrase = self.read_passphrase()?; + return Ok(passphrase); + }, + "2" => return Ok(encryption_password.clone()), + _ => {} + } + } } /// Print out info from the seedstore @@ -251,7 +298,7 @@ impl SeedStoreTool { let password = self.read_password()?; - let passphrase = self.read_passphrase()?; + let passphrase = self.read_passphrase_if_needed(&password)?; let seedstore = SeedStore::new_from_encrypted_file(&self.config.filename, &password, Some(&passphrase)) From 21cadb636c82ed82536e46b0814fd9f4c3b948ef Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 2 May 2025 22:16:34 +0200 Subject: [PATCH 4/7] More tests for tool --- seedstore/src/tool.rs | 108 ++++++++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 25 deletions(-) diff --git a/seedstore/src/tool.rs b/seedstore/src/tool.rs index 253fcf1..c7dcb1f 100644 --- a/seedstore/src/tool.rs +++ b/seedstore/src/tool.rs @@ -1,8 +1,8 @@ ///! Utility tool implementation: tool to create or check an encrypted secret seed file. use crate::{SeedStore, SeedStoreCreator}; use bip39::Mnemonic; +use std::io::{self, stdout, BufRead, Write}; use std::{fs, str::FromStr}; -use std::io::{BufRead, Write, self, stdout}; const DEFAULT_FILE_NAME: &str = "secret.sec"; const DEFAULT_NETWORK: u8 = 0; @@ -20,6 +20,7 @@ struct Config { filename: String, network: Option, program_name: String, + test_canned_input: Vec, } /// Utility tool implementation: tool to create or check an encrypted secret seed file. @@ -34,6 +35,7 @@ impl Config { filename: DEFAULT_FILE_NAME.to_owned(), network: None, program_name: "tool".to_owned(), + test_canned_input: Vec::new(), } } } @@ -147,7 +149,7 @@ impl SeedStoreTool { println!("Error processing arguments! {}", err); Self::print_usage(&args.get(0)); } - Ok(tool) => match tool.execute() { + Ok(mut tool) => match tool.execute() { Err(err) => println!("ERROR: {}", err), Ok(_) => { println!("Done."); @@ -157,7 +159,7 @@ impl SeedStoreTool { } /// Return XPub (for testing) - pub fn execute(&self) -> Result { + pub fn execute(&mut self) -> Result { println!("{}", self.config.to_string()); match self.config.mode { @@ -168,7 +170,7 @@ impl SeedStoreTool { /// Perform Set file operation /// Return XPub (for testing) - fn do_set(&self) -> Result { + fn do_set(&mut self) -> Result { let exists = fs::exists(&self.config.filename).unwrap_or(true); if exists { return Err(format!( @@ -192,7 +194,7 @@ impl SeedStoreTool { self.config.network.unwrap_or_default(), Some(&passphrase), ) - .map_err(|e| format!("Could not encrypt secret, {}", e))?; + .map_err(|e| format!("Could not encrypt secret, {}", e))?; let xpub = self.print_info(&seedstore)?; @@ -204,11 +206,28 @@ impl SeedStoreTool { Ok(xpub) } - fn read_password(&self) -> Result { - let password1 = rpassword::prompt_password("Enter the encryption password: ") - .map_err(|e| format!("Error reading password, {}", e.to_string()))?; - let password2 = rpassword::prompt_password("Repeat the encryption password: ") - .map_err(|e| format!("Error reading password, {}", e.to_string()))?; + fn next_canned_input(&mut self) -> Option { + if self.config.test_canned_input.is_empty() { + return None; + } + debug_assert!(self.config.test_canned_input.len() >= 1); + let next = self.config.test_canned_input.remove(0); + println!("Using canned input '{}'", next); + Some(next) + } + + fn read_no_echo(&mut self, item_name: &str, prompt: &str) -> Result { + if let Some(canned_input) = self.next_canned_input() { + return Ok(canned_input); + } + let result = rpassword::prompt_password(prompt) + .map_err(|e| format!("Error reading {}, {}", item_name, e.to_string()))?; + Ok(result) + } + + fn read_password(&mut self) -> Result { + let password1 = self.read_no_echo("password", "Enter the encryption password: ")?; + let password2 = self.read_no_echo("password", "Repeat the encryption password: ")?; if password1 != password2 { return Err("The two passwords don't match".to_owned()); } @@ -217,24 +236,27 @@ impl SeedStoreTool { Ok(password1) } - fn read_mnemonic(&self) -> Result { - let mnemonic = rpassword::prompt_password("Enter the seedphrase (mnemonic) words (input is hidden): ") - .map_err(|e| format!("Error reading mnemonic, {}", e.to_string()))?; + fn read_mnemonic(&mut self) -> Result { + let mnemonic = self.read_no_echo( + "mnemonic", + "Enter the seedphrase (mnemonic) words (input is hidden): ", + )?; Ok(mnemonic) } - fn read_line() -> String { + fn read_line(&mut self) -> String { + if let Some(canned_input) = self.next_canned_input() { + return canned_input; + } let stdin = io::stdin(); let mut iterator = stdin.lock().lines(); let line1 = iterator.next().unwrap().unwrap_or_default(); line1 } - fn read_passphrase(&self) -> Result { - let passphrase1 = rpassword::prompt_password("Enter the seed passphrase: ") - .map_err(|e| format!("Error reading passphrase, {}", e.to_string()))?; - let passphrase2 = rpassword::prompt_password("Repeat the seed passphrase: ") - .map_err(|e| format!("Error reading passphrase, {}", e.to_string()))?; + fn read_passphrase(&mut self) -> Result { + let passphrase1 = self.read_no_echo("passphrase", "Enter the seed passphrase: ")?; + let passphrase2 = self.read_no_echo("passphrase", "Repeat the seed passphrase: ")?; if passphrase1 != passphrase2 { return Err("The two passphrase don't match".to_owned()); } @@ -243,7 +265,10 @@ impl SeedStoreTool { Ok(passphrase1) } - fn read_passphrase_if_needed(&self, encryption_password: &String) -> Result { + fn read_passphrase_if_needed( + &mut self, + encryption_password: &String, + ) -> Result { println!("Optionally, a seed passphrase can be used. Please choose:"); println!(" Enter or 0 : no passphrase"); println!(" 1 : enter a passphrase"); @@ -251,13 +276,13 @@ impl SeedStoreTool { loop { print!("Enter your choice: "); let _res = stdout().flush(); - let resp = Self::read_line(); + let resp = self.read_line(); match resp.as_str() { "" | "0" => return Ok("".to_string()), "1" => { let passphrase = self.read_passphrase()?; return Ok(passphrase); - }, + } "2" => return Ok(encryption_password.clone()), _ => {} } @@ -287,7 +312,7 @@ impl SeedStoreTool { } /// Return XPub (for testing) - fn do_check(&self) -> Result { + fn do_check(&mut self) -> Result { let exists = fs::exists(&self.config.filename).unwrap_or(false); if !exists { return Err(format!( @@ -327,6 +352,7 @@ mod tests { const PAYLOAD_V1_EV2_SCRYPT: &str = "53530104002a2b2c020ee3d3970706fbe9f680eb68763af3c849100010907fc4f2740c7613422df300488137c1e6af59"; const PASSWORD1: &str = "password"; const XPUB1: &str = "xpub6CDDB17Xj7pDDWedpLsED1JbPPQmyuapHmAzQEEs2P57hciCjwQ3ov7TfGsTZftAM2gVdPzE55L6gUvHguwWjY82518zw1Z3VbDeWgx3Jqs"; + const MNEMO1: &str = "oil oil oil oil oil oil oil oil oil oil oil oil"; fn get_temp_file_name() -> String { format!( @@ -347,8 +373,13 @@ mod tests { filename: temp_file.clone(), network: None, program_name: "tool".to_owned(), + test_canned_input: vec![ + PASSWORD1.to_owned(), // encryption password + PASSWORD1.to_owned(), // repeat encryption password + "".to_owned(), // passphrase + ], }; - let tool = SeedStoreTool::new_from_config(config); + let mut tool = SeedStoreTool::new_from_config(config); let xpub = tool.execute().unwrap(); @@ -367,8 +398,14 @@ mod tests { filename: temp_file.clone(), network: Some(network), program_name: "tool".to_owned(), + test_canned_input: vec![ + MNEMO1.to_owned(), // mnemonic + PASSWORD1.to_owned(), // encryption password + PASSWORD1.to_owned(), // repeat encryption password + "".to_owned(), // passphrase + ], }; - let tool = SeedStoreTool::new_from_config(config); + let mut tool = SeedStoreTool::new_from_config(config); let xpub = tool.execute().unwrap(); @@ -385,4 +422,25 @@ mod tests { let _res = fs::remove_file(&temp_file); } + + #[test] + fn neg_set_pw_dont_match() { + let temp_file = get_temp_file_name(); + + let config = Config { + mode: Mode::Set, + filename: temp_file, + network: Some(0), + program_name: "tool".to_owned(), + test_canned_input: vec![ + MNEMO1.to_owned(), // mnemonic + "password".to_owned(), // encryption password + "passsword".to_owned(), // repeat encryption password + ], + }; + let mut tool = SeedStoreTool::new_from_config(config); + + let res = tool.execute(); + assert_eq!(res.err().unwrap(), "The two passwords don't match"); + } } From ad73333f6a361e2fcc80d99e5edd7ab615e70058 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 2 May 2025 22:24:11 +0200 Subject: [PATCH 5/7] Tool: help option --- seedstore/src/tool.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/seedstore/src/tool.rs b/seedstore/src/tool.rs index c7dcb1f..2ce2c76 100644 --- a/seedstore/src/tool.rs +++ b/seedstore/src/tool.rs @@ -13,6 +13,8 @@ enum Mode { Set, /// Check existing file Check, + /// Help only + Help, } struct Config { @@ -48,6 +50,7 @@ impl ToString for Config { s += match self.mode { Mode::Check => "Check only", Mode::Set => "Set", + Mode::Help => "Help only", }; s += &format!(" File: {}", self.filename); if let Some(n) = self.network { @@ -75,7 +78,7 @@ impl SeedStoreTool { println!("{}: Set or check secret seed file", progname); println!(""); println!( - "{} [--set] [--file ] [--signet] [--net ]", + "{} [--help] [--set] [--file ] [--signet] [--net ]", progname ); println!(" --set: If specified, mnemominc is prompted for, and secret is saved. Secret file must not exist."); @@ -92,6 +95,7 @@ impl SeedStoreTool { " --net Network byte. Default is mainnet ({})", DEFAULT_NETWORK ); + println!(" --help Print usage (this)"); println!(""); } @@ -130,13 +134,15 @@ impl SeedStoreTool { } else { return Err("--net requires an argument".to_owned()); } + } else if *a == "--help" { + config.mode = Mode::Help; } else { return Err(format!("Unknown argument {}", a)); } i += 1; } - if config.mode == Mode::Check && config.network.is_some() { + if config.network.is_some() && config.mode != Mode::Set { return Err("Network should be specified only in Set mode".to_owned()); } @@ -165,6 +171,10 @@ impl SeedStoreTool { match self.config.mode { Mode::Set => self.do_set(), Mode::Check => self.do_check(), + Mode::Help => { + Self::print_usage(&Some(&self.config.program_name)); + Ok("".to_owned()) + } } } From 6a7677e621d35fe5bc19b4d7918529a732042a3f Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 2 May 2025 22:34:57 +0200 Subject: [PATCH 6/7] Tool into Readme --- README.md | 25 ++++++++++++++++++++++--- seedstore-tool/src/main.rs | 3 ++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d48777b..800c64a 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,11 @@ Steps to mitigate the risks: - enforce restricted permissions on the file - encorce strong password -- seedstore tool (for prompting for and saving secret) - version 1.0, with format guarantee - (later) breaking challenge bounty -## Usage Example +## Usage -- Example usage of the code Reading secret from file: @@ -58,7 +57,7 @@ use seedstore::SeedStoreCreator; See the [example programs](seedstore/examples). -## Usage Guide +## Usage -- Building `SeedStore` is a simple Rust library. To compile it, use the usual Rust commands. @@ -75,6 +74,26 @@ cargo run --example create_seedstore _MSRV_: Rust 1.81 (due to `fs::exists`) +## Usage -- Tool + +`seedstore-tool` is a command-line utility to create or check secret files. +Here are some sample calls to get started: + +``` +cargo r -p seedstore-tool -- --help +``` + +Check existing secret file; type 'password' for encryption password, twice: +``` +cargo r -p seedstore-tool -- --file seedstore/sample_secret.sec +``` + +Create a new secret, enter secret information to store: +``` +cargo r -p seedstore-tool -- --set --file /tmp/newfile +``` + + ## Data Format The data format of the secret file is documented here: [Data_Format.md](Data_Format.md) diff --git a/seedstore-tool/src/main.rs b/seedstore-tool/src/main.rs index 8bc052a..1aa7fb6 100644 --- a/seedstore-tool/src/main.rs +++ b/seedstore-tool/src/main.rs @@ -1,8 +1,9 @@ -//! Executable shell for seedstore-tool +//! `seedstore-tool` is a command-line utility to create or check secret files. use seedstore::SeedStoreTool; use std::env; +/// Top-level executable implementation for seedstore-tool. fn main() { let args: Vec = env::args().collect(); SeedStoreTool::run(&args); From 86703eb67197c940cb2ed9482108b114161531a8 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 2 May 2025 22:38:01 +0200 Subject: [PATCH 7/7] Include new feature in CI --- .github/workflows/build_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 2982b83..f2faccb 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -21,6 +21,6 @@ jobs: - name: Build with default features run: cargo build --verbose - name: Build - run: cargo build --features accesssecret --verbose + run: cargo build --features accesssecret --features toolhelper --verbose - name: Run tests - run: cargo test --features accesssecret --verbose + run: cargo test --features accesssecret --features toolhelper --verbose