forensicnomicon_core/volume_encryption.rs
1//! Volume full-disk-encryption detection from a volume boot record.
2//!
3//! Complements [`crate::filesystems`]: where that identifies a plaintext filesystem, this
4//! identifies a **BitLocker** volume, which a filesystem detector cannot — a BitLocker To Go
5//! volume presents a genuine FAT/exFAT boot record and looks like an ordinary FAT volume.
6//!
7//! Detection is scheme- and bus-agnostic: it reads the volume boot record, so it is identical
8//! whether the volume is reached over USB, FireWire, Thunderbolt, or SATA, and whether the
9//! partition table is MBR, GPT, or APM.
10//!
11//! Reference: libbde, *BitLocker Drive Encryption (BDE) format* (Joachim Metz),
12//! <https://github.com/libyal/libbde/blob/main/documentation/BitLocker%20Drive%20Encryption%20(BDE)%20format.asciidoc>
13//! — volume-header tables.
14
15/// A detected volume-encryption type. LUKS and other encrypted-filesystem magics are surfaced
16/// by [`crate::filesystems::detect_name`]; this enum covers the Microsoft FVE volumes that a
17/// filesystem-signature scan cannot see.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20pub enum VolumeEncryption {
21 /// BitLocker on a **fixed drive** (or an NTFS-on-removable volume): the boot record's OEM
22 /// identifier at offset 3 is the documented `-FVE-FS-` signature (Windows Vista `EB 52 90`
23 /// / 7-10 `EB 58 90`).
24 BitLocker,
25 /// **BitLocker To Go** on removable media: the discovery volume presents a real FAT/exFAT
26 /// boot record (an ordinary OEM identifier at offset 3), so it is identified only by the
27 /// BitLocker identifier GUID carried in the volume header, not by `-FVE-FS-`.
28 BitLockerToGo,
29}
30
31/// The `-FVE-FS-` filesystem signature at volume-header offset 3 (fixed-drive BitLocker).
32const FVE_SIGNATURE: &[u8; 8] = b"-FVE-FS-";
33
34/// The BitLocker identifier GUID `4967D63B-2E29-4AD8-8399-F6A339E3D001`, in the mixed-endian
35/// byte order it is stored in a volume header (`Data1`-`Data3` little-endian, `Data4`
36/// big-endian). The volume header carries it in both layouts — offset 160 (fixed drive) and
37/// offset 424 (To Go) — so callers scan for it rather than reading a fixed offset.
38pub const BITLOCKER_IDENTIFIER_GUID: [u8; 16] = [
39 0x3B, 0xD6, 0x67, 0x49, 0x29, 0x2E, 0xD8, 0x4A, 0x83, 0x99, 0xF6, 0xA3, 0x39, 0xE3, 0xD0, 0x01,
40];
41
42/// Detect BitLocker volume encryption from a volume's leading bytes (its boot record).
43///
44/// - `-FVE-FS-` at offset 3 → [`VolumeEncryption::BitLocker`] (fixed drive; Vista/7/8/10).
45/// - otherwise, the [`BITLOCKER_IDENTIFIER_GUID`] present anywhere in the volume header →
46/// [`VolumeEncryption::BitLockerToGo`] (a FAT/exFAT discovery volume carrying the GUID).
47///
48/// `None` for a plaintext volume. Scanning the header for the 16-byte GUID (rather than a
49/// fixed offset) catches every BitLocker layout version and is effectively false-positive-free.
50#[must_use]
51pub fn detect_encryption(volume: &[u8]) -> Option<VolumeEncryption> {
52 if volume.len() >= 11 && &volume[3..11] == FVE_SIGNATURE {
53 return Some(VolumeEncryption::BitLocker);
54 }
55 if volume
56 .windows(BITLOCKER_IDENTIFIER_GUID.len())
57 .any(|w| w == BITLOCKER_IDENTIFIER_GUID)
58 {
59 return Some(VolumeEncryption::BitLockerToGo);
60 }
61 None
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 /// Offset of the BitLocker identifier GUID in a To Go volume header (libbde).
69 const TO_GO_GUID_OFFSET: usize = 424;
70
71 #[test]
72 fn fixed_drive_bitlocker_is_detected_by_the_fve_signature() {
73 let mut vbr = vec![0u8; 512];
74 vbr[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]); // Win7+ boot entry
75 vbr[3..11].copy_from_slice(b"-FVE-FS-");
76 assert_eq!(detect_encryption(&vbr), Some(VolumeEncryption::BitLocker));
77 }
78
79 #[test]
80 fn bitlocker_to_go_is_detected_by_the_identifier_guid_on_a_fat_volume() {
81 // A real FAT32 discovery volume: MSWIN4.1 OEM id, FAT32 signature — no -FVE-FS- — but
82 // carrying the BitLocker identifier GUID at the To Go offset.
83 let mut vbr = vec![0u8; 512];
84 vbr[0..3].copy_from_slice(&[0xEB, 0x58, 0x90]);
85 vbr[3..11].copy_from_slice(b"MSWIN4.1");
86 vbr[0x52..0x5A].copy_from_slice(b"FAT32 ");
87 vbr[TO_GO_GUID_OFFSET..TO_GO_GUID_OFFSET + 16].copy_from_slice(&BITLOCKER_IDENTIFIER_GUID);
88 assert_eq!(
89 detect_encryption(&vbr),
90 Some(VolumeEncryption::BitLockerToGo)
91 );
92 }
93
94 #[test]
95 fn the_fve_signature_takes_precedence_over_the_guid() {
96 // A fixed-drive BitLocker volume carries both -FVE-FS- and the GUID (at offset 160);
97 // it classifies as BitLocker, not To Go.
98 let mut vbr = vec![0u8; 512];
99 vbr[3..11].copy_from_slice(b"-FVE-FS-");
100 vbr[160..176].copy_from_slice(&BITLOCKER_IDENTIFIER_GUID);
101 assert_eq!(detect_encryption(&vbr), Some(VolumeEncryption::BitLocker));
102 }
103
104 #[test]
105 fn a_plaintext_fat_volume_is_not_flagged() {
106 // A real FAT32 boot record with no FVE signature and no GUID → not encrypted (the
107 // non-false-positive control).
108 let mut vbr = vec![0u8; 512];
109 vbr[3..11].copy_from_slice(b"MSDOS5.0");
110 vbr[0x52..0x5A].copy_from_slice(b"FAT32 ");
111 assert_eq!(detect_encryption(&vbr), None);
112 }
113
114 #[test]
115 fn a_plaintext_ntfs_volume_is_not_flagged() {
116 let mut vbr = vec![0u8; 512];
117 vbr[3..11].copy_from_slice(b"NTFS ");
118 assert_eq!(detect_encryption(&vbr), None);
119 }
120
121 #[test]
122 fn a_short_slice_does_not_panic() {
123 assert_eq!(detect_encryption(&[]), None);
124 assert_eq!(detect_encryption(&[0u8; 4]), None);
125 assert_eq!(detect_encryption(b"-FVE-FS"), None); // one byte short of the signature
126 }
127}