use crate::{input_parsers::pubkeys_sigs_of, offline::SIGNER_ARG, ArgConstant}; use bip39::{Language, Mnemonic, Seed}; use clap::{ArgMatches, Error, ErrorKind}; use rpassword::prompt_password_stderr; use solana_remote_wallet::{ remote_keypair::generate_remote_keypair, remote_wallet::{RemoteWalletError, RemoteWalletManager}, }; use solana_sdk::{ pubkey::Pubkey, signature::{ keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair, read_keypair_file, Keypair, Presigner, Signature, Signer, }, }; use std::{ error, io::{stdin, stdout, Write}, process::exit, str::FromStr, sync::Arc, }; pub enum KeypairUrl { Ask, Filepath(String), Usb(String), Stdin, Pubkey(Pubkey), } pub fn parse_keypair_path(path: &str) -> KeypairUrl { if path == "-" { KeypairUrl::Stdin } else if path == ASK_KEYWORD { KeypairUrl::Ask } else if path.starts_with("usb://") { KeypairUrl::Usb(path.to_string()) } else if let Ok(pubkey) = Pubkey::from_str(path) { KeypairUrl::Pubkey(pubkey) } else { KeypairUrl::Filepath(path.to_string()) } } pub fn presigner_from_pubkey_sigs( pubkey: &Pubkey, signers: &[(Pubkey, Signature)], ) -> Option { signers.iter().find_map(|(signer, sig)| { if *signer == *pubkey { Some(Presigner::new(signer, sig)) } else { None } }) } pub fn signer_from_path( matches: &ArgMatches, path: &str, keypair_name: &str, wallet_manager: Option<&Arc>, ) -> Result, Box> { match parse_keypair_path(path) { KeypairUrl::Ask => { let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); Ok(Box::new(keypair_from_seed_phrase( keypair_name, skip_validation, false, )?)) } KeypairUrl::Filepath(path) => Ok(Box::new(read_keypair_file(&path)?)), KeypairUrl::Stdin => { let mut stdin = std::io::stdin(); Ok(Box::new(read_keypair(&mut stdin)?)) } KeypairUrl::Usb(path) => { if let Some(wallet_manager) = wallet_manager { Ok(Box::new(generate_remote_keypair( path, wallet_manager, matches.is_present("confirm_key"), keypair_name, )?)) } else { Err(RemoteWalletError::NoDeviceFound.into()) } } KeypairUrl::Pubkey(pubkey) => { let presigner = pubkeys_sigs_of(matches, SIGNER_ARG.name) .as_ref() .and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners)); if let Some(presigner) = presigner { Ok(Box::new(presigner)) } else { Err(Error::with_description( "Missing signature for supplied pubkey", ErrorKind::MissingRequiredArgument, ) .into()) } } } } pub fn pubkey_from_path( matches: &ArgMatches, path: &str, keypair_name: &str, wallet_manager: Option<&Arc>, ) -> Result> { match parse_keypair_path(path) { KeypairUrl::Pubkey(pubkey) => Ok(pubkey), _ => Ok(signer_from_path(matches, path, keypair_name, wallet_manager)?.pubkey()), } } // Keyword used to indicate that the user should be asked for a keypair seed phrase pub const ASK_KEYWORD: &str = "ASK"; pub const SKIP_SEED_PHRASE_VALIDATION_ARG: ArgConstant<'static> = ArgConstant { long: "skip-seed-phrase-validation", name: "skip_seed_phrase_validation", help: "Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list", }; /// Prompts user for a passphrase and then asks for confirmirmation to check for mistakes pub fn prompt_passphrase(prompt: &str) -> Result> { let passphrase = prompt_password_stderr(&prompt)?; if !passphrase.is_empty() { let confirmed = rpassword::prompt_password_stderr("Enter same passphrase again: ")?; if confirmed != passphrase { return Err("Passphrases did not match".into()); } } Ok(passphrase) } /// 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 pub fn keypair_from_seed_phrase( keypair_name: &str, skip_validation: bool, confirm_pubkey: bool, ) -> Result> { let seed_phrase = prompt_password_stderr(&format!("[{}] seed phrase: ", keypair_name))?; let seed_phrase = seed_phrase.trim(); let passphrase_prompt = format!( "[{}] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: ", keypair_name, ); let keypair = if skip_validation { let passphrase = prompt_passphrase(&passphrase_prompt)?; keypair_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase)? } else { let sanitized = sanitize_seed_phrase(seed_phrase); let mnemonic = Mnemonic::from_phrase(&sanitized, Language::English)?; let passphrase = prompt_passphrase(&passphrase_prompt)?; let seed = Seed::new(&mnemonic, &passphrase); keypair_from_seed(seed.as_bytes())? }; if confirm_pubkey { let pubkey = keypair.pubkey(); print!("Recovered pubkey `{:?}`. Continue? (y/n): ", pubkey); let _ignored = stdout().flush(); let mut input = String::new(); stdin().read_line(&mut input).expect("Unexpected input"); if input.to_lowercase().trim() != "y" { println!("Exiting"); exit(1); } } Ok(keypair) } fn sanitize_seed_phrase(seed_phrase: &str) -> String { seed_phrase .split_whitespace() .collect::>() .join(" ") } #[cfg(test)] mod tests { use super::*; #[test] fn test_sanitize_seed_phrase() { let seed_phrase = " Mary had\ta\u{2009}little \n\t lamb"; assert_eq!( "Mary had a little lamb".to_owned(), sanitize_seed_phrase(seed_phrase) ); } }