[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:
parent
a7878313b8
commit
d67fa6c470
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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};
|
||||||
|
|
Loading…
Reference in New Issue