From fea6e1ee5168c24ea83609076a9078c4a254fd86 Mon Sep 17 00:00:00 2001 From: Jongwhan Lee Date: Wed, 16 Oct 2019 15:06:14 +0800 Subject: [PATCH] Problem:(CRO-490) tiny_hderive uses the same secp256k1 library and it's outdated Solution: remove dependency of tiny_hderive, replace with secp256k1 directly, which is already being used in core add module compile ok hdwallet working fix hdwallet display info tidy up add zeroize add copyright Update client-core/src/hdwallet/extended_key.rs thanks Co-Authored-By: Tomas Tauber <2410580+tomtau@users.noreply.github.com> add attribution header remove unnecessary code Update client-core/src/hdwallet/mod.rs thanks Co-Authored-By: Tomas Tauber <2410580+tomtau@users.noreply.github.com> remove .git add staking address unit-test --- NOTICE | 7 +- client-core/Cargo.toml | 7 +- client-core/src/hdwallet/error.rs | 30 ++ client-core/src/hdwallet/extended_key.rs | 282 +++++++++++++++++ .../src/hdwallet/extended_key/key_index.rs | 68 +++++ client-core/src/hdwallet/key_chain.rs | 288 ++++++++++++++++++ .../src/hdwallet/key_chain/chain_path.rs | 171 +++++++++++ client-core/src/hdwallet/mod.rs | 21 ++ client-core/src/hdwallet/traits.rs | 17 ++ client-core/src/lib.rs | 4 + client-core/src/service/key_service.rs | 81 ++--- 11 files changed, 922 insertions(+), 54 deletions(-) create mode 100644 client-core/src/hdwallet/error.rs create mode 100644 client-core/src/hdwallet/extended_key.rs create mode 100644 client-core/src/hdwallet/extended_key/key_index.rs create mode 100644 client-core/src/hdwallet/key_chain.rs create mode 100644 client-core/src/hdwallet/key_chain/chain_path.rs create mode 100644 client-core/src/hdwallet/mod.rs create mode 100644 client-core/src/hdwallet/traits.rs diff --git a/NOTICE b/NOTICE index 807a1972c..c8d85c39e 100644 --- a/NOTICE +++ b/NOTICE @@ -21,4 +21,9 @@ This project contains portions of code derived from the following libraries: * Etcommon * Copyright: Copyright (c) 2018 ETCDEV * License: Apache License 2.0 - * Repository: https://github.com/ETCDEVTeam/etcommon-rs \ No newline at end of file + * Repository: https://github.com/ETCDEVTeam/etcommon-rs + +* BIP44 HDWallet + * Copyright: Jiang Jinyang + * License: The MIT License (MIT) + * Repository: https://github.com/jjyr/hdwallet \ No newline at end of file diff --git a/client-core/Cargo.toml b/client-core/Cargo.toml index 1e0564b57..2bee8d711 100644 --- a/client-core/Cargo.toml +++ b/client-core/Cargo.toml @@ -33,12 +33,17 @@ jsonrpc-core = "13.2" log ="0.4.8" serde = { version = "1.0", features = ["derive"] } tokio="0.1.22" -tiny-hderive = "0.2.1" tiny-bip39 = "0.6" unicase="2.5.1" +lazy_static="1.4.0" +ring = "0.16.9" [dev-dependencies] chain-tx-validation = { path = "../chain-tx-validation" } +hex = "0.4.0" +base58 = "0.1.0" +ripemd160 = "0.8.0" + [features] default = ["sled"] diff --git a/client-core/src/hdwallet/error.rs b/client-core/src/hdwallet/error.rs new file mode 100644 index 000000000..e7c459dc2 --- /dev/null +++ b/client-core/src/hdwallet/error.rs @@ -0,0 +1,30 @@ +//! # Extended Key for HD-wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! + +pub use crate::hdwallet::ChainPathError; + +#[derive(Debug, Clone, Eq, PartialEq)] +/// Error code for hdwallet +pub enum Error { + /// Index is out of range + KeyIndexOutOfRange, + /// ChainPathError + ChainPath(ChainPathError), + /// secp256k1 errors + Secp(secp256k1::Error), +} + +impl From for Error { + fn from(err: ChainPathError) -> Error { + Error::ChainPath(err) + } +} + +impl From for Error { + fn from(err: secp256k1::Error) -> Error { + Error::Secp(err) + } +} diff --git a/client-core/src/hdwallet/extended_key.rs b/client-core/src/hdwallet/extended_key.rs new file mode 100644 index 000000000..f0228184a --- /dev/null +++ b/client-core/src/hdwallet/extended_key.rs @@ -0,0 +1,282 @@ +//! # Extended Key for HD-wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! + +/// Key-index for hdwallet +pub mod key_index; + +use crate::hdwallet::{ + error::Error, + traits::{Deserialize, Serialize}, +}; +use key_index::KeyIndex; +use rand::Rng; +use ring::hmac::{Context, Key, HMAC_SHA512}; +use secp256k1::{PublicKey, Secp256k1, SecretKey, SignOnly, VerifyOnly}; + +lazy_static! { + static ref SECP256K1_SIGN_ONLY: Secp256k1 = Secp256k1::signing_only(); + static ref SECP256K1_VERIFY_ONLY: Secp256k1 = Secp256k1::verification_only(); +} + +/// Random entropy, part of extended key. +type ChainCode = Vec; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// extended key for hdwallet +pub struct ExtendedPrivKey { + /// privatekey for extended key in hdwallet + pub private_key: SecretKey, + /// chain kind for hdwallet + pub chain_code: ChainCode, +} + +/// Indicate bits of random seed used to generate private key, 256 is recommended. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum KeySeed { + /// 128 seed + S128 = 128, + /// 256 seed + S256 = 256, + /// 512 seed + S512 = 512, +} + +impl ExtendedPrivKey { + /// Generate an ExtendedPrivKey, use 256 size random seed. + pub fn random() -> Result { + ExtendedPrivKey::random_with_seed_size(KeySeed::S256) + } + /// Generate an ExtendedPrivKey which use 128 or 256 or 512 bits random seed. + pub fn random_with_seed_size(seed_size: KeySeed) -> Result { + let seed = { + let mut seed = vec![0u8; seed_size as usize / 8]; + let mut rng = rand::thread_rng(); + rng.fill(seed.as_mut_slice()); + seed + }; + Self::with_seed(&seed) + } + + /// Generate an ExtendedPrivKey from seed + pub fn with_seed(seed: &[u8]) -> Result { + let signature = { + let signing_key = Key::new(HMAC_SHA512, b"Bitcoin seed"); + let mut h = Context::with_key(&signing_key); + h.update(&seed); + h.sign() + }; + let sig_bytes = signature.as_ref(); + let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2); + let private_key = SecretKey::from_slice(key)?; + Ok(ExtendedPrivKey { + private_key, + chain_code: chain_code.to_vec(), + }) + } + + fn sign_hardended_key(&self, index: u32) -> ring::hmac::Tag { + let signing_key = Key::new(HMAC_SHA512, &self.chain_code); + let mut h = Context::with_key(&signing_key); + h.update(&[0x00]); + h.update(&self.private_key[..]); + h.update(&index.to_be_bytes()); + h.sign() + } + + fn sign_normal_key(&self, index: u32) -> ring::hmac::Tag { + let signing_key = Key::new(HMAC_SHA512, &self.chain_code); + let mut h = Context::with_key(&signing_key); + let public_key = PublicKey::from_secret_key(&*SECP256K1_SIGN_ONLY, &self.private_key); + h.update(&public_key.serialize()); + h.update(&index.to_be_bytes()); + h.sign() + } + + /// Derive a child key from ExtendedPrivKey. + pub fn derive_private_key(&self, key_index: KeyIndex) -> Result { + if !key_index.is_valid() { + return Err(Error::KeyIndexOutOfRange); + } + let signature = match key_index { + KeyIndex::Hardened(index) => self.sign_hardended_key(index), + KeyIndex::Normal(index) => self.sign_normal_key(index), + }; + let sig_bytes = signature.as_ref(); + let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2); + let mut private_key = SecretKey::from_slice(key)?; + private_key.add_assign(&self.private_key[..])?; + Ok(ExtendedPrivKey { + private_key, + chain_code: chain_code.to_vec(), + }) + } +} + +/// ExtendedPubKey is used for public child key derivation. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtendedPubKey { + /// publickey for extended pub key + pub public_key: PublicKey, + /// chain code for extended pub key + pub chain_code: ChainCode, +} + +impl ExtendedPubKey { + /// Derive public normal child key from ExtendedPubKey, + /// will return error if key_index is a hardened key. + pub fn derive_public_key(&self, key_index: KeyIndex) -> Result { + if !key_index.is_valid() { + return Err(Error::KeyIndexOutOfRange); + } + + let index = match key_index { + KeyIndex::Normal(i) => i, + KeyIndex::Hardened(_) => return Err(Error::KeyIndexOutOfRange), + }; + + let signature = { + let signing_key = Key::new(HMAC_SHA512, &self.chain_code); + let mut h = Context::with_key(&signing_key); + h.update(&self.public_key.serialize()); + h.update(&index.to_be_bytes()); + h.sign() + }; + let sig_bytes = signature.as_ref(); + let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2); + let private_key = SecretKey::from_slice(key)?; + let mut public_key = self.public_key.clone(); + public_key.add_exp_assign(&*SECP256K1_VERIFY_ONLY, &private_key[..])?; + Ok(ExtendedPubKey { + public_key, + chain_code: chain_code.to_vec(), + }) + } + + /// ExtendedPubKey from ExtendedPrivKey + pub fn from_private_key(extended_key: &ExtendedPrivKey) -> Self { + let public_key = + PublicKey::from_secret_key(&*SECP256K1_SIGN_ONLY, &extended_key.private_key); + ExtendedPubKey { + public_key, + chain_code: extended_key.chain_code.clone(), + } + } +} + +impl Serialize> for ExtendedPrivKey { + fn serialize(&self) -> Vec { + let mut buf = self.private_key[..].to_vec(); + buf.extend(&self.chain_code); + buf + } +} +impl Deserialize<&[u8], Error> for ExtendedPrivKey { + fn deserialize(data: &[u8]) -> Result { + let private_key = SecretKey::from_slice(&data[..32])?; + let chain_code = data[32..].to_vec(); + Ok(ExtendedPrivKey { + private_key, + chain_code, + }) + } +} + +impl Serialize> for ExtendedPubKey { + fn serialize(&self) -> Vec { + let mut buf = self.public_key.serialize().to_vec(); + buf.extend(&self.chain_code); + buf + } +} +impl Deserialize<&[u8], Error> for ExtendedPubKey { + fn deserialize(data: &[u8]) -> Result { + let public_key = PublicKey::from_slice(&data[..33])?; + let chain_code = data[33..].to_vec(); + Ok(ExtendedPubKey { + public_key, + chain_code, + }) + } +} + +#[cfg(test)] +mod tests { + use super::{ExtendedPrivKey, ExtendedPubKey, KeyIndex}; + use crate::hdwallet::traits::{Deserialize, Serialize}; + + fn fetch_random_key() -> ExtendedPrivKey { + loop { + if let Ok(key) = ExtendedPrivKey::random() { + return key; + } + } + } + + #[test] + fn random_extended_priv_key() { + for _ in 0..10 { + if ExtendedPrivKey::random().is_ok() { + return; + } + } + panic!("can't generate valid ExtendedPrivKey"); + } + + #[test] + fn random_seed_not_empty() { + assert_ne!( + fetch_random_key(), + ExtendedPrivKey::with_seed(&[]).expect("privkey") + ); + } + + #[test] + fn extended_priv_key_derive_child_priv_key() { + let master_key = fetch_random_key(); + master_key + .derive_private_key(KeyIndex::hardened_from_normalize_index(0).unwrap()) + .expect("hardended_key"); + master_key + .derive_private_key(KeyIndex::Normal(0)) + .expect("normal_key"); + } + + #[test] + fn extended_pub_key_derive_child_pub_key() { + let parent_priv_key = fetch_random_key(); + let child_pub_key_from_child_priv_key = { + let child_priv_key = parent_priv_key + .derive_private_key(KeyIndex::Normal(0)) + .expect("hardended_key"); + ExtendedPubKey::from_private_key(&child_priv_key) + }; + let child_pub_key_from_parent_pub_key = { + let parent_pub_key = ExtendedPubKey::from_private_key(&parent_priv_key); + parent_pub_key + .derive_public_key(KeyIndex::Normal(0)) + .expect("public key") + }; + assert_eq!( + child_pub_key_from_child_priv_key, + child_pub_key_from_parent_pub_key + ) + } + + #[test] + fn priv_key_serialize_deserialize() { + let key = fetch_random_key(); + let buf = key.serialize(); + assert_eq!(ExtendedPrivKey::deserialize(&buf).expect("de"), key); + } + + #[test] + fn pub_key_serialize_deserialize() { + let key = ExtendedPubKey::from_private_key(&fetch_random_key()); + let buf = key.serialize(); + assert_eq!(ExtendedPubKey::deserialize(&buf).expect("de"), key); + } +} diff --git a/client-core/src/hdwallet/extended_key/key_index.rs b/client-core/src/hdwallet/extended_key/key_index.rs new file mode 100644 index 000000000..511d43660 --- /dev/null +++ b/client-core/src/hdwallet/extended_key/key_index.rs @@ -0,0 +1,68 @@ +//! # Extended Key for HD-wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! + +use crate::hdwallet::error::Error; + +const HARDENED_KEY_START_INDEX: u32 = 2_147_483_648; // 2 ** 31 + +/// KeyIndex indicates the key type and index of a child key. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum KeyIndex { + /// Normal key, index range is from 0 to 2 ** 31 - 1 + Normal(u32), + /// Hardened key, index range is from 2 ** 31 to 2 ** 32 - 1 + Hardened(u32), +} + +impl KeyIndex { + /// Return raw index value + pub fn raw_index(self) -> u32 { + match self { + KeyIndex::Normal(i) => i, + KeyIndex::Hardened(i) => i, + } + } + + /// Return normalize index, it will return index subtract 2 ** 31 for hardended key. + pub fn normalize_index(self) -> u32 { + match self { + KeyIndex::Normal(i) => i, + KeyIndex::Hardened(i) => i - HARDENED_KEY_START_INDEX, + } + } + + /// Check index range. + pub fn is_valid(self) -> bool { + match self { + KeyIndex::Normal(i) => i < HARDENED_KEY_START_INDEX, + KeyIndex::Hardened(i) => i >= HARDENED_KEY_START_INDEX, + } + } + + /// Generate Hardened KeyIndex from normalize index value. + pub fn hardened_from_normalize_index(i: u32) -> Result { + if i < HARDENED_KEY_START_INDEX { + Ok(KeyIndex::Hardened(HARDENED_KEY_START_INDEX + i)) + } else { + Ok(KeyIndex::Hardened(i)) + } + } + + /// Generate KeyIndex from raw index value. + pub fn from_index(i: u32) -> Result { + if i < HARDENED_KEY_START_INDEX { + Ok(KeyIndex::Normal(i)) + } else { + Ok(KeyIndex::Hardened(i)) + } + } +} + +impl From for KeyIndex { + fn from(index: u32) -> Self { + KeyIndex::from_index(index).expect("KeyIndex") + } +} diff --git a/client-core/src/hdwallet/key_chain.rs b/client-core/src/hdwallet/key_chain.rs new file mode 100644 index 000000000..fdc04be2f --- /dev/null +++ b/client-core/src/hdwallet/key_chain.rs @@ -0,0 +1,288 @@ +//! # Extended Key for HD-wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! + +/// chain path +pub mod chain_path; + +use crate::hdwallet::{ + error::Error, ChainPath, ChainPathError, ExtendedPrivKey, KeyIndex, SubPath, +}; + +/// KeyChain derivation info +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Derivation { + /// depth, 0 if it is master key + pub depth: u8, + /// parent key + pub parent_key: Option, + /// key_index which used with parent key to derive this key + pub key_index: Option, +} + +impl Derivation { + /// master of derivation, hdwallet + pub fn master() -> Self { + Derivation { + depth: 0, + parent_key: None, + key_index: None, + } + } +} + +impl Default for Derivation { + fn default() -> Self { + Derivation::master() + } +} + +/// KeyChain is used for derivation HDKey from master_key and chain_path. + +pub trait KeyChain { + /// derivate private key + fn derive_private_key( + &self, + chain_path: ChainPath, + ) -> Result<(ExtendedPrivKey, Derivation), Error>; +} + +/// default keychain +pub struct DefaultKeyChain { + master_key: ExtendedPrivKey, +} + +impl DefaultKeyChain { + /// make default keychain instance + pub fn new(master_key: ExtendedPrivKey) -> Self { + DefaultKeyChain { master_key } + } +} + +impl KeyChain for DefaultKeyChain { + fn derive_private_key( + &self, + chain_path: ChainPath, + ) -> Result<(ExtendedPrivKey, Derivation), Error> { + let mut iter = chain_path.iter(); + // chain_path must start with root + if iter.next() != Some(Ok(SubPath::Root)) { + return Err(ChainPathError::Invalid.into()); + } + let mut key = self.master_key.clone(); + let mut depth = 0; + let mut parent_key = None; + let mut key_index = None; + for sub_path in iter { + match sub_path? { + SubPath::Child(child_key_index) => { + depth += 1; + key_index = Some(child_key_index); + let child_key = key.derive_private_key(child_key_index)?; + parent_key = Some(key); + key = child_key; + } + _ => return Err(ChainPathError::Invalid.into()), + } + } + Ok(( + key, + Derivation { + depth, + parent_key, + key_index, + }, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hdwallet::{traits::Serialize, ExtendedPubKey}; + use base58::ToBase58; + use ring::digest; + use ripemd160::{Digest, Ripemd160}; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum ExtendedKey { + PrivKey(ExtendedPrivKey), + PubKey(ExtendedPubKey), + } + + #[allow(dead_code)] + #[derive(Clone, Copy, Debug)] + enum Network { + MainNet, + TestNet, + } + + #[derive(Clone, Debug)] + struct BitcoinKey { + pub network: Network, + pub depth: u8, + pub parent_key: Option, + pub key_index: Option, + pub key: ExtendedKey, + } + + impl BitcoinKey { + fn version_bytes(&self) -> Vec { + let hex_str = match self.network { + Network::MainNet => match self.key { + ExtendedKey::PrivKey(..) => "0x0488ADE4", + ExtendedKey::PubKey(..) => "0x0488B21E", + }, + Network::TestNet => match self.key { + ExtendedKey::PrivKey(..) => "0x04358394", + ExtendedKey::PubKey(..) => "0x043587CF", + }, + }; + from_hex(hex_str) + } + + fn parent_fingerprint(&self) -> Vec { + match self.parent_key { + Some(ref key) => { + let pubkey = ExtendedPubKey::from_private_key(key); + let buf = digest::digest(&digest::SHA256, &pubkey.public_key.serialize()); + let mut hasher = Ripemd160::new(); + hasher.input(&buf.as_ref()); + hasher.result()[0..4].to_vec() + } + None => vec![0; 4], + } + } + + fn public_key(&self) -> BitcoinKey { + match self.key { + ExtendedKey::PrivKey(ref key) => { + let pubkey = ExtendedPubKey::from_private_key(key); + let mut bitcoin_key = self.clone(); + bitcoin_key.key = ExtendedKey::PubKey(pubkey); + bitcoin_key + } + ExtendedKey::PubKey(..) => self.clone(), + } + } + } + + impl Serialize for BitcoinKey { + fn serialize(&self) -> String { + let mut buf: Vec = Vec::with_capacity(112); + buf.extend_from_slice(&self.version_bytes()); + buf.extend_from_slice(&self.depth.to_be_bytes()); + buf.extend_from_slice(&self.parent_fingerprint()); + match self.key_index { + Some(key_index) => { + buf.extend_from_slice(&key_index.raw_index().to_be_bytes()); + } + None => buf.extend_from_slice(&[0; 4]), + } + match self.key { + ExtendedKey::PrivKey(ref key) => { + buf.extend_from_slice(&key.chain_code); + buf.extend_from_slice(&[0]); + buf.extend_from_slice(&key.private_key[..]); + } + ExtendedKey::PubKey(ref key) => { + buf.extend_from_slice(&key.chain_code); + buf.extend_from_slice(&key.public_key.serialize()); + } + } + assert_eq!(buf.len(), 78); + + let check_sum = { + let buf = digest::digest(&digest::SHA256, &buf); + digest::digest(&digest::SHA256, &buf.as_ref()) + }; + + buf.extend_from_slice(&check_sum.as_ref()[0..4]); + (&buf).to_base58() + } + } + + fn from_hex(hex_string: &str) -> Vec { + if hex_string.starts_with("0x") { + hex::decode(&hex_string[2..]).expect("decode") + } else { + hex::decode(hex_string).expect("decode") + } + } + + #[test] + fn test_bip32_vector_1() { + let seed = from_hex("000102030405060708090a0b0c0d0e0f"); + let key_chain = + DefaultKeyChain::new(ExtendedPrivKey::with_seed(&seed).expect("master key")); + for (chain_path, hex_priv_key, hex_pub_key) in &[ + ("m", "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"), + ("m/0H", "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"), + ("m/0H/1", "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"), + ("m/0H/1/2H", "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"), + ("m/0H/1/2H/2", "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"), + ("m/0H/1/2H/2/1000000000", "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy") + ] { + let (key, derivation) = key_chain.derive_private_key(ChainPath::from(chain_path.to_string())).expect("fetch key"); + let priv_key = BitcoinKey{ + network: Network::MainNet, + depth: derivation.depth, + parent_key: derivation.parent_key, + key_index: derivation.key_index, + key: ExtendedKey::PrivKey(key), + }; + assert_eq!(&priv_key.serialize(), hex_priv_key); + assert_eq!(&priv_key.public_key().serialize(), hex_pub_key); + } + } + + #[test] + fn test_bip32_vector_2() { + let seed = from_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"); + let key_chain = + DefaultKeyChain::new(ExtendedPrivKey::with_seed(&seed).expect("master key")); + for (chain_path, hex_priv_key, hex_pub_key) in &[ + ("m", "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"), + ("m/0", "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"), + ("m/0/2147483647H", "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"), + ("m/0/2147483647H/1", "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"), + ("m/0/2147483647H/1/2147483646H", "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"), + ("m/0/2147483647H/1/2147483646H/2", "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt") + ] { + let (key, derivation) = key_chain.derive_private_key(ChainPath::from(chain_path.to_string())).expect("fetch key"); + let priv_key = BitcoinKey{ + network: Network::MainNet, + depth: derivation.depth, + parent_key: derivation.parent_key, + key_index: derivation.key_index, + key: ExtendedKey::PrivKey(key), + }; + assert_eq!(&priv_key.serialize(), hex_priv_key); + assert_eq!(&priv_key.public_key().serialize(), hex_pub_key); + } + } + + #[test] + fn test_bip32_vector_3() { + let seed = from_hex("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"); + let key_chain = + DefaultKeyChain::new(ExtendedPrivKey::with_seed(&seed).expect("master key")); + for (chain_path, hex_priv_key, hex_pub_key) in &[ + ("m", "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6", "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13"), + ("m/0H", "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L", "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y") + ] { + let (key, derivation) = key_chain.derive_private_key(ChainPath::from(chain_path.to_string())).expect("fetch key"); + let priv_key = BitcoinKey{ + network: Network::MainNet, + depth: derivation.depth, + parent_key: derivation.parent_key, + key_index: derivation.key_index, + key: ExtendedKey::PrivKey(key), + }; + assert_eq!(&priv_key.serialize(), hex_priv_key); + assert_eq!(&priv_key.public_key().serialize(), hex_pub_key); + } + } +} diff --git a/client-core/src/hdwallet/key_chain/chain_path.rs b/client-core/src/hdwallet/key_chain/chain_path.rs new file mode 100644 index 000000000..c32be8699 --- /dev/null +++ b/client-core/src/hdwallet/key_chain/chain_path.rs @@ -0,0 +1,171 @@ +//! # Extended Key for HD-wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! + +use crate::hdwallet::KeyIndex; +use std::fmt; + +const MASTER_SYMBOL: &str = "m"; +const HARDENED_SYMBOLS: [&str; 2] = ["H", "'"]; +const SEPARATOR: char = '/'; + +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +/// Error category +pub enum Error { + /// invalid error + Invalid, + /// blank error + Blank, + /// key index range error + KeyIndexOutOfRange, +} + +/// ChainPath is used to describe BIP-32 KeyChain path. + +#[derive(Debug, PartialEq, Eq)] +pub struct ChainPath(String); + +impl ChainPath { + /// An SubPath iterator over the ChainPath from Root to child keys. + pub fn iter(&self) -> impl Iterator> + '_ { + Iter(self.0.split_terminator(SEPARATOR)) + } + + /// make string + pub fn into_string(self) -> String { + self.0 + } + + /// Convert ChainPath to &str represent format + fn to_string(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, PartialEq, Eq)] +/// subpath of chain path +pub enum SubPath { + /// root of subpath + Root, + /// child of subpath + Child(KeyIndex), +} + +/// iterator +pub struct Iter<'a, I: Iterator>(I); + +impl<'a, I: Iterator> Iterator for Iter<'a, I> { + type Item = Result; + + fn next(&mut self) -> Option { + self.0.next().map(|sub_path| { + if sub_path == MASTER_SYMBOL { + return Ok(SubPath::Root); + } + if sub_path.is_empty() { + return Err(Error::Blank); + } + let last_char = &sub_path[(sub_path.len() - 1)..]; + let is_hardened = HARDENED_SYMBOLS.contains(&last_char); + let key_index = { + let key_index_result = if is_hardened { + sub_path[..sub_path.len() - 1] + .parse::() + .map_err(|_| Error::Invalid) + .and_then(|index| { + KeyIndex::hardened_from_normalize_index(index) + .map_err(|_| Error::KeyIndexOutOfRange) + }) + } else { + sub_path[..] + .parse::() + .map_err(|_| Error::Invalid) + .and_then(|index| { + KeyIndex::from_index(index).map_err(|_| Error::KeyIndexOutOfRange) + }) + }; + key_index_result? + }; + Ok(SubPath::Child(key_index)) + }) + } +} + +impl From for ChainPath { + fn from(path: String) -> Self { + ChainPath(path) + } +} + +impl From<&str> for ChainPath { + fn from(path: &str) -> Self { + ChainPath(path.to_string()) + } +} + +impl fmt::Display for ChainPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chain_path() { + assert_eq!( + ChainPath::from("m".to_string()) + .iter() + .collect::, _>>() + .unwrap(), + vec![SubPath::Root] + ); + assert_eq!( + ChainPath::from("m/1".to_string()) + .iter() + .collect::, _>>() + .unwrap(), + vec![SubPath::Root, SubPath::Child(KeyIndex::Normal(1))], + ); + assert_eq!( + ChainPath::from("m/2147483649H/1".to_string()) + .iter() + .collect::, _>>() + .unwrap(), + vec![ + SubPath::Root, + SubPath::Child(KeyIndex::hardened_from_normalize_index(1).unwrap()), + SubPath::Child(KeyIndex::Normal(1)) + ], + ); + // alternative hardened key represent + assert_eq!( + ChainPath::from("m/2147483649'/1".to_string()) + .iter() + .collect::, _>>() + .unwrap(), + vec![ + SubPath::Root, + SubPath::Child(KeyIndex::hardened_from_normalize_index(1).unwrap()), + SubPath::Child(KeyIndex::Normal(1)) + ], + ); + // from invalid string + assert!(ChainPath::from("m/2147483649h/1".to_string()) + .iter() + .collect::, _>>() + .is_err()); + assert!(ChainPath::from("/2147483649H/1".to_string()) + .iter() + .collect::, _>>() + .is_err()); + assert!(ChainPath::from("a".to_string()) + .iter() + .collect::, _>>() + .is_err()); + } +} diff --git a/client-core/src/hdwallet/mod.rs b/client-core/src/hdwallet/mod.rs new file mode 100644 index 000000000..b190d68f6 --- /dev/null +++ b/client-core/src/hdwallet/mod.rs @@ -0,0 +1,21 @@ +//! # HD Wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! +/// error code for hdwallet +pub mod error; +/// entended key for hdwallet +pub mod extended_key; +/// key-chain for hdwallet +pub mod key_chain; +/// traits for hdwallet +pub mod traits; + +pub use crate::hdwallet::extended_key::{ + key_index::KeyIndex, ExtendedPrivKey, ExtendedPubKey, KeySeed, +}; +pub use crate::hdwallet::key_chain::{ + chain_path::{ChainPath, Error as ChainPathError, SubPath}, + DefaultKeyChain, Derivation, KeyChain, +}; diff --git a/client-core/src/hdwallet/traits.rs b/client-core/src/hdwallet/traits.rs new file mode 100644 index 000000000..3ff981c97 --- /dev/null +++ b/client-core/src/hdwallet/traits.rs @@ -0,0 +1,17 @@ +//! # Extended Key for HD-wallet +//! adapted from https://github.com/jjyr/hdwallet (HDWallet) +//! Copyright (c) 2018, Jiang Jinyang (licensed under the MIT License) +//! Modifications Copyright (c) 2018 - 2019, Foris Limited (licensed under the Apache License, Version 2.0) +//! + +/// serialization for hdwallet +pub trait Serialize { + /// serialize of hdwallet + fn serialize(&self) -> T; +} + +/// deserialization for hdwallet +pub trait Deserialize: Sized { + /// deserialize of hdwallet + fn deserialize(t: T) -> Result; +} diff --git a/client-core/src/lib.rs b/client-core/src/lib.rs index 049708bcb..294860011 100644 --- a/client-core/src/lib.rs +++ b/client-core/src/lib.rs @@ -11,6 +11,7 @@ //! - Transaction creation and signing (with automatic unspent transaction selection) pub mod cipher; pub mod handler; +pub mod hdwallet; pub mod input_selection; pub mod service; pub mod signer; @@ -36,3 +37,6 @@ pub use crate::transaction_builder::TransactionBuilder; pub use crate::unspent_transactions::{SelectedUnspentTransactions, UnspentTransactions}; #[doc(inline)] pub use crate::wallet::{MultiSigWalletClient, WalletClient}; + +#[macro_use] +extern crate lazy_static; diff --git a/client-core/src/service/key_service.rs b/client-core/src/service/key_service.rs index 1499111dd..103fcf1e8 100644 --- a/client-core/src/service/key_service.rs +++ b/client-core/src/service/key_service.rs @@ -1,6 +1,8 @@ use secstr::SecUtf8; use zeroize::Zeroize; +use crate::hdwallet::traits::Serialize; +use crate::hdwallet::{ChainPath, DefaultKeyChain, ExtendedPrivKey, KeyChain}; use bip39::{Language, Mnemonic, MnemonicType, Seed}; use client_common::{Error, ErrorKind, Result}; @@ -10,7 +12,6 @@ const KEYSPACE_HD: &str = "hd_key"; use crate::types::WalletKind; use chain_core::init::network::get_bip44_coin_type; use log::debug; -use tiny_hderive::bip32::ExtendedPrivKey; /// get random mnemonic pub fn get_random_mnemonic() -> Mnemonic { @@ -132,51 +133,6 @@ where Ok(()) } - /// auto-matically generate staking, transfer addresses - /// with just one api call - pub fn auto_restore( - &self, - mnemonic: &Mnemonic, - name: &str, - passphrase: &SecUtf8, - count: i32, - ) -> Result<()> { - self.generate_seed(mnemonic, name, passphrase)?; - let cointype = get_bip44_coin_type(); - log::debug!("coin type={}", cointype); - let seed_bytes = self.storage.get_secure(KEYSPACE_HD, name, passphrase)?; - for index in 0..count { - for account in 0..2 { - let extended = ExtendedPrivKey::derive( - &seed_bytes.as_ref().expect("hdwallet get extended")[..], - format!("m/44'/{}'/{}'/0/{}", cointype, account, index).as_str(), - ) - .map_err(|_e| { - Error::new( - ErrorKind::InvalidInput, - "hdwallet cannot derive new address", - ) - })?; - let mut secret_key_bytes = extended.secret(); - - let private_key = - PrivateKey::deserialize_from(&secret_key_bytes).map_err(|_e| { - Error::new(ErrorKind::InvalidInput, "hdwallet load private_key") - })?; - secret_key_bytes.zeroize(); - let public_key = PublicKey::from(&private_key); - - self.storage.set_secure( - KEYSPACE, - public_key.serialize(), - private_key.serialize(), - passphrase, - )?; - } - } - Ok(()) - } - /// read value from db, if it's None, there value doesn't exist pub fn read_value(&self, passphrase: &SecUtf8, key: &[u8]) -> Result>> { Ok(self @@ -239,12 +195,18 @@ where let cointype = get_bip44_coin_type(); log::debug!("coin type={}", cointype); let account = if is_staking { 1 } else { 0 }; - let extended = ExtendedPrivKey::derive( - &seed_bytes.expect("generate_keypair_hd get seed bytes"), - format!("m/44'/{}'/{}'/0/{}", cointype, account, index).as_str(), - ) - .map_err(|_e| Error::new(ErrorKind::InvalidInput, "hdwallet derive new address"))?; - let mut secret_key_bytes = extended.secret(); + + let chain_path = format!("m/44'/{}'/{}'/0/{}", cointype, account, index); + let key_chain = DefaultKeyChain::new( + ExtendedPrivKey::with_seed(&seed_bytes.as_ref().expect("hdwallet get extended")[..]) + .map_err(|_e| Error::new(ErrorKind::InvalidInput, "invalid seed bytes"))?, + ); + let (key, _derivation) = key_chain + .derive_private_key(ChainPath::from(chain_path.to_string())) + .map_err(|_e| Error::new(ErrorKind::InvalidInput, "hdwallet derive private key"))?; + let mut secret = key.serialize(); + + let secret_key_bytes = &mut secret[0..32]; debug!("hdwallet save index={}", index); let private_key = PrivateKey::deserialize_from(&secret_key_bytes) .map_err(|_e| Error::new(ErrorKind::InvalidInput, "hdwallet privatekey deserialize"))?; @@ -339,6 +301,21 @@ mod tests { String::from("66b0a362da2332cb7fdc0c940acf9638f824fe7112d79d7ac7baf341033f8abf") == hex::encode(&private_key.serialize()) ); + { + let (public_key, private_key) = key_service + .generate_keypair_hd(name, &passphrase, true) + .expect("Unable to generate private key"); + + // check deterministic of hdwallet + assert!( + String::from("02f030e0ae3cec955edb750891f8819b37a7df6a41a1e5d59f93187d0c3d6dcf06") + == public_key.to_string() + ); + assert!( + String::from("72e1ac47503ec1b814fafda9df402b313a0b7f8d9cca0e30d92e686ca2ed02dd") + == hex::encode(&private_key.serialize()) + ); + } let retrieved_private_key = key_service .private_key(&public_key, &passphrase) .expect("hdwallet check_flow retrieve privatekey")