happy-cracking 0.5.0

A fast, comprehensive CTF toolkit for cryptographic encoding/decoding, classic ciphers, hash operations, and analysis tools
Documentation
use anyhow::{Context, Result};
use clap::Subcommand;
use std::path::PathBuf;

#[derive(Subcommand)]
pub enum FiletypeAction {
    #[command(about = "Identify file type from magic bytes")]
    Identify {
        #[arg(help = "Input as hex string (or use --file)")]
        input: Option<String>,
        #[arg(long, help = "Read input from a file path")]
        file: Option<PathBuf>,
    },
}

pub struct Signature {
    pub name: &'static str,
    pub description: &'static str,
    pub offset: usize,
}

pub fn run(action: FiletypeAction) -> Result<()> {
    match action {
        FiletypeAction::Identify { input, file } => {
            let data = match (input, file) {
                (Some(_), Some(_)) => {
                    anyhow::bail!("Provide exactly one of <input> or --file, not both")
                }
                (None, None) => {
                    anyhow::bail!("Provide exactly one of <input> or --file")
                }
                (Some(hex_str), None) => {
                    hex::decode(hex_str.trim()).context("Failed to decode input as hex")?
                }
                (None, Some(path)) => std::fs::read(&path)
                    .with_context(|| format!("Failed to read file: {}", path.display()))?,
            };

            let matches = identify(&data);
            if matches.is_empty() {
                println!("No known file type signature matched");
            } else {
                for sig in matches {
                    println!("{} - {} (offset {})", sig.name, sig.description, sig.offset);
                }
            }
        }
    }
    Ok(())
}

fn matches_at(data: &[u8], offset: usize, magic: &[u8]) -> bool {
    data.len() >= offset + magic.len() && &data[offset..offset + magic.len()] == magic
}

fn starts_with(data: &[u8], magic: &[u8]) -> bool {
    matches_at(data, 0, magic)
}

pub fn identify(data: &[u8]) -> Vec<Signature> {
    let mut out = Vec::new();

    let push = |out: &mut Vec<Signature>, name, description, offset| {
        out.push(Signature {
            name,
            description,
            offset,
        });
    };

    if starts_with(data, &[0x89, 0x50, 0x4E, 0x47]) {
        push(&mut out, "PNG", "Portable Network Graphics image", 0);
    }
    if starts_with(data, &[0xFF, 0xD8, 0xFF]) {
        push(&mut out, "JPEG", "JPEG image", 0);
    }
    if starts_with(data, b"GIF87a") {
        push(&mut out, "GIF", "GIF image (87a)", 0);
    }
    if starts_with(data, b"GIF89a") {
        push(&mut out, "GIF", "GIF image (89a)", 0);
    }
    if starts_with(data, b"BM") {
        push(&mut out, "BMP", "Bitmap image", 0);
    }
    if starts_with(data, &[0x00, 0x00, 0x01, 0x00]) {
        push(&mut out, "ICO", "Windows icon", 0);
    }

    if starts_with(data, b"%PDF") {
        push(&mut out, "PDF", "Portable Document Format", 0);
    }

    if starts_with(data, &[0x50, 0x4B, 0x03, 0x04]) {
        push(&mut out, "ZIP", "ZIP archive (also JAR/APK/DOCX/etc.)", 0);
    }
    if starts_with(data, &[0x50, 0x4B, 0x05, 0x06]) {
        push(&mut out, "ZIP", "ZIP archive (empty)", 0);
    }
    if starts_with(data, &[0x50, 0x4B, 0x07, 0x08]) {
        push(&mut out, "ZIP", "ZIP archive (spanned)", 0);
    }
    if starts_with(data, &[0x1F, 0x8B]) {
        push(&mut out, "GZIP", "gzip compressed data", 0);
    }
    if starts_with(data, b"BZh") {
        push(&mut out, "BZIP2", "bzip2 compressed data", 0);
    }
    if starts_with(data, &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]) {
        push(&mut out, "XZ", "XZ compressed data", 0);
    }
    if starts_with(data, &[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) {
        push(&mut out, "7Z", "7-Zip archive", 0);
    }
    if starts_with(data, &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]) {
        push(&mut out, "RAR", "RAR archive (v5)", 0);
    } else if starts_with(data, &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]) {
        push(&mut out, "RAR", "RAR archive (v4)", 0);
    }

    if starts_with(data, &[0x7F, 0x45, 0x4C, 0x46]) {
        push(&mut out, "ELF", "ELF executable", 0);
    }
    if starts_with(data, b"MZ") {
        push(&mut out, "PE", "DOS/PE executable (MZ)", 0);
    }
    if starts_with(data, &[0xCA, 0xFE, 0xBA, 0xBE]) {
        push(&mut out, "Java class", "Java class file", 0);
        push(&mut out, "Mach-O", "Mach-O universal (fat) binary", 0);
    }
    if starts_with(data, &[0xFE, 0xED, 0xFA, 0xCE]) {
        push(&mut out, "Mach-O", "Mach-O executable (32-bit)", 0);
    }
    if starts_with(data, &[0xFE, 0xED, 0xFA, 0xCF]) {
        push(&mut out, "Mach-O", "Mach-O executable (64-bit)", 0);
    }

    if starts_with(data, &[0x49, 0x44, 0x33]) {
        push(&mut out, "MP3", "MP3 audio (ID3 tag)", 0);
    }
    if starts_with(data, &[0xFF, 0xFB]) {
        push(&mut out, "MP3", "MP3 audio (MPEG frame)", 0);
    }
    if starts_with(data, b"OggS") {
        push(&mut out, "OGG", "Ogg container", 0);
    }
    if starts_with(data, b"fLaC") {
        push(&mut out, "FLAC", "FLAC audio", 0);
    }
    if starts_with(data, b"RIFF") {
        if matches_at(data, 8, b"WAVE") {
            push(&mut out, "WAV", "WAVE audio (RIFF)", 0);
        } else if matches_at(data, 8, b"WEBP") {
            push(&mut out, "WEBP", "WebP image (RIFF)", 0);
        } else if matches_at(data, 8, b"AVI ") {
            push(&mut out, "AVI", "AVI video (RIFF)", 0);
        } else {
            push(&mut out, "RIFF", "RIFF container (unknown form)", 0);
        }
    }

    if starts_with(data, b"SQLite format 3\0") {
        push(&mut out, "SQLite", "SQLite 3 database", 0);
    }

    if matches_at(data, 257, b"ustar") {
        push(&mut out, "TAR", "tar archive (ustar)", 257);
    }

    out
}