Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Wallet Export Functionality #227

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use anyhow::{Result, Context, anyhow};
use clap::Args;
use std::path::Path;
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,
Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like this is used anywhere. What does this mean?

Suggested change
/// 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<usize>,
Comment on lines +15 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use clap to set the default

Suggested change
/// How many accounts to cache by default (Default 10)
#[clap(short, long)]
pub cache_accounts: Option<usize>,
/// The number of accounts to cache
#[clap(long, default_value = 10)]
pub cache_accounts: usize,

}

/// Decrypts a wallet using provided password
fn decrypt_wallet(wallet_path: &Path, password: &str) -> Result<String> {
let phrase_bytes = eth_keystore::decrypt_key(wallet_path, password)
.map_err(|e| anyhow!("Failed to decrypt keystore: {}", e))?;

String::from_utf8(phrase_bytes)
.context("Invalid UTF-8 in mnemonic phrase")
}

/// Prompts for password securely
fn prompt_password() -> Result<String> {
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. ###")
}

/// Securely wipes sensitive data from memory
fn secure_wipe(data: &mut [u8]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! We should use zeroize instead to make sure this isn't optimized away by the compiler.

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<String> {
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 result = export_wallet(&wallet_path, TEST_PASSWORD);
assert!(result.is_ok());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be an unwrap


if let Ok(phrase) = result {
assert!(!phrase.is_empty());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should assert that it's the test mnemonic

}
});
}

#[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));
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod account;
pub mod balance;
pub mod export;
pub mod format;
pub mod import;
pub mod list;
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)?,
Expand Down