use super::Password;
use crate::bip39::{Language, Mnemonic};
use bitcoin::{
Address, CompressedPublicKey,
bip32::{ChainCode, ChildNumber, Xpriv},
hashes::{Hash, HashEngine, hmac, sha512},
hex::DisplayHex,
key::Secp256k1,
secp256k1::SecretKey,
};
use std::str::FromStr;
use xbits::XBits;
#[allow(unused)]
pub trait Bip85 {
fn bip85_mnemonic(&self, index: u32, count: u32, lang: Language) -> Bip85Result;
fn bip85_wallet(&self, index: u32) -> Bip85Result<Wif>;
fn bip85_master(&self, index: u32) -> Bip85Result;
fn bip85_password(&self, index: u32, pwd_len: usize, pwd_type: Password) -> Bip85Result;
}
fn bip85_derive(root: &Xpriv, path: &str) -> Bip85Result<[u8; 64]> {
let secp = bitcoin::secp256k1::Secp256k1::new();
let path = bitcoin::bip32::DerivationPath::from_str(path)?;
let derived = root.derive_priv(&secp, &path)?;
let mut hmac = hmac::HmacEngine::<sha512::Hash>::new("bip-entropy-from-k".as_bytes());
hmac.input(&derived.private_key.secret_bytes());
let data = hmac::Hmac::from_engine(hmac).to_byte_array();
Ok(data)
}
impl Bip85 for Xpriv {
fn bip85_mnemonic(&self, index: u32, count: u32, language: Language) -> Bip85Result {
if !Mnemonic::VALID_SIZES.contains(&(count as usize)) {
return Err(Bip85Error::InvalidParameter("count: 12, 15, 18, 21, 24"));
}
let path = format!("m/83696968'/39'/{}'/{count}'/{index}'", language as u32);
let entropy = bip85_derive(self, &path)?[..(count as usize * 4 / 3)].to_vec();
let mnemonic = Mnemonic::new(&entropy, language)?;
Ok(mnemonic.to_string())
}
fn bip85_wallet(&self, index: u32) -> Bip85Result<Wif> {
let path = format!("m/83696968'/2'/{index}'");
let entropy = bip85_derive(self, &path)?;
let priv_key = bitcoin::PrivateKey::from_slice(&entropy[..32], crate::NETWORK)?;
let pub_key = CompressedPublicKey::from_private_key(&Secp256k1::default(), &priv_key)?;
let addr = Address::p2shwpkh(&pub_key, crate::NETWORK);
Ok(Wif {
pk: priv_key.to_wif(),
addr: addr.to_string(),
})
}
fn bip85_master(&self, index: u32) -> Bip85Result {
let path = format!("m/83696968'/32'/{index}'");
let entropy = bip85_derive(self, &path)?;
let chain_code = ChainCode::from_hex(&entropy[..32].to_lower_hex_string())?;
let xpriv = Xpriv {
network: crate::NETWORK,
depth: 0,
parent_fingerprint: Default::default(),
child_number: ChildNumber::Normal { index: 0 },
private_key: SecretKey::from_slice(&entropy[32..])?,
chain_code,
};
Ok(xpriv.to_string())
}
fn bip85_password(&self, index: u32, pwd_len: usize, password: Password) -> Bip85Result {
if !matches!(pwd_len, 20..=86) {
return Err(Bip85Error::InvalidParameter("20 <= pwd_len <= 86"));
}
let path = format!("m/83696968'/707764'/{pwd_len}'/{index}'");
let entropy = bip85_derive(self, &path)?;
Ok(entropy
.bits()
.chunks(password.bits())
.take(pwd_len)
.map(|v| password.char_at(v))
.collect::<String>())
}
}
pub type Bip85Error = crate::Error;
pub type Bip85Result<T = String> = Result<T, crate::Error>;
#[cfg(test)]
mod bip85_test {
use super::*;
use bitcoin::hex::DisplayHex;
#[test]
fn test_bip85_mnemonic() -> Bip85Result<()> {
{
const MASTER_KEY: &str = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb";
const DERIVED_MNEMONIC: &str = "near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token";
let master = bitcoin::bip32::Xpriv::from_str(MASTER_KEY)?;
let mnemonic = master.bip85_mnemonic(0, 18, Default::default())?;
assert_eq!(mnemonic, DERIVED_MNEMONIC);
}
Ok(())
}
#[cfg(not(feature = "testnet"))]
#[test]
fn test_bip85_wallet() -> Bip85Result<()> {
const MASTER_KEY: &str = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb";
const DERIVED_WIF: &str = "Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp";
let master = bitcoin::bip32::Xpriv::from_str(MASTER_KEY)?;
let priv_key: String = master.bip85_wallet(0)?.pk;
assert_eq!(priv_key, DERIVED_WIF);
Ok(())
}
#[cfg(not(feature = "testnet"))]
#[test]
fn test_bip85_master() -> Bip85Result<()> {
const MASTER_KEY: &str = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb";
const DERIVED_XPRV: &str = "xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX";
let master = bitcoin::bip32::Xpriv::from_str(MASTER_KEY)?;
let xpriv = master.bip85_master(0)?;
assert_eq!(xpriv, DERIVED_XPRV);
Ok(())
}
#[test]
fn test_bip85_password() -> Bip85Result<()> {
const MASTER_KEY: &str = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb";
const DERIVED_PWD: &str = "dKLoepugzdVJvdL56ogNV";
let root = bitcoin::bip32::Xpriv::from_str(MASTER_KEY)?;
let pwd = root.bip85_password(0, 21, Password::Legacy)?;
assert_eq!(pwd, DERIVED_PWD);
Ok(())
}
#[test]
#[ignore = "pre test"]
fn test_bip85_derivation() -> Result<(), bitcoin::bip32::Error> {
const MASTER_BIP32_ROOT_KEY: &str = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb";
const PATH: &str = "m/83696968'/0'/0'";
const DERIVED_KEY: &str =
"cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0";
const DERIVED_ENTROPY: &str = "efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7";
let secp = bitcoin::secp256k1::Secp256k1::new();
let master = bitcoin::bip32::Xpriv::from_str(MASTER_BIP32_ROOT_KEY)?;
let path = bitcoin::bip32::DerivationPath::from_str(PATH)?;
let derived = master.derive_priv(&secp, &path)?;
let derived_key = derived.private_key.secret_bytes().to_lower_hex_string();
assert_eq!(derived_key, DERIVED_KEY);
let mut hmac = hmac::HmacEngine::<sha512::Hash>::new("bip-entropy-from-k".as_bytes());
hmac.input(&derived.private_key.secret_bytes());
let data = hmac::Hmac::from_engine(hmac).to_byte_array();
assert_eq!(data.to_lower_hex_string(), DERIVED_ENTROPY);
Ok(())
}
#[test]
#[ignore = "pre test"]
fn test_japanese_mnemonic_to_seed() {
const JPAN_WORDS: &str = "すおどり ひびく はんこ しはい しみん こたえる しあわせ たいいん えいせい こそだて ひかく とつにゅう えんぜつ うけつけ せんよう";
const BIP39_SEED: &str = "6059453e22e4fe02ddc75df607e53194d432e2838b20ae82a16f550f16e64869a9b0a3cda1dbadaf2febceceb5ec0fdf66fb0198306159411f3e2501de048ea7";
let words = JPAN_WORDS.replace(' ', " "); let salt = "mnemonic".as_bytes().to_vec();
let mut output: [u8; 64] = [0; 64];
pbkdf2::pbkdf2_hmac::<sha2::Sha512>(words.as_bytes(), &salt, u32::pow(2, 11), &mut output);
assert_eq!(output.to_lower_hex_string(), BIP39_SEED);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Wif {
pub pk: String,
pub addr: String,
}