diff --git a/clap-v3-utils/src/keypair.rs b/clap-v3-utils/src/keypair.rs index d10c1776b7..7013a7fa60 100644 --- a/clap-v3-utils/src/keypair.rs +++ b/clap-v3-utils/src/keypair.rs @@ -29,9 +29,8 @@ use { message::Message, pubkey::Pubkey, signature::{ - generate_seed_from_seed_phrase_and_passphrase, keypair_from_seed, - keypair_from_seed_and_derivation_path, keypair_from_seed_phrase_and_passphrase, - read_keypair, read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer, + generate_seed_from_seed_phrase_and_passphrase, read_keypair, read_keypair_file, + EncodableKey, Keypair, NullSigner, Presigner, Signature, Signer, }, }, std::{ @@ -1000,6 +999,30 @@ pub fn keypair_from_path( keypair_name: &str, confirm_pubkey: bool, ) -> Result> { + let keypair = encodable_key_from_path(matches, path, keypair_name)?; + if confirm_pubkey { + confirm_keypair_pubkey(&keypair); + } + Ok(keypair) +} + +fn confirm_keypair_pubkey(keypair: &Keypair) { + let pubkey = keypair.pubkey(); + print!("Recovered pubkey `{pubkey:?}`. Continue? (y/n): "); + 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); + } +} + +fn encodable_key_from_path( + matches: &ArgMatches, + path: &str, + keypair_name: &str, +) -> Result> { let SignerSource { kind, derivation_path, @@ -1008,15 +1031,14 @@ pub fn keypair_from_path( match kind { SignerSourceKind::Prompt => { let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); - Ok(keypair_from_seed_phrase( + Ok(encodable_key_from_seed_phrase( keypair_name, skip_validation, - confirm_pubkey, derivation_path, legacy, )?) } - SignerSourceKind::Filepath(path) => match read_keypair_file(&path) { + SignerSourceKind::Filepath(path) => match K::read_from_file(&path) { Err(e) => Err(std::io::Error::new( std::io::ErrorKind::Other, format!( @@ -1029,7 +1051,7 @@ pub fn keypair_from_path( }, SignerSourceKind::Stdin => { let mut stdin = std::io::stdin(); - Ok(read_keypair(&mut stdin)?) + Ok(K::read(&mut stdin)?) } _ => Err(std::io::Error::new( std::io::ErrorKind::Other, @@ -1050,19 +1072,33 @@ pub fn keypair_from_seed_phrase( derivation_path: Option, legacy: bool, ) -> Result> { - let seed_phrase = prompt_password(format!("[{keypair_name}] seed phrase: "))?; + let keypair: Keypair = + encodable_key_from_seed_phrase(keypair_name, skip_validation, derivation_path, legacy)?; + if confirm_pubkey { + confirm_keypair_pubkey(&keypair); + } + Ok(keypair) +} + +fn encodable_key_from_seed_phrase( + key_name: &str, + skip_validation: bool, + derivation_path: Option, + legacy: bool, +) -> Result> { + let seed_phrase = prompt_password(format!("[{key_name}] seed phrase: "))?; let seed_phrase = seed_phrase.trim(); let passphrase_prompt = format!( - "[{keypair_name}] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: ", + "[{key_name}] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: ", ); - let keypair = if skip_validation { + let key = if skip_validation { let passphrase = prompt_passphrase(&passphrase_prompt)?; if legacy { - keypair_from_seed_phrase_and_passphrase(seed_phrase, &passphrase)? + K::from_seed_phrase_and_passphrase(seed_phrase, &passphrase)? } else { let seed = generate_seed_from_seed_phrase_and_passphrase(seed_phrase, &passphrase); - keypair_from_seed_and_derivation_path(&seed, derivation_path)? + K::from_seed_and_derivation_path(&seed, derivation_path)? } } else { let sanitized = sanitize_seed_phrase(seed_phrase); @@ -1087,25 +1123,12 @@ pub fn keypair_from_seed_phrase( let passphrase = prompt_passphrase(&passphrase_prompt)?; let seed = Seed::new(&mnemonic, &passphrase); if legacy { - keypair_from_seed(seed.as_bytes())? + K::from_seed(seed.as_bytes())? } else { - keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? + K::from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? } }; - - if confirm_pubkey { - let pubkey = keypair.pubkey(); - print!("Recovered pubkey `{pubkey:?}`. Continue? (y/n): "); - 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) + Ok(key) } fn sanitize_seed_phrase(seed_phrase: &str) -> String { diff --git a/sdk/src/signer/keypair.rs b/sdk/src/signer/keypair.rs index 9d1155f417..e000bd86c2 100644 --- a/sdk/src/signer/keypair.rs +++ b/sdk/src/signer/keypair.rs @@ -5,7 +5,7 @@ use { derivation_path::DerivationPath, pubkey::Pubkey, signature::Signature, - signer::{Signer, SignerError}, + signer::{EncodableKey, Signer, SignerError}, }, ed25519_dalek::Signer as DalekSigner, ed25519_dalek_bip32::Error as Bip32Error, @@ -13,7 +13,6 @@ use { rand::{rngs::OsRng, CryptoRng, RngCore}, std::{ error, - fs::{self, File, OpenOptions}, io::{Read, Write}, path::Path, }, @@ -113,6 +112,34 @@ where } } +impl EncodableKey for Keypair { + fn read(reader: &mut R) -> Result> { + read_keypair(reader) + } + + fn write(&self, writer: &mut W) -> Result> { + write_keypair(self, writer) + } + + fn from_seed(seed: &[u8]) -> Result> { + keypair_from_seed(seed) + } + + fn from_seed_and_derivation_path( + seed: &[u8], + derivation_path: Option, + ) -> Result> { + keypair_from_seed_and_derivation_path(seed, derivation_path) + } + + fn from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, + ) -> Result> { + keypair_from_seed_phrase_and_passphrase(seed_phrase, passphrase) + } +} + /// Reads a JSON-encoded `Keypair` from a `Reader` implementor pub fn read_keypair(reader: &mut R) -> Result> { let bytes: Vec = serde_json::from_reader(reader)?; @@ -123,8 +150,7 @@ pub fn read_keypair(reader: &mut R) -> Result>(path: F) -> Result> { - let mut file = File::open(path.as_ref())?; - read_keypair(&mut file) + Keypair::read_from_file(path) } /// Writes a `Keypair` to a `Write` implementor with JSON-encoding @@ -143,29 +169,7 @@ pub fn write_keypair_file>( keypair: &Keypair, outfile: F, ) -> Result> { - let outfile = outfile.as_ref(); - - if let Some(outdir) = outfile.parent() { - fs::create_dir_all(outdir)?; - } - - let mut f = { - #[cfg(not(unix))] - { - OpenOptions::new() - } - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - OpenOptions::new().mode(0o600) - } - } - .write(true) - .truncate(true) - .create(true) - .open(outfile)?; - - write_keypair(keypair, &mut f) + keypair.write_to_file(outfile) } /// Constructs a `Keypair` from caller-provided seed entropy @@ -238,7 +242,10 @@ mod tests { use { super::*, bip39::{Language, Mnemonic, MnemonicType, Seed}, - std::mem, + std::{ + fs::{self, File}, + mem, + }, }; fn tmp_file_path(name: &str) -> String { diff --git a/sdk/src/signer/mod.rs b/sdk/src/signer/mod.rs index f47b2840dc..84db07c0f7 100644 --- a/sdk/src/signer/mod.rs +++ b/sdk/src/signer/mod.rs @@ -4,11 +4,18 @@ use { crate::{ + derivation_path::DerivationPath, pubkey::Pubkey, signature::{PresignerError, Signature}, transaction::TransactionError, }, itertools::Itertools, + std::{ + error, + fs::{self, File, OpenOptions}, + io::{Read, Write}, + path::Path, + }, thiserror::Error, }; @@ -103,6 +110,51 @@ pub fn unique_signers(signers: Vec<&dyn Signer>) -> Vec<&dyn Signer> { signers.into_iter().unique_by(|s| s.pubkey()).collect() } +/// The `EncodableKey` trait defines the interface by which cryptographic keys/keypairs are read, +/// written, and derived from sources. +pub trait EncodableKey: Sized { + fn read(reader: &mut R) -> Result>; + fn read_from_file>(path: F) -> Result> { + let mut file = File::open(path.as_ref())?; + Self::read(&mut file) + } + fn write(&self, writer: &mut W) -> Result>; + fn write_to_file>(&self, outfile: F) -> Result> { + let outfile = outfile.as_ref(); + + if let Some(outdir) = outfile.parent() { + fs::create_dir_all(outdir)?; + } + + let mut f = { + #[cfg(not(unix))] + { + OpenOptions::new() + } + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + OpenOptions::new().mode(0o600) + } + } + .write(true) + .truncate(true) + .create(true) + .open(outfile)?; + + self.write(&mut f) + } + fn from_seed(seed: &[u8]) -> Result>; + fn from_seed_and_derivation_path( + seed: &[u8], + derivation_path: Option, + ) -> Result>; + fn from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, + ) -> Result>; +} + #[cfg(test)] mod tests { use {super::*, crate::signer::keypair::Keypair};