use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256, Sha512};
use std::sync::LazyLock;
type HmacSha512 = Hmac<Sha512>;
static BIP39_WORDLIST: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
include_str!("data/bip39_english.txt")
.lines()
.collect()
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MultibitError {
InvalidMnemonic(String),
InvalidWordCount(usize),
UnknownWord(String),
DerivationFailed(String),
}
impl std::fmt::Display for MultibitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MultibitError::InvalidMnemonic(msg) => write!(f, "Invalid mnemonic: {}", msg),
MultibitError::InvalidWordCount(n) => {
write!(f, "Invalid word count: {} (expected 12, 15, 18, 21, or 24)", n)
}
MultibitError::UnknownWord(word) => write!(f, "Unknown BIP39 word: {}", word),
MultibitError::DerivationFailed(msg) => write!(f, "Derivation failed: {}", msg),
}
}
}
impl std::error::Error for MultibitError {}
#[derive(Clone)]
pub struct MultibitBugDeriver {
buggy_seed: [u8; 64],
master_key: [u8; 32],
chain_code: [u8; 32],
}
impl MultibitBugDeriver {
pub fn from_mnemonic(mnemonic: &str, passphrase: &str) -> Result<Self, MultibitError> {
let words: Vec<&str> = mnemonic.split_whitespace().collect();
validate_mnemonic(&words)?;
let original_seed = mnemonic_to_seed(mnemonic, passphrase);
let buggy_mnemonic = entropy_to_mnemonic(&original_seed);
let buggy_mnemonic_str = buggy_mnemonic.join(" ");
let buggy_seed = mnemonic_to_seed(&buggy_mnemonic_str, passphrase);
let (master_key, chain_code) = seed_to_master_key(&buggy_seed);
Ok(Self {
buggy_seed,
master_key,
chain_code,
})
}
pub fn buggy_seed(&self) -> &[u8; 64] {
&self.buggy_seed
}
pub fn derive_key(&self, index: u32) -> Result<[u8; 32], MultibitError> {
let (key_0h, chain_0h) = derive_hardened_child(&self.master_key, &self.chain_code, 0)?;
let (key_0h_0, chain_0h_0) = derive_normal_child(&key_0h, &chain_0h, 0)?;
let (key_final, _) = derive_normal_child(&key_0h_0, &chain_0h_0, index)?;
Ok(key_final)
}
pub fn derive_keys(&self, count: u32) -> Result<Vec<[u8; 32]>, MultibitError> {
(0..count).map(|i| self.derive_key(i)).collect()
}
}
fn validate_mnemonic(words: &[&str]) -> Result<(), MultibitError> {
let valid_counts = [12, 15, 18, 21, 24];
if !valid_counts.contains(&words.len()) {
return Err(MultibitError::InvalidWordCount(words.len()));
}
for word in words {
if !BIP39_WORDLIST.contains(word) {
return Err(MultibitError::UnknownWord(word.to_string()));
}
}
Ok(())
}
fn mnemonic_to_seed(mnemonic: &str, passphrase: &str) -> [u8; 64] {
let salt = format!("mnemonic{}", passphrase);
pbkdf2_hmac_sha512(mnemonic.as_bytes(), salt.as_bytes(), 2048)
}
fn pbkdf2_hmac_sha512(password: &[u8], salt: &[u8], iterations: u32) -> [u8; 64] {
let mut result = [0u8; 64];
let mut salt_with_index = salt.to_vec();
salt_with_index.extend_from_slice(&1u32.to_be_bytes());
let mut mac = HmacSha512::new_from_slice(password).expect("HMAC accepts any key length");
mac.update(&salt_with_index);
let mut u = mac.finalize().into_bytes();
result.copy_from_slice(&u);
for _ in 1..iterations {
let mut mac = HmacSha512::new_from_slice(password).expect("HMAC accepts any key length");
mac.update(&u);
u = mac.finalize().into_bytes();
for (r, ui) in result.iter_mut().zip(u.iter()) {
*r ^= ui;
}
}
result
}
fn entropy_to_mnemonic(entropy: &[u8]) -> Vec<String> {
let hash = Sha256::digest(entropy);
let checksum_bits = entropy.len() * 8 / 32;
let mut bits: Vec<bool> = Vec::with_capacity(entropy.len() * 8 + checksum_bits);
for byte in entropy {
for i in (0..8).rev() {
bits.push((byte >> i) & 1 == 1);
}
}
for i in 0..checksum_bits {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
bits.push((hash[byte_idx] >> bit_idx) & 1 == 1);
}
let num_words = bits.len() / 11;
let mut words = Vec::with_capacity(num_words);
for i in 0..num_words {
let mut index: usize = 0;
for j in 0..11 {
index = (index << 1) | (bits[i * 11 + j] as usize);
}
words.push(BIP39_WORDLIST[index].to_string());
}
words
}
fn seed_to_master_key(seed: &[u8; 64]) -> ([u8; 32], [u8; 32]) {
let mut mac = HmacSha512::new_from_slice(b"Bitcoin seed").expect("HMAC key");
mac.update(seed);
let result = mac.finalize().into_bytes();
let mut master_key = [0u8; 32];
let mut chain_code = [0u8; 32];
master_key.copy_from_slice(&result[0..32]);
chain_code.copy_from_slice(&result[32..64]);
(master_key, chain_code)
}
fn derive_hardened_child(
parent_key: &[u8; 32],
parent_chain_code: &[u8; 32],
index: u32,
) -> Result<([u8; 32], [u8; 32]), MultibitError> {
let hardened_index = index | 0x80000000;
let mut data = Vec::with_capacity(37);
data.push(0x00);
data.extend_from_slice(parent_key);
data.extend_from_slice(&hardened_index.to_be_bytes());
let mut mac = HmacSha512::new_from_slice(parent_chain_code).expect("HMAC key");
mac.update(&data);
let result = mac.finalize().into_bytes();
let il = &result[0..32];
let ir = &result[32..64];
let child_key = scalar_add(il, parent_key)?;
let mut chain_code = [0u8; 32];
chain_code.copy_from_slice(ir);
Ok((child_key, chain_code))
}
fn derive_normal_child(
parent_key: &[u8; 32],
parent_chain_code: &[u8; 32],
index: u32,
) -> Result<([u8; 32], [u8; 32]), MultibitError> {
let pubkey = private_to_public(parent_key)?;
let mut data = Vec::with_capacity(37);
data.extend_from_slice(&pubkey);
data.extend_from_slice(&index.to_be_bytes());
let mut mac = HmacSha512::new_from_slice(parent_chain_code).expect("HMAC key");
mac.update(&data);
let result = mac.finalize().into_bytes();
let il = &result[0..32];
let ir = &result[32..64];
let child_key = scalar_add(il, parent_key)?;
let mut chain_code = [0u8; 32];
chain_code.copy_from_slice(ir);
Ok((child_key, chain_code))
}
fn private_to_public(private_key: &[u8; 32]) -> Result<[u8; 33], MultibitError> {
use secp256k1::{Secp256k1, SecretKey};
let secp = Secp256k1::new();
let secret = SecretKey::from_slice(private_key)
.map_err(|e| MultibitError::DerivationFailed(format!("Invalid private key: {}", e)))?;
let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &secret);
Ok(pubkey.serialize())
}
fn scalar_add(a: &[u8], b: &[u8; 32]) -> Result<[u8; 32], MultibitError> {
use secp256k1::{Scalar, SecretKey};
let secret_b = SecretKey::from_slice(b)
.map_err(|e| MultibitError::DerivationFailed(format!("Invalid key b: {}", e)))?;
let scalar_a_bytes: [u8; 32] = a.try_into()
.map_err(|_| MultibitError::DerivationFailed("Invalid scalar length".to_string()))?;
let scalar_a = Scalar::from_be_bytes(scalar_a_bytes)
.map_err(|_| MultibitError::DerivationFailed("Scalar overflow".to_string()))?;
let result = secret_b.add_tweak(&scalar_a)
.map_err(|e| MultibitError::DerivationFailed(format!("Scalar addition failed: {}", e)))?;
Ok(result.secret_bytes())
}
pub fn truncate_mnemonic(mnemonic: &str) -> String {
let words: Vec<&str> = mnemonic.split_whitespace().collect();
if words.len() <= 4 {
mnemonic.to_string()
} else {
format!("{}...{}", words[..2].join(" "), words[words.len()-2..].join(" "))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bip39_wordlist_loaded() {
assert_eq!(BIP39_WORDLIST.len(), 2048);
assert_eq!(BIP39_WORDLIST[0], "abandon");
assert_eq!(BIP39_WORDLIST[2047], "zoo");
}
#[test]
fn test_pbkdf2_hmac_sha512() {
let password = b"password";
let salt = b"salt";
let result = pbkdf2_hmac_sha512(password, salt, 1);
assert_eq!(result.len(), 64);
}
#[test]
fn test_entropy_to_mnemonic_standard() {
let entropy = [0u8; 16];
let words = entropy_to_mnemonic(&entropy);
assert_eq!(words.len(), 12);
assert_eq!(words[0], "abandon");
}
#[test]
fn test_entropy_to_mnemonic_64_bytes() {
let entropy = [0u8; 64];
let words = entropy_to_mnemonic(&entropy);
assert_eq!(words.len(), 48);
}
#[test]
fn test_multibit_bug_issue_445() {
let mnemonic = "skin join dog sponsor camera puppy ritual diagram arrow poverty boy elbow";
let deriver = MultibitBugDeriver::from_mnemonic(mnemonic, "").unwrap();
let key = deriver.derive_key(0).unwrap();
let address = key_to_p2pkh_address(&key);
assert_eq!(address, "1LQ8XnNKqC7Vu7atH5k4X8qVCc9ug2q7WE",
"First address should match MultiBit HD buggy output");
}
#[test]
fn test_validate_mnemonic_valid() {
let words: Vec<&str> = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
.split_whitespace().collect();
assert!(validate_mnemonic(&words).is_ok());
}
#[test]
fn test_validate_mnemonic_invalid_count() {
let words: Vec<&str> = "abandon abandon abandon".split_whitespace().collect();
assert!(matches!(
validate_mnemonic(&words),
Err(MultibitError::InvalidWordCount(3))
));
}
#[test]
fn test_validate_mnemonic_unknown_word() {
let words: Vec<&str> = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon notaword"
.split_whitespace().collect();
assert!(matches!(
validate_mnemonic(&words),
Err(MultibitError::UnknownWord(_))
));
}
fn key_to_p2pkh_address(key: &[u8; 32]) -> String {
use bitcoin::key::Secp256k1;
use bitcoin::network::Network;
use bitcoin::secp256k1::SecretKey;
use bitcoin::{Address, PrivateKey, PublicKey};
let secp = Secp256k1::new();
let secret = SecretKey::from_slice(key).expect("valid key");
let mut priv_key = PrivateKey::new(secret, Network::Bitcoin);
priv_key.compressed = true;
let pub_key = PublicKey::from_private_key(&secp, &priv_key);
Address::p2pkh(&pub_key, Network::Bitcoin).to_string()
}
#[test]
fn test_buggy_mnemonic_generation() {
let mnemonic = "skin join dog sponsor camera puppy ritual diagram arrow poverty boy elbow";
let original_seed = mnemonic_to_seed(mnemonic, "");
let buggy_mnemonic = entropy_to_mnemonic(&original_seed);
assert_eq!(buggy_mnemonic.len(), 48);
assert_eq!(buggy_mnemonic[0], "trim");
assert_eq!(buggy_mnemonic[1], "snack");
assert_eq!(buggy_mnemonic[2], "gorilla");
assert_eq!(buggy_mnemonic[47], "coach");
}
}