sdk: Move Keypair
to signer
module
This commit is contained in:
committed by
Trent Nelson
parent
af6f3d776e
commit
0eba6eb401
@ -1,65 +1,18 @@
|
|||||||
//! The `signature` module provides functionality for public, and private keys.
|
//! The `signature` module provides functionality for public, and private keys.
|
||||||
#![cfg(feature = "full")]
|
#![cfg(feature = "full")]
|
||||||
|
|
||||||
use crate::{derivation_path::DerivationPath, pubkey::Pubkey};
|
use crate::pubkey::Pubkey;
|
||||||
use ed25519_dalek::Signer as DalekSigner;
|
|
||||||
use ed25519_dalek_bip32::Error as Bip32Error;
|
|
||||||
use generic_array::{typenum::U64, GenericArray};
|
use generic_array::{typenum::U64, GenericArray};
|
||||||
use hmac::Hmac;
|
|
||||||
use rand::{rngs::OsRng, CryptoRng, RngCore};
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::{Borrow, Cow},
|
borrow::{Borrow, Cow},
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
error, fmt,
|
fmt, mem,
|
||||||
fs::{self, File, OpenOptions},
|
|
||||||
io::{Read, Write},
|
|
||||||
mem,
|
|
||||||
path::Path,
|
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
// legacy module paths
|
// legacy module paths
|
||||||
pub use crate::signer::*;
|
pub use crate::signer::{keypair::*, *};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Keypair(ed25519_dalek::Keypair);
|
|
||||||
|
|
||||||
impl Keypair {
|
|
||||||
pub fn generate<R>(csprng: &mut R) -> Self
|
|
||||||
where
|
|
||||||
R: CryptoRng + RngCore,
|
|
||||||
{
|
|
||||||
Self(ed25519_dalek::Keypair::generate(csprng))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a new ED25519 keypair
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut rng = OsRng::default();
|
|
||||||
Self::generate(&mut rng)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
|
|
||||||
ed25519_dalek::Keypair::from_bytes(bytes).map(Self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> [u8; 64] {
|
|
||||||
self.0.to_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_base58_string(s: &str) -> Self {
|
|
||||||
Self::from_bytes(&bs58::decode(s).into_vec().unwrap()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_base58_string(&self) -> String {
|
|
||||||
// Remove .iter() once we're rust 1.47+
|
|
||||||
bs58::encode(&self.0.to_bytes().iter()).into_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secret(&self) -> &ed25519_dalek::SecretKey {
|
|
||||||
&self.0.secret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Number of bytes in a signature
|
/// Number of bytes in a signature
|
||||||
pub const SIGNATURE_BYTES: usize = 64;
|
pub const SIGNATURE_BYTES: usize = 64;
|
||||||
@ -160,34 +113,6 @@ impl FromStr for Signature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Signer for Keypair {
|
|
||||||
/// Return the public key for the given keypair
|
|
||||||
fn pubkey(&self) -> Pubkey {
|
|
||||||
Pubkey::new(self.0.public.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
|
|
||||||
Ok(self.pubkey())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_message(&self, message: &[u8]) -> Signature {
|
|
||||||
Signature::new(&self.0.sign(message).to_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
|
|
||||||
Ok(self.sign_message(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> PartialEq<T> for Keypair
|
|
||||||
where
|
|
||||||
T: Signer,
|
|
||||||
{
|
|
||||||
fn eq(&self, other: &T) -> bool {
|
|
||||||
self.pubkey() == other.pubkey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Presigner {
|
pub struct Presigner {
|
||||||
pubkey: Pubkey,
|
pubkey: Pubkey,
|
||||||
@ -265,203 +190,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 dalek_keypair = ed25519_dalek::Keypair::from_bytes(&bytes)
|
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
|
||||||
Ok(Keypair(dalek_keypair))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_keypair<W: Write>(
|
|
||||||
keypair: &Keypair,
|
|
||||||
writer: &mut W,
|
|
||||||
) -> Result<String, Box<dyn error::Error>> {
|
|
||||||
let keypair_bytes = keypair.0.to_bytes();
|
|
||||||
let serialized = serde_json::to_string(&keypair_bytes.to_vec())?;
|
|
||||||
writer.write_all(&serialized.clone().into_bytes())?;
|
|
||||||
Ok(serialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>> {
|
|
||||||
if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH {
|
|
||||||
return Err("Seed is too short".into());
|
|
||||||
}
|
|
||||||
let secret = ed25519_dalek::SecretKey::from_bytes(&seed[..ed25519_dalek::SECRET_KEY_LENGTH])
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let public = ed25519_dalek::PublicKey::from(&secret);
|
|
||||||
let dalek_keypair = ed25519_dalek::Keypair { secret, public };
|
|
||||||
Ok(Keypair(dalek_keypair))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided;
|
|
||||||
/// otherwise generates the base Bip44 Solana keypair from the seed
|
|
||||||
pub fn keypair_from_seed_and_derivation_path(
|
|
||||||
seed: &[u8],
|
|
||||||
derivation_path: Option<DerivationPath>,
|
|
||||||
) -> Result<Keypair, Box<dyn error::Error>> {
|
|
||||||
let derivation_path = derivation_path.unwrap_or_else(DerivationPath::default);
|
|
||||||
bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a Keypair using Bip32 Hierarchical Derivation
|
|
||||||
fn bip32_derived_keypair(
|
|
||||||
seed: &[u8],
|
|
||||||
derivation_path: DerivationPath,
|
|
||||||
) -> Result<Keypair, Bip32Error> {
|
|
||||||
let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed)
|
|
||||||
.and_then(|extended| extended.derive(&derivation_path))?;
|
|
||||||
let extended_public_key = extended.public_key();
|
|
||||||
Ok(Keypair(ed25519_dalek::Keypair {
|
|
||||||
secret: extended.secret_key,
|
|
||||||
public: extended_public_key,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_seed_from_seed_phrase_and_passphrase(
|
|
||||||
seed_phrase: &str,
|
|
||||||
passphrase: &str,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
const PBKDF2_ROUNDS: u32 = 2048;
|
|
||||||
const PBKDF2_BYTES: usize = 64;
|
|
||||||
|
|
||||||
let salt = format!("mnemonic{}", passphrase);
|
|
||||||
|
|
||||||
let mut seed = vec![0u8; PBKDF2_BYTES];
|
|
||||||
pbkdf2::pbkdf2::<Hmac<sha2::Sha512>>(
|
|
||||||
seed_phrase.as_bytes(),
|
|
||||||
salt.as_bytes(),
|
|
||||||
PBKDF2_ROUNDS,
|
|
||||||
&mut seed,
|
|
||||||
);
|
|
||||||
seed
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keypair_from_seed_phrase_and_passphrase(
|
|
||||||
seed_phrase: &str,
|
|
||||||
passphrase: &str,
|
|
||||||
) -> Result<Keypair, Box<dyn error::Error>> {
|
|
||||||
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
|
|
||||||
seed_phrase,
|
|
||||||
passphrase,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use bip39::{Language, Mnemonic, MnemonicType, Seed};
|
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
fn tmp_file_path(name: &str) -> String {
|
|
||||||
use std::env;
|
|
||||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
|
||||||
let keypair = Keypair::new();
|
|
||||||
|
|
||||||
format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_keypair_file() {
|
|
||||||
let outfile = tmp_file_path("test_write_keypair_file.json");
|
|
||||||
let serialized_keypair = write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
|
||||||
let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
|
|
||||||
assert!(Path::new(&outfile).exists());
|
|
||||||
assert_eq!(
|
|
||||||
keypair_vec,
|
|
||||||
read_keypair_file(&outfile).unwrap().0.to_bytes().to_vec()
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
assert_eq!(
|
|
||||||
File::open(&outfile)
|
|
||||||
.expect("open")
|
|
||||||
.metadata()
|
|
||||||
.expect("metadata")
|
|
||||||
.permissions()
|
|
||||||
.mode()
|
|
||||||
& 0o777,
|
|
||||||
0o600
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
read_keypair_file(&outfile).unwrap().pubkey().as_ref().len(),
|
|
||||||
mem::size_of::<Pubkey>()
|
|
||||||
);
|
|
||||||
fs::remove_file(&outfile).unwrap();
|
|
||||||
assert!(!Path::new(&outfile).exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_keypair_file_overwrite_ok() {
|
|
||||||
let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json");
|
|
||||||
|
|
||||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
|
||||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_keypair_file_truncate() {
|
|
||||||
let outfile = tmp_file_path("test_write_keypair_file_truncate.json");
|
|
||||||
|
|
||||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
|
||||||
read_keypair_file(&outfile).unwrap();
|
|
||||||
|
|
||||||
// Ensure outfile is truncated
|
|
||||||
{
|
|
||||||
let mut f = File::create(&outfile).unwrap();
|
|
||||||
f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes())
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
|
||||||
read_keypair_file(&outfile).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keypair_from_seed() {
|
|
||||||
let good_seed = vec![0; 32];
|
|
||||||
assert!(keypair_from_seed(&good_seed).is_ok());
|
|
||||||
|
|
||||||
let too_short_seed = vec![0; 31];
|
|
||||||
assert!(keypair_from_seed(&too_short_seed).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_signature_fromstr() {
|
fn test_signature_fromstr() {
|
||||||
let signature = Keypair::new().sign_message(&[0u8]);
|
let signature = Keypair::new().sign_message(&[0u8]);
|
||||||
@ -506,35 +237,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keypair_from_seed_phrase_and_passphrase() {
|
|
||||||
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
|
|
||||||
let passphrase = "42";
|
|
||||||
let seed = Seed::new(&mnemonic, passphrase);
|
|
||||||
let expected_keypair = keypair_from_seed(seed.as_bytes()).unwrap();
|
|
||||||
let keypair =
|
|
||||||
keypair_from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap();
|
|
||||||
assert_eq!(keypair.pubkey(), expected_keypair.pubkey());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keypair() {
|
|
||||||
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
|
||||||
let pubkey = keypair.pubkey();
|
|
||||||
let data = [1u8];
|
|
||||||
let sig = keypair.sign_message(&data);
|
|
||||||
|
|
||||||
// Signer
|
|
||||||
assert_eq!(keypair.try_pubkey().unwrap(), pubkey);
|
|
||||||
assert_eq!(keypair.pubkey(), pubkey);
|
|
||||||
assert_eq!(keypair.try_sign_message(&data).unwrap(), sig);
|
|
||||||
assert_eq!(keypair.sign_message(&data), sig);
|
|
||||||
|
|
||||||
// PartialEq
|
|
||||||
let keypair2 = keypair_from_seed(&[0u8; 32]).unwrap();
|
|
||||||
assert_eq!(keypair, keypair2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_presigner() {
|
fn test_presigner() {
|
||||||
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||||
|
327
sdk/src/signer/keypair.rs
Normal file
327
sdk/src/signer/keypair.rs
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
#![cfg(feature = "full")]
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
derivation_path::DerivationPath,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
signature::Signature,
|
||||||
|
signer::{Signer, SignerError},
|
||||||
|
},
|
||||||
|
ed25519_dalek::Signer as DalekSigner,
|
||||||
|
ed25519_dalek_bip32::Error as Bip32Error,
|
||||||
|
hmac::Hmac,
|
||||||
|
rand::{rngs::OsRng, CryptoRng, RngCore},
|
||||||
|
std::{
|
||||||
|
error,
|
||||||
|
fs::{self, File, OpenOptions},
|
||||||
|
io::{Read, Write},
|
||||||
|
path::Path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A vanilla Ed25519 key pair
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Keypair(ed25519_dalek::Keypair);
|
||||||
|
|
||||||
|
impl Keypair {
|
||||||
|
/// Constructs a new, random `Keypair` using a caller-proveded RNG
|
||||||
|
pub fn generate<R>(csprng: &mut R) -> Self
|
||||||
|
where
|
||||||
|
R: CryptoRng + RngCore,
|
||||||
|
{
|
||||||
|
Self(ed25519_dalek::Keypair::generate(csprng))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new, random `Keypair` using `OsRng`
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut rng = OsRng::default();
|
||||||
|
Self::generate(&mut rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recovers a `Keypair` from a byte array
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
|
||||||
|
ed25519_dalek::Keypair::from_bytes(bytes).map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this `Keypair` as a byte array
|
||||||
|
pub fn to_bytes(&self) -> [u8; 64] {
|
||||||
|
self.0.to_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recovers a `Keypair` from a base58-encoded string
|
||||||
|
pub fn from_base58_string(s: &str) -> Self {
|
||||||
|
Self::from_bytes(&bs58::decode(s).into_vec().unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this `Keypair` as a base58-encoded string
|
||||||
|
pub fn to_base58_string(&self) -> String {
|
||||||
|
// Remove .iter() once we're rust 1.47+
|
||||||
|
bs58::encode(&self.0.to_bytes().iter()).into_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets this `Keypair`'s SecretKey
|
||||||
|
pub fn secret(&self) -> &ed25519_dalek::SecretKey {
|
||||||
|
&self.0.secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Signer for Keypair {
|
||||||
|
fn pubkey(&self) -> Pubkey {
|
||||||
|
Pubkey::new(self.0.public.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
|
||||||
|
Ok(self.pubkey())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_message(&self, message: &[u8]) -> Signature {
|
||||||
|
Signature::new(&self.0.sign(message).to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
|
||||||
|
Ok(self.sign_message(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq<T> for Keypair
|
||||||
|
where
|
||||||
|
T: Signer,
|
||||||
|
{
|
||||||
|
fn eq(&self, other: &T) -> bool {
|
||||||
|
self.pubkey() == other.pubkey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)?;
|
||||||
|
let dalek_keypair = ed25519_dalek::Keypair::from_bytes(&bytes)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||||
|
Ok(Keypair(dalek_keypair))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a `Keypair` to a `Write` implementor with JSON-encoding
|
||||||
|
pub fn write_keypair<W: Write>(
|
||||||
|
keypair: &Keypair,
|
||||||
|
writer: &mut W,
|
||||||
|
) -> Result<String, Box<dyn error::Error>> {
|
||||||
|
let keypair_bytes = keypair.0.to_bytes();
|
||||||
|
let serialized = serde_json::to_string(&keypair_bytes.to_vec())?;
|
||||||
|
writer.write_all(&serialized.clone().into_bytes())?;
|
||||||
|
Ok(serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a `Keypair` to a file with JSON-encoding
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a `Keypair` from caller-provided seed entropy
|
||||||
|
pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
|
if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH {
|
||||||
|
return Err("Seed is too short".into());
|
||||||
|
}
|
||||||
|
let secret = ed25519_dalek::SecretKey::from_bytes(&seed[..ed25519_dalek::SECRET_KEY_LENGTH])
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let public = ed25519_dalek::PublicKey::from(&secret);
|
||||||
|
let dalek_keypair = ed25519_dalek::Keypair { secret, public };
|
||||||
|
Ok(Keypair(dalek_keypair))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided;
|
||||||
|
/// otherwise generates the base Bip44 Solana keypair from the seed
|
||||||
|
pub fn keypair_from_seed_and_derivation_path(
|
||||||
|
seed: &[u8],
|
||||||
|
derivation_path: Option<DerivationPath>,
|
||||||
|
) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
|
let derivation_path = derivation_path.unwrap_or_else(DerivationPath::default);
|
||||||
|
bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a Keypair using Bip32 Hierarchical Derivation
|
||||||
|
fn bip32_derived_keypair(
|
||||||
|
seed: &[u8],
|
||||||
|
derivation_path: DerivationPath,
|
||||||
|
) -> Result<Keypair, Bip32Error> {
|
||||||
|
let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed)
|
||||||
|
.and_then(|extended| extended.derive(&derivation_path))?;
|
||||||
|
let extended_public_key = extended.public_key();
|
||||||
|
Ok(Keypair(ed25519_dalek::Keypair {
|
||||||
|
secret: extended.secret_key,
|
||||||
|
public: extended_public_key,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_seed_from_seed_phrase_and_passphrase(
|
||||||
|
seed_phrase: &str,
|
||||||
|
passphrase: &str,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
const PBKDF2_ROUNDS: u32 = 2048;
|
||||||
|
const PBKDF2_BYTES: usize = 64;
|
||||||
|
|
||||||
|
let salt = format!("mnemonic{}", passphrase);
|
||||||
|
|
||||||
|
let mut seed = vec![0u8; PBKDF2_BYTES];
|
||||||
|
pbkdf2::pbkdf2::<Hmac<sha2::Sha512>>(
|
||||||
|
seed_phrase.as_bytes(),
|
||||||
|
salt.as_bytes(),
|
||||||
|
PBKDF2_ROUNDS,
|
||||||
|
&mut seed,
|
||||||
|
);
|
||||||
|
seed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keypair_from_seed_phrase_and_passphrase(
|
||||||
|
seed_phrase: &str,
|
||||||
|
passphrase: &str,
|
||||||
|
) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
|
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
|
||||||
|
seed_phrase,
|
||||||
|
passphrase,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {
|
||||||
|
super::*,
|
||||||
|
bip39::{Language, Mnemonic, MnemonicType, Seed},
|
||||||
|
std::mem,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn tmp_file_path(name: &str) -> String {
|
||||||
|
use std::env;
|
||||||
|
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
||||||
|
let keypair = Keypair::new();
|
||||||
|
|
||||||
|
format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_write_keypair_file() {
|
||||||
|
let outfile = tmp_file_path("test_write_keypair_file.json");
|
||||||
|
let serialized_keypair = write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||||
|
let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
|
||||||
|
assert!(Path::new(&outfile).exists());
|
||||||
|
assert_eq!(
|
||||||
|
keypair_vec,
|
||||||
|
read_keypair_file(&outfile).unwrap().0.to_bytes().to_vec()
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
assert_eq!(
|
||||||
|
File::open(&outfile)
|
||||||
|
.expect("open")
|
||||||
|
.metadata()
|
||||||
|
.expect("metadata")
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o777,
|
||||||
|
0o600
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
read_keypair_file(&outfile).unwrap().pubkey().as_ref().len(),
|
||||||
|
mem::size_of::<Pubkey>()
|
||||||
|
);
|
||||||
|
fs::remove_file(&outfile).unwrap();
|
||||||
|
assert!(!Path::new(&outfile).exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_write_keypair_file_overwrite_ok() {
|
||||||
|
let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json");
|
||||||
|
|
||||||
|
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||||
|
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_write_keypair_file_truncate() {
|
||||||
|
let outfile = tmp_file_path("test_write_keypair_file_truncate.json");
|
||||||
|
|
||||||
|
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||||
|
read_keypair_file(&outfile).unwrap();
|
||||||
|
|
||||||
|
// Ensure outfile is truncated
|
||||||
|
{
|
||||||
|
let mut f = File::create(&outfile).unwrap();
|
||||||
|
f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||||
|
read_keypair_file(&outfile).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keypair_from_seed() {
|
||||||
|
let good_seed = vec![0; 32];
|
||||||
|
assert!(keypair_from_seed(&good_seed).is_ok());
|
||||||
|
|
||||||
|
let too_short_seed = vec![0; 31];
|
||||||
|
assert!(keypair_from_seed(&too_short_seed).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keypair_from_seed_phrase_and_passphrase() {
|
||||||
|
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
|
||||||
|
let passphrase = "42";
|
||||||
|
let seed = Seed::new(&mnemonic, passphrase);
|
||||||
|
let expected_keypair = keypair_from_seed(seed.as_bytes()).unwrap();
|
||||||
|
let keypair =
|
||||||
|
keypair_from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap();
|
||||||
|
assert_eq!(keypair.pubkey(), expected_keypair.pubkey());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keypair() {
|
||||||
|
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||||
|
let pubkey = keypair.pubkey();
|
||||||
|
let data = [1u8];
|
||||||
|
let sig = keypair.sign_message(&data);
|
||||||
|
|
||||||
|
// Signer
|
||||||
|
assert_eq!(keypair.try_pubkey().unwrap(), pubkey);
|
||||||
|
assert_eq!(keypair.pubkey(), pubkey);
|
||||||
|
assert_eq!(keypair.try_sign_message(&data).unwrap(), sig);
|
||||||
|
assert_eq!(keypair.sign_message(&data), sig);
|
||||||
|
|
||||||
|
// PartialEq
|
||||||
|
let keypair2 = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||||
|
assert_eq!(keypair, keypair2);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user