[clap-v3-utils] Define `EncodableKey` and make `keypair_from_path` and `keypair_from_seed` generic functions (#30947)

* generalize `Keypair` to using `EncodableKey` trait in clap-v3-utils

* add associated `Pubkey` type to `EncodableKey`

* remove associated type `Pubkey` from `EncodableKey`

* rename `EncodableKey` associated function names

* remove default overrides for `{read,write}_file`

* resolve dependencies for test

* remove `pubkey_string` from `EncodableKey` trait
This commit is contained in:
samkim-crypto 2023-04-06 07:42:11 +09:00 committed by GitHub
parent a7878313b8
commit d67fa6c470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 138 additions and 56 deletions

View File

@ -29,9 +29,8 @@ use {
message::Message, message::Message,
pubkey::Pubkey, pubkey::Pubkey,
signature::{ signature::{
generate_seed_from_seed_phrase_and_passphrase, keypair_from_seed, generate_seed_from_seed_phrase_and_passphrase, read_keypair, read_keypair_file,
keypair_from_seed_and_derivation_path, keypair_from_seed_phrase_and_passphrase, EncodableKey, Keypair, NullSigner, Presigner, Signature, Signer,
read_keypair, read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer,
}, },
}, },
std::{ std::{
@ -1000,6 +999,30 @@ pub fn keypair_from_path(
keypair_name: &str, keypair_name: &str,
confirm_pubkey: bool, confirm_pubkey: bool,
) -> Result<Keypair, Box<dyn error::Error>> { ) -> Result<Keypair, Box<dyn error::Error>> {
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<K: EncodableKey>(
matches: &ArgMatches,
path: &str,
keypair_name: &str,
) -> Result<K, Box<dyn error::Error>> {
let SignerSource { let SignerSource {
kind, kind,
derivation_path, derivation_path,
@ -1008,15 +1031,14 @@ pub fn keypair_from_path(
match kind { match kind {
SignerSourceKind::Prompt => { SignerSourceKind::Prompt => {
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); 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, keypair_name,
skip_validation, skip_validation,
confirm_pubkey,
derivation_path, derivation_path,
legacy, 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( Err(e) => Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!( format!(
@ -1029,7 +1051,7 @@ pub fn keypair_from_path(
}, },
SignerSourceKind::Stdin => { SignerSourceKind::Stdin => {
let mut stdin = std::io::stdin(); let mut stdin = std::io::stdin();
Ok(read_keypair(&mut stdin)?) Ok(K::read(&mut stdin)?)
} }
_ => Err(std::io::Error::new( _ => Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
@ -1050,19 +1072,33 @@ pub fn keypair_from_seed_phrase(
derivation_path: Option<DerivationPath>, derivation_path: Option<DerivationPath>,
legacy: bool, legacy: bool,
) -> Result<Keypair, Box<dyn error::Error>> { ) -> Result<Keypair, Box<dyn error::Error>> {
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<K: EncodableKey>(
key_name: &str,
skip_validation: bool,
derivation_path: Option<DerivationPath>,
legacy: bool,
) -> Result<K, Box<dyn error::Error>> {
let seed_phrase = prompt_password(format!("[{key_name}] seed phrase: "))?;
let seed_phrase = seed_phrase.trim(); let seed_phrase = seed_phrase.trim();
let passphrase_prompt = format!( 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)?; let passphrase = prompt_passphrase(&passphrase_prompt)?;
if legacy { if legacy {
keypair_from_seed_phrase_and_passphrase(seed_phrase, &passphrase)? K::from_seed_phrase_and_passphrase(seed_phrase, &passphrase)?
} else { } else {
let seed = generate_seed_from_seed_phrase_and_passphrase(seed_phrase, &passphrase); 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 { } else {
let sanitized = sanitize_seed_phrase(seed_phrase); let sanitized = sanitize_seed_phrase(seed_phrase);
@ -1087,25 +1123,12 @@ pub fn keypair_from_seed_phrase(
let passphrase = prompt_passphrase(&passphrase_prompt)?; let passphrase = prompt_passphrase(&passphrase_prompt)?;
let seed = Seed::new(&mnemonic, &passphrase); let seed = Seed::new(&mnemonic, &passphrase);
if legacy { if legacy {
keypair_from_seed(seed.as_bytes())? K::from_seed(seed.as_bytes())?
} else { } else {
keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? K::from_seed_and_derivation_path(seed.as_bytes(), derivation_path)?
} }
}; };
Ok(key)
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)
} }
fn sanitize_seed_phrase(seed_phrase: &str) -> String { fn sanitize_seed_phrase(seed_phrase: &str) -> String {

View File

@ -5,7 +5,7 @@ use {
derivation_path::DerivationPath, derivation_path::DerivationPath,
pubkey::Pubkey, pubkey::Pubkey,
signature::Signature, signature::Signature,
signer::{Signer, SignerError}, signer::{EncodableKey, Signer, SignerError},
}, },
ed25519_dalek::Signer as DalekSigner, ed25519_dalek::Signer as DalekSigner,
ed25519_dalek_bip32::Error as Bip32Error, ed25519_dalek_bip32::Error as Bip32Error,
@ -13,7 +13,6 @@ use {
rand::{rngs::OsRng, CryptoRng, RngCore}, rand::{rngs::OsRng, CryptoRng, RngCore},
std::{ std::{
error, error,
fs::{self, File, OpenOptions},
io::{Read, Write}, io::{Read, Write},
path::Path, path::Path,
}, },
@ -113,6 +112,34 @@ where
} }
} }
impl EncodableKey for Keypair {
fn read<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
read_keypair(reader)
}
fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
write_keypair(self, writer)
}
fn from_seed(seed: &[u8]) -> Result<Self, Box<dyn error::Error>> {
keypair_from_seed(seed)
}
fn from_seed_and_derivation_path(
seed: &[u8],
derivation_path: Option<DerivationPath>,
) -> Result<Self, Box<dyn error::Error>> {
keypair_from_seed_and_derivation_path(seed, derivation_path)
}
fn from_seed_phrase_and_passphrase(
seed_phrase: &str,
passphrase: &str,
) -> Result<Self, Box<dyn error::Error>> {
keypair_from_seed_phrase_and_passphrase(seed_phrase, passphrase)
}
}
/// Reads a JSON-encoded `Keypair` from a `Reader` implementor /// Reads a JSON-encoded `Keypair` from a `Reader` implementor
pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> { pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> {
let bytes: Vec<u8> = serde_json::from_reader(reader)?; let bytes: Vec<u8> = serde_json::from_reader(reader)?;
@ -123,8 +150,7 @@ pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::E
/// Reads a `Keypair` from a file /// Reads a `Keypair` from a file
pub fn read_keypair_file<F: AsRef<Path>>(path: F) -> Result<Keypair, Box<dyn error::Error>> { pub fn read_keypair_file<F: AsRef<Path>>(path: F) -> Result<Keypair, Box<dyn error::Error>> {
let mut file = File::open(path.as_ref())?; Keypair::read_from_file(path)
read_keypair(&mut file)
} }
/// Writes a `Keypair` to a `Write` implementor with JSON-encoding /// Writes a `Keypair` to a `Write` implementor with JSON-encoding
@ -143,29 +169,7 @@ pub fn write_keypair_file<F: AsRef<Path>>(
keypair: &Keypair, keypair: &Keypair,
outfile: F, outfile: F,
) -> Result<String, Box<dyn error::Error>> { ) -> Result<String, Box<dyn error::Error>> {
let outfile = outfile.as_ref(); keypair.write_to_file(outfile)
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)
} }
/// Constructs a `Keypair` from caller-provided seed entropy /// Constructs a `Keypair` from caller-provided seed entropy
@ -238,7 +242,10 @@ mod tests {
use { use {
super::*, super::*,
bip39::{Language, Mnemonic, MnemonicType, Seed}, bip39::{Language, Mnemonic, MnemonicType, Seed},
std::mem, std::{
fs::{self, File},
mem,
},
}; };
fn tmp_file_path(name: &str) -> String { fn tmp_file_path(name: &str) -> String {

View File

@ -4,11 +4,18 @@
use { use {
crate::{ crate::{
derivation_path::DerivationPath,
pubkey::Pubkey, pubkey::Pubkey,
signature::{PresignerError, Signature}, signature::{PresignerError, Signature},
transaction::TransactionError, transaction::TransactionError,
}, },
itertools::Itertools, itertools::Itertools,
std::{
error,
fs::{self, File, OpenOptions},
io::{Read, Write},
path::Path,
},
thiserror::Error, 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() 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<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>>;
fn read_from_file<F: AsRef<Path>>(path: F) -> Result<Self, Box<dyn error::Error>> {
let mut file = File::open(path.as_ref())?;
Self::read(&mut file)
}
fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>>;
fn write_to_file<F: AsRef<Path>>(&self, outfile: F) -> Result<String, Box<dyn error::Error>> {
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<Self, Box<dyn error::Error>>;
fn from_seed_and_derivation_path(
seed: &[u8],
derivation_path: Option<DerivationPath>,
) -> Result<Self, Box<dyn error::Error>>;
fn from_seed_phrase_and_passphrase(
seed_phrase: &str,
passphrase: &str,
) -> Result<Self, Box<dyn error::Error>>;
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use {super::*, crate::signer::keypair::Keypair}; use {super::*, crate::signer::keypair::Keypair};