From 9cf8f45192c4d25c463674dcdec61b1bab230030 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Tue, 8 Sep 2020 12:17:51 +0000 Subject: [PATCH] Mnemonic key recovery (#1579) ## Issue Addressed N/A ## Proposed Changes Add a `lighthouse am wallet recover` command that recreates a wallet from a mnemonic but no validator keys. Add a `lighthouse am validator recover` command which would directly create keys from a mnemonic for a given index and count. ## Additional Info Co-authored-by: Paul Hauner --- account_manager/src/common.rs | 49 +++++++ account_manager/src/validator/mod.rs | 3 + account_manager/src/validator/recover.rs | 156 +++++++++++++++++++++++ account_manager/src/wallet/create.rs | 78 +++++++----- account_manager/src/wallet/mod.rs | 3 + account_manager/src/wallet/recover.rs | 87 +++++++++++++ book/src/SUMMARY.md | 1 + book/src/key-recovery.md | 65 ++++++++++ common/account_utils/src/lib.rs | 17 +++ crypto/eth2_wallet/src/lib.rs | 4 +- crypto/eth2_wallet/src/wallet.rs | 16 +++ 11 files changed, 443 insertions(+), 36 deletions(-) create mode 100644 account_manager/src/validator/recover.rs create mode 100644 account_manager/src/wallet/recover.rs create mode 100644 book/src/key-recovery.md diff --git a/account_manager/src/common.rs b/account_manager/src/common.rs index d2339c2d6..d7ab3f3ef 100644 --- a/account_manager/src/common.rs +++ b/account_manager/src/common.rs @@ -1,6 +1,15 @@ +use account_utils::PlainText; +use account_utils::{read_mnemonic_from_user, strip_off_newlines}; use clap::ArgMatches; +use eth2_wallet::bip39::{Language, Mnemonic}; +use std::fs; use std::fs::create_dir_all; use std::path::{Path, PathBuf}; +use std::str::from_utf8; +use std::thread::sleep; +use std::time::Duration; + +pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; pub fn ensure_dir_exists>(path: P) -> Result<(), String> { let path = path.as_ref(); @@ -19,3 +28,43 @@ pub fn base_wallet_dir(matches: &ArgMatches, arg: &'static str) -> Result, + stdin_password: bool, +) -> Result { + let mnemonic = match mnemonic_path { + Some(path) => fs::read(&path) + .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) + .and_then(|bytes| { + let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); + let phrase = from_utf8(&bytes_no_newlines.as_ref()) + .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; + Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { + format!( + "Unable to derive mnemonic from string {:?}: {:?}", + phrase, e + ) + }) + })?, + None => loop { + eprintln!(""); + eprintln!("{}", MNEMONIC_PROMPT); + + let mnemonic = read_mnemonic_from_user(stdin_password)?; + + match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { + Ok(mnemonic_m) => { + eprintln!("Valid mnemonic provided."); + eprintln!(""); + sleep(Duration::from_secs(1)); + break mnemonic_m; + } + Err(_) => { + eprintln!("Invalid mnemonic"); + } + } + }, + }; + Ok(mnemonic) +} diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 0df22d7fb..84ad6df39 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -2,6 +2,7 @@ pub mod create; pub mod deposit; pub mod import; pub mod list; +pub mod recover; use crate::common::base_wallet_dir; use clap::{App, Arg, ArgMatches}; @@ -24,6 +25,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(deposit::cli_app()) .subcommand(import::cli_app()) .subcommand(list::cli_app()) + .subcommand(recover::cli_app()) } pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { @@ -34,6 +36,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< (deposit::CMD, Some(matches)) => deposit::cli_run::(matches, env), (import::CMD, Some(matches)) => import::cli_run(matches), (list::CMD, Some(matches)) => list::cli_run(matches), + (recover::CMD, Some(matches)) => recover::cli_run(matches), (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs new file mode 100644 index 000000000..bc9624099 --- /dev/null +++ b/account_manager/src/validator/recover.rs @@ -0,0 +1,156 @@ +use super::create::STORE_WITHDRAW_FLAG; +use super::import::STDIN_PASSWORD_FLAG; +use crate::common::{ensure_dir_exists, read_mnemonic_from_cli}; +use crate::validator::create::COUNT_FLAG; +use crate::{SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG}; +use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; +use account_utils::random_password; +use clap::{App, Arg, ArgMatches}; +use eth2_wallet::bip39::Seed; +use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; +use std::path::PathBuf; +use validator_dir::Builder as ValidatorDirBuilder; +pub const CMD: &str = "recover"; +pub const FIRST_INDEX_FLAG: &str = "first-index"; +pub const MNEMONIC_FLAG: &str = "mnemonic-path"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about( + "Recovers validator private keys given a BIP-39 mnemonic phrase. \ + If you did not specify a `--first-index` or count `--count`, by default this will \ + only recover the keys associated with the validator at index 0 for an HD wallet \ + in accordance with the EIP-2333 spec.") + .arg( + Arg::with_name(FIRST_INDEX_FLAG) + .long(FIRST_INDEX_FLAG) + .value_name("FIRST_INDEX") + .help("The first of consecutive key indexes you wish to recover.") + .takes_value(true) + .required(false) + .default_value("0"), + ) + .arg( + Arg::with_name(COUNT_FLAG) + .long(COUNT_FLAG) + .value_name("COUNT") + .help("The number of validator keys you wish to recover. Counted consecutively from the provided `--first_index`.") + .takes_value(true) + .required(false) + .default_value("1"), + ) + .arg( + Arg::with_name(MNEMONIC_FLAG) + .long(MNEMONIC_FLAG) + .value_name("MNEMONIC_PATH") + .help( + "If present, the mnemonic will be read in from this file.", + ) + .takes_value(true) + ) + .arg( + Arg::with_name(VALIDATOR_DIR_FLAG) + .long(VALIDATOR_DIR_FLAG) + .value_name("VALIDATOR_DIRECTORY") + .help( + "The path where the validator directories will be created. \ + Defaults to ~/.lighthouse/validators", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(SECRETS_DIR_FLAG) + .long(SECRETS_DIR_FLAG) + .value_name("SECRETS_DIR") + .help( + "The path where the validator keystore passwords will be stored. \ + Defaults to ~/.lighthouse/secrets", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(STORE_WITHDRAW_FLAG) + .long(STORE_WITHDRAW_FLAG) + .help( + "If present, the withdrawal keystore will be stored alongside the voting \ + keypair. It is generally recommended to *not* store the withdrawal key and \ + instead generate them from the wallet seed when required.", + ), + ) + .arg( + Arg::with_name(STDIN_PASSWORD_FLAG) + .long(STDIN_PASSWORD_FLAG) + .help("If present, read passwords from stdin instead of tty."), + ) +} + +pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { + let validator_dir = clap_utils::parse_path_with_default_in_home_dir( + matches, + VALIDATOR_DIR_FLAG, + PathBuf::new().join(".lighthouse").join("validators"), + )?; + let secrets_dir = clap_utils::parse_path_with_default_in_home_dir( + matches, + SECRETS_DIR_FLAG, + PathBuf::new().join(".lighthouse").join("secrets"), + )?; + let first_index: u32 = clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?; + let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?; + let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; + let stdin_password = matches.is_present(STDIN_PASSWORD_FLAG); + + ensure_dir_exists(&validator_dir)?; + ensure_dir_exists(&secrets_dir)?; + + eprintln!(""); + eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."); + eprintln!(""); + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_password)?; + + let seed = Seed::new(&mnemonic, ""); + + for index in first_index..first_index + count { + let voting_password = random_password(); + let withdrawal_password = random_password(); + + let derive = |key_type: KeyType, password: &[u8]| -> Result { + let (secret, path) = + recover_validator_secret_from_mnemonic(seed.as_bytes(), index, key_type) + .map_err(|e| format!("Unable to recover validator keys: {:?}", e))?; + + let keypair = keypair_from_secret(secret.as_bytes()) + .map_err(|e| format!("Unable build keystore: {:?}", e))?; + + KeystoreBuilder::new(&keypair, password, format!("{}", path)) + .map_err(|e| format!("Unable build keystore: {:?}", e))? + .build() + .map_err(|e| format!("Unable build keystore: {:?}", e)) + }; + + let keystores = ValidatorKeystores { + voting: derive(KeyType::Voting, voting_password.as_bytes())?, + withdrawal: derive(KeyType::Withdrawal, withdrawal_password.as_bytes())?, + }; + + let voting_pubkey = keystores.voting.pubkey().to_string(); + + ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone()) + .voting_keystore(keystores.voting, voting_password.as_bytes()) + .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) + .store_withdrawal_keystore(matches.is_present(STORE_WITHDRAW_FLAG)) + .build() + .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; + + println!( + "{}/{}\tIndex: {}\t0x{}", + index - first_index, + count - first_index, + index, + voting_pubkey + ); + } + + Ok(()) +} diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index f8a5c0776..0bf9905e2 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -5,7 +5,7 @@ use eth2_wallet::{ bip39::{Language, Mnemonic, MnemonicType}, PlainText, }; -use eth2_wallet_manager::{WalletManager, WalletType}; +use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType}; use std::ffi::OsStr; use std::fs::{self, File}; use std::io::prelude::*; @@ -70,46 +70,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> { - let name: String = clap_utils::parse_required(matches, NAME_FLAG)?; - let wallet_password_path: PathBuf = clap_utils::parse_required(matches, PASSWORD_FLAG)?; let mnemonic_output_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; - let type_field: String = clap_utils::parse_required(matches, TYPE_FLAG)?; - - let wallet_type = match type_field.as_ref() { - HD_TYPE => WalletType::Hd, - unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)), - }; - - let mgr = WalletManager::open(&base_dir) - .map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?; // Create a new random mnemonic. // // The `tiny-bip39` crate uses `thread_rng()` for this entropy. let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); - // Create a random password if the file does not exist. - if !wallet_password_path.exists() { - // To prevent users from accidentally supplying their password to the PASSWORD_FLAG and - // create a file with that name, we require that the password has a .pass suffix. - if wallet_password_path.extension() != Some(&OsStr::new("pass")) { - return Err(format!( - "Only creates a password file if that file ends in .pass: {:?}", - wallet_password_path - )); - } - - create_with_600_perms(&wallet_password_path, random_password().as_bytes()) - .map_err(|e| format!("Unable to write to {:?}: {:?}", wallet_password_path, e))?; - } - - let wallet_password = fs::read(&wallet_password_path) - .map_err(|e| format!("Unable to read {:?}: {:?}", wallet_password_path, e)) - .map(|bytes| PlainText::from(strip_off_newlines(bytes)))?; - - let wallet = mgr - .create_wallet(name, wallet_type, &mnemonic, wallet_password.as_bytes()) - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + let wallet = create_wallet_from_mnemonic(matches, &base_dir.as_path(), &mnemonic)?; if let Some(path) = mnemonic_output_path { create_with_600_perms(&path, mnemonic.phrase().as_bytes()) @@ -140,6 +108,48 @@ pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> { Ok(()) } +pub fn create_wallet_from_mnemonic( + matches: &ArgMatches, + base_dir: &Path, + mnemonic: &Mnemonic, +) -> Result { + let name: String = clap_utils::parse_required(matches, NAME_FLAG)?; + let wallet_password_path: PathBuf = clap_utils::parse_required(matches, PASSWORD_FLAG)?; + let type_field: String = clap_utils::parse_required(matches, TYPE_FLAG)?; + + let wallet_type = match type_field.as_ref() { + HD_TYPE => WalletType::Hd, + unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)), + }; + + let mgr = WalletManager::open(&base_dir) + .map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?; + + // Create a random password if the file does not exist. + if !wallet_password_path.exists() { + // To prevent users from accidentally supplying their password to the PASSWORD_FLAG and + // create a file with that name, we require that the password has a .pass suffix. + if wallet_password_path.extension() != Some(&OsStr::new("pass")) { + return Err(format!( + "Only creates a password file if that file ends in .pass: {:?}", + wallet_password_path + )); + } + + create_with_600_perms(&wallet_password_path, random_password().as_bytes()) + .map_err(|e| format!("Unable to write to {:?}: {:?}", wallet_password_path, e))?; + } + + let wallet_password = fs::read(&wallet_password_path) + .map_err(|e| format!("Unable to read {:?}: {:?}", wallet_password_path, e)) + .map(|bytes| PlainText::from(strip_off_newlines(bytes)))?; + + let wallet = mgr + .create_wallet(name, wallet_type, &mnemonic, wallet_password.as_bytes()) + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + Ok(wallet) +} + /// Creates a file with `600 (-rw-------)` permissions. pub fn create_with_600_perms>(path: P, bytes: &[u8]) -> Result<(), String> { let path = path.as_ref(); diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index fbb1207de..e8315b77a 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -1,5 +1,6 @@ pub mod create; pub mod list; +pub mod recover; use crate::{ common::{base_wallet_dir, ensure_dir_exists}, @@ -21,6 +22,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .subcommand(create::cli_app()) .subcommand(list::cli_app()) + .subcommand(recover::cli_app()) } pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { @@ -30,6 +32,7 @@ pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { match matches.subcommand() { (create::CMD, Some(matches)) => create::cli_run(matches, base_dir), (list::CMD, Some(_)) => list::cli_run(base_dir), + (recover::CMD, Some(matches)) => recover::cli_run(matches, base_dir), (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs new file mode 100644 index 000000000..9e96de60d --- /dev/null +++ b/account_manager/src/wallet/recover.rs @@ -0,0 +1,87 @@ +use crate::common::read_mnemonic_from_cli; +use crate::wallet::create::create_wallet_from_mnemonic; +use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; +use clap::{App, Arg, ArgMatches}; +use std::path::PathBuf; + +pub const CMD: &str = "recover"; +pub const MNEMONIC_FLAG: &str = "mnemonic-path"; +pub const STDIN_PASSWORD_FLAG: &str = "stdin-passwords"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about("Recovers an EIP-2386 wallet from a given a BIP-39 mnemonic phrase.") + .arg( + Arg::with_name(NAME_FLAG) + .long(NAME_FLAG) + .value_name("WALLET_NAME") + .help( + "The wallet will be created with this name. It is not allowed to \ + create two wallets with the same name for the same --base-dir.", + ) + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name(PASSWORD_FLAG) + .long(PASSWORD_FLAG) + .value_name("PASSWORD_FILE_PATH") + .help( + "This will be the new password for your recovered wallet. \ + A path to a file containing the password which will unlock the wallet. \ + If the file does not exist, a random password will be generated and \ + saved at that path. To avoid confusion, if the file does not already \ + exist it must include a '.pass' suffix.", + ) + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name(MNEMONIC_FLAG) + .long(MNEMONIC_FLAG) + .value_name("MNEMONIC_PATH") + .help("If present, the mnemonic will be read in from this file.") + .takes_value(true), + ) + .arg( + Arg::with_name(TYPE_FLAG) + .long(TYPE_FLAG) + .value_name("WALLET_TYPE") + .help( + "The type of wallet to create. Only HD (hierarchical-deterministic) \ + wallets are supported presently..", + ) + .takes_value(true) + .possible_values(&[HD_TYPE]) + .default_value(HD_TYPE), + ) + .arg( + Arg::with_name(STDIN_PASSWORD_FLAG) + .long(STDIN_PASSWORD_FLAG) + .help("If present, read passwords from stdin instead of tty."), + ) +} + +pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { + let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; + let stdin_password = matches.is_present(STDIN_PASSWORD_FLAG); + + eprintln!(""); + eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."); + eprintln!(""); + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_password)?; + + let wallet = create_wallet_from_mnemonic(matches, &wallet_base_dir.as_path(), &mnemonic) + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + println!("Your wallet has been successfully recovered."); + println!(); + println!("Your wallet's UUID is:"); + println!(); + println!("\t{}", wallet.wallet().uuid()); + println!(); + println!("You do not need to backup your UUID or keep it secret."); + + Ok(()) +} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 88d82e4d9..18e0ccad2 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -11,6 +11,7 @@ * [Key Management](./key-management.md) * [Create a wallet](./wallet-create.md) * [Create a validator](./validator-create.md) + * [Key recovery](./key-recovery.md) * [Validator Management](./validator-management.md) * [Importing from the Eth2 Launchpad](./validator-import-launchpad.md) * [Local Testnets](./local-testnets.md) diff --git a/book/src/key-recovery.md b/book/src/key-recovery.md new file mode 100644 index 000000000..ae7dc677f --- /dev/null +++ b/book/src/key-recovery.md @@ -0,0 +1,65 @@ +# Key recovery + + +Generally, validator keystore files are generated alongside a *mnemonic*. If +the keystore and/or the keystore password are lost this mnemonic can +regenerate a new, equivalent keystore with a new password. + +There are two ways to recover keys using the `lighthouse` CLI: + +- `lighthouse account validator recover`: recover one or more EIP-2335 keystores from a mnemonic. + These keys can be used directly in a validator client. +- `lighthouse account wallet recover`: recover an EIP-2386 wallet from a + mnemonic. + +## ⚠️ Warning + +**Recovering validator keys from a mnemonic should only be used as a last +resort.** Key recovery entails significant risks: + +- Exposing your mnemonic to a computer at any time puts it at risk of being + compromised. Your mnemonic is **not encrypted** and is a target for theft. +- It's completely possible to regenerate a validator keypairs that is already active + on some other validator client. Running the same keypairs on two different + validator clients is very likely to result in slashing. + +## Recover EIP-2335 validator keystores + +A single mnemonic can generate a practically unlimited number of validator +keystores using an *index*. Generally, the first time you generate a keystore +you'll use index 0, the next time you'll use index 1, and so on. Using the same +index on the same mnemonic always results in the same validator keypair being +generated (see [EIP-2334](https://eips.ethereum.org/EIPS/eip-2334) for more +detail). + + +Using the `lighthouse account validator recover` command you can generate the +keystores that correspond to one or more indices in the mnemonic: + +- `lighthouse account validator recover`: recover only index `0`. +- `lighthouse account validator recover --count 2`: recover indices `0, 1`. +- `lighthouse account validator recover --first-index 1`: recover only index `1`. +- `lighthouse account validator recover --first-index 1 --count 2`: recover indices `1, 2`. + + +For each of the indices recovered in the above commands, a directory will be +created in the `--validator-dir` location (default `~/.lighthouse/validator`) +which contains all the information necessary to run a validator using the +`lighthouse vc` command. The password to this new keystore will be placed in +the `--secrets-dir` (default `~/.lighthouse/secrets`). + +## Recover a EIP-2386 wallet + +Instead of creating EIP-2335 keystores directly, an EIP-2386 wallet can be +generated from the mnemonic. This wallet can then be used to generate validator +keystores, if desired. For example, the following command will create an +encrypted wallet named `wally-recovered` from a mnemonic: + +``` +lighthouse account wallet recover --name wally-recovered +``` + +**⚠️ Warning:** the wallet will be created with a `nextaccount` value of `0`. +This means that if you have already generated `n` validators, then the next `n` +validators generated by this wallet will be duplicates. As mentioned +previously, running duplicate validators is likely to result in slashing. diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 48cf09134..8af026641 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -107,6 +107,23 @@ pub fn read_password_from_user(use_stdin: bool) -> Result result.map(ZeroizeString::from) } +/// Reads a mnemonic phrase from TTY or stdin if `use_stdin == true`. +pub fn read_mnemonic_from_user(use_stdin: bool) -> Result { + let mut input = String::new(); + if use_stdin { + io::stdin() + .read_line(&mut input) + .map_err(|e| format!("Error reading from stdin: {}", e))?; + } else { + let tty = File::open("/dev/tty").map_err(|e| format!("Error opening tty: {}", e))?; + let mut buf_reader = io::BufReader::new(tty); + buf_reader + .read_line(&mut input) + .map_err(|e| format!("Error reading from tty: {}", e))?; + } + Ok(input) +} + /// Provides a new-type wrapper around `String` that is zeroized on `Drop`. /// /// Useful for ensuring that password memory is zeroed-out on drop. diff --git a/crypto/eth2_wallet/src/lib.rs b/crypto/eth2_wallet/src/lib.rs index 94a840a92..492024d26 100644 --- a/crypto/eth2_wallet/src/lib.rs +++ b/crypto/eth2_wallet/src/lib.rs @@ -6,6 +6,6 @@ pub mod json_wallet; pub use bip39; pub use validator_path::{KeyType, ValidatorPath, COIN_TYPE, PURPOSE}; pub use wallet::{ - recover_validator_secret, DerivedKey, Error, KeystoreError, PlainText, Uuid, - ValidatorKeystores, Wallet, WalletBuilder, + recover_validator_secret, recover_validator_secret_from_mnemonic, DerivedKey, Error, + KeystoreError, PlainText, Uuid, ValidatorKeystores, Wallet, WalletBuilder, }; diff --git a/crypto/eth2_wallet/src/wallet.rs b/crypto/eth2_wallet/src/wallet.rs index 925282e7d..47b2d329d 100644 --- a/crypto/eth2_wallet/src/wallet.rs +++ b/crypto/eth2_wallet/src/wallet.rs @@ -285,3 +285,19 @@ pub fn recover_validator_secret( Ok((destination.secret().to_vec().into(), path)) } + +/// Returns `(secret, path)` for the `key_type` for the validator at `index`. +/// +/// This function should only be used for key recovery since it can easily lead to key duplication. +pub fn recover_validator_secret_from_mnemonic( + secret: &[u8], + index: u32, + key_type: KeyType, +) -> Result<(PlainText, ValidatorPath), Error> { + let path = ValidatorPath::new(index, key_type); + let master = DerivedKey::from_seed(secret).map_err(|()| Error::EmptyPassword)?; + + let destination = path.iter_nodes().fold(master, |dk, i| dk.child(*i)); + + Ok((destination.secret().to_vec().into(), path)) +}