Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wallet private key, encrypted wrapper #358

Merged
merged 1 commit into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pallas-txbuilder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions pallas-txbuilder/src/transaction/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use pallas_crypto::{
key::ed25519,
};
use pallas_primitives::{babbage, Fragment};
use pallas_wallet::PrivateKey;

use std::{collections::HashMap, ops::Deref};

Expand Down Expand Up @@ -608,14 +609,14 @@ pub struct BuiltTransaction {
}

impl BuiltTransaction {
pub fn sign(mut self, secret_key: ed25519::SecretKey) -> Result<Self, TxBuilderError> {
let pubkey: [u8; 32] = secret_key
pub fn sign(mut self, private_key: PrivateKey) -> Result<Self, TxBuilderError> {
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 => {
Expand Down
1 change: 1 addition & 0 deletions pallas-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
192 changes: 192 additions & 0 deletions pallas-wallet/src/hd.rs
Original file line number Diff line number Diff line change
@@ -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<T: RngCore + CryptoRng>(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<T: RngCore + CryptoRng>(
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<Self, Error> {
XPrv::from_bytes_verified(bytes)
.map(Self)
.map_err(Error::Xprv)
}

pub fn as_bytes(&self) -> Vec<u8> {
self.0.as_ref().to_vec()
}

pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result<Self, Error> {
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<Self, Error> {
let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?;
if hrp != Self::BECH32_HRP {
Err(Error::InvalidBech32Hrp)
} else {
let data = Vec::<u8>::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<u8> {
self.0.as_ref().to_vec()
}

pub fn derive(&self, index: u32) -> Result<Self, Error> {
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<Self, Error> {
let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?;
if hrp != Self::BECH32_HRP {
Err(Error::InvalidBech32Hrp)
} else {
let data = Vec::<u8>::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)
}
}
Loading