Skip to main content

usb_forensic/sources/
device_image.rs

1//! Source: a physical device's own boot sectors (a raw disk image) → USB-history
2//! [`Claim`]s — the strongest device attribution.
3#![allow(clippy::doc_markdown)] // forensic proper nouns (BitLocker, FVE, …) read cleaner bare
4//!
5//! When the suspect USB device itself is imaged, its **MBR disk signature** and **FAT
6//! volume serial** tie it directly to the host's footprint: the disk signature matches a
7//! `MountedDevices` MBR record (→ drive letter, volume GUID), and the FAT volume serial
8//! matches an `EMDMgmt`/`.lnk` volume serial (→ label, files opened). This closes the loop
9//! that host artifacts alone cannot — attributing a *physical device in evidence* to what
10//! it did on the machine.
11//!
12//! Partition-scheme dispatch (MBR / GPT / APM) and filesystem-signature detection are
13//! delegated to the [`disk_forensic`] crate, which is tested against real disk corpora
14//! covering every partition-table + filesystem combination — so this source never re-parses
15//! a partition table by hand (a GPT protective MBR is recognized as GPT, not mis-walked as
16//! MBR partitions). On top of that partition list this source reads two USB-attribution
17//! values, each from a **volume**-analysis function owned by [`forensicnomicon`] (the fleet
18//! knowledge base), because a volume's serial and its encryption are properties of the
19//! volume, not of the bus or partition scheme (ADR 0003):
20//!
21//! - the FAT/exFAT **volume serial** ([`forensicnomicon::volume_serial`]) — the 4-byte
22//!   `EMDMgmt`/`.lnk` join key; and
23//! - **BitLocker** ([`forensicnomicon::volume_encryption`]) — fixed-drive `-FVE-FS-` and, the
24//!   case that matters most for removable media, **BitLocker To Go**, whose discovery volume
25//!   presents a real FAT boot record so only the identifier GUID reveals it.
26//!
27//! A [`disk_forensic`]-reported LUKS or unrecognized filesystem is surfaced likewise. This
28//! source holds no BitLocker signatures or field offsets of its own; the knowledge lives in
29//! forensicnomicon, validated there and cross-checked here against real unencrypted media
30//! (which must NOT false-positive).
31
32use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
33use disk_forensic::{analyse_disk, DiskReport};
34use forensicnomicon::volume_serial::VolumeSerial;
35use mbr_partition_forensic::DetectedFs;
36use std::io::{Read, Seek, SeekFrom};
37
38/// A detected volume-encryption / inaccessible-contents state.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum EncryptionKind {
41    /// Microsoft BitLocker on a **fixed drive** (or an NTFS-on-removable volume): the VBR
42    /// OEM identifier at offset 3 is the documented `-FVE-FS-` signature (Windows Vista
43    /// `EB 52 90` / 7-10 `EB 58 90`; see the module reference).
44    BitLocker,
45    /// Microsoft **BitLocker To Go** on removable media (the USB-forensics case): the
46    /// discovery volume presents a normal FAT/exFAT OEM identifier, so it is identified by
47    /// the BitLocker identifier GUID `4967D63B-2E29-4AD8-8399-F6A339E3D001` carried in the
48    /// volume header — not by the `-FVE-FS-` string (libbde BDE format, volume header).
49    BitLockerToGo,
50    /// A LUKS-encrypted volume (`LUKS\xba\xbe` magic) — Linux full-disk encryption on the
51    /// media, surfaced by the filesystem-signature detector.
52    Luks,
53    /// A partition whose VBR matches **no known filesystem** signature (not NTFS, FAT,
54    /// exFAT, LUKS, or BitLocker). Consistent with an on-disk encrypted container (VeraCrypt
55    /// / TrueCrypt, whose volume is indistinguishable from random data and carries no
56    /// filesystem header) or a wiped/raw volume — the contents are not readable as a
57    /// filesystem. Stated as an observation, not a claim that it *is* any specific tool.
58    UnrecognizedFilesystem,
59}
60
61impl EncryptionKind {
62    /// A stable display name.
63    #[must_use]
64    pub const fn name(self) -> &'static str {
65        match self {
66            Self::BitLocker => "BitLocker",
67            Self::BitLockerToGo => "BitLocker To Go",
68            Self::Luks => "LUKS",
69            Self::UnrecognizedFilesystem => {
70                "unrecognized-filesystem (possible encrypted container)"
71            }
72        }
73    }
74
75    /// Specificity rank, so that when several partitions carry different states the most
76    /// definite one is surfaced on the device: a positive BitLocker/LUKS identification
77    /// outranks a heuristic "unrecognized filesystem".
78    const fn rank(self) -> u8 {
79        match self {
80            Self::BitLocker | Self::BitLockerToGo => 3,
81            Self::Luks => 2,
82            Self::UnrecognizedFilesystem => 1,
83        }
84    }
85}
86
87/// A physical device's boot-sector identity, decoded from its raw disk image.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct DeviceImage {
90    /// MBR disk signature (4 bytes at offset `0x1B8`) — joins the `MountedDevices` bridge.
91    pub disk_signature: u32,
92    /// FAT volume serial (`BS_VolID`) of the first FAT partition, when present — the
93    /// 4-byte serial `EMDMgmt` and Shell Links store.
94    pub fat_volume_serial: Option<u32>,
95    /// The volume-encryption type, when a boot sector carries an encryption signature.
96    pub encryption: Option<EncryptionKind>,
97    /// The raw 512-byte MBR sector, retained for export/verification.
98    pub mbr: [u8; 512],
99}
100
101/// Decode a raw disk image's boot sectors from an in-memory slice — the convenience entry
102/// for a fully-read image (and the fuzz target). Wraps the slice in a cursor and defers to
103/// [`analyse_device_image`]. `None` when the image carries no MBR/GPT partition scheme.
104#[must_use]
105pub fn parse_boot_sectors(image: &[u8]) -> Option<DeviceImage> {
106    analyse_device_image(&mut std::io::Cursor::new(image), image.len() as u64)
107}
108
109/// Decode a device image's boot sectors from any seekable reader (a raw slice cursor or an
110/// [`ewf::EwfReader`] over an E01), sized by `disk_size`.
111///
112/// Partition-scheme detection and per-partition filesystem detection come from
113/// [`disk_forensic::analyse_disk`]; on top of its partition list this reads, per partition,
114/// the FAT `BS_VolID` (for the `EMDMgmt`/`.lnk` volume-serial join) and the BitLocker /
115/// LUKS / unrecognized-filesystem state. `None` when no MBR/GPT scheme is present (a
116/// non-disk input) or an Apple Partition Map (no Windows USB-attribution value).
117pub fn analyse_device_image<R: Read + Seek>(reader: &mut R, disk_size: u64) -> Option<DeviceImage> {
118    // disk-forensic owns scheme dispatch: a GPT protective MBR is parsed as GPT (partitions
119    // from the GPT entries), never mis-walked as MBR partitions. `Gpt` still carries the
120    // protective-MBR analysis (disk signature + partition list); `Apm` carries none.
121    let report = analyse_disk(reader, disk_size).ok()?;
122    let mbr = match &report {
123        DiskReport::Mbr(m) | DiskReport::Gpt(m) => m,
124        DiskReport::Apm(_) => return None,
125    };
126    // Partition byte-offsets from whichever table is authoritative for the scheme: for a GPT
127    // disk `mbr.partitions` holds only the protective `0xEE` entry, so the real partitions
128    // come from the GPT entry array (used entries only); otherwise the MBR partition table.
129    let disk_signature = mbr.disk_serial;
130    let mbr_bytes: [u8; 512] = read_region(reader, 0, 512)?.try_into().ok()?;
131    let mut fat_volume_serial = None;
132    let mut encryption: Option<EncryptionKind> = None;
133    let mut record = |serial: Option<VolumeSerial>, enc: Option<EncryptionKind>| {
134        if let Some(kind) = enc {
135            if encryption.is_none_or(|cur| kind.rank() > cur.rank()) {
136                encryption = Some(kind);
137            }
138        }
139        if fat_volume_serial.is_none() {
140            if let Some(VolumeSerial::Short(v)) = serial {
141                fat_volume_serial = Some(v);
142            }
143        }
144    };
145    match &mbr.gpt {
146        // MBR: consume the per-partition volume info disk-forensic/mbr-partition-forensic
147        // already decoded (ADR 0003) — no boot-record reading in this source.
148        None => {
149            for p in &mbr.partitions {
150                record(p.volume_serial, encryption_of(p.encryption, p.detected_fs));
151            }
152        }
153        // GPT: consume the same per-partition volume info, now carried on each `GptEntry`
154        // (populated by gpt-partition-forensic). `GptEntry` has no detected filesystem, so
155        // LUKS / unrecognized-container classification is not available for GPT partitions.
156        Some(g) => {
157            for e in g.partitions.iter().filter(|e| e.is_used()) {
158                record(e.volume_serial, encryption_of(e.encryption, None));
159            }
160        }
161    }
162    Some(DeviceImage {
163        disk_signature,
164        fat_volume_serial,
165        encryption,
166        mbr: mbr_bytes,
167    })
168}
169
170/// Seek to `offset` and read `len` bytes, zero-padding a short final read to `len`. `None`
171/// when the offset is at/after EOF (nothing readable) or the seek/read errors.
172fn read_region<R: Read + Seek>(reader: &mut R, offset: u64, len: usize) -> Option<Vec<u8>> {
173    reader.seek(SeekFrom::Start(offset)).ok()?;
174    let mut buf = vec![0u8; len];
175    let mut filled = 0;
176    while filled < len {
177        match reader.read(&mut buf[filled..]) {
178            Ok(0) => break,
179            Ok(n) => filled += n,
180            Err(_) => return None,
181        }
182    }
183    (filled != 0).then_some(buf)
184}
185
186/// Map a partition's pre-decoded volume-encryption + detected filesystem (as
187/// disk-forensic/mbr-partition-forensic already surface them) to this source's
188/// [`EncryptionKind`]. BitLocker comes from forensicnomicon's volume-encryption detection;
189/// LUKS / unrecognized follow the filesystem detector.
190fn encryption_of(
191    enc: Option<forensicnomicon::volume_encryption::VolumeEncryption>,
192    detected_fs: Option<DetectedFs>,
193) -> Option<EncryptionKind> {
194    use forensicnomicon::volume_encryption::VolumeEncryption;
195    if let Some(e) = enc {
196        return Some(match e {
197            VolumeEncryption::BitLocker => EncryptionKind::BitLocker,
198            VolumeEncryption::BitLockerToGo => EncryptionKind::BitLockerToGo,
199        });
200    }
201    match detected_fs {
202        Some(DetectedFs::Luks) => Some(EncryptionKind::Luks),
203        Some(DetectedFs::Unknown) => Some(EncryptionKind::UnrecognizedFilesystem),
204        _ => None,
205    }
206}
207
208/// A [`HistorySource`] over one decoded device image.
209pub struct DeviceImageSource<'a> {
210    image: &'a DeviceImage,
211    locator: String,
212}
213
214impl<'a> DeviceImageSource<'a> {
215    /// Wrap a decoded device image with the on-disk locator of the image it came from.
216    #[must_use]
217    pub fn new(image: &'a DeviceImage, locator: impl Into<String>) -> Self {
218        Self {
219            image,
220            locator: locator.into(),
221        }
222    }
223}
224
225/// Render a 4-byte serial as `XXXX-XXXX` (the canonical `vol` form other sources use).
226fn fmt_serial(serial: u32) -> String {
227    format!("{:04X}-{:04X}", serial >> 16, serial & 0xFFFF)
228}
229
230/// Export a device image's raw 512-byte MBR sector as an annotated hex dump (16 bytes per
231/// line, `offset  hex  |ascii|`), headed by the source locator and disk signature — for an
232/// examiner to inspect or archive the boot sector alongside the analysis.
233#[must_use]
234pub fn export_mbr_hex(image: &DeviceImage, locator: &str) -> String {
235    use std::fmt::Write as _;
236    let mut out = String::new();
237    let _ = writeln!(
238        out,
239        "MBR of {locator} (disk signature {})",
240        fmt_serial(image.disk_signature)
241    );
242    for (i, chunk) in image.mbr.chunks(16).enumerate() {
243        let hex = chunk.iter().fold(String::new(), |mut acc, b| {
244            let _ = write!(acc, "{b:02X} ");
245            acc
246        });
247        let ascii: String = chunk
248            .iter()
249            .map(|&b| {
250                if (0x20..0x7F).contains(&b) {
251                    b as char
252                } else {
253                    '.'
254                }
255            })
256            .collect();
257        let _ = writeln!(out, "{:08X}  {hex:<48} |{ascii}|", i * 16);
258    }
259    out
260}
261
262impl HistorySource for DeviceImageSource<'_> {
263    fn claims(&self) -> Vec<Claim> {
264        // Key the physical device by its MBR disk signature — a stable media identity.
265        let device = DeviceKey(format!("disk-{:08X}", self.image.disk_signature));
266        let make = |attribute, value| Claim {
267            device: device.clone(),
268            attribute,
269            value: Value::Text(value),
270            provenance: Provenance {
271                source: SourceKind::DeviceImage,
272                locator: self.locator.clone(),
273            },
274        };
275        let mut out = Vec::new();
276        // The FAT volume serial, so an EMDMgmt/LNK record (keyed by that serial) reconciles
277        // ONTO the physical device — carrying its label and the files opened from it.
278        if let Some(vsn) = self.image.fat_volume_serial {
279            out.push(make(Attribute::VolumeSerial, fmt_serial(vsn)));
280        }
281        // The volume-encryption type detected on the media — surfaced on the device record.
282        if let Some(enc) = self.image.encryption {
283            out.push(make(Attribute::Encryption, enc.name().to_string()));
284        }
285        out
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    /// A FAT32 VBR: `MSDOS5.0` OEM id (offset 3), `FAT32   ` FS signature (0x52), and
294    /// `BS_VolID` (0x43) — how a Windows-formatted FAT32 volume appears.
295    fn fat32_vbr(bs_volid: u32) -> [u8; 512] {
296        let mut vbr = [0u8; 512];
297        vbr[3..11].copy_from_slice(b"MSDOS5.0");
298        vbr[0x52..0x5A].copy_from_slice(b"FAT32   ");
299        vbr[0x43..0x47].copy_from_slice(&bs_volid.to_le_bytes());
300        vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
301        vbr
302    }
303
304    /// A FAT16 VBR: `MSDOS5.0` OEM id, no FAT32 signature, `BS_VolID` at 0x27.
305    fn fat16_vbr(bs_volid: u32) -> [u8; 512] {
306        let mut vbr = [0u8; 512];
307        vbr[3..11].copy_from_slice(b"MSDOS5.0");
308        vbr[0x36..0x3E].copy_from_slice(b"FAT16   "); // BS_FilSysType (a real FAT16 boot record)
309        vbr[0x27..0x2B].copy_from_slice(&bs_volid.to_le_bytes());
310        vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
311        vbr
312    }
313
314    /// An NTFS VBR: `NTFS    ` OEM id at offset 3.
315    fn ntfs_vbr() -> [u8; 512] {
316        let mut vbr = [0u8; 512];
317        vbr[3..11].copy_from_slice(b"NTFS    ");
318        vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
319        vbr
320    }
321
322    #[test]
323    fn parses_mbr_disk_signature_and_fat_volume_serial() {
324        let img = mbr_disk(0xE221_034C, 0x0B, 2, &fat32_vbr(0xB4D8_5399));
325        let d = parse_boot_sectors(&img).expect("valid MBR");
326        assert_eq!(d.disk_signature, 0xE221_034C);
327        assert_eq!(d.fat_volume_serial, Some(0xB4D8_5399));
328        assert_eq!(d.encryption, None);
329    }
330
331    #[test]
332    fn a_fat16_partition_reads_bs_volid_at_0x27() {
333        let img = mbr_disk(1, 0x06, 2, &fat16_vbr(0x1234_5678));
334        let d = parse_boot_sectors(&img).expect("valid MBR");
335        assert_eq!(d.fat_volume_serial, Some(0x1234_5678));
336    }
337
338    #[test]
339    fn a_non_mbr_image_is_rejected() {
340        assert_eq!(parse_boot_sectors(&[0u8; 512]), None);
341        assert_eq!(parse_boot_sectors(&[0u8; 10]), None);
342    }
343
344    #[test]
345    fn read_region_returns_none_past_eof_and_zero_pads_a_short_read() {
346        use std::io::Cursor;
347        // A seek past EOF reads nothing → None (a truncated/carved capture is skipped, not
348        // panicked on).
349        assert_eq!(
350            read_region(&mut Cursor::new(vec![0u8; 16]), 4096, 512),
351            None
352        );
353        // A short final read is zero-padded to the requested length and returned.
354        let mut backing = vec![0xAAu8; 512 + 100];
355        backing[512..].fill(0xBB);
356        let s = read_region(&mut Cursor::new(backing), 512, 512)
357            .expect("short read still yields a buffer");
358        assert_eq!(s.len(), 512);
359        assert_eq!(&s[..100], &[0xBBu8; 100]);
360        assert_eq!(&s[100..], &[0u8; 412]); // zero-padded tail
361    }
362
363    #[test]
364    fn read_region_propagates_a_read_error() {
365        // A reader that errors mid-read must surface as None (skip), never a panic.
366        struct FailingReader;
367        impl std::io::Read for FailingReader {
368            fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
369                Err(std::io::Error::other("boom"))
370            }
371        }
372        impl std::io::Seek for FailingReader {
373            fn seek(&mut self, _: std::io::SeekFrom) -> std::io::Result<u64> {
374                Ok(0)
375            }
376        }
377        assert_eq!(read_region(&mut FailingReader, 0, 512), None);
378    }
379
380    #[test]
381    fn the_most_specific_encryption_state_wins_across_partitions() {
382        // Three partitions in ascending specificity — unrecognized filesystem, LUKS, then
383        // BitLocker. Each definite identification outranks the less-specific one already
384        // recorded, so the device surfaces the most definite (BitLocker).
385        let mut img = mbr_disk(0x0AAA_0BBB, 0x07, 2, &[0xABu8; 512]); // p0: unrecognized
386        let mut luks = [0u8; 512];
387        luks[0..6].copy_from_slice(b"LUKS\xba\xbe");
388        for (i, lba, ptype, vbr) in [(1u8, 4u32, 0x83u8, luks), (2, 6, 0x07, bitlocker_vbr())] {
389            let e = 0x1BE + i as usize * 16;
390            img[e + 4] = ptype;
391            img[e + 8..e + 12].copy_from_slice(&lba.to_le_bytes());
392            img[e + 12..e + 16].copy_from_slice(&8u32.to_le_bytes());
393            let off = lba as usize * 512;
394            img[off..off + 512].copy_from_slice(&vbr);
395        }
396        let d = parse_boot_sectors(&img).expect("valid MBR");
397        assert_eq!(d.encryption, Some(EncryptionKind::BitLocker));
398    }
399
400    #[test]
401    fn the_first_fat_partitions_serial_is_kept() {
402        // Two FAT32 partitions: the first partition's serial is the device's; the second is
403        // not overwritten (the FAT volume serial is taken once).
404        let mut img = mbr_disk(0xF00D_0001, 0x0B, 2, &fat32_vbr(0x1111_2222));
405        let e = 0x1BE + 16;
406        img[e + 4] = 0x0B;
407        img[e + 8..e + 12].copy_from_slice(&8u32.to_le_bytes());
408        img[e + 12..e + 16].copy_from_slice(&8u32.to_le_bytes());
409        let off = 8 * 512;
410        img[off..off + 512].copy_from_slice(&fat32_vbr(0x9999_8888));
411        let d = parse_boot_sectors(&img).expect("valid MBR");
412        assert_eq!(d.fat_volume_serial, Some(0x1111_2222));
413    }
414
415    #[test]
416    fn a_partition_declared_beyond_the_image_is_skipped() {
417        // A valid FAT32 partition plus a second entry whose start LBA lies past the image end
418        // (a truncated capture): the first is read, the out-of-range VBR is skipped.
419        let mut img = mbr_disk(0xCAFE_0001, 0x0B, 2, &fat32_vbr(0xAABB_CCDD));
420        let e = 0x1BE + 16;
421        img[e + 4] = 0x07;
422        img[e + 8..e + 12].copy_from_slice(&9000u32.to_le_bytes()); // far beyond the image
423        img[e + 12..e + 16].copy_from_slice(&8u32.to_le_bytes());
424        let d = parse_boot_sectors(&img).expect("valid MBR");
425        assert_eq!(d.fat_volume_serial, Some(0xAABB_CCDD));
426    }
427
428    #[test]
429    fn an_ntfs_mbr_yields_the_disk_signature_with_no_fat_serial() {
430        let img = mbr_disk(0xDEAD_BEEF, 0x07, 2, &ntfs_vbr());
431        let d = parse_boot_sectors(&img).expect("valid MBR");
432        assert_eq!(d.disk_signature, 0xDEAD_BEEF);
433        assert_eq!(d.fat_volume_serial, None);
434        assert_eq!(d.encryption, None);
435    }
436
437    #[test]
438    fn source_emits_the_fat_volume_serial_keyed_by_disk_signature() {
439        let img = DeviceImage {
440            disk_signature: 0xE221_034C,
441            fat_volume_serial: Some(0xB4D8_5399),
442            encryption: None,
443            mbr: [0u8; 512],
444        };
445        let claims = DeviceImageSource::new(&img, "rm2.raw").claims();
446        assert_eq!(claims.len(), 1);
447        // Keyed by the media identity (disk signature); the value is the FAT volume serial
448        // that reconciles with an EMDMgmt/LNK record.
449        assert_eq!(claims[0].device, DeviceKey("disk-E221034C".to_string()));
450        assert_eq!(claims[0].attribute, Attribute::VolumeSerial);
451        assert_eq!(claims[0].value, Value::Text("B4D8-5399".to_string()));
452        assert_eq!(claims[0].provenance.source, SourceKind::DeviceImage);
453        assert_eq!(claims[0].provenance.locator, "rm2.raw");
454    }
455
456    #[test]
457    fn export_mbr_hex_dumps_the_boot_sector_with_signature_and_ascii() {
458        let img = mbr_disk(0xE221_034C, 0x0B, 2, &fat32_vbr(0xB4D8_5399));
459        let d = parse_boot_sectors(&img).expect("valid MBR");
460        let dump = export_mbr_hex(&d, "rm2.raw");
461        assert!(dump.contains("MBR of rm2.raw"));
462        assert!(dump.contains("E221-034C"), "disk signature in header");
463        assert!(dump.contains("00000000 "), "offset column");
464        assert!(
465            dump.contains("55 AA"),
466            "the boot signature bytes are present"
467        );
468        // 512 bytes / 16 per line = 32 lines + 1 header.
469        assert_eq!(dump.lines().count(), 33);
470    }
471
472    #[test]
473    fn source_without_a_fat_serial_or_encryption_emits_nothing() {
474        // A device with no FAT partition and no encryption carries nothing to correlate on.
475        let img = DeviceImage {
476            disk_signature: 1,
477            fat_volume_serial: None,
478            encryption: None,
479            mbr: [0u8; 512],
480        };
481        assert!(DeviceImageSource::new(&img, "x").claims().is_empty());
482    }
483
484    /// A fixed-drive BitLocker VBR: jump `EB 58 90`, then the `-FVE-FS-` OEM id at offset 3.
485    fn bitlocker_vbr() -> [u8; 512] {
486        let mut vbr = [0u8; 512];
487        vbr[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]);
488        vbr[3..11].copy_from_slice(b"-FVE-FS-");
489        vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
490        vbr
491    }
492
493    #[test]
494    fn bitlocker_signature_is_detected_from_the_vbr() {
495        let img = mbr_disk(0xABCD_1234, 0x07, 2, &bitlocker_vbr());
496        let d = parse_boot_sectors(&img).expect("valid MBR");
497        assert_eq!(d.encryption, Some(EncryptionKind::BitLocker));
498        assert_eq!(EncryptionKind::BitLocker.name(), "BitLocker");
499        // A BitLocker volume is not FAT → no FAT serial.
500        assert_eq!(d.fat_volume_serial, None);
501    }
502
503    #[test]
504    fn plain_filesystem_media_is_not_flagged_as_encrypted() {
505        use forensicnomicon::volume_encryption::VolumeEncryption;
506        // A real FAT32 volume must NOT false-positive as encrypted or unrecognized.
507        let img = mbr_disk(1, 0x0B, 2, &fat32_vbr(42));
508        assert_eq!(
509            parse_boot_sectors(&img).expect("valid MBR").encryption,
510            None
511        );
512        // The mapping from disk-forensic's pre-decoded volume state to an EncryptionKind.
513        assert_eq!(encryption_of(None, Some(DetectedFs::Ntfs)), None);
514        assert_eq!(encryption_of(None, None), None);
515        assert_eq!(
516            encryption_of(Some(VolumeEncryption::BitLocker), None),
517            Some(EncryptionKind::BitLocker)
518        );
519        assert_eq!(
520            encryption_of(Some(VolumeEncryption::BitLockerToGo), None),
521            Some(EncryptionKind::BitLockerToGo)
522        );
523        assert_eq!(
524            encryption_of(None, Some(DetectedFs::Luks)),
525            Some(EncryptionKind::Luks)
526        );
527        assert_eq!(
528            encryption_of(None, Some(DetectedFs::Unknown)),
529            Some(EncryptionKind::UnrecognizedFilesystem)
530        );
531    }
532
533    #[test]
534    fn an_unrecognized_filesystem_partition_is_flagged_as_possibly_encrypted() {
535        // A partition whose VBR matches no known filesystem (all-random, as a VeraCrypt /
536        // TrueCrypt container appears) → flagged as possibly-encrypted.
537        let img = mbr_disk(7, 0x07, 2, &[0xABu8; 512]);
538        let d = parse_boot_sectors(&img).expect("valid MBR");
539        assert_eq!(d.encryption, Some(EncryptionKind::UnrecognizedFilesystem));
540        assert_eq!(
541            EncryptionKind::UnrecognizedFilesystem.name(),
542            "unrecognized-filesystem (possible encrypted container)"
543        );
544    }
545
546    #[test]
547    fn source_emits_an_encryption_claim_for_an_encrypted_device() {
548        let img = DeviceImage {
549            disk_signature: 0xABCD_1234,
550            fat_volume_serial: None,
551            encryption: Some(EncryptionKind::BitLocker),
552            mbr: [0u8; 512],
553        };
554        let claims = DeviceImageSource::new(&img, "x").claims();
555        assert_eq!(claims.len(), 1);
556        assert_eq!(claims[0].attribute, Attribute::Encryption);
557        assert_eq!(claims[0].value, Value::Text("BitLocker".to_string()));
558    }
559
560    // ---- fixtures parseable by the authoritative disk-forensic parser ----
561
562    /// A classic-MBR disk holding one partition (`ptype`, starting at `start_lba`) whose
563    /// 512-byte VBR is `vbr`. Sized to contain the VBR plus slack for FS detection.
564    fn mbr_disk(disk_sig: u32, ptype: u8, start_lba: u32, vbr: &[u8]) -> Vec<u8> {
565        let sectors = start_lba as usize + 16;
566        let mut v = vec![0u8; sectors * 512];
567        v[0x1B8..0x1BC].copy_from_slice(&disk_sig.to_le_bytes());
568        v[0x1FE..0x200].copy_from_slice(&[0x55, 0xAA]);
569        v[0x1BE + 4] = ptype;
570        v[0x1BE + 8..0x1BE + 12].copy_from_slice(&start_lba.to_le_bytes());
571        v[0x1BE + 12..0x1BE + 16].copy_from_slice(&8u32.to_le_bytes()); // sector count
572        let off = start_lba as usize * 512;
573        v[off..off + vbr.len()].copy_from_slice(vbr);
574        v
575    }
576
577    /// A BitLocker To Go discovery-volume VBR: a real FAT32 boot record (`MSWIN4.1` OEM id,
578    /// `FAT32   ` FS signature) carrying the BitLocker identifier GUID — how removable-media
579    /// BitLocker appears (libbde BDE format, "BitLocker To Go" volume header).
580    fn to_go_vbr() -> [u8; 512] {
581        let mut vbr = [0u8; 512];
582        vbr[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]);
583        vbr[3..11].copy_from_slice(b"MSWIN4.1");
584        vbr[0x52..0x5A].copy_from_slice(b"FAT32   ");
585        // identifier GUID (To Go offset) — from forensicnomicon, the knowledge owner
586        vbr[424..440]
587            .copy_from_slice(&forensicnomicon::volume_encryption::BITLOCKER_IDENTIFIER_GUID);
588        vbr[510..512].copy_from_slice(&[0x55, 0xAA]);
589        vbr
590    }
591
592    #[test]
593    fn bitlocker_to_go_detected_via_identifier_guid_on_a_fat_discovery_volume() {
594        // The removable-media case: the volume looks like FAT32 to a generic FS detector, so
595        // detection MUST key off the BitLocker identifier GUID, not the `-FVE-FS-` string.
596        let img = mbr_disk(0x1111_2222, 0x0B, 2, &to_go_vbr());
597        let d = parse_boot_sectors(&img).expect("valid MBR");
598        assert_eq!(d.encryption, Some(EncryptionKind::BitLockerToGo));
599        assert_eq!(EncryptionKind::BitLockerToGo.name(), "BitLocker To Go");
600    }
601
602    #[test]
603    fn a_luks_partition_is_flagged_as_luks_encryption() {
604        let mut vbr = [0u8; 512];
605        vbr[0..6].copy_from_slice(b"LUKS\xba\xbe"); // LUKS magic at offset 0
606        let img = mbr_disk(0x3333_4444, 0x83, 2, &vbr);
607        let d = parse_boot_sectors(&img).expect("valid MBR");
608        assert_eq!(d.encryption, Some(EncryptionKind::Luks));
609        assert_eq!(EncryptionKind::Luks.name(), "LUKS");
610    }
611
612    #[test]
613    fn an_apple_partition_map_yields_no_device_image() {
614        // An APM disk (Apple partitioning, `ER` DDR + `PM` map entry) carries no Windows
615        // USB-attribution value, so it is not turned into a device image.
616        let bs = 512usize;
617        let mut d = vec![0u8; bs * 2];
618        d[0..2].copy_from_slice(b"ER"); // Driver Descriptor Map signature
619        d[2..4].copy_from_slice(&512u16.to_be_bytes()); // block size
620        d[4..8].copy_from_slice(&4u32.to_be_bytes()); // device block count
621        d[bs..bs + 2].copy_from_slice(b"PM"); // partition map entry signature
622        d[bs + 4..bs + 8].copy_from_slice(&1u32.to_be_bytes()); // map entry count
623        d[bs + 8..bs + 12].copy_from_slice(&1u32.to_be_bytes()); // start block
624        d[bs + 12..bs + 16].copy_from_slice(&1u32.to_be_bytes()); // block count
625        assert_eq!(parse_boot_sectors(&d), None);
626    }
627
628    // ---- GPT fixture: same in-memory recipe disk-forensic's own dispatch tests use ----
629
630    fn guid_bytes(s: &str) -> [u8; 16] {
631        let g: Vec<&str> = s.split('-').collect();
632        let mut b = [0u8; 16];
633        b[0..4].copy_from_slice(&u32::from_str_radix(g[0], 16).unwrap().to_le_bytes());
634        b[4..6].copy_from_slice(&u16::from_str_radix(g[1], 16).unwrap().to_le_bytes());
635        b[6..8].copy_from_slice(&u16::from_str_radix(g[2], 16).unwrap().to_le_bytes());
636        b[8..10].copy_from_slice(&u16::from_str_radix(g[3], 16).unwrap().to_be_bytes());
637        b[10..16].copy_from_slice(&u64::from_str_radix(g[4], 16).unwrap().to_be_bytes()[2..8]);
638        b
639    }
640
641    fn gpt_entry(type_guid: &str, first: u64, last: u64) -> [u8; 128] {
642        let mut e = [0u8; 128];
643        e[0..16].copy_from_slice(&guid_bytes(type_guid));
644        e[16..32].copy_from_slice(&guid_bytes("00000000-0000-0000-0000-000000000001"));
645        e[32..40].copy_from_slice(&first.to_le_bytes());
646        e[40..48].copy_from_slice(&last.to_le_bytes());
647        e
648    }
649
650    fn gpt_header(my_lba: u64, alt_lba: u64, entry_lba: u64, array_crc: u32) -> [u8; 512] {
651        let mut s = [0u8; 512];
652        s[0..8].copy_from_slice(b"EFI PART");
653        s[8..12].copy_from_slice(&0x0001_0000u32.to_le_bytes());
654        s[12..16].copy_from_slice(&92u32.to_le_bytes());
655        s[24..32].copy_from_slice(&my_lba.to_le_bytes());
656        s[32..40].copy_from_slice(&alt_lba.to_le_bytes());
657        s[40..48].copy_from_slice(&3u64.to_le_bytes()); // first usable
658        s[48..56].copy_from_slice(&61u64.to_le_bytes()); // last usable
659        s[56..72].copy_from_slice(&guid_bytes("12345678-1234-5678-1234-567812345678"));
660        s[72..80].copy_from_slice(&entry_lba.to_le_bytes());
661        s[80..84].copy_from_slice(&4u32.to_le_bytes()); // num entries
662        s[84..88].copy_from_slice(&128u32.to_le_bytes()); // entry size
663        s[88..92].copy_from_slice(&array_crc.to_le_bytes());
664        let crc = gpt_partition_forensic::crc32::checksum(&s[..92]);
665        s[16..20].copy_from_slice(&crc.to_le_bytes());
666        s
667    }
668
669    /// A spec-valid GPT disk (protective MBR + primary/backup headers + entry array) with one
670    /// Microsoft Basic Data partition whose VBR is FAT32 (serial `bs_volid`).
671    fn build_gpt(bs_volid: u32) -> Vec<u8> {
672        const SECTOR: usize = 512;
673        const SECTORS: usize = 64;
674        let mut disk = vec![0u8; SECTOR * SECTORS];
675        disk[450] = 0xEE; // protective-MBR partition type
676        disk[454..458].copy_from_slice(&1u32.to_le_bytes());
677        disk[458..462].copy_from_slice(&((SECTORS - 1) as u32).to_le_bytes());
678        disk[510..512].copy_from_slice(&[0x55, 0xAA]);
679
680        let mut array = vec![0u8; 4 * 128];
681        array[0..128].copy_from_slice(&gpt_entry(
682            "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", // Microsoft Basic Data
683            3,
684            30,
685        ));
686        let array_crc = gpt_partition_forensic::crc32::checksum(&array);
687        disk[SECTOR..SECTOR + 512].copy_from_slice(&gpt_header(1, 63, 2, array_crc));
688        disk[2 * SECTOR..2 * SECTOR + array.len()].copy_from_slice(&array);
689        disk[62 * SECTOR..62 * SECTOR + array.len()].copy_from_slice(&array);
690        disk[63 * SECTOR..63 * SECTOR + 512].copy_from_slice(&gpt_header(63, 1, 62, array_crc));
691        // FAT32 VBR at the partition's first LBA (3).
692        let vbr = 3 * SECTOR;
693        disk[vbr + 3..vbr + 11].copy_from_slice(b"MSDOS5.0");
694        disk[vbr + 0x52..vbr + 0x5A].copy_from_slice(b"FAT32   ");
695        disk[vbr + 0x43..vbr + 0x47].copy_from_slice(&bs_volid.to_le_bytes());
696        disk[vbr + 510..vbr + 512].copy_from_slice(&[0x55, 0xAA]);
697        disk
698    }
699
700    #[test]
701    fn a_gpt_disk_is_not_false_flagged_and_its_fat_partition_is_read() {
702        // Regression: a GPT protective MBR (type 0xEE, "EFI PART" at LBA 1) must be parsed as
703        // GPT — never walked as MBR partitions, which mis-read the GPT header as an
704        // unrecognized-filesystem VBR. Its real FAT partition's serial is still recovered.
705        let img = build_gpt(0xB4D8_5399);
706        let d = parse_boot_sectors(&img).expect("valid GPT");
707        assert_eq!(
708            d.encryption, None,
709            "GPT header must not be flagged as encrypted"
710        );
711        assert_eq!(d.fat_volume_serial, Some(0xB4D8_5399));
712    }
713
714    #[test]
715    fn a_gpt_partition_beyond_the_image_is_skipped() {
716        // A GPT with a used partition whose first LBA lies past the image end (a truncated
717        // capture): the in-image FAT partition is read, the out-of-range one is skipped.
718        const SECTOR: usize = 512;
719        const SECTORS: usize = 64;
720        let mut disk = vec![0u8; SECTOR * SECTORS];
721        disk[450] = 0xEE;
722        disk[454..458].copy_from_slice(&1u32.to_le_bytes());
723        disk[458..462].copy_from_slice(&((SECTORS - 1) as u32).to_le_bytes());
724        disk[510..512].copy_from_slice(&[0x55, 0xAA]);
725        let mut array = vec![0u8; 4 * 128];
726        array[0..128].copy_from_slice(&gpt_entry("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", 3, 30));
727        // second used entry starting far past the 64-sector image
728        array[128..256].copy_from_slice(&gpt_entry(
729            "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7",
730            9000,
731            9030,
732        ));
733        let array_crc = gpt_partition_forensic::crc32::checksum(&array);
734        disk[SECTOR..SECTOR + 512].copy_from_slice(&gpt_header(1, 63, 2, array_crc));
735        disk[2 * SECTOR..2 * SECTOR + array.len()].copy_from_slice(&array);
736        disk[62 * SECTOR..62 * SECTOR + array.len()].copy_from_slice(&array);
737        disk[63 * SECTOR..63 * SECTOR + 512].copy_from_slice(&gpt_header(63, 1, 62, array_crc));
738        let vbr = 3 * SECTOR;
739        disk[vbr + 3..vbr + 11].copy_from_slice(b"MSDOS5.0");
740        disk[vbr + 0x52..vbr + 0x5A].copy_from_slice(b"FAT32   ");
741        disk[vbr + 0x43..vbr + 0x47].copy_from_slice(&0xAABB_CCDDu32.to_le_bytes());
742        disk[vbr + 510..vbr + 512].copy_from_slice(&[0x55, 0xAA]);
743        let d = parse_boot_sectors(&disk).expect("valid GPT");
744        assert_eq!(d.fat_volume_serial, Some(0xAABB_CCDD));
745    }
746}