Skip to main content

happy_cracking/crypto/
filetype.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3use std::path::PathBuf;
4
5#[derive(Subcommand)]
6pub enum FiletypeAction {
7    #[command(about = "Identify file type from magic bytes")]
8    Identify {
9        #[arg(help = "Input as hex string (or use --file)")]
10        input: Option<String>,
11        #[arg(long, help = "Read input from a file path")]
12        file: Option<PathBuf>,
13    },
14}
15
16pub struct Signature {
17    pub name: &'static str,
18    pub description: &'static str,
19    pub offset: usize,
20}
21
22pub fn run(action: FiletypeAction) -> Result<()> {
23    match action {
24        FiletypeAction::Identify { input, file } => {
25            let data = match (input, file) {
26                (Some(_), Some(_)) => {
27                    anyhow::bail!("Provide exactly one of <input> or --file, not both")
28                }
29                (None, None) => {
30                    anyhow::bail!("Provide exactly one of <input> or --file")
31                }
32                (Some(hex_str), None) => {
33                    hex::decode(hex_str.trim()).context("Failed to decode input as hex")?
34                }
35                (None, Some(path)) => std::fs::read(&path)
36                    .with_context(|| format!("Failed to read file: {}", path.display()))?,
37            };
38
39            let matches = identify(&data);
40            if matches.is_empty() {
41                println!("No known file type signature matched");
42            } else {
43                for sig in matches {
44                    println!("{} - {} (offset {})", sig.name, sig.description, sig.offset);
45                }
46            }
47        }
48    }
49    Ok(())
50}
51
52fn matches_at(data: &[u8], offset: usize, magic: &[u8]) -> bool {
53    data.len() >= offset + magic.len() && &data[offset..offset + magic.len()] == magic
54}
55
56fn starts_with(data: &[u8], magic: &[u8]) -> bool {
57    matches_at(data, 0, magic)
58}
59
60pub fn identify(data: &[u8]) -> Vec<Signature> {
61    let mut out = Vec::new();
62
63    let push = |out: &mut Vec<Signature>, name, description, offset| {
64        out.push(Signature {
65            name,
66            description,
67            offset,
68        });
69    };
70
71    if starts_with(data, &[0x89, 0x50, 0x4E, 0x47]) {
72        push(&mut out, "PNG", "Portable Network Graphics image", 0);
73    }
74    if starts_with(data, &[0xFF, 0xD8, 0xFF]) {
75        push(&mut out, "JPEG", "JPEG image", 0);
76    }
77    if starts_with(data, b"GIF87a") {
78        push(&mut out, "GIF", "GIF image (87a)", 0);
79    }
80    if starts_with(data, b"GIF89a") {
81        push(&mut out, "GIF", "GIF image (89a)", 0);
82    }
83    if starts_with(data, b"BM") {
84        push(&mut out, "BMP", "Bitmap image", 0);
85    }
86    if starts_with(data, &[0x00, 0x00, 0x01, 0x00]) {
87        push(&mut out, "ICO", "Windows icon", 0);
88    }
89
90    if starts_with(data, b"%PDF") {
91        push(&mut out, "PDF", "Portable Document Format", 0);
92    }
93
94    if starts_with(data, &[0x50, 0x4B, 0x03, 0x04]) {
95        push(&mut out, "ZIP", "ZIP archive (also JAR/APK/DOCX/etc.)", 0);
96    }
97    if starts_with(data, &[0x50, 0x4B, 0x05, 0x06]) {
98        push(&mut out, "ZIP", "ZIP archive (empty)", 0);
99    }
100    if starts_with(data, &[0x50, 0x4B, 0x07, 0x08]) {
101        push(&mut out, "ZIP", "ZIP archive (spanned)", 0);
102    }
103    if starts_with(data, &[0x1F, 0x8B]) {
104        push(&mut out, "GZIP", "gzip compressed data", 0);
105    }
106    if starts_with(data, b"BZh") {
107        push(&mut out, "BZIP2", "bzip2 compressed data", 0);
108    }
109    if starts_with(data, &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]) {
110        push(&mut out, "XZ", "XZ compressed data", 0);
111    }
112    if starts_with(data, &[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) {
113        push(&mut out, "7Z", "7-Zip archive", 0);
114    }
115    if starts_with(data, &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]) {
116        push(&mut out, "RAR", "RAR archive (v5)", 0);
117    } else if starts_with(data, &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]) {
118        push(&mut out, "RAR", "RAR archive (v4)", 0);
119    }
120
121    if starts_with(data, &[0x7F, 0x45, 0x4C, 0x46]) {
122        push(&mut out, "ELF", "ELF executable", 0);
123    }
124    if starts_with(data, b"MZ") {
125        push(&mut out, "PE", "DOS/PE executable (MZ)", 0);
126    }
127    if starts_with(data, &[0xCA, 0xFE, 0xBA, 0xBE]) {
128        push(&mut out, "Java class", "Java class file", 0);
129        push(&mut out, "Mach-O", "Mach-O universal (fat) binary", 0);
130    }
131    if starts_with(data, &[0xFE, 0xED, 0xFA, 0xCE]) {
132        push(&mut out, "Mach-O", "Mach-O executable (32-bit)", 0);
133    }
134    if starts_with(data, &[0xFE, 0xED, 0xFA, 0xCF]) {
135        push(&mut out, "Mach-O", "Mach-O executable (64-bit)", 0);
136    }
137
138    if starts_with(data, &[0x49, 0x44, 0x33]) {
139        push(&mut out, "MP3", "MP3 audio (ID3 tag)", 0);
140    }
141    if starts_with(data, &[0xFF, 0xFB]) {
142        push(&mut out, "MP3", "MP3 audio (MPEG frame)", 0);
143    }
144    if starts_with(data, b"OggS") {
145        push(&mut out, "OGG", "Ogg container", 0);
146    }
147    if starts_with(data, b"fLaC") {
148        push(&mut out, "FLAC", "FLAC audio", 0);
149    }
150    if starts_with(data, b"RIFF") {
151        if matches_at(data, 8, b"WAVE") {
152            push(&mut out, "WAV", "WAVE audio (RIFF)", 0);
153        } else if matches_at(data, 8, b"WEBP") {
154            push(&mut out, "WEBP", "WebP image (RIFF)", 0);
155        } else if matches_at(data, 8, b"AVI ") {
156            push(&mut out, "AVI", "AVI video (RIFF)", 0);
157        } else {
158            push(&mut out, "RIFF", "RIFF container (unknown form)", 0);
159        }
160    }
161
162    if starts_with(data, b"SQLite format 3\0") {
163        push(&mut out, "SQLite", "SQLite 3 database", 0);
164    }
165
166    if matches_at(data, 257, b"ustar") {
167        push(&mut out, "TAR", "tar archive (ustar)", 257);
168    }
169
170    out
171}