From c2c77d737a5b1c9f6c792d5efac73d37acec8c98 Mon Sep 17 00:00:00 2001 From: Aditya Rola <9098273+adirola@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:55:21 +0000 Subject: [PATCH 1/2] added wallet mnemonic export functionality --- src/export.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 4 ++++ 3 files changed, 47 insertions(+) create mode 100644 src/export.rs diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..d962cb7 --- /dev/null +++ b/src/export.rs @@ -0,0 +1,42 @@ +use anyhow::{Result}; +use clap::Args; +use std::path::Path; +use crate::utils::display_string_discreetly; +use rpassword; + +#[derive(Debug, Args)] +pub struct Export { + /// Forces export even if it might be unsafe + #[clap(short, long)] + pub force: bool, +} + +pub fn export_wallet_cli(wallet_path: &Path, _export: Export) -> Result<()> { + let prompt = "Please enter your wallet password to export the mnemonic phrase: "; + let password = rpassword::prompt_password(prompt)?; + + // Attempt to decrypt the keystore with the provided password + let phrase_recovered = eth_keystore::decrypt_key(wallet_path, &password)?; + let phrase = String::from_utf8(phrase_recovered)?; + + // Display the mnemonic phrase discreetly + let mnemonic_string = format!("Mnemonic phrase: {}\n", phrase); + display_string_discreetly(&mnemonic_string, "### Press any key to complete. ###")?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::test_utils::{with_tmp_dir_and_wallet, TEST_PASSWORD}; + + #[test] + fn test_export_wallet() { + with_tmp_dir_and_wallet(|_dir, wallet_path| { + let export = Export { force: false }; + // This test will fail in CI since it requires user input + // export_wallet_cli(wallet_path, export).unwrap(); + }); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 89ac734..fe4a6ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod account; pub mod balance; +pub mod export; pub mod format; pub mod import; pub mod list; diff --git a/src/main.rs b/src/main.rs index 801f6c7..02cf266 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use forc_tracing::{init_tracing_subscriber, println_error}; use forc_wallet::{ account::{self, Account, Accounts}, balance::{self, Balance}, + export::{export_wallet_cli, Export}, import::{import_wallet_cli, Import}, list::{list_wallet_cli, List}, new::{new_wallet_cli, New}, @@ -44,6 +45,8 @@ enum Command { /// If a '--fore' is specified, will automatically removes the existing wallet at the same /// path. Import(Import), + /// Export the mnemonic phrase from an existing wallet. + Export(Export), /// Lists all accounts derived for the wallet so far. /// /// Note that this only includes accounts that have been previously derived @@ -140,6 +143,7 @@ async fn run() -> Result<()> { Command::New(new) => new_wallet_cli(&wallet_path, new)?, Command::List(list) => list_wallet_cli(&wallet_path, list).await?, Command::Import(import) => import_wallet_cli(&wallet_path, import)?, + Command::Export(export) => export_wallet_cli(&wallet_path, export)?, Command::Accounts(accounts) => account::print_accounts_cli(&wallet_path, accounts)?, Command::Account(account) => account::cli(&wallet_path, account).await?, Command::Sign(sign) => sign::cli(&wallet_path, sign)?, From 48d60eac77da0c9484e76568ba243f156bd653bd Mon Sep 17 00:00:00 2001 From: Aditya Rola <9098273+adirola@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:02:33 +0000 Subject: [PATCH 2/2] refactor the code and added test cases --- src/export.rs | 116 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 18 deletions(-) diff --git a/src/export.rs b/src/export.rs index d962cb7..ef37ce5 100644 --- a/src/export.rs +++ b/src/export.rs @@ -1,42 +1,122 @@ -use anyhow::{Result}; +use anyhow::{Result, Context, anyhow}; use clap::Args; use std::path::Path; -use crate::utils::display_string_discreetly; -use rpassword; +use crate::{ + account::derive_and_cache_addresses, + utils::{display_string_discreetly, load_wallet}, + DEFAULT_CACHE_ACCOUNTS, +}; #[derive(Debug, Args)] pub struct Export { /// Forces export even if it might be unsafe #[clap(short, long)] pub force: bool, + /// How many accounts to cache by default (Default 10) + #[clap(short, long)] + pub cache_accounts: Option, } -pub fn export_wallet_cli(wallet_path: &Path, _export: Export) -> Result<()> { - let prompt = "Please enter your wallet password to export the mnemonic phrase: "; - let password = rpassword::prompt_password(prompt)?; - - // Attempt to decrypt the keystore with the provided password - let phrase_recovered = eth_keystore::decrypt_key(wallet_path, &password)?; - let phrase = String::from_utf8(phrase_recovered)?; +/// Decrypts a wallet using provided password +fn decrypt_wallet(wallet_path: &Path, password: &str) -> Result { + let phrase_bytes = eth_keystore::decrypt_key(wallet_path, password) + .map_err(|e| anyhow!("Failed to decrypt keystore: {}", e))?; - // Display the mnemonic phrase discreetly + String::from_utf8(phrase_bytes) + .context("Invalid UTF-8 in mnemonic phrase") +} + +/// Prompts for password securely +fn prompt_password() -> Result { + const PROMPT: &str = "Please enter your wallet password to export the mnemonic phrase: "; + rpassword::prompt_password(PROMPT) + .map_err(|e| anyhow!("Password prompt error: {}", e)) +} + +/// Displays mnemonic in alternate screen +fn display_mnemonic(phrase: &str) -> Result<()> { let mnemonic_string = format!("Mnemonic phrase: {}\n", phrase); - display_string_discreetly(&mnemonic_string, "### Press any key to complete. ###")?; - + display_string_discreetly(&mnemonic_string, "### Press any key to complete. ###") +} + +/// Securely wipes sensitive data from memory +fn secure_wipe(data: &mut [u8]) { + for byte in data.iter_mut() { + *byte = 0; + } +} + +pub fn export_wallet_cli(wallet_path: &Path, export: Export) -> Result<()> { + let password = prompt_password()?; + let phrase = export_wallet(wallet_path, &password)?; + + // Display phrase in alternate screen + display_mnemonic(&phrase) + .context("Failed to display mnemonic")?; + + let wallet = load_wallet(wallet_path)?; + + // After user exits alternate screen, derive and cache addresses + derive_and_cache_addresses( + &wallet, + &phrase, + 0..export.cache_accounts.unwrap_or(DEFAULT_CACHE_ACCOUNTS), + ).context("Failed to derive and cache addresses")?; + + secure_wipe(&mut phrase.into_bytes()); Ok(()) } +fn export_wallet(wallet_path: &Path, password: &str) -> Result { + let phrase = decrypt_wallet(wallet_path, password) + .context("Failed to decrypt wallet")?; + + Ok(phrase) +} + #[cfg(test)] mod tests { use super::*; use crate::utils::test_utils::{with_tmp_dir_and_wallet, TEST_PASSWORD}; - + + #[test] + fn test_decrypt_wallet() { + with_tmp_dir_and_wallet(|_dir, wallet_path| { + let result = decrypt_wallet(&wallet_path, TEST_PASSWORD); + assert!(result.is_ok()); + }); + } + + #[test] + fn test_decrypt_wallet_wrong_password() { + with_tmp_dir_and_wallet(|_dir, wallet_path| { + let result = decrypt_wallet(&wallet_path, "wrong_password"); + assert!(result.is_err()); + }); + } + #[test] fn test_export_wallet() { with_tmp_dir_and_wallet(|_dir, wallet_path| { - let export = Export { force: false }; - // This test will fail in CI since it requires user input - // export_wallet_cli(wallet_path, export).unwrap(); + let result = export_wallet(&wallet_path, TEST_PASSWORD); + assert!(result.is_ok()); + + if let Ok(phrase) = result { + assert!(!phrase.is_empty()); + } }); } -} \ No newline at end of file + + #[test] + fn test_display_mnemonic() { + let result = display_mnemonic("test phrase"); + assert!(result.is_ok()); + } + + #[test] + fn test_secure_wipe() { + let mut sensitive_data = vec![1u8, 2, 3, 4, 5]; + secure_wipe(&mut sensitive_data); + assert!(sensitive_data.iter().all(|&byte| byte == 0)); + } +} \ No newline at end of file