bitlocker_forensic/
lib.rs1#![forbid(unsafe_code)]
28#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
29
30use std::io::{Read, Seek, SeekFrom};
31
32use bitlocker::{BdeError, BdeVariant, BitLockerVolume, FveMetadata, VolumeHeader};
33use forensicnomicon::report::{Category, Evidence, Finding, Observation, Severity, Source};
34
35pub const ANALYZER: &str = "bitlocker-forensic";
37
38const PROT_CLEAR_KEY: u16 = 0x0000;
40const PROT_TPM: u16 = 0x0100;
41const PROT_STARTUP_KEY: u16 = 0x0200;
42const PROT_TPM_PIN: u16 = 0x0500;
43const PROT_RECOVERY: u16 = 0x0800;
44const PROT_PASSWORD: u16 = 0x2000;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum AnomalyKind {
49 ClearKeyPresent,
52 Protector {
54 protector_type: u16,
56 },
57 WeakCipher {
60 method: u16,
62 },
63 ToGo,
65}
66
67impl AnomalyKind {
68 #[must_use]
70 pub fn severity(&self) -> Severity {
71 match self {
72 AnomalyKind::ClearKeyPresent => Severity::High,
73 AnomalyKind::WeakCipher { .. } => Severity::Low,
74 AnomalyKind::Protector { .. } | AnomalyKind::ToGo => Severity::Info,
75 }
76 }
77
78 #[must_use]
80 pub fn code(&self) -> &'static str {
81 match self {
82 AnomalyKind::ClearKeyPresent => "BDE-CLEAR-KEY-PRESENT",
83 AnomalyKind::Protector { .. } => "BDE-PROTECTOR-INVENTORY",
84 AnomalyKind::WeakCipher { .. } => "BDE-WEAK-CIPHER",
85 AnomalyKind::ToGo => "BDE-TO-GO",
86 }
87 }
88
89 #[must_use]
91 pub fn category(&self) -> Category {
92 match self {
93 AnomalyKind::ClearKeyPresent | AnomalyKind::WeakCipher { .. } => Category::Integrity,
94 AnomalyKind::Protector { .. } => Category::Provenance,
95 AnomalyKind::ToGo => Category::Structure,
96 }
97 }
98
99 #[must_use]
101 pub fn note(&self) -> String {
102 match self {
103 AnomalyKind::ClearKeyPresent => "a clear-key protector (type 0x0000) is present; the \
104 volume master key is stored unprotected, so the volume can be decrypted with no \
105 credential — it is effectively unencrypted (bitlocker-core unlocks it via \
106 unlock_clear_key)"
107 .to_string(),
108 AnomalyKind::Protector { protector_type } => format!(
109 "key protector present: {} (type 0x{protector_type:04x})",
110 protector_name(*protector_type)
111 ),
112 AnomalyKind::WeakCipher { method } => format!(
113 "volume cipher is {} (method 0x{method:04x}); AES-CBC, with or without the Elephant \
114 Diffuser, is weaker than AES-XTS and is consistent with an older Windows version",
115 cipher_name(*method)
116 ),
117 AnomalyKind::ToGo => "the volume is a BitLocker To Go volume (removable-media BitLocker \
118 on a FAT-formatted volume)"
119 .to_string(),
120 }
121 }
122
123 fn evidence(&self) -> Vec<Evidence> {
124 match self {
125 AnomalyKind::Protector { protector_type } => {
126 vec![evidence(
127 "protector_type",
128 format!("0x{protector_type:04x}"),
129 )]
130 }
131 AnomalyKind::WeakCipher { method } => {
132 vec![evidence("encryption_method", format!("0x{method:04x}"))]
133 }
134 AnomalyKind::ClearKeyPresent | AnomalyKind::ToGo => Vec::new(),
135 }
136 }
137}
138
139fn evidence(field: &str, value: String) -> Evidence {
140 Evidence {
141 field: field.to_string(),
142 value,
143 location: None,
144 }
145}
146
147#[must_use]
149pub fn protector_name(protector_type: u16) -> &'static str {
150 match protector_type {
151 PROT_CLEAR_KEY => "clear key",
152 PROT_TPM => "TPM",
153 PROT_STARTUP_KEY => "startup key",
154 PROT_TPM_PIN => "TPM and PIN",
155 PROT_RECOVERY => "recovery password",
156 PROT_PASSWORD => "password",
157 _ => "other/unknown",
158 }
159}
160
161#[must_use]
163pub fn cipher_name(method: u16) -> &'static str {
164 match method {
165 0x8000 => "AES-128-CBC + Elephant Diffuser",
166 0x8001 => "AES-256-CBC + Elephant Diffuser",
167 0x8002 => "AES-128-CBC",
168 0x8003 => "AES-256-CBC",
169 0x8004 => "AES-128-XTS",
170 0x8005 => "AES-256-XTS",
171 _ => "unknown",
172 }
173}
174
175#[must_use]
177fn is_aes_xts(method: u16) -> bool {
178 matches!(method, 0x8004 | 0x8005)
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct Anomaly {
185 pub severity: Severity,
187 pub code: &'static str,
189 pub kind: AnomalyKind,
191 pub note: String,
193}
194
195impl Anomaly {
196 #[must_use]
198 pub fn new(kind: AnomalyKind) -> Self {
199 Anomaly {
200 severity: kind.severity(),
201 code: kind.code(),
202 note: kind.note(),
203 kind,
204 }
205 }
206}
207
208impl Observation for Anomaly {
209 fn severity(&self) -> Option<Severity> {
210 Some(self.severity)
211 }
212 fn code(&self) -> &'static str {
213 self.code
214 }
215 fn note(&self) -> String {
216 self.note.clone()
217 }
218 fn category(&self) -> Category {
219 self.kind.category()
220 }
221 fn evidence(&self) -> Vec<Evidence> {
222 self.kind.evidence()
223 }
224}
225
226#[must_use]
229pub fn audit(metadata: &FveMetadata, to_go: bool) -> Vec<Anomaly> {
230 let mut out = Vec::new();
231
232 if to_go {
233 out.push(Anomaly::new(AnomalyKind::ToGo));
234 }
235
236 if !is_aes_xts(metadata.encryption_method) {
237 out.push(Anomaly::new(AnomalyKind::WeakCipher {
238 method: metadata.encryption_method,
239 }));
240 }
241
242 for protector_type in metadata.protector_types() {
243 if protector_type == PROT_CLEAR_KEY {
244 out.push(Anomaly::new(AnomalyKind::ClearKeyPresent));
245 }
246 out.push(Anomaly::new(AnomalyKind::Protector { protector_type }));
247 }
248
249 out
250}
251
252pub fn audit_reader<R: Read + Seek>(reader: &mut R) -> Result<Vec<Anomaly>, BdeError> {
258 let mut header = [0u8; 512];
259 reader.seek(SeekFrom::Start(0))?;
260 reader.read_exact(&mut header)?;
261 let variant = VolumeHeader::parse(&header)?.variant;
262 let metadata = BitLockerVolume::read_metadata(reader)?;
263 Ok(audit(&metadata, variant == BdeVariant::BitLockerToGo))
264}
265
266pub fn audit_findings<R: Read + Seek>(
272 reader: &mut R,
273 scope: impl Into<String>,
274) -> Result<Vec<Finding>, BdeError> {
275 let source = Source {
276 analyzer: ANALYZER.to_string(),
277 scope: scope.into(),
278 version: Some(env!("CARGO_PKG_VERSION").to_string()),
279 };
280 Ok(audit_reader(reader)?
281 .into_iter()
282 .map(|anomaly| anomaly.to_finding(source.clone()))
283 .collect())
284}
285
286#[cfg(test)]
287mod tests;