use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
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 BitimageError {
InvalidPath(String),
DerivationFailed(String),
}
impl std::fmt::Display for BitimageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BitimageError::InvalidPath(msg) => write!(f, "Invalid derivation path: {}", msg),
BitimageError::DerivationFailed(msg) => write!(f, "Derivation failed: {}", msg),
}
}
}
impl std::error::Error for BitimageError {}
pub struct BitimageDeriver {
master_key: [u8; 32],
chain_code: [u8; 32],
}
impl BitimageDeriver {
pub fn from_file_bytes(data: &[u8], passphrase: &str) -> Self {
let base64_encoded = BASE64.encode(data);
let hash = Sha256::digest(base64_encoded.as_bytes());
let mut entropy = [0u8; 32];
entropy.copy_from_slice(&hash);
let mnemonic = entropy_to_mnemonic(&entropy);
let mnemonic_str = mnemonic.join(" ");
let seed = mnemonic_to_seed(&mnemonic_str, passphrase);
let (master_key, chain_code) = seed_to_master_key(&seed);
Self {
master_key,
chain_code,
}
}
pub fn derive_path(&self, path: &str) -> Result<[u8; 32], BitimageError> {
let components = parse_derivation_path(path)?;
let mut key = self.master_key;
let mut chain = self.chain_code;
for (index, hardened) in components {
let (new_key, new_chain) = if hardened {
derive_hardened_child(&key, &chain, index)?
} else {
derive_normal_child(&key, &chain, index)?
};
key = new_key;
chain = new_chain;
}
Ok(key)
}
}
fn parse_derivation_path(path: &str) -> Result<Vec<(u32, bool)>, BitimageError> {
let path = path.trim();
if !path.starts_with("m/") && path != "m" {
return Err(BitimageError::InvalidPath(
"Path must start with 'm/' or be 'm'".to_string(),
));
}
if path == "m" {
return Ok(vec![]);
}
let components = path[2..].split('/');
let mut result = Vec::new();
for component in components {
if component.is_empty() {
continue;
}
let (index_str, hardened) = if component.ends_with('\'') || component.ends_with('h') {
(&component[..component.len() - 1], true)
} else {
(component, false)
};
let index: u32 = index_str
.parse()
.map_err(|_| BitimageError::InvalidPath(format!("Invalid index: {}", component)))?;
if index >= 0x8000_0000 {
return Err(BitimageError::InvalidPath(format!(
"Index {} exceeds BIP32 limit (must be < 2^31)",
index
)));
}
result.push((index, hardened));
}
Ok(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 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 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]), BitimageError> {
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]), BitimageError> {
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], BitimageError> {
use secp256k1::{Secp256k1, SecretKey};
let secp = Secp256k1::new();
let secret = SecretKey::from_slice(private_key)
.map_err(|e| BitimageError::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], BitimageError> {
use secp256k1::{Scalar, SecretKey};
let secret_b = SecretKey::from_slice(b)
.map_err(|e| BitimageError::DerivationFailed(format!("Invalid key b: {}", e)))?;
let scalar_a_bytes: [u8; 32] = a
.try_into()
.map_err(|_| BitimageError::DerivationFailed("Invalid scalar length".to_string()))?;
let scalar_a = Scalar::from_be_bytes(scalar_a_bytes)
.map_err(|_| BitimageError::DerivationFailed("Scalar overflow".to_string()))?;
let result = secret_b
.add_tweak(&scalar_a)
.map_err(|e| BitimageError::DerivationFailed(format!("Scalar addition failed: {}", e)))?;
Ok(result.secret_bytes())
}
pub fn increment_path_index(path: &str) -> String {
if let Some(last_slash) = path.rfind('/') {
let prefix = &path[..=last_slash];
let suffix = &path[last_slash + 1..];
let (index_str, hardened_marker) = if suffix.ends_with('\'') || suffix.ends_with('h') {
(&suffix[..suffix.len() - 1], &suffix[suffix.len() - 1..])
} else {
(suffix, "")
};
if let Ok(index) = index_str.parse::<u32>() {
return format!("{}{}{}", prefix, index + 1, hardened_marker);
}
}
path.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_derivation_path_bip84() {
let path = "m/84'/0'/0'/0/0";
let components = parse_derivation_path(path).unwrap();
assert_eq!(
components,
vec![(84, true), (0, true), (0, true), (0, false), (0, false),]
);
}
#[test]
fn test_parse_derivation_path_master_only() {
let components = parse_derivation_path("m").unwrap();
assert!(components.is_empty());
}
#[test]
fn test_parse_derivation_path_invalid() {
assert!(parse_derivation_path("84'/0'/0'/0/0").is_err());
assert!(parse_derivation_path("m/abc").is_err());
}
#[test]
fn test_increment_path_index() {
assert_eq!(increment_path_index("m/84'/0'/0'/0/0"), "m/84'/0'/0'/0/1");
assert_eq!(increment_path_index("m/84'/0'/0'/0/5"), "m/84'/0'/0'/0/6");
assert_eq!(increment_path_index("m/44'/0'/0'/0'"), "m/44'/0'/0'/1'");
}
#[test]
fn test_bitimage_deterministic() {
let data = b"hello world";
let deriver = BitimageDeriver::from_file_bytes(data, "");
let key1 = deriver.derive_path("m/84'/0'/0'/0/0").unwrap();
let deriver2 = BitimageDeriver::from_file_bytes(data, "");
let key2 = deriver2.derive_path("m/84'/0'/0'/0/0").unwrap();
assert_eq!(key1, key2);
}
#[test]
fn test_bitimage_passphrase_changes_key() {
let data = b"hello world";
let key_no_pass = BitimageDeriver::from_file_bytes(data, "")
.derive_path("m/84'/0'/0'/0/0")
.unwrap();
let key_with_pass = BitimageDeriver::from_file_bytes(data, "secret")
.derive_path("m/84'/0'/0'/0/0")
.unwrap();
assert_ne!(key_no_pass, key_with_pass);
}
#[test]
fn test_different_paths_different_keys() {
let data = b"test";
let deriver = BitimageDeriver::from_file_bytes(data, "");
let key0 = deriver.derive_path("m/84'/0'/0'/0/0").unwrap();
let key1 = deriver.derive_path("m/84'/0'/0'/0/1").unwrap();
assert_ne!(key0, key1);
}
#[test]
fn test_parse_derivation_path_index_overflow() {
let result = parse_derivation_path("m/2147483648'/0'/0'/0/0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("exceeds BIP32 limit"));
}
#[test]
fn test_parse_derivation_path_max_valid_index() {
let result = parse_derivation_path("m/2147483647'/0'/0'/0/0");
assert!(result.is_ok());
}
}