Skip to main content

sqlite_forensic/
blob.rs

1//! Recovered-BLOB enrichment (roadmap §4.5): make carved binary payloads
2//! addressable in a case by a magic-based media type and a SHA-256 content hash.
3//!
4//! The media detection reads only documented file-format **magic numbers** (each
5//! cited below) from the leading bytes — a signature match, not a decode, so it is
6//! panic-free and never mis-parses a truncated payload. The hash is SHA-256 via
7//! the audited `RustCrypto` `sha2` crate (never a hand-rolled digest), giving the
8//! court-standard content address every recovered image/document can be keyed on.
9
10use sha2::{Digest, Sha256};
11
12/// Identify a BLOB's media type from its leading magic bytes, or `None` when no
13/// known signature matches (or the payload is too short to carry one).
14///
15/// Signature match only — it does NOT validate the rest of the file, so a
16/// truncated or partially-overwritten carve still types by its surviving header.
17/// The type is an *observation* ("the leading bytes are a PNG signature"), never a
18/// guarantee the whole payload decodes.
19#[must_use]
20pub fn identify_media_type(bytes: &[u8]) -> Option<&'static str> {
21    // RIFF containers carry the concrete type at bytes 8..12 (e.g. WEBP, WAVE).
22    if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" {
23        match &bytes[8..12] {
24            b"WEBP" => return Some("image/webp"),
25            b"WAVE" => return Some("audio/wav"),
26            b"AVI " => return Some("video/x-msvideo"),
27            _ => {}
28        }
29    }
30    // ISO base-media (MP4 and friends): `....ftyp` at offset 4.
31    if bytes.len() >= 8 && &bytes[4..8] == b"ftyp" {
32        return Some("video/mp4");
33    }
34    // Prefix signatures, longest/most-specific first. Each magic is a documented
35    // file-format constant.
36    const SIGNATURES: &[(&[u8], &str)] = &[
37        (b"\x89PNG\r\n\x1a\n", "image/png"), // PNG, RFC 2083 §3.1
38        (b"\xFF\xD8\xFF", "image/jpeg"),     // JPEG/JFIF/EXIF SOI + marker
39        (b"GIF87a", "image/gif"),            // GIF87a
40        (b"GIF89a", "image/gif"),            // GIF89a
41        (b"BM", "image/bmp"),                // BMP (Windows bitmap)
42        (b"II\x2a\x00", "image/tiff"),       // TIFF little-endian
43        (b"MM\x00\x2a", "image/tiff"),       // TIFF big-endian
44        (b"%PDF-", "application/pdf"),       // PDF, ISO 32000 §7.5.2
45        (b"\x1f\x8b", "application/gzip"),   // gzip, RFC 1952 §2.3.1
46        (b"PK\x03\x04", "application/zip"),  // ZIP local file header
47        (b"BZh", "application/x-bzip2"),     // bzip2
48        (b"7z\xbc\xaf\x27\x1c", "application/x-7z-compressed"), // 7-Zip
49        (b"SQLite format 3\x00", "application/vnd.sqlite3"), // SQLite db header
50        (b"OggS", "application/ogg"),        // Ogg
51        (b"fLaC", "audio/flac"),             // FLAC
52        (b"ID3", "audio/mpeg"),              // MP3 with ID3 tag
53        (b"\x25\x21PS", "application/postscript"), // PostScript "%!PS"
54        (
55            b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1",
56            "application/x-ole-storage",
57        ), // OLE (legacy .doc/.xls)
58    ];
59    SIGNATURES
60        .iter()
61        .find(|(magic, _)| bytes.starts_with(magic))
62        .map(|(_, mime)| *mime)
63}
64
65/// The lowercase hex SHA-256 of `bytes` — the court-standard content address for a
66/// recovered BLOB, computed with the audited `RustCrypto` `sha2` implementation.
67#[must_use]
68pub fn sha256_hex(bytes: &[u8]) -> String {
69    let digest = Sha256::digest(bytes);
70    let mut out = String::with_capacity(digest.len() * 2);
71    for byte in digest {
72        // Two lowercase hex nibbles per byte.
73        out.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0'));
74        out.push(char::from_digit(u32::from(byte & 0x0f), 16).unwrap_or('0'));
75    }
76    out
77}