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"
106 .to_string(),
107 AnomalyKind::Protector { protector_type } => format!(
108 "key protector present: {} (type 0x{protector_type:04x})",
109 protector_name(*protector_type)
110 ),
111 AnomalyKind::WeakCipher { method } => format!(
112 "volume cipher is {} (method 0x{method:04x}); AES-CBC, with or without the Elephant \
113 Diffuser, is weaker than AES-XTS and is consistent with an older Windows version",
114 cipher_name(*method)
115 ),
116 AnomalyKind::ToGo => "the volume is a BitLocker To Go volume (removable-media BitLocker \
117 on a FAT-formatted volume)"
118 .to_string(),
119 }
120 }
121
122 fn evidence(&self) -> Vec<Evidence> {
123 match self {
124 AnomalyKind::Protector { protector_type } => {
125 vec![evidence(
126 "protector_type",
127 format!("0x{protector_type:04x}"),
128 )]
129 }
130 AnomalyKind::WeakCipher { method } => {
131 vec![evidence("encryption_method", format!("0x{method:04x}"))]
132 }
133 AnomalyKind::ClearKeyPresent | AnomalyKind::ToGo => Vec::new(),
134 }
135 }
136}
137
138fn evidence(field: &str, value: String) -> Evidence {
139 Evidence {
140 field: field.to_string(),
141 value,
142 location: None,
143 }
144}
145
146#[must_use]
148pub fn protector_name(protector_type: u16) -> &'static str {
149 match protector_type {
150 PROT_CLEAR_KEY => "clear key",
151 PROT_TPM => "TPM",
152 PROT_STARTUP_KEY => "startup key",
153 PROT_TPM_PIN => "TPM and PIN",
154 PROT_RECOVERY => "recovery password",
155 PROT_PASSWORD => "password",
156 _ => "other/unknown",
157 }
158}
159
160#[must_use]
162pub fn cipher_name(method: u16) -> &'static str {
163 match method {
164 0x8000 => "AES-128-CBC + Elephant Diffuser",
165 0x8001 => "AES-256-CBC + Elephant Diffuser",
166 0x8002 => "AES-128-CBC",
167 0x8003 => "AES-256-CBC",
168 0x8004 => "AES-128-XTS",
169 0x8005 => "AES-256-XTS",
170 _ => "unknown",
171 }
172}
173
174#[must_use]
176fn is_aes_xts(method: u16) -> bool {
177 matches!(method, 0x8004 | 0x8005)
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Anomaly {
184 pub severity: Severity,
186 pub code: &'static str,
188 pub kind: AnomalyKind,
190 pub note: String,
192}
193
194impl Anomaly {
195 #[must_use]
197 pub fn new(kind: AnomalyKind) -> Self {
198 Anomaly {
199 severity: kind.severity(),
200 code: kind.code(),
201 note: kind.note(),
202 kind,
203 }
204 }
205}
206
207impl Observation for Anomaly {
208 fn severity(&self) -> Option<Severity> {
209 Some(self.severity)
210 }
211 fn code(&self) -> &'static str {
212 self.code
213 }
214 fn note(&self) -> String {
215 self.note.clone()
216 }
217 fn category(&self) -> Category {
218 self.kind.category()
219 }
220 fn evidence(&self) -> Vec<Evidence> {
221 self.kind.evidence()
222 }
223}
224
225#[must_use]
228pub fn audit(metadata: &FveMetadata, to_go: bool) -> Vec<Anomaly> {
229 let mut out = Vec::new();
230
231 if to_go {
232 out.push(Anomaly::new(AnomalyKind::ToGo));
233 }
234
235 if !is_aes_xts(metadata.encryption_method) {
236 out.push(Anomaly::new(AnomalyKind::WeakCipher {
237 method: metadata.encryption_method,
238 }));
239 }
240
241 for protector_type in metadata.protector_types() {
242 if protector_type == PROT_CLEAR_KEY {
243 out.push(Anomaly::new(AnomalyKind::ClearKeyPresent));
244 }
245 out.push(Anomaly::new(AnomalyKind::Protector { protector_type }));
246 }
247
248 out
249}
250
251pub fn audit_reader<R: Read + Seek>(reader: &mut R) -> Result<Vec<Anomaly>, BdeError> {
257 let mut header = [0u8; 512];
258 reader.seek(SeekFrom::Start(0))?;
259 reader.read_exact(&mut header)?;
260 let variant = VolumeHeader::parse(&header)?.variant;
261 let metadata = BitLockerVolume::read_metadata(reader)?;
262 Ok(audit(&metadata, variant == BdeVariant::BitLockerToGo))
263}
264
265pub fn audit_findings<R: Read + Seek>(
271 reader: &mut R,
272 scope: impl Into<String>,
273) -> Result<Vec<Finding>, BdeError> {
274 let source = Source {
275 analyzer: ANALYZER.to_string(),
276 scope: scope.into(),
277 version: Some(env!("CARGO_PKG_VERSION").to_string()),
278 };
279 Ok(audit_reader(reader)?
280 .into_iter()
281 .map(|anomaly| anomaly.to_finding(source.clone()))
282 .collect())
283}
284
285#[cfg(test)]
286mod tests;