diff --git a/sdk/src/signature.rs b/sdk/src/signature.rs index d91a8ba2b6..0548d7c2e4 100644 --- a/sdk/src/signature.rs +++ b/sdk/src/signature.rs @@ -1,65 +1,18 @@ //! The `signature` module provides functionality for public, and private keys. #![cfg(feature = "full")] -use crate::{derivation_path::DerivationPath, pubkey::Pubkey}; -use ed25519_dalek::Signer as DalekSigner; -use ed25519_dalek_bip32::Error as Bip32Error; +use crate::pubkey::Pubkey; use generic_array::{typenum::U64, GenericArray}; -use hmac::Hmac; -use rand::{rngs::OsRng, CryptoRng, RngCore}; use std::{ borrow::{Borrow, Cow}, convert::TryInto, - error, fmt, - fs::{self, File, OpenOptions}, - io::{Read, Write}, - mem, - path::Path, + fmt, mem, str::FromStr, }; use thiserror::Error; // legacy module paths -pub use crate::signer::*; - -#[derive(Debug)] -pub struct Keypair(ed25519_dalek::Keypair); - -impl Keypair { - pub fn generate(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 { - 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 - } -} +pub use crate::signer::{keypair::*, *}; /// Number of bytes in a signature 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 { - 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 { - Ok(self.sign_message(message)) - } -} - -impl PartialEq for Keypair -where - T: Signer, -{ - fn eq(&self, other: &T) -> bool { - self.pubkey() == other.pubkey() - } -} - #[derive(Clone, Debug, Default)] pub struct Presigner { pubkey: Pubkey, @@ -265,203 +190,9 @@ where } } -pub fn read_keypair(reader: &mut R) -> Result> { - let bytes: Vec = 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>(path: F) -> Result> { - let mut file = File::open(path.as_ref())?; - read_keypair(&mut file) -} - -pub fn write_keypair( - keypair: &Keypair, - writer: &mut W, -) -> Result> { - 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>( - keypair: &Keypair, - outfile: F, -) -> Result> { - 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> { - 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, -) -> Result> { - 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 { - 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 { - const PBKDF2_ROUNDS: u32 = 2048; - const PBKDF2_BYTES: usize = 64; - - let salt = format!("mnemonic{}", passphrase); - - let mut seed = vec![0u8; PBKDF2_BYTES]; - pbkdf2::pbkdf2::>( - 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_from_seed(&generate_seed_from_seed_phrase_and_passphrase( - seed_phrase, - passphrase, - )) -} - #[cfg(test)] mod tests { 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 = 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::() - ); - 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_signature_fromstr() { 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] fn test_presigner() { let keypair = keypair_from_seed(&[0u8; 32]).unwrap(); diff --git a/sdk/src/signer/keypair.rs b/sdk/src/signer/keypair.rs new file mode 100644 index 0000000000..25d2265ca1 --- /dev/null +++ b/sdk/src/signer/keypair.rs @@ -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(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 { + 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 { + 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 { + Ok(self.sign_message(message)) + } +} + +impl PartialEq 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(reader: &mut R) -> Result> { + let bytes: Vec = 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>(path: F) -> Result> { + 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( + keypair: &Keypair, + writer: &mut W, +) -> Result> { + 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>( + keypair: &Keypair, + outfile: F, +) -> Result> { + 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> { + 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, +) -> Result> { + 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 { + 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 { + const PBKDF2_ROUNDS: u32 = 2048; + const PBKDF2_BYTES: usize = 64; + + let salt = format!("mnemonic{}", passphrase); + + let mut seed = vec![0u8; PBKDF2_BYTES]; + pbkdf2::pbkdf2::>( + 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_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 = 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::() + ); + 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); + } +}