diff --git a/pallas-txbuilder/Cargo.toml b/pallas-txbuilder/Cargo.toml index 5ff9983f..b8947f75 100644 --- a/pallas-txbuilder/Cargo.toml +++ b/pallas-txbuilder/Cargo.toml @@ -18,6 +18,7 @@ pallas-crypto = { path = "../pallas-crypto", version = "=0.20.0" } pallas-primitives = { path = "../pallas-primitives", version = "=0.20.0" } pallas-traverse = { path = "../pallas-traverse", version = "=0.20.0" } pallas-addresses = { path = "../pallas-addresses", version = "=0.20.0" } +pallas-wallet = { path = "../pallas-wallet", version = "=0.20.0" } serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" thiserror = "1.0.44" diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs index adc0e60b..b6097730 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -4,6 +4,7 @@ use pallas_crypto::{ key::ed25519, }; use pallas_primitives::{babbage, Fragment}; +use pallas_wallet::PrivateKey; use std::{collections::HashMap, ops::Deref}; @@ -608,14 +609,14 @@ pub struct BuiltTransaction { } impl BuiltTransaction { - pub fn sign(mut self, secret_key: ed25519::SecretKey) -> Result { - let pubkey: [u8; 32] = secret_key + pub fn sign(mut self, private_key: PrivateKey) -> Result { + let pubkey: [u8; 32] = private_key .public_key() .as_ref() .try_into() .map_err(|_| TxBuilderError::MalformedKey)?; - let signature: [u8; 64] = secret_key.sign(self.tx_hash.0).as_ref().try_into().unwrap(); + let signature: [u8; ed25519::Signature::SIZE] = private_key.sign(self.tx_hash.0).as_ref().try_into().unwrap(); match self.era { BuilderEra::Babbage => { diff --git a/pallas-wallet/Cargo.toml b/pallas-wallet/Cargo.toml index a3fa7d33..8dab08e0 100644 --- a/pallas-wallet/Cargo.toml +++ b/pallas-wallet/Cargo.toml @@ -16,3 +16,4 @@ ed25519-bip32 = "0.4.1" bip39 = { version = "2.0.0", features = ["rand_core"] } cryptoxide = "0.4.4" bech32 = "0.9.1" +rand = "0.8.5" diff --git a/pallas-wallet/src/hd.rs b/pallas-wallet/src/hd.rs new file mode 100644 index 00000000..668a6b72 --- /dev/null +++ b/pallas-wallet/src/hd.rs @@ -0,0 +1,192 @@ +use bech32::{FromBase32, ToBase32}; +use bip39::rand_core::{CryptoRng, RngCore}; +use bip39::{Language, Mnemonic}; +use cryptoxide::{hmac::Hmac, pbkdf2::pbkdf2, sha2::Sha512}; +use ed25519_bip32::{self, XPrv, XPub, XPRV_SIZE}; +use pallas_crypto::key::ed25519; + +use crate::{Error, PrivateKey}; + +/// Ed25519-BIP32 HD Private Key +#[derive(Debug, PartialEq, Eq)] +pub struct Bip32PrivateKey(ed25519_bip32::XPrv); + +impl Bip32PrivateKey { + const BECH32_HRP: &'static str = "xprv"; + + pub fn generate(mut rng: T) -> Self { + let mut buf = [0u8; XPRV_SIZE]; + rng.fill_bytes(&mut buf); + let xprv = XPrv::normalize_bytes_force3rd(buf); + + Self(xprv) + } + + pub fn generate_with_mnemonic( + mut rng: T, + password: String, + ) -> (Self, Mnemonic) { + let mut buf = [0u8; 64]; + rng.fill_bytes(&mut buf); + + let bip39 = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap(); + + let entropy = bip39.clone().to_entropy(); + + let mut pbkdf2_result = [0; XPRV_SIZE]; + + const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096? + + let mut mac = Hmac::new(Sha512::new(), password.as_bytes()); + pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result); + + (Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)), bip39) + } + + pub fn from_bytes(bytes: [u8; 96]) -> Result { + XPrv::from_bytes_verified(bytes) + .map(Self) + .map_err(Error::Xprv) + } + + pub fn as_bytes(&self) -> Vec { + self.0.as_ref().to_vec() + } + + pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result { + let bip39 = Mnemonic::parse(mnemonic).map_err(Error::Mnemonic)?; + let entropy = bip39.to_entropy(); + + let mut pbkdf2_result = [0; XPRV_SIZE]; + + const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096? + + let mut mac = Hmac::new(Sha512::new(), password.as_bytes()); + pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result); + + Ok(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result))) + } + + pub fn derive(&self, index: u32) -> Self { + Self(self.0.derive(ed25519_bip32::DerivationScheme::V2, index)) + } + + pub fn to_ed25519_private_key(&self) -> PrivateKey { + PrivateKey::Extended(self.0.extended_secret_key().into()) + } + + pub fn to_public(&self) -> Bip32PublicKey { + Bip32PublicKey(self.0.public()) + } + + pub fn chain_code(&self) -> [u8; 32] { + *self.0.chain_code() + } + + pub fn to_bech32(&self) -> String { + bech32::encode( + Self::BECH32_HRP, + self.as_bytes().to_base32(), + bech32::Variant::Bech32, + ) + .unwrap() + } + + pub fn from_bech32(bech32: String) -> Result { + let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?; + if hrp != Self::BECH32_HRP { + Err(Error::InvalidBech32Hrp) + } else { + let data = Vec::::from_base32(&data).map_err(Error::InvalidBech32)?; + Self::from_bytes(data.try_into().map_err(|_| Error::UnexpectedBech32Length)?) + } + } +} + +/// Ed25519-BIP32 HD Public Key +#[derive(Debug, PartialEq, Eq)] +pub struct Bip32PublicKey(ed25519_bip32::XPub); + +impl Bip32PublicKey { + const BECH32_HRP: &'static str = "xpub"; + + pub fn from_bytes(bytes: [u8; 64]) -> Self { + Self(XPub::from_bytes(bytes)) + } + + pub fn as_bytes(&self) -> Vec { + self.0.as_ref().to_vec() + } + + pub fn derive(&self, index: u32) -> Result { + self.0 + .derive(ed25519_bip32::DerivationScheme::V2, index) + .map(Self) + .map_err(Error::DerivationError) + } + + pub fn to_ed25519_pubkey(&self) -> ed25519::PublicKey { + self.0.public_key().into() + } + + pub fn chain_code(&self) -> [u8; 32] { + *self.0.chain_code() + } + + pub fn to_bech32(&self) -> String { + bech32::encode( + Self::BECH32_HRP, + self.as_bytes().to_base32(), + bech32::Variant::Bech32, + ) + .unwrap() + } + + pub fn from_bech32(bech32: String) -> Result { + let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?; + if hrp != Self::BECH32_HRP { + Err(Error::InvalidBech32Hrp) + } else { + let data = Vec::::from_base32(&data).map_err(Error::InvalidBech32)?; + Ok(Self::from_bytes( + data.try_into().map_err(|_| Error::UnexpectedBech32Length)?, + )) + } + } +} + +#[cfg(test)] +mod test { + use bip39::rand_core::OsRng; + + use super::{Bip32PrivateKey, Bip32PublicKey}; + + #[test] + fn mnemonic_roundtrip() { + let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng, "".into()); + + let xprv_from_mne = + Bip32PrivateKey::from_bip39_mnenomic(mne.to_string(), "".into()).unwrap(); + + assert_eq!(xprv, xprv_from_mne) + } + + #[test] + fn bech32_roundtrip() { + let xprv = Bip32PrivateKey::generate(OsRng); + + let xprv_bech32 = xprv.to_bech32(); + + let decoded_xprv = Bip32PrivateKey::from_bech32(xprv_bech32).unwrap(); + + assert_eq!(xprv, decoded_xprv); + + let xpub = xprv.to_public(); + + let xpub_bech32 = xpub.to_bech32(); + + let decoded_xpub = Bip32PublicKey::from_bech32(xpub_bech32).unwrap(); + + assert_eq!(xpub, decoded_xpub) + } +} diff --git a/pallas-wallet/src/lib.rs b/pallas-wallet/src/lib.rs index a5a899f0..e472678d 100644 --- a/pallas-wallet/src/lib.rs +++ b/pallas-wallet/src/lib.rs @@ -1,13 +1,18 @@ -use bech32::{FromBase32, ToBase32}; -use bip39::rand_core::{CryptoRng, RngCore}; -use bip39::{Language, Mnemonic}; -use cryptoxide::{hmac::Hmac, pbkdf2::pbkdf2, sha2::Sha512}; -use ed25519_bip32::{self, XPrv, XPub, XPRV_SIZE}; -use pallas_crypto::key::ed25519::{self}; +use ed25519_bip32; +use pallas_crypto::key::ed25519::{PublicKey, SecretKey, SecretKeyExtended, Signature}; use thiserror::Error; +pub mod hd; +pub mod wrapper; + #[derive(Error, Debug)] pub enum Error { + /// Private key wrapper data of unexpected length + #[error("Wrapped private key data invalid length")] + WrapperDataInvalidSize, + /// Failed to decrypt private key wrapper data + #[error("Failed to decrypt private key wrapper data")] + WrapperDataFailedToDecrypt, /// Unexpected bech32 HRP prefix #[error("Unexpected bech32 HRP prefix")] InvalidBech32Hrp, @@ -28,186 +33,59 @@ pub enum Error { DerivationError(ed25519_bip32::DerivationError), } -/// ED25519-BIP32 HD Private Key -#[derive(Debug, PartialEq, Eq)] -pub struct Bip32PrivateKey(ed25519_bip32::XPrv); - -impl Bip32PrivateKey { - const BECH32_HRP: &'static str = "xprv"; - - pub fn generate(mut rng: T) -> Self { - let mut buf = [0u8; XPRV_SIZE]; - rng.fill_bytes(&mut buf); - let xprv = XPrv::normalize_bytes_force3rd(buf); - - Self(xprv) - } - - pub fn generate_with_mnemonic( - mut rng: T, - password: String, - ) -> (Self, Mnemonic) { - let mut buf = [0u8; 64]; - rng.fill_bytes(&mut buf); - - let bip39 = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap(); - - let entropy = bip39.clone().to_entropy(); - - let mut pbkdf2_result = [0; XPRV_SIZE]; - - const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096? - - let mut mac = Hmac::new(Sha512::new(), password.as_bytes()); - pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result); - - (Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)), bip39) - } - - pub fn from_bytes(bytes: [u8; 96]) -> Result { - XPrv::from_bytes_verified(bytes) - .map(Self) - .map_err(Error::Xprv) - } - - pub fn as_bytes(&self) -> Vec { - self.0.as_ref().to_vec() - } - - pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result { - let bip39 = Mnemonic::parse(mnemonic).map_err(Error::Mnemonic)?; - let entropy = bip39.to_entropy(); - - let mut pbkdf2_result = [0; XPRV_SIZE]; - - const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096? - - let mut mac = Hmac::new(Sha512::new(), password.as_bytes()); - pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result); - - Ok(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result))) - } - - pub fn derive(&self, index: u32) -> Self { - Self(self.0.derive(ed25519_bip32::DerivationScheme::V2, index)) - } - - pub fn to_ed25519_privkey(&self) -> ed25519::SecretKeyExtended { - self.0.extended_secret_key().into() - } - - pub fn to_public(&self) -> Bip32PublicKey { - Bip32PublicKey(self.0.public()) - } - - pub fn chain_code(&self) -> [u8; 32] { - *self.0.chain_code() - } +/// A standard or extended Ed25519 secret key +pub enum PrivateKey { + Normal(SecretKey), + Extended(SecretKeyExtended), +} - pub fn to_bech32(&self) -> String { - bech32::encode( - Self::BECH32_HRP, - self.as_bytes().to_base32(), - bech32::Variant::Bech32, - ) - .unwrap() +impl PrivateKey { + pub fn len(&self) -> usize { + match self { + Self::Normal(_) => SecretKey::SIZE, + Self::Extended(_) => SecretKeyExtended::SIZE, + } } - pub fn from_bech32(bech32: String) -> Result { - let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?; - if hrp != Self::BECH32_HRP { - Err(Error::InvalidBech32Hrp) - } else { - let data = Vec::::from_base32(&data).map_err(Error::InvalidBech32)?; - Self::from_bytes(data.try_into().map_err(|_| Error::UnexpectedBech32Length)?) + pub fn public_key(&self) -> PublicKey { + match self { + Self::Normal(x) => x.public_key(), + Self::Extended(x) => x.public_key(), } } -} - -/// ED25519-BIP32 HD Public Key -#[derive(Debug, PartialEq, Eq)] -pub struct Bip32PublicKey(ed25519_bip32::XPub); -impl Bip32PublicKey { - const BECH32_HRP: &'static str = "xpub"; - - pub fn from_bytes(bytes: [u8; 64]) -> Self { - Self(XPub::from_bytes(bytes)) + pub fn sign(&self, msg: T) -> Signature + where + T: AsRef<[u8]>, + { + match self { + Self::Normal(x) => x.sign(msg), + Self::Extended(x) => x.sign(msg), + } } pub fn as_bytes(&self) -> Vec { - self.0.as_ref().to_vec() - } - - pub fn derive(&self, index: u32) -> Result { - self.0 - .derive(ed25519_bip32::DerivationScheme::V2, index) - .map(Self) - .map_err(Error::DerivationError) - } - - pub fn to_ed25519_pubkey(&self) -> ed25519::PublicKey { - self.0.public_key().into() - } - - pub fn chain_code(&self) -> [u8; 32] { - *self.0.chain_code() - } - - pub fn to_bech32(&self) -> String { - bech32::encode( - Self::BECH32_HRP, - self.as_bytes().to_base32(), - bech32::Variant::Bech32, - ) - .unwrap() - } - - pub fn from_bech32(bech32: String) -> Result { - let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?; - if hrp != Self::BECH32_HRP { - Err(Error::InvalidBech32Hrp) - } else { - let data = Vec::::from_base32(&data).map_err(Error::InvalidBech32)?; - Ok(Self::from_bytes( - data.try_into().map_err(|_| Error::UnexpectedBech32Length)?, - )) + match self { + Self::Normal(x) => { + let bytes: [u8; SecretKey::SIZE] = x.clone().into(); + bytes.to_vec() + } + Self::Extended(x) => { + let bytes: [u8; SecretKeyExtended::SIZE] = x.clone().into(); + bytes.to_vec() + } } } } -#[cfg(test)] -mod test { - use bip39::rand_core::OsRng; - - use crate::{Bip32PrivateKey, Bip32PublicKey}; - - #[test] - fn mnemonic_roundtrip() { - let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng, "".into()); - - let xprv_from_mne = - Bip32PrivateKey::from_bip39_mnenomic(mne.to_string(), "".into()).unwrap(); - - assert_eq!(xprv, xprv_from_mne) +impl From for PrivateKey { + fn from(key: SecretKey) -> Self { + PrivateKey::Normal(key) } +} - #[test] - fn bech32_roundtrip() { - let xprv = Bip32PrivateKey::generate(OsRng); - - let xprv_bech32 = xprv.to_bech32(); - - let decoded_xprv = Bip32PrivateKey::from_bech32(xprv_bech32).unwrap(); - - assert_eq!(xprv, decoded_xprv); - - let xpub = xprv.to_public(); - - let xpub_bech32 = xpub.to_bech32(); - - let decoded_xpub = Bip32PublicKey::from_bech32(xpub_bech32).unwrap(); - - assert_eq!(xpub, decoded_xpub) +impl From for PrivateKey { + fn from(key: SecretKeyExtended) -> Self { + PrivateKey::Extended(key) } } diff --git a/pallas-wallet/src/wrapper.rs b/pallas-wallet/src/wrapper.rs new file mode 100644 index 00000000..07ce8098 --- /dev/null +++ b/pallas-wallet/src/wrapper.rs @@ -0,0 +1,165 @@ +use cryptoxide::chacha20poly1305::ChaCha20Poly1305; +use cryptoxide::kdf::argon2; +use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended}; +use rand::{CryptoRng, RngCore}; + +use crate::{Error, PrivateKey}; + +const ITERATIONS: u32 = 2500; +const VERSION_SIZE: usize = 1; +const SALT_SIZE: usize = 16; +const NONCE_SIZE: usize = 12; +const TAG_SIZE: usize = 16; + +pub fn encrypt_private_key(mut rng: Rng, private_key: PrivateKey, password: &String) -> Vec +where + Rng: RngCore + CryptoRng, +{ + let salt = { + let mut salt = [0u8; SALT_SIZE]; + rng.fill_bytes(&mut salt); + salt + }; + + let sym_key: [u8; 32] = argon2::argon2( + &argon2::Params::argon2d().iterations(ITERATIONS).unwrap(), + password.as_bytes(), + &salt, + &[], + &[], + ); + + let nonce = { + let mut nonce = [0u8; NONCE_SIZE]; + rng.fill_bytes(&mut nonce); + nonce + }; + + let mut chacha20 = ChaCha20Poly1305::new(&sym_key, &nonce, &[]); + + let data_size = private_key.len(); + + let (ciphertext, ct_tag) = { + let mut ciphertext = vec![0u8; data_size]; + let mut ct_tag = [0u8; 16]; + chacha20.encrypt(&private_key.as_bytes(), &mut ciphertext, &mut ct_tag); + + (ciphertext, ct_tag) + }; + + // (version || salt || nonce || tag || ciphertext) + let mut out = Vec::with_capacity(VERSION_SIZE + SALT_SIZE + NONCE_SIZE + TAG_SIZE + data_size); + + out.push(1); + out.extend_from_slice(&salt); + out.extend_from_slice(&nonce); + out.extend_from_slice(&ct_tag); + out.extend_from_slice(&ciphertext); + + out +} + +#[allow(unused)] +pub fn decrypt_private_key(password: &String, data: Vec) -> Result { + let data_len_without_ct = VERSION_SIZE + SALT_SIZE + NONCE_SIZE + TAG_SIZE; + + let ciphertext_len = if data.len() == (data_len_without_ct + SecretKey::SIZE) { + SecretKey::SIZE + } else if data.len() == (data_len_without_ct + SecretKeyExtended::SIZE) { + SecretKeyExtended::SIZE + } else { + return Err(Error::WrapperDataInvalidSize); + }; + + let mut cursor = 0; + + let _version = &data[cursor]; + cursor += VERSION_SIZE; + + let salt = &data[cursor..cursor + SALT_SIZE]; + cursor += SALT_SIZE; + + let nonce = &data[cursor..cursor + NONCE_SIZE]; + cursor += NONCE_SIZE; + + let tag = &data[cursor..cursor + TAG_SIZE]; + cursor += TAG_SIZE; + + let ciphertext = &data[cursor..cursor + ciphertext_len]; + + let sym_key: [u8; 32] = argon2::argon2( + &argon2::Params::argon2d().iterations(ITERATIONS).unwrap(), + password.as_bytes(), + salt, + &[], + &[], + ); + + let mut chacha20 = ChaCha20Poly1305::new(&sym_key, nonce, &[]); + + match ciphertext_len { + SecretKey::SIZE => { + let mut plaintext = [0u8; SecretKey::SIZE]; + + if chacha20.decrypt(ciphertext, &mut plaintext, tag) { + let secret_key: SecretKey = plaintext.into(); + + Ok(secret_key.into()) + } else { + Err(Error::WrapperDataFailedToDecrypt) + } + } + SecretKeyExtended::SIZE => { + let mut plaintext = [0u8; SecretKeyExtended::SIZE]; + + if chacha20.decrypt(ciphertext, &mut plaintext, tag) { + let secret_key: SecretKeyExtended = plaintext.into(); + + Ok(secret_key.into()) + } else { + Err(Error::WrapperDataFailedToDecrypt) + } + } + _ => unreachable!(), + } +} + +#[cfg(test)] +mod tests { + use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended}; + use rand::rngs::OsRng; + + use crate::{ + wrapper::{decrypt_private_key, encrypt_private_key}, + PrivateKey, + }; + + #[test] + fn private_key_encryption_roundtrip() { + let password = "hunter123"; + + // --- standard + + let private_key = PrivateKey::Normal(SecretKey::new(OsRng)); + + let private_key_bytes = private_key.as_bytes(); + + let encrypted_priv_key = encrypt_private_key(OsRng, private_key, &password.into()); + + let decrypted_privkey = decrypt_private_key(&password.into(), encrypted_priv_key).unwrap(); + + assert_eq!(private_key_bytes, decrypted_privkey.as_bytes()); + + // --- extended + + let private_key = PrivateKey::Extended(SecretKeyExtended::new(OsRng)); + + let private_key_bytes = private_key.as_bytes(); + + let encrypted_priv_key = encrypt_private_key(OsRng, private_key, &password.into()); + + let decrypted_privkey = decrypt_private_key(&password.into(), encrypted_priv_key).unwrap(); + + assert_eq!(private_key_bytes, decrypted_privkey.as_bytes()) + } +}