[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,
|
||||
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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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};
|
||||
|
|
Loading…
Reference in New Issue