vuke 0.9.0

Research tool for studying vulnerable Bitcoin key generation practices
Documentation
//! LCG analyzer - brute-force search for Linear Congruential Generator seeds.

use indicatif::ProgressBar;
use rayon::prelude::*;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Mutex;

use crate::lcg::{generate_key, LcgEndian, LcgVariant, ALL_VARIANTS};
use super::{Analyzer, AnalysisConfig, AnalysisResult, AnalysisStatus};

pub struct LcgAnalyzer {
    variant: Option<LcgVariant>,
    endian: LcgEndian,
}

impl LcgAnalyzer {
    pub fn new() -> Self {
        Self {
            variant: None,
            endian: LcgEndian::Big,
        }
    }

    pub fn with_variant(variant: LcgVariant) -> Self {
        Self {
            variant: Some(variant),
            endian: LcgEndian::Big,
        }
    }

    pub fn with_endian(mut self, endian: LcgEndian) -> Self {
        self.endian = endian;
        self
    }

    fn analyze_variant(
        &self,
        key: &[u8; 32],
        config: &AnalysisConfig,
        variant: &LcgVariant,
        progress: Option<&ProgressBar>,
    ) -> Option<AnalysisResult> {
        match config.mask_bits {
            Some(bits) => self.analyze_masked(key, variant, bits, progress),
            None => self.analyze_exact(key, variant, progress),
        }
    }

    fn analyze_exact(
        &self,
        key: &[u8; 32],
        variant: &LcgVariant,
        progress: Option<&ProgressBar>,
    ) -> Option<AnalysisResult> {
        let target_key = *key;
        let endian = self.endian;

        let result = brute_force_search(
            variant,
            progress,
            &format!("lcg:{} brute-force", variant.name),
            move |seed, var, end| {
                let candidate = generate_key(seed, var, end);
                if candidate == target_key {
                    Some(candidate)
                } else {
                    None
                }
            },
            endian,
        );

        result.map(|(seed, _)| AnalysisResult {
            analyzer: "lcg",
            status: AnalysisStatus::Confirmed,
            details: Some(format!(
                "variant={}, seed={}, endian={}",
                variant.name, seed, self.endian.as_str()
            )),
        })
    }

    fn analyze_masked(
        &self,
        target: &[u8; 32],
        variant: &LcgVariant,
        mask_bits: u8,
        progress: Option<&ProgressBar>,
    ) -> Option<AnalysisResult> {
        if mask_bits == 0 {
            return None;
        }

        let target_u64 = u64::from_be_bytes(target[24..32].try_into().unwrap());
        let mask: u64 = if mask_bits >= 64 { u64::MAX } else { (1u64 << mask_bits) - 1 };
        let high_bit: u64 = 1u64 << (mask_bits - 1);
        let endian = self.endian;

        let result = brute_force_search(
            variant,
            progress,
            &format!("lcg:{} masked brute-force", variant.name),
            move |seed, var, end| {
                let candidate = generate_key(seed, var, end);
                let full_key_u64 = u64::from_be_bytes(candidate[24..32].try_into().unwrap());
                let masked = (full_key_u64 & mask) | high_bit;
                if masked == target_u64 {
                    Some(candidate)
                } else {
                    None
                }
            },
            endian,
        );

        result.map(|(seed, full_key)| {
            let full_key_hex = hex::encode(full_key);
            let full_key_u64 = u64::from_be_bytes(full_key[24..32].try_into().unwrap());
            let masked_value = (full_key_u64 & mask) | high_bit;

            AnalysisResult {
                analyzer: "lcg",
                status: AnalysisStatus::Confirmed,
                details: Some(format!(
                    "variant={}, seed={}, full_key={}, masked=0x{:x}, mask_bits={}, endian={}, formula=(key & 0x{:x}) | 0x{:x}",
                    variant.name, seed, full_key_hex, masked_value, mask_bits, self.endian.as_str(), mask, high_bit
                )),
            }
        })
    }
}

fn brute_force_search<F>(
    variant: &LcgVariant,
    progress: Option<&ProgressBar>,
    message: &str,
    matcher: F,
    endian: LcgEndian,
) -> Option<(u32, [u8; 32])>
where
    F: Fn(u32, &LcgVariant, LcgEndian) -> Option<[u8; 32]> + Sync,
{
    let found_seed = AtomicU32::new(0);
    let found = AtomicBool::new(false);
    let found_key = Mutex::new([0u8; 32]);

    let max_seed = variant.max_seed().min(u32::MAX as u64) as u32;
    let total = max_seed as u64 + 1;

    if let Some(pb) = progress {
        pb.set_length(total);
        pb.set_message(message.to_string());
    }

    let chunk_size = 1_000_000u32;
    let progress_interval = 100_000u32;
    let num_chunks = (max_seed / chunk_size) + 1;
    let chunks: Vec<u32> = (0..num_chunks).collect();

    chunks.par_iter().for_each(|&chunk_idx| {
        if found.load(Ordering::Acquire) {
            return;
        }

        let start = chunk_idx.saturating_mul(chunk_size);
        let end = start.saturating_add(chunk_size - 1).min(max_seed);
        let mut last_progress = start;

        for seed in start..=end {
            if found.load(Ordering::Acquire) {
                return;
            }

            if let Some(key) = matcher(seed, variant, endian) {
                found_seed.store(seed, Ordering::Release);
                found.store(true, Ordering::Release);
                if let Ok(mut fk) = found_key.lock() {
                    *fk = key;
                }
                if let Some(pb) = progress {
                    pb.inc((seed - last_progress) as u64);
                }
                return;
            }

            if let Some(pb) = progress {
                if seed - last_progress >= progress_interval {
                    pb.inc((seed - last_progress) as u64);
                    last_progress = seed;
                }
            }
        }

        if let Some(pb) = progress {
            pb.inc((end - last_progress + 1) as u64);
        }
    });

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

    if found.load(Ordering::Acquire) {
        let seed = found_seed.load(Ordering::Acquire);
        let key = *found_key.lock().unwrap();
        Some((seed, key))
    } else {
        None
    }
}

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

impl Analyzer for LcgAnalyzer {
    fn name(&self) -> &'static str {
        "lcg"
    }

    fn supports_mask(&self) -> bool {
        true
    }

    fn is_brute_force(&self) -> bool {
        true
    }

    fn analyze(
        &self,
        key: &[u8; 32],
        config: &AnalysisConfig,
        progress: Option<&ProgressBar>,
    ) -> AnalysisResult {
        let variants: Vec<&LcgVariant> = match &self.variant {
            Some(v) => vec![v],
            None => ALL_VARIANTS.iter().collect(),
        };

        let mut checked_seeds: u64 = 0;
        let mut checked_variants = Vec::new();

        for variant in &variants {
            if let Some(result) = self.analyze_variant(key, config, variant, progress) {
                return result;
            }
            checked_seeds += variant.max_seed().min(u32::MAX as u64) + 1;
            checked_variants.push(variant.name);
        }

        AnalysisResult {
            analyzer: self.name(),
            status: AnalysisStatus::NotFound,
            details: Some(format!(
                "checked {} seeds across variants: {}",
                checked_seeds,
                checked_variants.join(", ")
            )),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lcg::{GLIBC, MINSTD, MSVC};

    #[test]
    fn test_find_known_glibc_seed() {
        let seed = 42u32;
        let key = generate_key(seed, &GLIBC, LcgEndian::Big);

        let analyzer = LcgAnalyzer::with_variant(GLIBC);
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);

        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.as_ref().unwrap().contains("seed=42"));
        assert!(result.details.as_ref().unwrap().contains("variant=glibc"));
    }

    #[test]
    fn test_find_known_minstd_seed() {
        let seed = 12345u32;
        let key = generate_key(seed, &MINSTD, LcgEndian::Big);

        let analyzer = LcgAnalyzer::with_variant(MINSTD);
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);

        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.as_ref().unwrap().contains("seed=12345"));
    }

    #[test]
    fn test_find_seed_zero() {
        let key = generate_key(0, &GLIBC, LcgEndian::Big);

        let analyzer = LcgAnalyzer::with_variant(GLIBC);
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);

        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.as_ref().unwrap().contains("seed=0"));
    }

    #[test]
    fn test_find_with_little_endian() {
        let seed = 100u32;
        let key = generate_key(seed, &GLIBC, LcgEndian::Little);

        let analyzer = LcgAnalyzer::with_variant(GLIBC).with_endian(LcgEndian::Little);
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);

        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.as_ref().unwrap().contains("endian=le"));
    }

    #[test]
    #[ignore]
    fn test_not_found_wrong_endian() {
        let key = generate_key(42, &GLIBC, LcgEndian::Little);

        let analyzer = LcgAnalyzer::with_variant(GLIBC).with_endian(LcgEndian::Big);
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);

        assert_eq!(result.status, AnalysisStatus::NotFound);
    }

    #[test]
    #[ignore]
    fn test_all_variants_finds_correct_one() {
        let seed = 999u32;
        let key = generate_key(seed, &MSVC, LcgEndian::Big);

        let analyzer = LcgAnalyzer::new();
        let result = analyzer.analyze(&key, &AnalysisConfig::default(), None);

        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.as_ref().unwrap().contains("variant=msvc"));
    }

    #[test]
    fn test_masked_analysis() {
        let seed = 42u32;
        let full_key = generate_key(seed, &GLIBC, LcgEndian::Big);

        let mask_bits: u8 = 10;
        let mask: u64 = (1u64 << mask_bits) - 1;
        let high_bit: u64 = 1u64 << (mask_bits - 1);

        let full_key_u64 = u64::from_be_bytes(full_key[24..32].try_into().unwrap());
        let masked_value = (full_key_u64 & mask) | high_bit;

        let mut target = [0u8; 32];
        target[24..32].copy_from_slice(&masked_value.to_be_bytes());

        let config = AnalysisConfig { mask_bits: Some(mask_bits), ..Default::default() };
        let analyzer = LcgAnalyzer::with_variant(GLIBC);
        let result = analyzer.analyze(&target, &config, None);

        assert_eq!(result.status, AnalysisStatus::Confirmed);
        assert!(result.details.as_ref().unwrap().contains("seed=42"));
        assert!(result.details.as_ref().unwrap().contains("mask_bits=10"));
    }
}