Skip to main content

bitlocker/
volume.rs

1//! Public API: parse a BitLocker volume's metadata and unlock it from a password.
2//!
3//! [`BitLockerVolume::unlock_with_password`] runs the full chain — parse the
4//! volume header, locate and parse the FVE metadata, derive the VMK from the
5//! password (stretch → AES-CCM), unwrap the FVEK/TWEAK, and return a
6//! [`DecryptedVolume`] exposing a plaintext `Read + Seek` view that honours
7//! BitLocker's volume-header relocation and metadata-region blanking.
8
9use std::io::{Read, Seek, SeekFrom};
10
11use crate::crypto::{aes_ccm_unwrap, password_hash, stretch_key, SectorCipher};
12use crate::error::{BdeError, Result};
13use crate::header::VolumeHeader;
14use crate::metadata::{FveMetadata, PROTECTION_PASSWORD, VALUE_TYPE_AES_CCM, VALUE_TYPE_STRETCH};
15
16/// Encryption method decrypted by this build: AES-128-CBC + Elephant Diffuser.
17const METHOD_AES128_CBC_DIFFUSER: u16 = 0x8000;
18/// Encryption method sentinel: the volume is not encrypted.
19const METHOD_NONE: u16 = 0x0000;
20/// The fixed BitLocker sector size.
21const SECTOR_SIZE: usize = 512;
22/// How many bytes to read at a candidate metadata offset (reserved FVE metadata
23/// blocks are well under this).
24const METADATA_READ_LEN: usize = 64 * 1024;
25
26/// Namespace for opening a BitLocker volume. All state lives in the returned
27/// [`DecryptedVolume`]; this type carries no data itself.
28pub struct BitLockerVolume;
29
30impl BitLockerVolume {
31    /// Parse the volume header and the first valid FVE metadata block from
32    /// `reader`, without unlocking. Used by the forensic analyzer to audit the
33    /// key protectors even when no password is known.
34    ///
35    /// # Errors
36    /// [`BdeError::NotBitLocker`] if the header signature is absent, or
37    /// [`BdeError::NoValidMetadata`] if no `-FVE-FS-` block is found.
38    pub fn read_metadata<R: Read + Seek>(reader: &mut R) -> Result<FveMetadata> {
39        let mut header = [0u8; SECTOR_SIZE];
40        reader.seek(SeekFrom::Start(0))?;
41        read_fill(reader, &mut header)?;
42        let volume_header = VolumeHeader::parse(&header)?;
43
44        let offsets = volume_header.fve_metadata_offsets;
45        for &offset in &offsets {
46            if offset == 0 {
47                continue;
48            }
49            let mut block = vec![0u8; METADATA_READ_LEN];
50            reader.seek(SeekFrom::Start(offset))?;
51            let n = read_available(reader, &mut block)?;
52            block.truncate(n);
53            if let Some(meta) = FveMetadata::parse(&block, volume_header.bytes_per_sector) {
54                return Ok(meta);
55            }
56        }
57        Err(BdeError::NoValidMetadata { offsets })
58    }
59
60    /// Unlock the volume with `password`, returning a plaintext view.
61    ///
62    /// # Errors
63    /// Fails loud on a non-BitLocker image, an unsupported cipher, a missing
64    /// password protector, absent key material, or a wrong password (the AES-CCM
65    /// tag fails to verify).
66    pub fn unlock_with_password<R: Read + Seek>(
67        mut reader: R,
68        password: &str,
69    ) -> Result<DecryptedVolume<R>> {
70        let metadata = Self::read_metadata(&mut reader)?;
71
72        if metadata.encryption_method != METHOD_AES128_CBC_DIFFUSER {
73            return Err(BdeError::UnsupportedEncryptionMethod {
74                method: metadata.encryption_method,
75            });
76        }
77
78        let cipher = derive_cipher(&metadata, password)?;
79        let total_size = reader.seek(SeekFrom::End(0))?;
80
81        Ok(DecryptedVolume {
82            reader,
83            cipher,
84            metadata,
85            total_size,
86            position: 0,
87        })
88    }
89}
90
91/// Derive the sector cipher (FVEK + TWEAK) from the password-protected VMK.
92fn derive_cipher(metadata: &FveMetadata, password: &str) -> Result<SectorCipher> {
93    // 1. Locate the password-protected VMK.
94    let vmk = metadata
95        .vmk_entries()
96        .find(|e| e.protection_type() == Some(PROTECTION_PASSWORD))
97        .ok_or_else(|| BdeError::NoPasswordProtector {
98            found: metadata.protector_types(),
99        })?;
100
101    // VMK properties are nested entries starting at value-data offset 28.
102    let props = vmk.nested(28);
103    let stretch = props
104        .iter()
105        .find(|e| e.value_type == VALUE_TYPE_STRETCH)
106        .ok_or(BdeError::MissingKeyMaterial {
107            what: "stretch key",
108        })?;
109    let vmk_ccm = props
110        .iter()
111        .find(|e| e.value_type == VALUE_TYPE_AES_CCM)
112        .ok_or(BdeError::MissingKeyMaterial {
113            what: "VMK AES-CCM key",
114        })?;
115
116    // Salt is at stretch value-data offset 4 (after the 4-byte method).
117    let mut salt = [0u8; 16];
118    let salt_src = stretch
119        .data
120        .get(4..20)
121        .ok_or(BdeError::MissingKeyMaterial {
122            what: "stretch salt",
123        })?;
124    salt.copy_from_slice(salt_src);
125
126    // 2. Password -> stretched key -> unwrap the VMK.
127    let stretched = stretch_key(&password_hash(password), &salt);
128    let vmk_container =
129        aes_ccm_unwrap(&stretched, &vmk_ccm.data).ok_or(BdeError::AuthenticationFailed {
130            what: "volume master key",
131        })?;
132    let vmk_key = take_key32(&vmk_container, 12, "volume master key")?;
133
134    // 3. FVEK entry -> unwrap with the VMK -> FVEK + TWEAK (first 16 bytes each).
135    let fvek_entry = metadata
136        .fvek_entry()
137        .ok_or(BdeError::MissingKeyMaterial { what: "FVEK entry" })?;
138    let fvek_container = aes_ccm_unwrap(&vmk_key, &fvek_entry.data)
139        .ok_or(BdeError::AuthenticationFailed { what: "FVEK" })?;
140    let fvek = take_key16(&fvek_container, 12, "FVEK")?;
141    let tweak = take_key16(&fvek_container, 44, "FVEK")?;
142
143    Ok(SectorCipher::new(fvek, tweak))
144}
145
146fn take_key32(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 32]> {
147    let s = container
148        .get(off..off + 32)
149        .ok_or(BdeError::MalformedKeyContainer {
150            what,
151            got: container.len(),
152            need: off + 32,
153        })?;
154    let mut k = [0u8; 32];
155    k.copy_from_slice(s);
156    Ok(k)
157}
158
159fn take_key16(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 16]> {
160    let s = container
161        .get(off..off + 16)
162        .ok_or(BdeError::MalformedKeyContainer {
163            what,
164            got: container.len(),
165            need: off + 16,
166        })?;
167    let mut k = [0u8; 16];
168    k.copy_from_slice(s);
169    Ok(k)
170}
171
172/// A plaintext view of an unlocked BitLocker volume.
173pub struct DecryptedVolume<R> {
174    reader: R,
175    cipher: SectorCipher,
176    metadata: FveMetadata,
177    total_size: u64,
178    position: u64,
179}
180
181impl<R: Read + Seek> DecryptedVolume<R> {
182    /// The parsed FVE metadata (cipher, protectors, volume GUID, …).
183    #[must_use]
184    pub fn metadata(&self) -> &FveMetadata {
185        &self.metadata
186    }
187
188    /// The total size of the volume in bytes.
189    #[must_use]
190    pub fn volume_size(&self) -> u64 {
191        self.total_size
192    }
193
194    /// Read decrypted bytes at logical `offset` into `buf`, filling it completely
195    /// (bytes past the end of the volume read back as zero).
196    ///
197    /// # Errors
198    /// Propagates I/O errors from the underlying reader.
199    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
200        let mut done = 0usize;
201        while done < buf.len() {
202            let pos = offset + done as u64;
203            let sector_start = pos - (pos % SECTOR_SIZE as u64);
204            let within = (pos - sector_start) as usize;
205            let plain = self.decrypt_logical_sector(sector_start)?;
206            let take = (SECTOR_SIZE - within).min(buf.len() - done);
207            buf[done..done + take].copy_from_slice(&plain[within..within + take]);
208            done += take;
209        }
210        Ok(())
211    }
212
213    /// Decrypt the logical 512-byte sector starting at `sector_start` (a multiple
214    /// of 512), applying metadata-region blanking, volume-header relocation, and
215    /// the encrypted-volume-size boundary exactly as BitLocker's read path does.
216    fn decrypt_logical_sector(&mut self, sector_start: u64) -> Result<[u8; SECTOR_SIZE]> {
217        // Metadata block regions read back as zero in the decrypted view.
218        let meta_size = u64::from(self.metadata.metadata_size);
219        for &m in &self.metadata.metadata_offsets {
220            if m != 0 && sector_start >= m && sector_start < m + meta_size {
221                return Ok([0u8; SECTOR_SIZE]);
222            }
223        }
224
225        // The first `volume_header_size` bytes are stored, encrypted, elsewhere;
226        // the physical location doubles as the IV sector address.
227        let physical = if sector_start < self.metadata.volume_header_size {
228            self.metadata.volume_header_offset + sector_start
229        } else {
230            sector_start
231        };
232
233        let mut ct = [0u8; SECTOR_SIZE];
234        self.reader.seek(SeekFrom::Start(physical))?;
235        read_available(&mut self.reader, &mut ct)?;
236
237        // Not encrypted: NONE method, or past the still-encrypted region.
238        let evs = self.metadata.encrypted_volume_size;
239        if self.metadata.encryption_method == METHOD_NONE || (evs != 0 && physical >= evs) {
240            return Ok(ct);
241        }
242
243        let plain = self.cipher.decrypt_sector(&ct, physical);
244        let mut out = [0u8; SECTOR_SIZE];
245        let n = plain.len().min(SECTOR_SIZE);
246        out[..n].copy_from_slice(&plain[..n]);
247        Ok(out)
248    }
249}
250
251impl<R: Read + Seek> Read for DecryptedVolume<R> {
252    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
253        if self.position >= self.total_size {
254            return Ok(0);
255        }
256        let remaining = self.total_size - self.position;
257        let n = (buf.len() as u64).min(remaining) as usize;
258        self.read_at(self.position, &mut buf[..n])
259            .map_err(|e| std::io::Error::other(e.to_string()))?;
260        self.position += n as u64;
261        Ok(n)
262    }
263}
264
265impl<R: Read + Seek> Seek for DecryptedVolume<R> {
266    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
267        let new = match pos {
268            SeekFrom::Start(o) => i128::from(o),
269            SeekFrom::End(o) => i128::from(self.total_size) + i128::from(o),
270            SeekFrom::Current(o) => i128::from(self.position) + i128::from(o),
271        };
272        if new < 0 {
273            return Err(std::io::Error::new(
274                std::io::ErrorKind::InvalidInput,
275                "seek before start",
276            ));
277        }
278        self.position = new as u64;
279        Ok(self.position)
280    }
281}
282
283/// Read exactly `buf.len()` bytes, erroring on premature EOF.
284fn read_fill<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
285    reader.read_exact(buf)?;
286    Ok(())
287}
288
289/// Read up to `buf.len()` bytes, zero-filling the remainder on EOF. Returns the
290/// number of real bytes read.
291fn read_available<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
292    let mut filled = 0;
293    while filled < buf.len() {
294        match reader.read(&mut buf[filled..]) {
295            Ok(0) => break,
296            Ok(n) => filled += n,
297            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
298            Err(e) => return Err(e.into()),
299        }
300    }
301    for b in &mut buf[filled..] {
302        *b = 0;
303    }
304    Ok(filled)
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::crypto::{aes_ccm_wrap, password_hash, stretch_key, SectorCipher};
311    use crate::metadata::{
312        ENTRY_TYPE_FVEK, ENTRY_TYPE_VMK, ENTRY_TYPE_VOLUME_HEADER, PROTECTION_PASSWORD,
313    };
314    use std::io::Cursor;
315
316    const RELOCATED_OFFSET: u64 = 0x4000;
317    const META_BLOCK_OFFSET: u64 = 0x1000;
318    const IMAGE_SIZE: usize = 0x5000;
319    // Smaller than the image so a physical offset past it exercises the
320    // still-encrypted-region boundary (bytes beyond are returned raw).
321    const ENCRYPTED_SIZE: u64 = 0x4800;
322
323    fn entry(entry_type: u16, value_type: u16, data: &[u8]) -> Vec<u8> {
324        let size = (8 + data.len()) as u16;
325        let mut v = Vec::new();
326        v.extend_from_slice(&size.to_le_bytes());
327        v.extend_from_slice(&entry_type.to_le_bytes());
328        v.extend_from_slice(&value_type.to_le_bytes());
329        v.extend_from_slice(&1u16.to_le_bytes());
330        v.extend_from_slice(data);
331        v
332    }
333
334    /// Build a minimal synthetic BitLocker To Go volume plus the plaintext of its
335    /// relocated first sector.
336    fn build_volume(password: &str) -> (Vec<u8>, [u8; 512]) {
337        let salt = [0x33u8; 16];
338        let fvek = [0x11u8; 16];
339        let tweak = [0x22u8; 16];
340        let vmk = [0x44u8; 32];
341
342        let mut vmk_container = vec![0u8; 44];
343        vmk_container[12..44].copy_from_slice(&vmk);
344        let stretched = stretch_key(&password_hash(password), &salt);
345        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x55; 12], &vmk_container);
346
347        let mut fvek_container = vec![0u8; 76];
348        fvek_container[12..28].copy_from_slice(&fvek);
349        fvek_container[44..60].copy_from_slice(&tweak);
350        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x66; 12], &fvek_container);
351
352        let mut stretch_data = vec![0u8; 4]; // 4-byte method
353        stretch_data.extend_from_slice(&salt);
354        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
355        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
356
357        let mut vmk_data = vec![0u8; 28];
358        vmk_data[26..28].copy_from_slice(&PROTECTION_PASSWORD.to_le_bytes());
359        vmk_data.extend_from_slice(&stretch_entry);
360        vmk_data.extend_from_slice(&vmk_ccm_entry);
361        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
362
363        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
364
365        let mut vh_data = Vec::new();
366        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
367        vh_data.extend_from_slice(&512u64.to_le_bytes());
368        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
369
370        let mut entries = Vec::new();
371        entries.extend_from_slice(&vh_entry);
372        entries.extend_from_slice(&vmk_entry);
373        entries.extend_from_slice(&fvek_entry);
374        let metadata_size = 48 + entries.len();
375
376        let mut image = vec![0u8; IMAGE_SIZE];
377        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
378        image[3..11].copy_from_slice(b"MSWIN4.1");
379        image[12] = 0x02; // bytes per sector = 512
380        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
381
382        let mb = META_BLOCK_OFFSET as usize;
383        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
384        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
385        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
386        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
387        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
388        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
389        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
390        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8000u16.to_le_bytes());
391        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
392
393        let mut plain = [0u8; 512];
394        for (i, b) in plain.iter_mut().enumerate() {
395            *b = (i as u8) ^ 0x5a;
396        }
397        let cipher = SectorCipher::new(fvek, tweak);
398        let ct = cipher.encrypt_sector(&plain, RELOCATED_OFFSET);
399        let ro = RELOCATED_OFFSET as usize;
400        image[ro..ro + 512].copy_from_slice(&ct);
401
402        (image, plain)
403    }
404
405    use crate::metadata::{VALUE_TYPE_AES_CCM, VALUE_TYPE_STRETCH, VALUE_TYPE_VMK};
406
407    #[test]
408    fn unlock_and_read_synthetic_volume() {
409        // Self-consistency (Tier-3): we author both encoder and decoder here, so
410        // this proves the pipeline is internally consistent, NOT that it matches
411        // BitLocker. The Tier-1 pybde oracle (oracle_bdetogo.rs) is the real proof.
412        let (image, plain) = build_volume("test-pw");
413        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
414        let mut buf = [0u8; 512];
415        vol.read_at(0, &mut buf).unwrap();
416        assert_eq!(buf, plain);
417        assert_eq!(vol.metadata().encryption_method, 0x8000);
418
419        // A metadata-block region reads back as zero in the decrypted view.
420        let mut z = [1u8; 512];
421        vol.read_at(META_BLOCK_OFFSET, &mut z).unwrap();
422        assert_eq!(z, [0u8; 512]);
423
424        // A sector past the relocated volume-header region but still encrypted:
425        // physical == logical, decrypted in place (no panic).
426        let mut e = [0u8; 512];
427        vol.read_at(0x600, &mut e).unwrap();
428
429        // A sector at/after the encrypted-volume-size boundary is returned raw
430        // (the image is zero there).
431        let mut r = [0u8; 512];
432        vol.read_at(ENCRYPTED_SIZE, &mut r).unwrap();
433        assert_eq!(r, [0u8; 512]);
434    }
435
436    #[test]
437    fn seek_variants_and_eof() {
438        use std::io::{Read as _, Seek as _};
439        let (image, _) = build_volume("test-pw");
440        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
441
442        assert_eq!(vol.seek(SeekFrom::End(0)).unwrap(), IMAGE_SIZE as u64);
443        let mut b = [0u8; 16];
444        assert_eq!(vol.read(&mut b).unwrap(), 0); // read at EOF
445
446        assert_eq!(vol.seek(SeekFrom::Start(10)).unwrap(), 10);
447        assert_eq!(vol.seek(SeekFrom::Current(5)).unwrap(), 15);
448        assert!(vol.seek(SeekFrom::Current(-100)).is_err()); // before start
449    }
450
451    /// A metadata-only image (no key material) with configurable cipher,
452    /// protectors, header offsets, and whether the block is actually present.
453    fn meta_only_image(
454        method: u16,
455        protectors: &[u16],
456        header_offsets: [u64; 3],
457        block_at: Option<usize>,
458    ) -> Vec<u8> {
459        let mut entries = Vec::new();
460        for p in protectors {
461            let mut d = vec![0u8; 28];
462            d[26..28].copy_from_slice(&p.to_le_bytes());
463            entries.extend(entry(0x0002, 0x0008, &d));
464        }
465        let metadata_size = 48 + entries.len();
466        let mut image = vec![0u8; 0x2000];
467        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
468        image[3..11].copy_from_slice(b"MSWIN4.1");
469        image[12] = 0x02;
470        image[440..448].copy_from_slice(&header_offsets[0].to_le_bytes());
471        image[448..456].copy_from_slice(&header_offsets[1].to_le_bytes());
472        image[456..464].copy_from_slice(&header_offsets[2].to_le_bytes());
473        if let Some(mb) = block_at {
474            image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
475            image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
476            image[mb + 32..mb + 40].copy_from_slice(&(mb as u64).to_le_bytes());
477            image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
478            image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&method.to_le_bytes());
479            image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
480        }
481        image
482    }
483
484    #[test]
485    fn unsupported_method_errors() {
486        let img = meta_only_image(0x8004, &[0x2000], [0x1000, 0, 0], Some(0x1000));
487        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
488        assert!(matches!(
489            res,
490            Err(BdeError::UnsupportedEncryptionMethod { method: 0x8004 })
491        ));
492    }
493
494    #[test]
495    fn no_password_protector_errors() {
496        // Recovery-only volume — no password protector to unlock with.
497        let img = meta_only_image(0x8000, &[0x0800], [0x1000, 0, 0], Some(0x1000));
498        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
499        assert!(matches!(res, Err(BdeError::NoPasswordProtector { .. })));
500    }
501
502    #[test]
503    fn no_valid_metadata_errors() {
504        // Valid BitLocker header, but the block offsets point to non-FVE bytes.
505        let img = meta_only_image(0x8000, &[0x2000], [0x1000, 0x1200, 0x1400], None);
506        let err = BitLockerVolume::read_metadata(&mut Cursor::new(img)).unwrap_err();
507        assert!(matches!(err, BdeError::NoValidMetadata { .. }));
508    }
509
510    #[test]
511    fn read_metadata_skips_zero_first_offset() {
512        // First offset zero (skipped), second valid — covers the `continue`.
513        let img = meta_only_image(0x8000, &[0x2000], [0, 0x1000, 0], Some(0x1000));
514        let meta = BitLockerVolume::read_metadata(&mut Cursor::new(img)).unwrap();
515        assert_eq!(meta.encryption_method, 0x8000);
516    }
517
518    /// A reader that returns a valid header once, then a transient `Interrupted`,
519    /// then a hard error — to exercise the I/O-error arms of `read_available`.
520    struct FlakyReader {
521        header: Vec<u8>,
522        phase: usize,
523    }
524
525    impl Read for FlakyReader {
526        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
527            self.phase += 1;
528            match self.phase {
529                1 => {
530                    let n = buf.len().min(self.header.len());
531                    buf[..n].copy_from_slice(&self.header[..n]);
532                    Ok(n)
533                }
534                2 => Err(std::io::Error::new(
535                    std::io::ErrorKind::Interrupted,
536                    "eintr",
537                )),
538                _ => Err(std::io::Error::other("boom")),
539            }
540        }
541    }
542
543    impl Seek for FlakyReader {
544        fn seek(&mut self, _pos: SeekFrom) -> std::io::Result<u64> {
545            Ok(0)
546        }
547    }
548
549    #[test]
550    fn io_error_during_block_read_propagates() {
551        let mut header = vec![0u8; 512];
552        header[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
553        header[3..11].copy_from_slice(b"MSWIN4.1");
554        header[12] = 0x02;
555        header[440..448].copy_from_slice(&0x1000u64.to_le_bytes());
556        let mut reader = FlakyReader { header, phase: 0 };
557        let res = BitLockerVolume::read_metadata(&mut reader);
558        assert!(matches!(res, Err(BdeError::Io(_))));
559    }
560
561    #[test]
562    fn wrong_password_fails_authentication() {
563        let (image, _) = build_volume("test-pw");
564        let res = BitLockerVolume::unlock_with_password(Cursor::new(image), "wrong");
565        assert!(matches!(
566            res,
567            Err(BdeError::AuthenticationFailed { what }) if what == "volume master key"
568        ));
569    }
570
571    #[test]
572    fn read_and_seek_traits() {
573        use std::io::{Read as _, Seek as _};
574        let (image, plain) = build_volume("test-pw");
575        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
576        vol.seek(SeekFrom::Start(0)).unwrap();
577        let mut buf = [0u8; 256];
578        vol.read_exact(&mut buf).unwrap();
579        assert_eq!(&buf[..], &plain[..256]);
580        assert_eq!(vol.volume_size(), IMAGE_SIZE as u64);
581    }
582
583    #[test]
584    fn non_bitlocker_reader_errors() {
585        let r = BitLockerVolume::unlock_with_password(Cursor::new(vec![0u8; 1024]), "x");
586        assert!(matches!(r, Err(BdeError::NotBitLocker { .. })));
587    }
588
589    #[test]
590    fn read_metadata_reports_protectors() {
591        let (image, _) = build_volume("test-pw");
592        let meta = BitLockerVolume::read_metadata(&mut Cursor::new(image)).unwrap();
593        assert_eq!(meta.protector_types(), vec![PROTECTION_PASSWORD]);
594    }
595}