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"));
}
}