vuke 0.9.0

Research tool for studying vulnerable Bitcoin key generation practices
Documentation
//! MultiBit HD analyzer - check if a key was generated by the seed-as-entropy bug.

use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use indicatif::ProgressBar;

use crate::multibit::{MultibitBugDeriver, truncate_mnemonic};
use super::{Analyzer, AnalysisConfig, AnalysisResult, AnalysisStatus};

const DEFAULT_DERIVATION_DEPTH: u32 = 20;

pub struct MultibitAnalyzer {
    mnemonic: Option<String>,
    mnemonic_file: Option<PathBuf>,
    passphrase: String,
    derivation_depth: u32,
}

impl MultibitAnalyzer {
    pub fn new() -> Self {
        Self {
            mnemonic: None,
            mnemonic_file: None,
            passphrase: String::new(),
            derivation_depth: DEFAULT_DERIVATION_DEPTH,
        }
    }

    pub fn with_mnemonic(mut self, mnemonic: String) -> Self {
        self.mnemonic = Some(mnemonic);
        self
    }

    pub fn with_mnemonic_file(mut self, path: PathBuf) -> Self {
        self.mnemonic_file = Some(path);
        self
    }

    pub fn with_passphrase(mut self, passphrase: String) -> Self {
        self.passphrase = passphrase;
        self
    }

    pub fn with_derivation_depth(mut self, depth: u32) -> Self {
        self.derivation_depth = depth;
        self
    }

    fn check_mnemonic(&self, key: &[u8; 32], mnemonic: &str) -> Option<(u32, String)> {
        let deriver = match MultibitBugDeriver::from_mnemonic(mnemonic, &self.passphrase) {
            Ok(d) => d,
            Err(_) => return None,
        };

        for i in 0..self.derivation_depth {
            if let Ok(derived_key) = deriver.derive_key(i) {
                if derived_key == *key {
                    return Some((i, mnemonic.to_string()));
                }
            }
        }
        None
    }

    fn analyze_single_mnemonic(&self, key: &[u8; 32], mnemonic: &str) -> AnalysisResult {
        match self.check_mnemonic(key, mnemonic) {
            Some((index, mnemonic)) => {
                let truncated = truncate_mnemonic(&mnemonic);
                AnalysisResult {
                    analyzer: self.name(),
                    status: AnalysisStatus::Confirmed,
                    details: Some(format!(
                        "mnemonic=\"{}\", path=m/0'/0/{}, passphrase=\"{}\"",
                        truncated, index, 
                        if self.passphrase.is_empty() { "<empty>" } else { "<set>" }
                    )),
                }
            }
            None => AnalysisResult {
                analyzer: self.name(),
                status: AnalysisStatus::NotFound,
                details: Some(format!(
                    "mnemonic does not produce this key (checked {} derivations)",
                    self.derivation_depth
                )),
            },
        }
    }

    fn analyze_mnemonic_file(&self, key: &[u8; 32], path: &PathBuf, progress: Option<&ProgressBar>) -> AnalysisResult {
        let file = match File::open(path) {
            Ok(f) => f,
            Err(e) => {
                return AnalysisResult {
                    analyzer: self.name(),
                    status: AnalysisStatus::Unknown,
                    details: Some(format!("Failed to open mnemonic file: {}", e)),
                };
            }
        };

        let reader = BufReader::new(file);
        let lines: Vec<String> = reader.lines().filter_map(|l| l.ok()).collect();
        
        if let Some(pb) = progress {
            pb.set_length(lines.len() as u64);
            pb.set_message("multibit-hd dictionary");
        }

        for (idx, mnemonic) in lines.iter().enumerate() {
            let mnemonic = mnemonic.trim();
            if mnemonic.is_empty() || mnemonic.starts_with('#') {
                if let Some(pb) = progress {
                    pb.inc(1);
                }
                continue;
            }

            if let Some((index, found_mnemonic)) = self.check_mnemonic(key, mnemonic) {
                if let Some(pb) = progress {
                    pb.finish_and_clear();
                }
                let truncated = truncate_mnemonic(&found_mnemonic);
                return AnalysisResult {
                    analyzer: self.name(),
                    status: AnalysisStatus::Confirmed,
                    details: Some(format!(
                        "mnemonic=\"{}\", path=m/0'/0/{}, line={}",
                        truncated, index, idx + 1
                    )),
                };
            }

            if let Some(pb) = progress {
                pb.inc(1);
            }
        }

        if let Some(pb) = progress {
            pb.finish_and_clear();
        }

        AnalysisResult {
            analyzer: self.name(),
            status: AnalysisStatus::NotFound,
            details: Some(format!(
                "checked {} mnemonics × {} derivations",
                lines.len(), self.derivation_depth
            )),
        }
    }
}

impl Default for MultibitAnalyzer {
    fn default() -> Self {
        Self::new()
    }
}

impl Analyzer for MultibitAnalyzer {
    fn name(&self) -> &'static str {
        "multibit-hd"
    }

    fn analyze(&self, key: &[u8; 32], _config: &AnalysisConfig, progress: Option<&ProgressBar>) -> AnalysisResult {
        if let Some(ref mnemonic) = self.mnemonic {
            return self.analyze_single_mnemonic(key, mnemonic);
        }

        if let Some(ref path) = self.mnemonic_file {
            return self.analyze_mnemonic_file(key, path, progress);
        }

        AnalysisResult {
            analyzer: self.name(),
            status: AnalysisStatus::Unknown,
            details: Some("requires --mnemonic or --mnemonic-file to check".to_string()),
        }
    }

    fn is_brute_force(&self) -> bool {
        self.mnemonic_file.is_some()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_analyzer_with_matching_mnemonic() {
        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 analyzer = MultibitAnalyzer::new().with_mnemonic(mnemonic.to_string());
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);
        
        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.unwrap().contains("m/0'/0/0"));
    }

    #[test]
    fn test_analyzer_with_non_matching_mnemonic() {
        let wrong_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
        let target_key = [0x42u8; 32];
        
        let analyzer = MultibitAnalyzer::new()
            .with_mnemonic(wrong_mnemonic.to_string())
            .with_derivation_depth(5);
        let result = analyzer.analyze(&target_key, &AnalysisConfig::default(), None);
        
        assert_eq!(result.status, AnalysisStatus::NotFound);
    }

    #[test]
    fn test_analyzer_without_mnemonic() {
        let key = [0x42u8; 32];
        let analyzer = MultibitAnalyzer::new();
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);
        
        assert_eq!(result.status, AnalysisStatus::Unknown);
        assert!(result.details.unwrap().contains("requires"));
    }

    #[test]
    fn test_analyzer_finds_key_at_index_5() {
        let mnemonic = "skin join dog sponsor camera puppy ritual diagram arrow poverty boy elbow";
        
        let deriver = MultibitBugDeriver::from_mnemonic(mnemonic, "").unwrap();
        let key_at_5 = deriver.derive_key(5).unwrap();
        
        let analyzer = MultibitAnalyzer::new()
            .with_mnemonic(mnemonic.to_string())
            .with_derivation_depth(10);
        let result = analyzer.analyze(&key_at_5, &AnalysisConfig::default(), None);
        
        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.unwrap().contains("m/0'/0/5"));
    }
}