[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,
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<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 {
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<DerivationPath>,
legacy: bool,
) -> 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 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 {

View File

@ -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<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
pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> {
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
pub fn read_keypair_file<F: AsRef<Path>>(path: F) -> Result<Keypair, Box<dyn error::Error>> {
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<F: AsRef<Path>>(
keypair: &Keypair,
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)?;
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 {

View File

@ -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<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)]
mod tests {
use {super::*, crate::signer::keypair::Keypair};