hashcracker 1.1.2

GPU-accelerated password cracker — 42 hash types, Vulkan/Metal/DX12, single binary
use std::path::PathBuf;
use std::str::FromStr;
use crate::hash_backend::{HashType, parse_hex_hash_opt};
use crate::hashes::registry;
use crate::potfile::Potfile;

pub fn suggest_hash_types(s: &str) -> String {
    let clean = s.trim().split(':').next().unwrap_or(s.trim());
    let suggestions: Vec<&str> = match clean.len() {
         8 => vec!["CRC32", "try --hash-type crc32"],
         32 => vec!["MD5 (common)", "NTLM", "MD4", "LM", "DCC", "try --hash-type md5, ntlm, or lm"],
         40 => vec!["SHA-1", "MySQL 4.1", "DB2 (hash:salt)", "try --hash-type sha1, mysql41, or db2"],
         56 => vec!["SHA-224", "try --hash-type sha224"],
         64 => vec!["SHA-256", "SHA-256d / Bitcoin", "try --hash-type sha256 or sha256d"],
         96 => vec!["SHA-384", "try --hash-type sha384"],
        128 => vec!["SHA-512", "SHA-512d", "HMAC-SHA512", "try --hash-type sha512, sha512d, or hmac-sha512"],
        _ if clean.starts_with('$') => vec!["Unrecognized $prefix — check hash format"],
        _ => vec!["Could be MD5 (common), NTLM, or MD4; try --hash-type md5"],
    };
    suggestions.join("; ")
}

pub fn maybe_suggest(s: &str) {
    let suggestion = suggest_hash_types(s);
    eprintln!("  Hint: {}", suggestion);
}

pub fn salt_vec_to_arr(salt: &[u8]) -> ([u32; 16], u32) {
    let len = salt.len().min(16) as u32;
    let mut arr = [0u32; 16];
    for (i, &b) in salt.iter().rev().enumerate().take(len as usize) {
        arr[i] = b as u32;
    }
    (arr, len)
}

pub fn salt_from_arr(arr: &[u32; 16], len: u32) -> Vec<u8> {
    (0..len as usize)
        .map(|i| arr[len as usize - 1 - i] as u8)
        .collect()
}

pub fn parse_hashlist_file_opt(path: &PathBuf, preferred: Option<HashType>) -> Vec<(String, Option<String>, [u32; 8], [u32; 8], HashType)> {
    let content = std::fs::read_to_string(path).unwrap_or_else(|e| {
        eprintln!("Failed to read hashlist '{}': {}", path.display(), e);
        std::process::exit(1);
    });

    let mut entries: Vec<(String, Option<String>, [u32; 8], [u32; 8], HashType)> = Vec::new();
    let mut detected_type: Option<HashType> = None;

    for (lineno, line) in content.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }

        let (username, effective_line) = if let Some(colon_pos) = trimmed.find(':') {
            let candidate = &trimmed[..colon_pos];
            if !candidate.chars().all(|c| c.is_ascii_hexdigit()) && !candidate.starts_with('$') {
                (Some(candidate.to_string()), &trimmed[colon_pos + 1..])
            } else {
                (None, trimmed)
            }
        } else {
            (None, trimmed)
        };

        let (th, the, ht) = if effective_line.starts_with('$') {
            if let Some(module) = registry::autodetect(effective_line) {
                let ht = HashType::from_str(module.name()).unwrap_or_else(|_| {
                    eprintln!("Error on line {}: autodetected '{}' but cannot map", lineno + 1, module.name());
                    std::process::exit(1);
                });
                let parsed = ht.module().parse_hash_string(effective_line).unwrap_or_else(|e| {
                    eprintln!("Error on line {}: {}", lineno + 1, e);
                    std::process::exit(1);
                });
                (parsed.hash_words, parsed.extra_words, ht)
            } else {
                eprintln!("Error on line {}: unable to autodetect hash type from '{}'", lineno + 1, effective_line);
                maybe_suggest(effective_line);
                std::process::exit(1);
            }
        } else if effective_line.contains(':') {
            if let Some(module) = registry::autodetect(effective_line) {
                let ht = HashType::from_str(module.name()).unwrap_or_else(|_| {
                    eprintln!("Error on line {}: autodetected '{}' but cannot map", lineno + 1, module.name());
                    std::process::exit(1);
                });
                let parsed = ht.module().parse_hash_string(effective_line).unwrap_or_else(|e| {
                    eprintln!("Error on line {}: {}", lineno + 1, e);
                    std::process::exit(1);
                });
                (parsed.hash_words, parsed.extra_words, ht)
            } else {
                eprintln!("Error on line {}: unable to autodetect hash type from '{}'", lineno + 1, effective_line);
                maybe_suggest(effective_line);
                std::process::exit(1);
            }
        } else {
            parse_hex_hash_opt(effective_line, preferred).unwrap_or_else(|e| {
                eprintln!("Error on line {} of hashlist '{}': {}", lineno + 1, path.display(), e);
                maybe_suggest(effective_line);
                std::process::exit(1);
            })
        };
        if let Some(ref dt) = detected_type {
            if &ht != dt {
                eprintln!("Error on line {}: mixed hash types detected ({} vs {:?})", lineno + 1, ht.name(), dt);
                std::process::exit(1);
            }
        } else {
            detected_type = Some(ht);
        }
        entries.push((effective_line.to_string(), username, th, the, ht));
    }

    if entries.is_empty() {
        eprintln!("Hashlist '{}' is empty", path.display());
        std::process::exit(1);
    }

    entries
}

pub fn read_wordlist_words(loopback: bool, wordlist_path: Option<PathBuf>, potfile: &Potfile) -> Vec<String> {
    let words: Vec<String> = if loopback {
        potfile.entries().iter()
            .map(|(_, pw)| pw.to_string())
            .filter(|w| !w.is_empty())
            .collect()
    } else {
        let path = wordlist_path.unwrap_or_else(|| {
            std::process::exit(1);
        });
        let content = std::fs::read_to_string(&path).unwrap_or_else(|e| {
            eprintln!("Failed to read wordlist '{}': {}", path.display(), e);
            std::process::exit(1);
        });
        content.lines()
            .map(|l| l.trim().to_string())
            .filter(|l| !l.is_empty() && l.len() <= 4)
            .collect()
    };
    if words.is_empty() {
        eprintln!("No words available (wordlist is empty or all filtered out)");
        std::process::exit(1);
    }
    words
}

pub fn save_session_state(
    sess: &crate::session::Session,
    hash_type: &str,
    args: &crate::cli::Args,
    entry_hex: &str,
    salt_arr: &[u32; 16],
    salt_len: u32,
    password_len: u32,
    keyspace: u32,
    progress: u32,
    total_found: &[(usize, String, String)],
) {
    let salt = salt_from_arr(salt_arr, salt_len);
    let salt_str = String::from_utf8_lossy(&salt).to_string();
    let state = crate::session::SessionState {
        hash_type: hash_type.to_string(),
        attack_mode: args.mode.clone(),
        target_hash: entry_hex.to_string(),
        salt: salt_str,
        password_len,
        keyspace: keyspace as u64,
        progress: progress as u64,
        mask: args.mask.clone(),
        wordlist: args.wordlist.as_ref().map(|p| p.to_string_lossy().to_string()),
        rules_file: args.rules.as_ref().map(|p| p.to_string_lossy().to_string()),
        timestamp: std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs().to_string())
            .unwrap_or_default(),
        cracked_hashes: total_found.iter().map(|(_, h, p)| (h.clone(), p.clone())).collect(),
    };
    if let Err(e) = sess.save(&state) {
        eprintln!("Warning: failed to save session: {}", e);
    }
}