hashcracker 1.0.0

GPU-accelerated password cracker — 41 hash types, Vulkan/Metal/DX12, single binary
use md4::{Digest, Md4};
use crate::hashes::{AttackModeType, HashModule, HashPattern, ParsedHash};

pub struct RawDcc;

impl HashModule for RawDcc {
    fn name(&self) -> &'static str { "dcc" }
    fn mode(&self) -> u32 { 1100 }
    fn digest_words(&self) -> u32 { 4 }
    fn needs_int64(&self) -> bool { false }

    fn cpu_verify(&self, password: &str, salt: &[u8], hash: &[u32]) -> bool {
        let mut utf16 = Vec::with_capacity(password.len() * 2);
        for b in password.encode_utf16() {
            utf16.extend_from_slice(&b.to_le_bytes());
        }
        let mut inner = Md4::new();
        inner.update(&utf16);
        let ntlm_hash = inner.finalize();

        let mut outer = Md4::new();
        outer.update(&ntlm_hash);
        outer.update(salt);
        let result = outer.finalize();

        let mut computed = [0u32; 8];
        for i in 0..4 {
            computed[i] = u32::from_le_bytes(result[i * 4..i * 4 + 4].try_into().unwrap());
        }
        computed[..4] == hash[..4]
    }

    fn shader_source(&self, mode: &AttackModeType) -> &'static str {
        match mode {
            _ => "",
        }
    }

    fn detect_patterns(&self) -> &[HashPattern] {
        &[HashPattern { prefix: None, hex_len: Some(32), priority: 80 }]
    }

    fn parse_hash_string(&self, s: &str) -> Result<ParsedHash, String> {
        if let Some(colon_pos) = s.find(':') {
            let hash_part = &s[..colon_pos];
            let username = &s[colon_pos + 1..];
            let clean = hash_part.strip_prefix("0x").unwrap_or(hash_part);
            if clean.len() != 32 {
                return Err(format!("Expected 32 hex chars for DCC, got {}", clean.len()));
            }
            let mut target = [0u32; 8];
            for i in 0..4 {
                target[i] = u32::from_str_radix(&clean[i * 8..i * 8 + 8], 16)
                    .map_err(|_| format!("Invalid hex at position {}", i * 8))?;
            }
            Ok(ParsedHash {
                hash_words: target,
                extra_words: [0u32; 8],
                salt: username.as_bytes().to_vec(),
                digest_words: 4,
            })
        } else {
            Err("DCC hash requires hash:username format".to_string())
        }
    }
}

pub fn dcc_hash(password: &str, username: &str) -> ([u32; 8], [u32; 8]) {
    let mut utf16 = Vec::with_capacity(password.len() * 2);
    for b in password.encode_utf16() {
        utf16.extend_from_slice(&b.to_le_bytes());
    }
    let mut inner = Md4::new();
    inner.update(&utf16);
    let ntlm_hash = inner.finalize();

    let mut outer = Md4::new();
    outer.update(&ntlm_hash);
    outer.update(username.as_bytes());
    let result = outer.finalize();

    let mut target = [0u32; 8];
    for i in 0..4 {
        target[i] = u32::from_le_bytes(result[i * 4..i * 4 + 4].try_into().unwrap());
    }
    (target, [0u32; 8])
}

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

    #[test]
    fn test_dcc_vector() {
        // Known DCC hash for password "test" with username "admin"
        // MD4(UTF16-LE("test")) = NTLM hash for "test"
        // Then MD4(NTLM_hash || "admin") = DCC hash
        let dcc = RawDcc;
        let (hash, _) = dcc_hash("test", "admin");
        let hex = format!("{:08x}{:08x}{:08x}{:08x}",
            hash[0], hash[1], hash[2], hash[3]);
        assert_eq!(hex.len(), 32);

        // Verify via cpu_verify
        let salt = "admin".as_bytes();
        assert!(dcc.cpu_verify("test", salt, &hash[..4]));

        // Wrong password should fail
        assert!(!dcc.cpu_verify("wrong", salt, &hash[..4]));

        // parse_hash_string round-trip
        let full = format!("{}:admin", hex);
        let parsed = dcc.parse_hash_string(&full).unwrap();
        assert_eq!(parsed.hash_words[..4], hash[..4]);
        assert_eq!(parsed.salt, b"admin");
    }
}