hashcracker 0.1.0

GPU-accelerated password cracker — 41 hash types, Vulkan/Metal/DX12, single binary
#![allow(deprecated)]
use crate::hashes::{AttackModeType, HashModule, HashPattern, ParsedHash};
use aes::Aes256Dec;
use cipher::block::BlockCipherDecrypt;
use cipher::typenum::consts::U32;
use cipher::{Block, KeyInit};

pub struct RawKeePass;

impl HashModule for RawKeePass {
    fn name(&self) -> &'static str { "keepass" }
    fn mode(&self) -> u32 { 13400 }
    fn digest_words(&self) -> u32 { 4 }
    fn needs_int64(&self) -> bool { false }
    fn cpu_verify(&self, password: &str, salt: &[u8], hash: &[u32]) -> bool {
        use sha2::{Digest, Sha256};

        let mut pos = 0;
        let version_end = salt[pos..].iter().position(|&b| b == b'*').unwrap_or(0);
        pos += version_end + 1;

        let master_seed = match salt.get(pos..pos + 32) { Some(s) => s, None => return false };
        pos += 32 + 1;
        let encrypted_key = match salt.get(pos..pos + 32) { Some(s) => s, None => return false };
        pos += 32 + 1;
        let iv = match salt.get(pos..pos + 16) { Some(s) => s, None => return false };
        pos += 16 + 1;
        let content_hash = match salt.get(pos..pos + 32) { Some(s) => s, None => return false };
        pos += 32 + 1;
        let transform_seed = match salt.get(pos..pos + 32) { Some(s) => s, None => return false };
        pos += 32 + 1;
        let rounds_bytes: [u8; 8] = match salt.get(pos..pos + 8) {
            Some(b) => match b.try_into() { Ok(r) => r, Err(_) => return false },
            None => return false,
        };
        let transform_rounds = u64::from_le_bytes(rounds_bytes);

        let key = Sha256::digest(password.as_bytes());
        let mut current = key;
        for _ in 0..transform_rounds {
            let mut hasher = Sha256::new();
            hasher.update(transform_seed);
            hasher.update(current);
            current = hasher.finalize();
        }

        let mut hasher = Sha256::new();
        hasher.update(master_seed);
        hasher.update(current);
        hasher.update(content_hash);
        let final_key = hasher.finalize();

        if encrypted_key.len() % 16 != 0 { return false; }
        let cipher = Aes256Dec::new(cipher::Array::<u8, U32>::from_slice(&final_key));
        let n = encrypted_key.len();
        let mut decrypted = vec![0u8; n];
        for i in (0..n).step_by(16) {
            let mut block = *Block::<Aes256Dec>::from_slice(&encrypted_key[i..i + 16]);
            cipher.decrypt_block(&mut block);
            let prev = if i == 0 { iv } else { &encrypted_key[i - 16..i] };
            for j in 0..16 {
                decrypted[i + j] = block[j] ^ prev[j];
            }
        }

        let computed_hash = Sha256::digest(&decrypted);
        computed_hash[..] == content_hash[..]
            && computed_hash[..16].chunks_exact(4).enumerate().all(|(i, chunk)| {
                u32::from_le_bytes(chunk.try_into().unwrap()) == hash[i]
            })
    }

    fn shader_source(&self, _mode: &AttackModeType) -> &'static str { "" }

    fn detect_patterns(&self) -> &[HashPattern] {
        &[HashPattern { prefix: Some("$keepass$"), hex_len: None, priority: 100 }]
    }

    fn parse_hash_string(&self, s: &str) -> Result<ParsedHash, String> {
        let parts: Vec<&str> = s.split('*').collect();
        if parts.len() < 7 || parts[0] != "$keepass$" {
            return Err("Invalid KeePass format".to_string());
        }
        let _version = parts[1];
        let master_seed = hex::decode(parts[2]).map_err(|_| "Invalid master seed hex".to_string())?;
        let encrypted_key = hex::decode(parts[3]).map_err(|_| "Invalid encrypted key hex".to_string())?;
        let iv = hex::decode(parts[4]).map_err(|_| "Invalid IV hex".to_string())?;
        let content_hash = hex::decode(parts[5]).map_err(|_| "Invalid content hash hex".to_string())?;
        let transform_seed = hex::decode(parts[6]).map_err(|_| "Invalid transform seed hex".to_string())?;
        let transform_rounds = if parts.len() > 7 {
            parts[7].parse::<u64>().map_err(|_| "Invalid transform rounds".to_string())?
        } else {
            6000
        };

        if master_seed.len() != 32 {
            return Err("master seed must be 32 bytes".to_string());
        }
        if encrypted_key.len() != 32 {
            return Err("encrypted key must be 32 bytes".to_string());
        }
        if iv.len() != 16 {
            return Err("IV must be 16 bytes".to_string());
        }
        if content_hash.len() != 32 {
            return Err("content hash must be 32 bytes".to_string());
        }
        if transform_seed.len() != 32 {
            return Err("transform seed must be 32 bytes".to_string());
        }

        let mut salt = Vec::new();
        salt.extend_from_slice(_version.as_bytes());
        salt.push(b'*');
        salt.extend_from_slice(&master_seed);
        salt.push(b'*');
        salt.extend_from_slice(&encrypted_key);
        salt.push(b'*');
        salt.extend_from_slice(&iv);
        salt.push(b'*');
        salt.extend_from_slice(&content_hash);
        salt.push(b'*');
        salt.extend_from_slice(&transform_seed);
        salt.push(b'*');
        salt.extend_from_slice(&transform_rounds.to_le_bytes());

        let hash_words = [
            u32::from_le_bytes(content_hash[0..4].try_into().unwrap()),
            u32::from_le_bytes(content_hash[4..8].try_into().unwrap()),
            u32::from_le_bytes(content_hash[8..12].try_into().unwrap()),
            u32::from_le_bytes(content_hash[12..16].try_into().unwrap()),
            0, 0, 0, 0,
        ];

        Ok(ParsedHash { hash_words, extra_words: [0u32; 8], salt, digest_words: 4 })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use aes::Aes256Enc;
    use cipher::block::BlockCipherEncrypt;
    use sha2::{Digest, Sha256};

    fn aes256_cbc_encrypt(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Vec<u8> {
        let cipher = Aes256Enc::new(cipher::Array::<u8, U32>::from_slice(key));
        let n = plaintext.len();
        let mut result = vec![0u8; n];
        let mut prev = iv.to_vec();
        for i in (0..n).step_by(16) {
            let mut block = *Block::<Aes256Enc>::from_slice(&plaintext[i..i + 16]);
            for j in 0..16 { block[j] ^= prev[j]; }
            cipher.encrypt_block(&mut block);
            result[i..i + 16].copy_from_slice(&block);
            prev.copy_from_slice(&block);
        }
        result
    }

    fn make_keepass_hash(
        password: &str,
        master_seed_hex: &str,
        iv_hex: &str,
        transform_seed_hex: &str,
        transform_rounds: u64,
    ) -> String {
        let master_seed = hex::decode(master_seed_hex).unwrap();
        let iv = hex::decode(iv_hex).unwrap();
        let transform_seed = hex::decode(transform_seed_hex).unwrap();

        let plaintext_key: [u8; 32] = [
            0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
            0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
            0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE,
            0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01,
        ];

        let content_hash = Sha256::digest(&plaintext_key);

        let key = Sha256::digest(password.as_bytes());
        let mut current = key;
        for _ in 0..transform_rounds {
            let mut hasher = Sha256::new();
            hasher.update(&transform_seed);
            hasher.update(current);
            current = hasher.finalize();
        }
        let mut hasher = Sha256::new();
        hasher.update(&master_seed);
        hasher.update(current);
        hasher.update(&content_hash);
        let final_key = hasher.finalize();

        let encrypted = aes256_cbc_encrypt(&final_key, &iv, &plaintext_key);

        format!(
            "$keepass$*1*{}*{}*{}*{}*{}*{}",
            hex::encode(master_seed),
            hex::encode(&encrypted[..32]),
            hex::encode(iv),
            hex::encode(content_hash),
            hex::encode(transform_seed),
            transform_rounds,
        )
    }

    #[test]
    fn test_keepass_parse_valid_format() {
        let m = RawKeePass;
        let hash_str = make_keepass_hash(
            "testpassword",
            "90DFF48AD45DB0F5E0FC8B0A9B1C1F5A90DFF48AD45DB0F5E0FC8B0A9B1C1F5A",
            "D7B3A5F2E8C4A0B1D6E9F3C8B2A5D4E7",
            "B1C3D5E7F9A0B2C4D6E8F0A2B4C6D8E0B1C3D5E7F9A0B2C4D6E8F0A2B4C6D8E0",
            6000,
        );

        let parsed = m.parse_hash_string(&hash_str).unwrap();
        assert_eq!(parsed.digest_words, 4);
        assert!(parsed.salt.len() > 0);
        assert!(m.cpu_verify("testpassword", &parsed.salt, &parsed.hash_words));
        assert!(!m.cpu_verify("wrongpassword", &parsed.salt, &parsed.hash_words));
    }

    #[test]
    fn test_keepass_starts_with_prefix() {
        let m = RawKeePass;
        assert_eq!(m.detect_patterns()[0].prefix.unwrap(), "$keepass$");
    }

    #[test]
    fn test_keepass_parse_bad_hex() {
        let m = RawKeePass;
        let s = "$keepass$*1*ZZ*encrypted_key_hex*iv_hex*content_hash_hex*transform_seed_hex*6000";
        assert!(m.parse_hash_string(s).is_err());
    }

    #[test]
    fn test_keepass_parse_wrong_prefix() {
        let m = RawKeePass;
        let s = "$invalid$*1*aa*bb*cc*dd*ee*6000";
        assert!(m.parse_hash_string(s).is_err());
    }
}