From b437b0a49d029176e130fa0a531a4fd055bd658c Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Wed, 12 May 2021 13:33:11 -0600 Subject: [PATCH] Add bip32 support to solana-keygen recover (#17180) * Fix spelling * Add validator for SignerSources * Add helper to generate Keypair from supporting SignerSources * Add bip32 support to solana-keygen recover * Make SignerSourceKind const strs, use for Debug impl and URI schemes --- clap-utils/src/input_validators.rs | 20 +++++++ clap-utils/src/keypair.rs | 83 ++++++++++++++++++++++++++++-- keygen/src/keygen.rs | 21 ++++++-- sdk/src/signer/mod.rs | 2 +- 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index 05c710a528..33f2eeb51d 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -96,6 +96,26 @@ where .map_err(|err| format!("{}", err)) } +// Return an error if a `SignerSourceKind::Prompt` cannot be parsed +pub fn is_prompt_signer_source(string: T) -> Result<(), String> +where + T: AsRef + Display, +{ + if string.as_ref() == ASK_KEYWORD { + return Ok(()); + } + match parse_signer_source(string.as_ref()) + .map_err(|err| format!("{}", err))? + .kind + { + SignerSourceKind::Prompt => Ok(()), + _ => Err(format!( + "Unable to parse input as `prompt:` URI scheme or `ASK` keyword: {}", + string + )), + } +} + // Return an error if string cannot be parsed as pubkey string or keypair file location pub fn is_pubkey_or_keypair(string: T) -> Result<(), String> where diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index c767468543..bace212413 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -162,6 +162,12 @@ impl SignerSource { } } +const SIGNER_SOURCE_PROMPT: &str = "prompt"; +const SIGNER_SOURCE_FILEPATH: &str = "file"; +const SIGNER_SOURCE_USB: &str = "usb"; +const SIGNER_SOURCE_STDIN: &str = "stdin"; +const SIGNER_SOURCE_PUBKEY: &str = "pubkey"; + pub(crate) enum SignerSourceKind { Prompt, Filepath(String), @@ -170,6 +176,25 @@ pub(crate) enum SignerSourceKind { Pubkey(Pubkey), } +impl AsRef for SignerSourceKind { + fn as_ref(&self) -> &str { + match self { + Self::Prompt => SIGNER_SOURCE_PROMPT, + Self::Filepath(_) => SIGNER_SOURCE_FILEPATH, + Self::Usb(_) => SIGNER_SOURCE_USB, + Self::Stdin => SIGNER_SOURCE_STDIN, + Self::Pubkey(_) => SIGNER_SOURCE_PUBKEY, + } + } +} + +impl std::fmt::Debug for SignerSourceKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s: &str = self.as_ref(); + write!(f, "{}", s) + } +} + #[derive(Debug, Error)] pub(crate) enum SignerSourceError { #[error("unrecognized signer source")] @@ -192,20 +217,20 @@ pub(crate) fn parse_signer_source>( if let Some(scheme) = uri.scheme() { let scheme = scheme.as_str().to_ascii_lowercase(); match scheme.as_str() { - "prompt" => Ok(SignerSource { + SIGNER_SOURCE_PROMPT => Ok(SignerSource { kind: SignerSourceKind::Prompt, derivation_path: DerivationPath::from_uri_any_query(&uri)?, legacy: false, }), - "file" => Ok(SignerSource::new(SignerSourceKind::Filepath( + SIGNER_SOURCE_FILEPATH => Ok(SignerSource::new(SignerSourceKind::Filepath( uri.path().to_string(), ))), - "stdin" => Ok(SignerSource::new(SignerSourceKind::Stdin)), - "usb" => Ok(SignerSource { + SIGNER_SOURCE_USB => Ok(SignerSource { kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?), derivation_path: DerivationPath::from_uri_key_query(&uri)?, legacy: false, }), + SIGNER_SOURCE_STDIN => Ok(SignerSource::new(SignerSourceKind::Stdin)), _ => Err(SignerSourceError::UnrecognizedSource), } } else { @@ -431,6 +456,56 @@ pub fn prompt_passphrase(prompt: &str) -> Result> Ok(passphrase) } +/// Parses a path into a SignerSource and returns a Keypair for supporting SignerSourceKinds +pub fn keypair_from_path( + matches: &ArgMatches, + path: &str, + keypair_name: &str, + confirm_pubkey: bool, +) -> Result> { + let SignerSource { + kind, + derivation_path, + legacy, + } = parse_signer_source(path)?; + match kind { + SignerSourceKind::Prompt => { + let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); + Ok(keypair_from_seed_phrase( + keypair_name, + skip_validation, + confirm_pubkey, + derivation_path, + legacy, + )?) + } + SignerSourceKind::Filepath(path) => match read_keypair_file(&path) { + Err(e) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "could not read keypair file \"{}\". \ + Run \"solana-keygen new\" to create a keypair file: {}", + path, e + ), + ) + .into()), + Ok(file) => Ok(file), + }, + SignerSourceKind::Stdin => { + let mut stdin = std::io::stdin(); + Ok(read_keypair(&mut stdin)?) + } + _ => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "signer of type `{:?}` does not support Keypair output", + kind + ), + ) + .into()), + } +} + /// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation /// Optionally skips validation of seed phrase /// Optionally confirms recovered public key diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index 0b7b603c05..2ad9fc1a20 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -5,9 +5,9 @@ use clap::{ Arg, ArgMatches, SubCommand, }; use solana_clap_utils::{ - input_validators::is_parsable, + input_validators::{is_parsable, is_prompt_signer_source}, keypair::{ - keypair_from_seed_phrase, prompt_passphrase, signer_from_path, + keypair_from_path, keypair_from_seed_phrase, prompt_passphrase, signer_from_path, SKIP_SEED_PHRASE_VALIDATION_ARG, }, ArgConstant, DisplayError, @@ -482,6 +482,14 @@ fn main() -> Result<(), Box> { SubCommand::with_name("recover") .about("Recover keypair from seed phrase and optional BIP39 passphrase") .setting(AppSettings::DisableVersion) + .arg( + Arg::with_name("prompt_signer") + .index(1) + .value_name("KEYPAIR") + .takes_value(true) + .validator(is_prompt_signer_source) + .help("`prompt:` URI scheme or `ASK` keyword"), + ) .arg( Arg::with_name("outfile") .short("o") @@ -588,8 +596,13 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { check_for_overwrite(&outfile, &matches); } - let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); - let keypair = keypair_from_seed_phrase("recover", skip_validation, true, None, true)?; + let keypair_name = "recover"; + let keypair = if let Some(path) = matches.value_of("prompt_signer") { + keypair_from_path(matches, path, keypair_name, true)? + } else { + let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); + keypair_from_seed_phrase(keypair_name, skip_validation, true, None, true)? + }; output_keypair(&keypair, &outfile, "recovered")?; } ("grind", Some(matches)) => { diff --git a/sdk/src/signer/mod.rs b/sdk/src/signer/mod.rs index 5a8e0e27cc..bf7dcc52e0 100644 --- a/sdk/src/signer/mod.rs +++ b/sdk/src/signer/mod.rs @@ -61,7 +61,7 @@ pub trait Signer { } /// Fallibly gets the implementor's public key fn try_pubkey(&self) -> Result; - /// Invallibly produces an Ed25519 signature over the provided `message` + /// Infallibly produces an Ed25519 signature over the provided `message` /// bytes. Returns the all-zeros `Signature` if signing is not possible. fn sign_message(&self, message: &[u8]) -> Signature { self.try_sign_message(message).unwrap_or_default()