hashcracker 1.1.3

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

pub struct RawDcc2;

impl HashModule for RawDcc2 {
    fn name(&self) -> &'static str { "dcc2" }
    fn mode(&self) -> u32 { 2100 }
    fn digest_words(&self) -> u32 { 5 }
    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 = Sha1::new();
        outer.update(&ntlm_hash);
        outer.update(salt);
        let result = outer.finalize();

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

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

    fn detect_patterns(&self) -> &[HashPattern] {
        &[HashPattern { prefix: None, hex_len: Some(40), 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() != 40 {
                return Err(format!("Expected 40 hex chars for DCC2, got {}", clean.len()));
            }
            let mut target = [0u32; 8];
            for i in 0..5 {
                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: 5,
            })
        } else {
            Err("DCC2 hash requires hash:username format".to_string())
        }
    }
}

pub fn dcc2_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 = Sha1::new();
    outer.update(&ntlm_hash);
    outer.update(username.as_bytes());
    let result = outer.finalize();

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

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

    #[test]
    fn test_dcc2_vector() {
        let dcc2 = RawDcc2;
        let (hash, _) = dcc2_hash("test", "admin");
        let hex = format!("{:08x}{:08x}{:08x}{:08x}{:08x}",
            hash[0], hash[1], hash[2], hash[3], hash[4]);
        assert_eq!(hex.len(), 40);

        let salt = "admin".as_bytes();
        assert!(dcc2.cpu_verify("test", salt, &hash[..5]));

        assert!(!dcc2.cpu_verify("wrong", salt, &hash[..5]));

        let full = format!("{}:admin", hex);
        let parsed = dcc2.parse_hash_string(&full).unwrap();
        assert_eq!(parsed.hash_words[..5], hash[..5]);
        assert_eq!(parsed.salt, b"admin");
    }
}