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, recovery_key_hash, stretch_key, SectorCipher};
12use crate::error::{BdeError, Result};
13use crate::header::VolumeHeader;
14use crate::metadata::{
15    FveMetadata, MetadataEntry, PROTECTION_CLEAR, PROTECTION_PASSWORD, PROTECTION_RECOVERY,
16    PROTECTION_STARTUP_KEY, VALUE_TYPE_AES_CCM, VALUE_TYPE_EXTERNAL_KEY, VALUE_TYPE_KEY,
17    VALUE_TYPE_STRETCH,
18};
19use crate::method::{EncryptionMethod, SectorCipherKind};
20
21/// Encryption method sentinel: the volume is not encrypted.
22const METHOD_NONE: u16 = 0x0000;
23/// The fixed BitLocker sector size.
24const SECTOR_SIZE: usize = 512;
25/// How many bytes to read at a candidate metadata offset (reserved FVE metadata
26/// blocks are well under this).
27const METADATA_READ_LEN: usize = 64 * 1024;
28
29/// Namespace for opening a BitLocker volume. All state lives in the returned
30/// [`DecryptedVolume`]; this type carries no data itself.
31pub struct BitLockerVolume;
32
33impl BitLockerVolume {
34    /// Parse the volume header and the first valid FVE metadata block from
35    /// `reader`, without unlocking. Used by the forensic analyzer to audit the
36    /// key protectors even when no password is known.
37    ///
38    /// # Errors
39    /// [`BdeError::NotBitLocker`] if the header signature is absent, or
40    /// [`BdeError::NoValidMetadata`] if no `-FVE-FS-` block is found.
41    pub fn read_metadata<R: Read + Seek>(reader: &mut R) -> Result<FveMetadata> {
42        let mut header = [0u8; SECTOR_SIZE];
43        reader.seek(SeekFrom::Start(0))?;
44        read_fill(reader, &mut header)?;
45        let volume_header = VolumeHeader::parse(&header)?;
46
47        let offsets = volume_header.fve_metadata_offsets;
48        for &offset in &offsets {
49            if offset == 0 {
50                continue;
51            }
52            let mut block = vec![0u8; METADATA_READ_LEN];
53            reader.seek(SeekFrom::Start(offset))?;
54            let n = read_available(reader, &mut block)?;
55            block.truncate(n);
56            if let Some(meta) = FveMetadata::parse(&block, volume_header.bytes_per_sector) {
57                return Ok(meta);
58            }
59        }
60        Err(BdeError::NoValidMetadata { offsets })
61    }
62
63    /// Unlock the volume with `password`, returning a plaintext view.
64    ///
65    /// # Errors
66    /// Fails loud on a non-BitLocker image, an unsupported cipher, a missing
67    /// password protector, absent key material, or a wrong password (the AES-CCM
68    /// tag fails to verify).
69    pub fn unlock_with_password<R: Read + Seek>(
70        reader: R,
71        password: &str,
72    ) -> Result<DecryptedVolume<R>> {
73        Self::unlock(
74            reader,
75            PROTECTION_PASSWORD,
76            "password",
77            &VmkUnwrap::Stretch(password_hash(password)),
78        )
79    }
80
81    /// Unlock the volume with a 48-digit `recovery` password, returning a
82    /// plaintext view. Uses the recovery protector (`0x0800`) and the recovery
83    /// key-derivation (`recovery_key_hash`).
84    ///
85    /// # Errors
86    /// [`BdeError::InvalidRecoveryPassword`] if the recovery password is
87    /// malformed; otherwise the same failures as [`Self::unlock_with_password`]
88    /// (non-BitLocker image, unsupported/unvalidated cipher, no recovery
89    /// protector, absent key material, or a wrong key).
90    pub fn unlock_with_recovery_password<R: Read + Seek>(
91        reader: R,
92        recovery: &str,
93    ) -> Result<DecryptedVolume<R>> {
94        let key_hash = recovery_key_hash(recovery)
95            .map_err(|reason| BdeError::InvalidRecoveryPassword { reason })?;
96        Self::unlock(
97            reader,
98            PROTECTION_RECOVERY,
99            "recovery password",
100            &VmkUnwrap::Stretch(key_hash),
101        )
102    }
103
104    /// Unlock the volume with **no credential** via its clear-key protector
105    /// (`0x0000`), returning a plaintext view. A clear-key protector stores the
106    /// VMK unprotected — BitLocker adds it when protection is *suspended* — so the
107    /// clear key held in the VMK's KEY property unwraps the VMK directly, with no
108    /// stretch and no external key. A clear-key volume is effectively unencrypted.
109    ///
110    /// # Errors
111    /// [`BdeError::NoUnlockProtector`] if the volume carries no clear-key protector;
112    /// otherwise the same failures as [`Self::unlock_with_password`] (non-BitLocker
113    /// image, unsupported/unvalidated cipher, absent key material, or a wrong key).
114    pub fn unlock_clear_key<R: Read + Seek>(reader: R) -> Result<DecryptedVolume<R>> {
115        Self::unlock(reader, PROTECTION_CLEAR, "clear key", &VmkUnwrap::Clear)
116    }
117
118    /// Unlock the volume with a startup-key (`.BEK`) external key, returning a
119    /// plaintext view. `startup_key` is the raw bytes of the `.BEK` file. Uses the
120    /// startup-key protector (`0x0200`): the 256-bit external key held in the
121    /// `.BEK` AES-CCM-unwraps the VMK directly — no stretch, no password.
122    ///
123    /// # Errors
124    /// [`BdeError::MissingKeyMaterial`] if the `.BEK` carries no external key,
125    /// [`BdeError::NoUnlockProtector`] if the volume has no startup-key protector;
126    /// otherwise the same failures as [`Self::unlock_with_password`] (non-BitLocker
127    /// image, unsupported/unvalidated cipher, absent key material, or a wrong key).
128    pub fn unlock_with_startup_key<R: Read + Seek>(
129        reader: R,
130        startup_key: &[u8],
131    ) -> Result<DecryptedVolume<R>> {
132        let external_key = parse_startup_key(startup_key)?;
133        Self::unlock(
134            reader,
135            PROTECTION_STARTUP_KEY,
136            "startup key",
137            &VmkUnwrap::External(external_key),
138        )
139    }
140
141    /// Shared unlock path: parse metadata, gate the cipher method, derive the
142    /// sector cipher from the given protector, and return the plaintext view.
143    fn unlock<R: Read + Seek>(
144        mut reader: R,
145        protector_type: u16,
146        protector_name: &'static str,
147        unwrap: &VmkUnwrap,
148    ) -> Result<DecryptedVolume<R>> {
149        let metadata = Self::read_metadata(&mut reader)?;
150
151        // Decode the method into its three axes, then gate on whether we have an
152        // oracle-validated decrypt for it: unrecognized ⇒ Unsupported; recognized
153        // but no oracle (0x8001/0x8003/0x8004/0x8005) ⇒ Unvalidated (refuse, never
154        // decrypt by construction); validated ⇒ build the sector cipher.
155        let raw = metadata.encryption_method;
156        let kind = EncryptionMethod::decode(raw)
157            .ok_or(BdeError::UnsupportedEncryptionMethod { method: raw })?
158            .validated_kind()
159            .ok_or(BdeError::UnvalidatedEncryptionMethod { method: raw })?;
160
161        let cipher = derive_cipher(&metadata, kind, protector_type, protector_name, unwrap)?;
162        let total_size = reader.seek(SeekFrom::End(0))?;
163
164        Ok(DecryptedVolume {
165            reader,
166            cipher,
167            metadata,
168            total_size,
169            position: 0,
170        })
171    }
172}
173
174/// How the AES-CCM key that unwraps the VMK is obtained.
175enum VmkUnwrap {
176    /// Stretch this credential hash with the VMK's stretch-key salt (password /
177    /// recovery-password protector).
178    Stretch([u8; 32]),
179    /// Read the clear key stored in the VMK's KEY property — no credential, no
180    /// stretch (clear-key protector `0x0000`).
181    Clear,
182    /// Use this 256-bit external key (parsed from a startup-key `.BEK`) directly
183    /// as the AES-CCM key that unwraps the VMK — no credential, no stretch, no
184    /// stored key (startup-key protector `0x0200`).
185    External([u8; 32]),
186}
187
188/// Extract the 256-bit external key from a startup-key `.BEK` file.
189///
190/// The `.BEK` is a 48-byte FVE metadata header followed by an external-key entry
191/// (value type `0x0009`) whose payload is a 16-byte key GUID, an 8-byte FILETIME,
192/// then nested entries — one of which is a KEY property (`0x0001`: a 4-byte
193/// key-type then the raw 32-byte external key). Bounds-checked throughout: a
194/// truncated or malformed `.BEK` yields a loud error, never a panic.
195fn parse_startup_key(bek: &[u8]) -> Result<[u8; 32]> {
196    let body = bek.get(48..).unwrap_or_default();
197    let external = MetadataEntry::parse_sequence(body)
198        .into_iter()
199        .find(|e| e.value_type == VALUE_TYPE_EXTERNAL_KEY)
200        .ok_or(BdeError::MissingKeyMaterial {
201            what: "external key",
202        })?;
203    // The external-key payload is a 16-byte key GUID + 8-byte FILETIME, then the
204    // nested entries hold the KEY property with the raw 32-byte key.
205    let key = external
206        .nested(24)
207        .into_iter()
208        .find(|e| e.value_type == VALUE_TYPE_KEY)
209        .ok_or(BdeError::MissingKeyMaterial {
210            what: "external key property",
211        })?;
212    take_key32(&key.data, 4, "external key")
213}
214
215/// Derive the sector cipher from the VMK protected by `protector_type`, obtaining
216/// the VMK's AES-CCM unwrap key per `unwrap` (stretch a credential, or read the
217/// stored clear key), and build the transform for the already-validated `kind`.
218/// `protector_name` names the protector in the not-found error.
219fn derive_cipher(
220    metadata: &FveMetadata,
221    kind: SectorCipherKind,
222    protector_type: u16,
223    protector_name: &'static str,
224    unwrap: &VmkUnwrap,
225) -> Result<SectorCipher> {
226    // 1. Locate the VMK for the requested protector.
227    let vmk = metadata
228        .vmk_entries()
229        .find(|e| e.protection_type() == Some(protector_type))
230        .ok_or_else(|| BdeError::NoUnlockProtector {
231            protector: protector_name,
232            found: metadata.protector_types(),
233        })?;
234
235    // VMK properties are nested entries starting at value-data offset 28.
236    let props = vmk.nested(28);
237    let vmk_ccm = props
238        .iter()
239        .find(|e| e.value_type == VALUE_TYPE_AES_CCM)
240        .ok_or(BdeError::MissingKeyMaterial {
241            what: "VMK AES-CCM key",
242        })?;
243
244    // 2. Obtain the 32-byte key that AES-CCM-unwraps the VMK: stretch the
245    //    credential with the VMK's stretch-key salt, or read the clear key stored
246    //    in the VMK's KEY property (clear-key protector — no credential).
247    let unwrap_key = match unwrap {
248        VmkUnwrap::Stretch(key_hash) => {
249            let stretch = props
250                .iter()
251                .find(|e| e.value_type == VALUE_TYPE_STRETCH)
252                .ok_or(BdeError::MissingKeyMaterial {
253                    what: "stretch key",
254                })?;
255            // Salt is at stretch value-data offset 4 (after the 4-byte method).
256            let mut salt = [0u8; 16];
257            let salt_src = stretch
258                .data
259                .get(4..20)
260                .ok_or(BdeError::MissingKeyMaterial {
261                    what: "stretch salt",
262                })?;
263            salt.copy_from_slice(salt_src);
264            stretch_key(key_hash, &salt)
265        }
266        VmkUnwrap::Clear => {
267            // FVE KEY property: a 4-byte method, then the 32-byte clear key.
268            let key = props
269                .iter()
270                .find(|e| e.value_type == VALUE_TYPE_KEY)
271                .ok_or(BdeError::MissingKeyMaterial { what: "clear key" })?;
272            take_key32(&key.data, 4, "clear")?
273        }
274        // The external key from the .BEK is the AES-CCM unwrap key directly.
275        VmkUnwrap::External(key) => *key,
276    };
277
278    let vmk_container =
279        aes_ccm_unwrap(&unwrap_key, &vmk_ccm.data).ok_or(BdeError::AuthenticationFailed {
280            what: "volume master key",
281        })?;
282    let vmk_key = take_key32(&vmk_container, 12, "volume master key")?;
283
284    // 3. FVEK entry -> unwrap with the VMK -> FVEK (first 16 bytes). The
285    //    diffuser kind additionally carries a 16-byte TWEAK key at offset 44.
286    let fvek_entry = metadata
287        .fvek_entry()
288        .ok_or(BdeError::MissingKeyMaterial { what: "FVEK entry" })?;
289    let fvek_container = aes_ccm_unwrap(&vmk_key, &fvek_entry.data)
290        .ok_or(BdeError::AuthenticationFailed { what: "FVEK" })?;
291
292    match kind {
293        SectorCipherKind::Cbc128Diffuser => {
294            let fvek = take_key16(&fvek_container, 12, "FVEK")?;
295            let tweak = take_key16(&fvek_container, 44, "FVEK")?;
296            Ok(SectorCipher::new(fvek, tweak))
297        }
298        SectorCipherKind::Cbc128 => {
299            let fvek = take_key16(&fvek_container, 12, "FVEK")?;
300            Ok(SectorCipher::new_cbc(fvek))
301        }
302        SectorCipherKind::Cbc256 => {
303            let fvek = take_key32(&fvek_container, 12, "FVEK")?;
304            Ok(SectorCipher::new_cbc256(fvek))
305        }
306        SectorCipherKind::Xts128 => {
307            // XTS-128 carries a 32-byte key (two AES-128 keys) at offset 12.
308            let fvek = take_key32(&fvek_container, 12, "FVEK")?;
309            Ok(SectorCipher::new_xts128(fvek))
310        }
311        SectorCipherKind::Xts256 => {
312            // XTS-256 carries a 64-byte key (two AES-256 keys) at offset 12.
313            let fvek = take_key64(&fvek_container, 12, "FVEK")?;
314            Ok(SectorCipher::new_xts256(fvek))
315        }
316    }
317}
318
319fn take_key64(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 64]> {
320    let s = container
321        .get(off..off + 64)
322        .ok_or(BdeError::MalformedKeyContainer {
323            what,
324            got: container.len(),
325            need: off + 64,
326        })?;
327    let mut k = [0u8; 64];
328    k.copy_from_slice(s);
329    Ok(k)
330}
331
332fn take_key32(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 32]> {
333    let s = container
334        .get(off..off + 32)
335        .ok_or(BdeError::MalformedKeyContainer {
336            what,
337            got: container.len(),
338            need: off + 32,
339        })?;
340    let mut k = [0u8; 32];
341    k.copy_from_slice(s);
342    Ok(k)
343}
344
345fn take_key16(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 16]> {
346    let s = container
347        .get(off..off + 16)
348        .ok_or(BdeError::MalformedKeyContainer {
349            what,
350            got: container.len(),
351            need: off + 16,
352        })?;
353    let mut k = [0u8; 16];
354    k.copy_from_slice(s);
355    Ok(k)
356}
357
358/// A plaintext view of an unlocked BitLocker volume.
359pub struct DecryptedVolume<R> {
360    reader: R,
361    cipher: SectorCipher,
362    metadata: FveMetadata,
363    total_size: u64,
364    position: u64,
365}
366
367impl<R: Read + Seek> DecryptedVolume<R> {
368    /// The parsed FVE metadata (cipher, protectors, volume GUID, …).
369    #[must_use]
370    pub fn metadata(&self) -> &FveMetadata {
371        &self.metadata
372    }
373
374    /// The total size of the volume in bytes.
375    #[must_use]
376    pub fn volume_size(&self) -> u64 {
377        self.total_size
378    }
379
380    /// Read decrypted bytes at logical `offset` into `buf`, filling it completely
381    /// (bytes past the end of the volume read back as zero).
382    ///
383    /// # Errors
384    /// Propagates I/O errors from the underlying reader.
385    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
386        let mut done = 0usize;
387        while done < buf.len() {
388            let pos = offset + done as u64;
389            let sector_start = pos - (pos % SECTOR_SIZE as u64);
390            let within = (pos - sector_start) as usize;
391            let plain = self.decrypt_logical_sector(sector_start)?;
392            let take = (SECTOR_SIZE - within).min(buf.len() - done);
393            buf[done..done + take].copy_from_slice(&plain[within..within + take]);
394            done += take;
395        }
396        Ok(())
397    }
398
399    /// Decrypt the logical 512-byte sector starting at `sector_start` (a multiple
400    /// of 512), applying metadata-region blanking, volume-header relocation, and
401    /// the encrypted-volume-size boundary exactly as BitLocker's read path does.
402    fn decrypt_logical_sector(&mut self, sector_start: u64) -> Result<[u8; SECTOR_SIZE]> {
403        // Metadata block regions read back as zero in the decrypted view.
404        let meta_size = u64::from(self.metadata.metadata_size);
405        for &m in &self.metadata.metadata_offsets {
406            if m != 0 && sector_start >= m && sector_start < m + meta_size {
407                return Ok([0u8; SECTOR_SIZE]);
408            }
409        }
410
411        // The first `volume_header_size` bytes are stored, encrypted, elsewhere;
412        // the physical location doubles as the IV sector address.
413        let physical = if sector_start < self.metadata.volume_header_size {
414            self.metadata.volume_header_offset + sector_start
415        } else {
416            sector_start
417        };
418
419        let mut ct = [0u8; SECTOR_SIZE];
420        self.reader.seek(SeekFrom::Start(physical))?;
421        read_available(&mut self.reader, &mut ct)?;
422
423        // Not encrypted: NONE method, or past the still-encrypted region.
424        let evs = self.metadata.encrypted_volume_size;
425        if self.metadata.encryption_method == METHOD_NONE || (evs != 0 && physical >= evs) {
426            return Ok(ct);
427        }
428
429        let plain = self.cipher.decrypt_sector(&ct, physical);
430        let mut out = [0u8; SECTOR_SIZE];
431        let n = plain.len().min(SECTOR_SIZE);
432        out[..n].copy_from_slice(&plain[..n]);
433        Ok(out)
434    }
435}
436
437impl<R: Read + Seek> Read for DecryptedVolume<R> {
438    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
439        if self.position >= self.total_size {
440            return Ok(0);
441        }
442        let remaining = self.total_size - self.position;
443        let n = (buf.len() as u64).min(remaining) as usize;
444        self.read_at(self.position, &mut buf[..n])
445            .map_err(|e| std::io::Error::other(e.to_string()))?;
446        self.position += n as u64;
447        Ok(n)
448    }
449}
450
451impl<R: Read + Seek> Seek for DecryptedVolume<R> {
452    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
453        let new = match pos {
454            SeekFrom::Start(o) => i128::from(o),
455            SeekFrom::End(o) => i128::from(self.total_size) + i128::from(o),
456            SeekFrom::Current(o) => i128::from(self.position) + i128::from(o),
457        };
458        if new < 0 {
459            return Err(std::io::Error::new(
460                std::io::ErrorKind::InvalidInput,
461                "seek before start",
462            ));
463        }
464        self.position = new as u64;
465        Ok(self.position)
466    }
467}
468
469/// Read exactly `buf.len()` bytes, erroring on premature EOF.
470fn read_fill<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
471    reader.read_exact(buf)?;
472    Ok(())
473}
474
475/// Read up to `buf.len()` bytes, zero-filling the remainder on EOF. Returns the
476/// number of real bytes read.
477fn read_available<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
478    let mut filled = 0;
479    while filled < buf.len() {
480        match reader.read(&mut buf[filled..]) {
481            Ok(0) => break,
482            Ok(n) => filled += n,
483            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
484            Err(e) => return Err(e.into()),
485        }
486    }
487    for b in &mut buf[filled..] {
488        *b = 0;
489    }
490    Ok(filled)
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::crypto::{aes_ccm_wrap, password_hash, stretch_key, SectorCipher};
497    use crate::metadata::{
498        ENTRY_TYPE_FVEK, ENTRY_TYPE_VMK, ENTRY_TYPE_VOLUME_HEADER, PROTECTION_CLEAR,
499        PROTECTION_PASSWORD,
500    };
501    use std::io::Cursor;
502
503    const RELOCATED_OFFSET: u64 = 0x4000;
504    const META_BLOCK_OFFSET: u64 = 0x1000;
505    const IMAGE_SIZE: usize = 0x5000;
506    // Smaller than the image so a physical offset past it exercises the
507    // still-encrypted-region boundary (bytes beyond are returned raw).
508    const ENCRYPTED_SIZE: u64 = 0x4800;
509
510    fn entry(entry_type: u16, value_type: u16, data: &[u8]) -> Vec<u8> {
511        let size = (8 + data.len()) as u16;
512        let mut v = Vec::new();
513        v.extend_from_slice(&size.to_le_bytes());
514        v.extend_from_slice(&entry_type.to_le_bytes());
515        v.extend_from_slice(&value_type.to_le_bytes());
516        v.extend_from_slice(&1u16.to_le_bytes());
517        v.extend_from_slice(data);
518        v
519    }
520
521    /// Build a minimal synthetic BitLocker To Go volume plus the plaintext of its
522    /// relocated first sector.
523    fn build_volume(password: &str) -> (Vec<u8>, [u8; 512]) {
524        let salt = [0x33u8; 16];
525        let fvek = [0x11u8; 16];
526        let tweak = [0x22u8; 16];
527        let vmk = [0x44u8; 32];
528
529        let mut vmk_container = vec![0u8; 44];
530        vmk_container[12..44].copy_from_slice(&vmk);
531        let stretched = stretch_key(&password_hash(password), &salt);
532        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x55; 12], &vmk_container);
533
534        let mut fvek_container = vec![0u8; 76];
535        fvek_container[12..28].copy_from_slice(&fvek);
536        fvek_container[44..60].copy_from_slice(&tweak);
537        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x66; 12], &fvek_container);
538
539        let mut stretch_data = vec![0u8; 4]; // 4-byte method
540        stretch_data.extend_from_slice(&salt);
541        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
542        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
543
544        let mut vmk_data = vec![0u8; 28];
545        vmk_data[26..28].copy_from_slice(&PROTECTION_PASSWORD.to_le_bytes());
546        vmk_data.extend_from_slice(&stretch_entry);
547        vmk_data.extend_from_slice(&vmk_ccm_entry);
548        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
549
550        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
551
552        let mut vh_data = Vec::new();
553        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
554        vh_data.extend_from_slice(&512u64.to_le_bytes());
555        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
556
557        let mut entries = Vec::new();
558        entries.extend_from_slice(&vh_entry);
559        entries.extend_from_slice(&vmk_entry);
560        entries.extend_from_slice(&fvek_entry);
561        let metadata_size = 48 + entries.len();
562
563        let mut image = vec![0u8; IMAGE_SIZE];
564        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
565        image[3..11].copy_from_slice(b"MSWIN4.1");
566        image[12] = 0x02; // bytes per sector = 512
567        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
568
569        let mb = META_BLOCK_OFFSET as usize;
570        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
571        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
572        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
573        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
574        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
575        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
576        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
577        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8000u16.to_le_bytes());
578        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
579
580        let mut plain = [0u8; 512];
581        for (i, b) in plain.iter_mut().enumerate() {
582            *b = (i as u8) ^ 0x5a;
583        }
584        let cipher = SectorCipher::new(fvek, tweak);
585        let ct = cipher.encrypt_sector(&plain, RELOCATED_OFFSET);
586        let ro = RELOCATED_OFFSET as usize;
587        image[ro..ro + 512].copy_from_slice(&ct);
588
589        (image, plain)
590    }
591
592    use crate::metadata::{VALUE_TYPE_AES_CCM, VALUE_TYPE_KEY, VALUE_TYPE_STRETCH, VALUE_TYPE_VMK};
593
594    #[test]
595    fn unlock_and_read_synthetic_volume() {
596        // Self-consistency (Tier-3): we author both encoder and decoder here, so
597        // this proves the pipeline is internally consistent, NOT that it matches
598        // BitLocker. The Tier-1 pybde oracle (oracle_bdetogo.rs) is the real proof.
599        let (image, plain) = build_volume("test-pw");
600        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
601        let mut buf = [0u8; 512];
602        vol.read_at(0, &mut buf).unwrap();
603        assert_eq!(buf, plain);
604        assert_eq!(vol.metadata().encryption_method, 0x8000);
605
606        // A metadata-block region reads back as zero in the decrypted view.
607        let mut z = [1u8; 512];
608        vol.read_at(META_BLOCK_OFFSET, &mut z).unwrap();
609        assert_eq!(z, [0u8; 512]);
610
611        // A sector past the relocated volume-header region but still encrypted:
612        // physical == logical, decrypted in place (no panic).
613        let mut e = [0u8; 512];
614        vol.read_at(0x600, &mut e).unwrap();
615
616        // A sector at/after the encrypted-volume-size boundary is returned raw
617        // (the image is zero there).
618        let mut r = [0u8; 512];
619        vol.read_at(ENCRYPTED_SIZE, &mut r).unwrap();
620        assert_eq!(r, [0u8; 512]);
621    }
622
623    /// CBC-encrypt one sector for method 0x8002: IV = ECB(FVEK, LE128(off)),
624    /// then AES-128-CBC encrypt with the FVEK — no diffuser, no tweak. This is a
625    /// test-authored encoder (Tier-3 self-consistency); the Tier-1 proof that the
626    /// *decrypt* matches BitLocker is `core/tests/oracle_bitlocker1.rs`.
627    fn cbc_encrypt_sector(fvek: &[u8; 16], byte_offset: u64, plain: &[u8; 512]) -> [u8; 512] {
628        use aes::cipher::block_padding::NoPadding;
629        use aes::cipher::generic_array::GenericArray;
630        use aes::cipher::{BlockEncrypt, BlockEncryptMut, KeyInit, KeyIvInit};
631        use aes::Aes128;
632
633        let mut iv = [0u8; 16];
634        iv[0..8].copy_from_slice(&byte_offset.to_le_bytes());
635        let mut iv_block = GenericArray::clone_from_slice(&iv);
636        Aes128::new(GenericArray::from_slice(fvek)).encrypt_block(&mut iv_block);
637
638        let mut buf = *plain;
639        cbc::Encryptor::<Aes128>::new(GenericArray::from_slice(fvek), &iv_block)
640            .encrypt_padded_mut::<NoPadding>(&mut buf, 512)
641            .unwrap();
642        buf
643    }
644
645    /// Build a minimal synthetic method-0x8002 (AES-128-CBC, no diffuser) volume
646    /// plus the plaintext of its relocated first sector, wrapping the VMK under
647    /// `protector_type` with `key_hash` as the stretch input. The FVEK container
648    /// holds only a 128-bit FVEK (no TWEAK) — the key layout for a no-diffuser
649    /// volume.
650    fn build_cbc_volume(protector_type: u16, key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
651        let salt = [0x37u8; 16];
652        let fvek = [0x13u8; 16];
653        let vmk = [0x46u8; 32];
654
655        let mut vmk_container = vec![0u8; 44];
656        vmk_container[12..44].copy_from_slice(&vmk);
657        let stretched = stretch_key(&key_hash, &salt);
658        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x57; 12], &vmk_container);
659
660        // No TWEAK for 0x8002: the container carries only the FVEK at offset 12.
661        let mut fvek_container = vec![0u8; 44];
662        fvek_container[12..28].copy_from_slice(&fvek);
663        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x68; 12], &fvek_container);
664
665        let mut stretch_data = vec![0u8; 4];
666        stretch_data.extend_from_slice(&salt);
667        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
668        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
669
670        let mut vmk_data = vec![0u8; 28];
671        vmk_data[26..28].copy_from_slice(&protector_type.to_le_bytes());
672        vmk_data.extend_from_slice(&stretch_entry);
673        vmk_data.extend_from_slice(&vmk_ccm_entry);
674        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
675
676        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
677
678        let mut vh_data = Vec::new();
679        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
680        vh_data.extend_from_slice(&512u64.to_le_bytes());
681        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
682
683        let mut entries = Vec::new();
684        entries.extend_from_slice(&vh_entry);
685        entries.extend_from_slice(&vmk_entry);
686        entries.extend_from_slice(&fvek_entry);
687        let metadata_size = 48 + entries.len();
688
689        let mut image = vec![0u8; IMAGE_SIZE];
690        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
691        image[3..11].copy_from_slice(b"MSWIN4.1");
692        image[12] = 0x02;
693        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
694
695        let mb = META_BLOCK_OFFSET as usize;
696        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
697        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
698        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
699        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
700        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
701        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
702        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
703        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8002u16.to_le_bytes());
704        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
705
706        let mut plain = [0u8; 512];
707        for (i, b) in plain.iter_mut().enumerate() {
708            *b = (i as u8) ^ 0x3c;
709        }
710        let ct = cbc_encrypt_sector(&fvek, RELOCATED_OFFSET, &plain);
711        let ro = RELOCATED_OFFSET as usize;
712        image[ro..ro + 512].copy_from_slice(&ct);
713
714        (image, plain)
715    }
716
717    #[test]
718    fn unlock_and_read_synthetic_cbc_volume() {
719        // Self-consistency (Tier-3): we author both the CBC encoder and the
720        // decoder here, so this proves the 0x8002 pipeline is internally
721        // consistent, NOT that it matches BitLocker. The Tier-1 pybde oracle
722        // (oracle_bitlocker1.rs) is the real proof.
723        let (image, plain) = build_cbc_volume(PROTECTION_PASSWORD, password_hash("cbc-pw"));
724        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "cbc-pw").unwrap();
725        let mut buf = [0u8; 512];
726        vol.read_at(0, &mut buf).unwrap();
727        assert_eq!(buf, plain);
728        assert_eq!(vol.metadata().encryption_method, 0x8002);
729    }
730
731    #[test]
732    fn unlock_and_read_synthetic_recovery_volume() {
733        // Recovery-password unlock over a synthetic 0x8002 volume: the VMK is
734        // wrapped with the recovery-key hash under a recovery protector (0x0800).
735        // Tier-3 self-consistency for the recovery-pw wiring; the real end-to-end
736        // proof is the m8003/vault Tier-1/2 oracles.
737        let rk = "111111-111111-111111-111111-111111-111111-111111-111111";
738        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
739        let (image, plain) = build_cbc_volume(crate::metadata::PROTECTION_RECOVERY, key_hash);
740        let mut vol =
741            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
742        let mut buf = [0u8; 512];
743        vol.read_at(0, &mut buf).unwrap();
744        assert_eq!(buf, plain);
745        assert_eq!(vol.metadata().encryption_method, 0x8002);
746    }
747
748    #[test]
749    fn recovery_unlock_rejects_malformed_password() {
750        let rk = "111111-111111-111111-111111-111111-111111-111111-111111";
751        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
752        let (image, _) = build_cbc_volume(crate::metadata::PROTECTION_RECOVERY, key_hash);
753        let res = BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), "nope");
754        assert!(matches!(res, Err(BdeError::InvalidRecoveryPassword { .. })));
755    }
756
757    #[test]
758    fn no_recovery_protector_errors() {
759        // A password-only volume has no recovery protector to unlock with.
760        let rk = "111111-111111-111111-111111-111111-111111-111111-111111";
761        let (image, _) = build_cbc_volume(PROTECTION_PASSWORD, password_hash("cbc-pw"));
762        let res = BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk);
763        assert!(matches!(res, Err(BdeError::NoUnlockProtector { .. })));
764    }
765
766    /// Build a synthetic method-0x8003 (AES-256-CBC, no diffuser) volume plus the
767    /// plaintext of its relocated first sector. The FVEK container carries a
768    /// 256-bit FVEK at offset 12 and no TWEAK.
769    fn build_cbc256_volume(key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
770        let salt = [0x39u8; 16];
771        let fvek = [0x24u8; 32];
772        let vmk = [0x48u8; 32];
773
774        let mut vmk_container = vec![0u8; 44];
775        vmk_container[12..44].copy_from_slice(&vmk);
776        let stretched = stretch_key(&key_hash, &salt);
777        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x59; 12], &vmk_container);
778
779        // 0x8003: 32-byte FVEK at offset 12, no TWEAK.
780        let mut fvek_container = vec![0u8; 76];
781        fvek_container[12..44].copy_from_slice(&fvek);
782        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6a; 12], &fvek_container);
783
784        let mut stretch_data = vec![0u8; 4];
785        stretch_data.extend_from_slice(&salt);
786        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
787        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
788
789        let mut vmk_data = vec![0u8; 28];
790        vmk_data[26..28].copy_from_slice(&PROTECTION_RECOVERY.to_le_bytes());
791        vmk_data.extend_from_slice(&stretch_entry);
792        vmk_data.extend_from_slice(&vmk_ccm_entry);
793        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
794
795        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
796
797        let mut vh_data = Vec::new();
798        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
799        vh_data.extend_from_slice(&512u64.to_le_bytes());
800        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
801
802        let mut entries = Vec::new();
803        entries.extend_from_slice(&vh_entry);
804        entries.extend_from_slice(&vmk_entry);
805        entries.extend_from_slice(&fvek_entry);
806        let metadata_size = 48 + entries.len();
807
808        let mut image = vec![0u8; IMAGE_SIZE];
809        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
810        image[3..11].copy_from_slice(b"MSWIN4.1");
811        image[12] = 0x02;
812        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
813
814        let mb = META_BLOCK_OFFSET as usize;
815        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
816        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
817        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
818        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
819        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
820        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
821        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
822        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8003u16.to_le_bytes());
823        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
824
825        let mut plain = [0u8; 512];
826        for (i, b) in plain.iter_mut().enumerate() {
827            *b = (i as u8) ^ 0x71;
828        }
829        let ct = SectorCipher::new_cbc256(fvek).encrypt_sector(&plain, RELOCATED_OFFSET);
830        let ro = RELOCATED_OFFSET as usize;
831        image[ro..ro + 512].copy_from_slice(&ct);
832
833        (image, plain)
834    }
835
836    #[test]
837    fn unlock_and_read_synthetic_cbc256_volume() {
838        // Tier-3 self-consistency for the 0x8003 pipeline; the Tier-2 proof is
839        // oracle_m8003.rs.
840        let rk = "222222-222222-222222-222222-222222-222222-222222-222222";
841        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
842        let (image, plain) = build_cbc256_volume(key_hash);
843        let mut vol =
844            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
845        let mut buf = [0u8; 512];
846        vol.read_at(0, &mut buf).unwrap();
847        assert_eq!(buf, plain);
848        assert_eq!(vol.metadata().encryption_method, 0x8003);
849    }
850
851    /// Build a synthetic method-0x8004 (XTS-AES-128) volume plus the plaintext of
852    /// its relocated first sector. The FVEK container carries the 32-byte XTS key
853    /// (two AES-128 keys) at offset 12.
854    fn build_xts128_volume(key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
855        let salt = [0x3bu8; 16];
856        let xts_key = [0x35u8; 32];
857        let vmk = [0x4au8; 32];
858
859        let mut vmk_container = vec![0u8; 44];
860        vmk_container[12..44].copy_from_slice(&vmk);
861        let stretched = stretch_key(&key_hash, &salt);
862        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x5b; 12], &vmk_container);
863
864        // 0x8004: 32-byte XTS key at offset 12, no separate TWEAK entry.
865        let mut fvek_container = vec![0u8; 76];
866        fvek_container[12..44].copy_from_slice(&xts_key);
867        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6c; 12], &fvek_container);
868
869        let mut stretch_data = vec![0u8; 4];
870        stretch_data.extend_from_slice(&salt);
871        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
872        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
873
874        let mut vmk_data = vec![0u8; 28];
875        vmk_data[26..28].copy_from_slice(&PROTECTION_RECOVERY.to_le_bytes());
876        vmk_data.extend_from_slice(&stretch_entry);
877        vmk_data.extend_from_slice(&vmk_ccm_entry);
878        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
879
880        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
881
882        let mut vh_data = Vec::new();
883        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
884        vh_data.extend_from_slice(&512u64.to_le_bytes());
885        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
886
887        let mut entries = Vec::new();
888        entries.extend_from_slice(&vh_entry);
889        entries.extend_from_slice(&vmk_entry);
890        entries.extend_from_slice(&fvek_entry);
891        let metadata_size = 48 + entries.len();
892
893        let mut image = vec![0u8; IMAGE_SIZE];
894        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
895        image[3..11].copy_from_slice(b"MSWIN4.1");
896        image[12] = 0x02;
897        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
898
899        let mb = META_BLOCK_OFFSET as usize;
900        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
901        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
902        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
903        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
904        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
905        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
906        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
907        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8004u16.to_le_bytes());
908        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
909
910        let mut plain = [0u8; 512];
911        for (i, b) in plain.iter_mut().enumerate() {
912            *b = (i as u8) ^ 0x2e;
913        }
914        let ct = SectorCipher::new_xts128(xts_key).encrypt_sector(&plain, RELOCATED_OFFSET);
915        let ro = RELOCATED_OFFSET as usize;
916        image[ro..ro + 512].copy_from_slice(&ct);
917
918        (image, plain)
919    }
920
921    #[test]
922    fn unlock_and_read_synthetic_xts128_volume() {
923        // Tier-3 self-consistency for the 0x8004 pipeline; the Tier-1 proof is
924        // oracle_vault.rs.
925        let rk = "333333-333333-333333-333333-333333-333333-333333-333333";
926        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
927        let (image, plain) = build_xts128_volume(key_hash);
928        let mut vol =
929            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
930        let mut buf = [0u8; 512];
931        vol.read_at(0, &mut buf).unwrap();
932        assert_eq!(buf, plain);
933        assert_eq!(vol.metadata().encryption_method, 0x8004);
934    }
935
936    /// Build a synthetic **clear-key** (protector 0x0000) method-0x8004 volume plus
937    /// the plaintext of its relocated first sector. The clear-key VMK carries a KEY
938    /// property (value type 0x0001: `method(u32)@0` then the 32-byte clear key) that
939    /// AES-CCM-unwraps the VMK directly — no stretch, no credential. `include_key`
940    /// false omits the KEY property to exercise the missing-key-material error.
941    fn build_clearkey_volume(include_key: bool) -> (Vec<u8>, [u8; 512]) {
942        let clear_key = [0x77u8; 32];
943        let xts_key = [0x35u8; 32];
944        let vmk = [0x4au8; 32];
945
946        let mut vmk_container = vec![0u8; 44];
947        vmk_container[12..44].copy_from_slice(&vmk);
948        // The clear key unwraps the VMK directly — no stretch stage.
949        let vmk_ccm = aes_ccm_wrap(&clear_key, &[0x5b; 12], &vmk_container);
950
951        // 0x8004: 32-byte XTS key at offset 12.
952        let mut fvek_container = vec![0u8; 76];
953        fvek_container[12..44].copy_from_slice(&xts_key);
954        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6c; 12], &fvek_container);
955
956        // KEY property: 4-byte method then the 32-byte clear key.
957        let mut key_data = vec![0u8; 4];
958        key_data.extend_from_slice(&clear_key);
959        let key_entry = entry(0, VALUE_TYPE_KEY, &key_data);
960        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
961
962        let mut vmk_data = vec![0u8; 28];
963        vmk_data[26..28].copy_from_slice(&PROTECTION_CLEAR.to_le_bytes());
964        if include_key {
965            vmk_data.extend_from_slice(&key_entry);
966        }
967        vmk_data.extend_from_slice(&vmk_ccm_entry);
968        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
969
970        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
971
972        let mut vh_data = Vec::new();
973        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
974        vh_data.extend_from_slice(&512u64.to_le_bytes());
975        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
976
977        let mut entries = Vec::new();
978        entries.extend_from_slice(&vh_entry);
979        entries.extend_from_slice(&vmk_entry);
980        entries.extend_from_slice(&fvek_entry);
981        let metadata_size = 48 + entries.len();
982
983        let mut image = vec![0u8; IMAGE_SIZE];
984        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
985        image[3..11].copy_from_slice(b"MSWIN4.1");
986        image[12] = 0x02;
987        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
988
989        let mb = META_BLOCK_OFFSET as usize;
990        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
991        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
992        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
993        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
994        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
995        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
996        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
997        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8004u16.to_le_bytes());
998        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
999
1000        let mut plain = [0u8; 512];
1001        for (i, b) in plain.iter_mut().enumerate() {
1002            *b = (i as u8) ^ 0x2e;
1003        }
1004        let ct = SectorCipher::new_xts128(xts_key).encrypt_sector(&plain, RELOCATED_OFFSET);
1005        let ro = RELOCATED_OFFSET as usize;
1006        image[ro..ro + 512].copy_from_slice(&ct);
1007
1008        (image, plain)
1009    }
1010
1011    #[test]
1012    fn unlock_and_read_synthetic_clearkey_volume() {
1013        // Clear-key unlock with NO credential over a synthetic 0x8004 volume: the
1014        // VMK is wrapped directly by the clear key stored in the KEY property.
1015        // Tier-3 self-consistency for the clear-key wiring; the Tier-1 proof is the
1016        // pybde oracle (oracle_clearkey.rs).
1017        let (image, plain) = build_clearkey_volume(true);
1018        let mut vol = BitLockerVolume::unlock_clear_key(Cursor::new(image)).unwrap();
1019        let mut buf = [0u8; 512];
1020        vol.read_at(0, &mut buf).unwrap();
1021        assert_eq!(buf, plain);
1022        assert_eq!(vol.metadata().encryption_method, 0x8004);
1023    }
1024
1025    #[test]
1026    fn no_clear_key_protector_errors() {
1027        // A password-only volume has no clear-key protector to unlock with.
1028        let (image, _) = build_volume("test-pw");
1029        let res = BitLockerVolume::unlock_clear_key(Cursor::new(image));
1030        assert!(matches!(
1031            res,
1032            Err(BdeError::NoUnlockProtector { protector, .. }) if protector == "clear key"
1033        ));
1034    }
1035
1036    #[test]
1037    fn clear_key_missing_key_property_errors() {
1038        // A clear-key VMK with no KEY property cannot yield the unwrap key.
1039        let (image, _) = build_clearkey_volume(false);
1040        let res = BitLockerVolume::unlock_clear_key(Cursor::new(image));
1041        assert!(matches!(
1042            res,
1043            Err(BdeError::MissingKeyMaterial { what }) if what == "clear key"
1044        ));
1045    }
1046
1047    /// Build a `.BEK` startup-key file byte blob carrying `external_key`, in the
1048    /// libbde external-key layout: a 48-byte FVE metadata header, then an
1049    /// external-key entry (value type 0x0009) whose payload is a 16-byte key GUID +
1050    /// 8-byte FILETIME followed by a nested KEY property (0x0001: `type(u32)@0` then
1051    /// the 32-byte key).
1052    fn build_bek(external_key: &[u8; 32]) -> Vec<u8> {
1053        let mut key_data = vec![0u8; 4]; // 4-byte key type
1054        key_data.extend_from_slice(external_key);
1055        let key_entry = entry(0, VALUE_TYPE_KEY, &key_data);
1056
1057        let mut ext_data = vec![0u8; 24]; // 16-byte key GUID + 8-byte FILETIME
1058        ext_data.extend_from_slice(&key_entry);
1059        let ext_entry = entry(0, crate::metadata::VALUE_TYPE_EXTERNAL_KEY, &ext_data);
1060
1061        let mut bek = vec![0u8; 48]; // FVE metadata header
1062        bek.extend_from_slice(&ext_entry);
1063        bek
1064    }
1065
1066    /// Build a synthetic method-0x8004 startup-key (protector 0x0200) volume plus
1067    /// the plaintext of its relocated first sector and the matching `.BEK` bytes.
1068    /// The VMK is AES-CCM-wrapped directly by the 256-bit external key (no stretch,
1069    /// no KEY property in the VMK — the key lives in the `.BEK`).
1070    fn build_startupkey_volume() -> (Vec<u8>, [u8; 512], Vec<u8>) {
1071        let external_key = [0x9au8; 32];
1072        let xts_key = [0x35u8; 32];
1073        let vmk = [0x4au8; 32];
1074
1075        let mut vmk_container = vec![0u8; 44];
1076        vmk_container[12..44].copy_from_slice(&vmk);
1077        // The external key unwraps the VMK directly — no stretch stage.
1078        let vmk_ccm = aes_ccm_wrap(&external_key, &[0x5c; 12], &vmk_container);
1079
1080        let mut fvek_container = vec![0u8; 76];
1081        fvek_container[12..44].copy_from_slice(&xts_key);
1082        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6d; 12], &fvek_container);
1083
1084        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
1085
1086        let mut vmk_data = vec![0u8; 28];
1087        vmk_data[26..28].copy_from_slice(&crate::metadata::PROTECTION_STARTUP_KEY.to_le_bytes());
1088        vmk_data.extend_from_slice(&vmk_ccm_entry);
1089        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
1090
1091        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
1092
1093        let mut vh_data = Vec::new();
1094        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
1095        vh_data.extend_from_slice(&512u64.to_le_bytes());
1096        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
1097
1098        let mut entries = Vec::new();
1099        entries.extend_from_slice(&vh_entry);
1100        entries.extend_from_slice(&vmk_entry);
1101        entries.extend_from_slice(&fvek_entry);
1102        let metadata_size = 48 + entries.len();
1103
1104        let mut image = vec![0u8; IMAGE_SIZE];
1105        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
1106        image[3..11].copy_from_slice(b"MSWIN4.1");
1107        image[12] = 0x02;
1108        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
1109
1110        let mb = META_BLOCK_OFFSET as usize;
1111        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
1112        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
1113        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
1114        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
1115        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
1116        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
1117        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
1118        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8004u16.to_le_bytes());
1119        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
1120
1121        let mut plain = [0u8; 512];
1122        for (i, b) in plain.iter_mut().enumerate() {
1123            *b = (i as u8) ^ 0x2e;
1124        }
1125        let ct = SectorCipher::new_xts128(xts_key).encrypt_sector(&plain, RELOCATED_OFFSET);
1126        let ro = RELOCATED_OFFSET as usize;
1127        image[ro..ro + 512].copy_from_slice(&ct);
1128
1129        (image, plain, build_bek(&external_key))
1130    }
1131
1132    #[test]
1133    fn unlock_and_read_synthetic_startupkey_volume() {
1134        // Startup-key (.BEK) unlock over a synthetic 0x8004 volume: the VMK is
1135        // wrapped directly by the 256-bit external key held in the .BEK. Tier-3
1136        // self-consistency for the startup-key wiring; the Tier-2 proof is the
1137        // pybde-verified oracle (tests/oracle_startupkey.rs).
1138        let (image, plain, bek) = build_startupkey_volume();
1139        let mut vol = BitLockerVolume::unlock_with_startup_key(Cursor::new(image), &bek).unwrap();
1140        let mut buf = [0u8; 512];
1141        vol.read_at(0, &mut buf).unwrap();
1142        assert_eq!(buf, plain);
1143        assert_eq!(vol.metadata().encryption_method, 0x8004);
1144    }
1145
1146    #[test]
1147    fn no_startup_key_protector_errors() {
1148        // A password-only volume has no startup-key protector to unlock with.
1149        let (image, _) = build_volume("test-pw");
1150        let (_, _, bek) = build_startupkey_volume();
1151        let res = BitLockerVolume::unlock_with_startup_key(Cursor::new(image), &bek);
1152        assert!(matches!(
1153            res,
1154            Err(BdeError::NoUnlockProtector { protector, .. }) if protector == "startup key"
1155        ));
1156    }
1157
1158    #[test]
1159    fn startup_key_without_external_key_errors() {
1160        // A .BEK with no external-key entry cannot yield the unwrap key.
1161        let (image, _, _) = build_startupkey_volume();
1162        let empty_bek = vec![0u8; 48]; // header only, no external-key entry
1163        let res = BitLockerVolume::unlock_with_startup_key(Cursor::new(image), &empty_bek);
1164        assert!(matches!(
1165            res,
1166            Err(BdeError::MissingKeyMaterial { what }) if what == "external key"
1167        ));
1168    }
1169
1170    #[test]
1171    fn startup_key_external_key_without_key_property_errors() {
1172        // A .BEK whose external-key entry carries no nested KEY property.
1173        let (image, _, _) = build_startupkey_volume();
1174        // External-key entry payload: 16-byte GUID + 8-byte FILETIME, no nested KEY.
1175        let ext_entry = entry(0, crate::metadata::VALUE_TYPE_EXTERNAL_KEY, &[0u8; 24]);
1176        let mut bek = vec![0u8; 48];
1177        bek.extend_from_slice(&ext_entry);
1178        let res = BitLockerVolume::unlock_with_startup_key(Cursor::new(image), &bek);
1179        assert!(matches!(
1180            res,
1181            Err(BdeError::MissingKeyMaterial { what }) if what == "external key property"
1182        ));
1183    }
1184
1185    /// Build a synthetic method-0x8005 (XTS-AES-256) volume plus the plaintext of
1186    /// its relocated first sector. The FVEK container carries the 64-byte XTS key
1187    /// (two AES-256 keys) at offset 12.
1188    fn build_xts256_volume(key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
1189        let salt = [0x3du8; 16];
1190        let mut xts_key = [0u8; 64];
1191        for (i, b) in xts_key.iter_mut().enumerate() {
1192            *b = 0x46u8.wrapping_add(i as u8);
1193        }
1194        let vmk = [0x4cu8; 32];
1195
1196        let mut vmk_container = vec![0u8; 44];
1197        vmk_container[12..44].copy_from_slice(&vmk);
1198        let stretched = stretch_key(&key_hash, &salt);
1199        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x5d; 12], &vmk_container);
1200
1201        // 0x8005: 64-byte XTS key at offset 12.
1202        let mut fvek_container = vec![0u8; 76];
1203        fvek_container[12..76].copy_from_slice(&xts_key);
1204        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6e; 12], &fvek_container);
1205
1206        let mut stretch_data = vec![0u8; 4];
1207        stretch_data.extend_from_slice(&salt);
1208        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
1209        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
1210
1211        let mut vmk_data = vec![0u8; 28];
1212        vmk_data[26..28].copy_from_slice(&PROTECTION_RECOVERY.to_le_bytes());
1213        vmk_data.extend_from_slice(&stretch_entry);
1214        vmk_data.extend_from_slice(&vmk_ccm_entry);
1215        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
1216
1217        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
1218
1219        let mut vh_data = Vec::new();
1220        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
1221        vh_data.extend_from_slice(&512u64.to_le_bytes());
1222        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
1223
1224        let mut entries = Vec::new();
1225        entries.extend_from_slice(&vh_entry);
1226        entries.extend_from_slice(&vmk_entry);
1227        entries.extend_from_slice(&fvek_entry);
1228        let metadata_size = 48 + entries.len();
1229
1230        let mut image = vec![0u8; IMAGE_SIZE];
1231        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
1232        image[3..11].copy_from_slice(b"MSWIN4.1");
1233        image[12] = 0x02;
1234        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
1235
1236        let mb = META_BLOCK_OFFSET as usize;
1237        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
1238        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
1239        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
1240        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
1241        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
1242        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
1243        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
1244        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8005u16.to_le_bytes());
1245        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
1246
1247        let mut plain = [0u8; 512];
1248        for (i, b) in plain.iter_mut().enumerate() {
1249            *b = (i as u8) ^ 0x17;
1250        }
1251        let ct = SectorCipher::new_xts256(xts_key).encrypt_sector(&plain, RELOCATED_OFFSET);
1252        let ro = RELOCATED_OFFSET as usize;
1253        image[ro..ro + 512].copy_from_slice(&ct);
1254
1255        (image, plain)
1256    }
1257
1258    #[test]
1259    fn unlock_and_read_synthetic_xts256_volume() {
1260        // Tier-3 self-consistency for the 0x8005 pipeline; the Tier-2 proof is
1261        // oracle_m8005.rs.
1262        let rk = "444444-444444-444444-444444-444444-444444-444444-444444";
1263        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
1264        let (image, plain) = build_xts256_volume(key_hash);
1265        let mut vol =
1266            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
1267        let mut buf = [0u8; 512];
1268        vol.read_at(0, &mut buf).unwrap();
1269        assert_eq!(buf, plain);
1270        assert_eq!(vol.metadata().encryption_method, 0x8005);
1271    }
1272
1273    #[test]
1274    fn seek_variants_and_eof() {
1275        use std::io::{Read as _, Seek as _};
1276        let (image, _) = build_volume("test-pw");
1277        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
1278
1279        assert_eq!(vol.seek(SeekFrom::End(0)).unwrap(), IMAGE_SIZE as u64);
1280        let mut b = [0u8; 16];
1281        assert_eq!(vol.read(&mut b).unwrap(), 0); // read at EOF
1282
1283        assert_eq!(vol.seek(SeekFrom::Start(10)).unwrap(), 10);
1284        assert_eq!(vol.seek(SeekFrom::Current(5)).unwrap(), 15);
1285        assert!(vol.seek(SeekFrom::Current(-100)).is_err()); // before start
1286    }
1287
1288    /// A metadata-only image (no key material) with configurable cipher,
1289    /// protectors, header offsets, and whether the block is actually present.
1290    fn meta_only_image(
1291        method: u16,
1292        protectors: &[u16],
1293        header_offsets: [u64; 3],
1294        block_at: Option<usize>,
1295    ) -> Vec<u8> {
1296        let mut entries = Vec::new();
1297        for p in protectors {
1298            let mut d = vec![0u8; 28];
1299            d[26..28].copy_from_slice(&p.to_le_bytes());
1300            entries.extend(entry(0x0002, 0x0008, &d));
1301        }
1302        let metadata_size = 48 + entries.len();
1303        let mut image = vec![0u8; 0x2000];
1304        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
1305        image[3..11].copy_from_slice(b"MSWIN4.1");
1306        image[12] = 0x02;
1307        image[440..448].copy_from_slice(&header_offsets[0].to_le_bytes());
1308        image[448..456].copy_from_slice(&header_offsets[1].to_le_bytes());
1309        image[456..464].copy_from_slice(&header_offsets[2].to_le_bytes());
1310        if let Some(mb) = block_at {
1311            image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
1312            image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
1313            image[mb + 32..mb + 40].copy_from_slice(&(mb as u64).to_le_bytes());
1314            image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
1315            image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&method.to_le_bytes());
1316            image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
1317        }
1318        image
1319    }
1320
1321    #[test]
1322    fn recognized_but_unvalidated_method_refuses() {
1323        // Only 0x8001 (CBC-256 + Elephant Diffuser) is still recognized by the
1324        // dispatch without a Tier-1/2 oracle — it must refuse (naming the
1325        // method), never by-construction decrypt. The refusal is gated before
1326        // key derivation. (0x8002–0x8005 are all now oracle-validated.)
1327        let img = meta_only_image(0x8001, &[0x2000], [0x1000, 0, 0], Some(0x1000));
1328        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
1329        assert!(matches!(
1330            res,
1331            Err(BdeError::UnvalidatedEncryptionMethod { method: 0x8001 })
1332        ));
1333    }
1334
1335    #[test]
1336    fn unrecognized_method_errors() {
1337        // Not a 0x800x BDE cipher at all — a distinct "unsupported" error.
1338        let img = meta_only_image(0x1234, &[0x2000], [0x1000, 0, 0], Some(0x1000));
1339        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
1340        assert!(matches!(
1341            res,
1342            Err(BdeError::UnsupportedEncryptionMethod { method: 0x1234 })
1343        ));
1344    }
1345
1346    #[test]
1347    fn no_password_protector_errors() {
1348        // Recovery-only volume — no password protector to unlock with.
1349        let img = meta_only_image(0x8000, &[0x0800], [0x1000, 0, 0], Some(0x1000));
1350        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
1351        assert!(matches!(
1352            res,
1353            Err(BdeError::NoUnlockProtector { protector, .. }) if protector == "password"
1354        ));
1355    }
1356
1357    #[test]
1358    fn no_valid_metadata_errors() {
1359        // Valid BitLocker header, but the block offsets point to non-FVE bytes.
1360        let img = meta_only_image(0x8000, &[0x2000], [0x1000, 0x1200, 0x1400], None);
1361        let err = BitLockerVolume::read_metadata(&mut Cursor::new(img)).unwrap_err();
1362        assert!(matches!(err, BdeError::NoValidMetadata { .. }));
1363    }
1364
1365    #[test]
1366    fn read_metadata_skips_zero_first_offset() {
1367        // First offset zero (skipped), second valid — covers the `continue`.
1368        let img = meta_only_image(0x8000, &[0x2000], [0, 0x1000, 0], Some(0x1000));
1369        let meta = BitLockerVolume::read_metadata(&mut Cursor::new(img)).unwrap();
1370        assert_eq!(meta.encryption_method, 0x8000);
1371    }
1372
1373    /// A reader that returns a valid header once, then a transient `Interrupted`,
1374    /// then a hard error — to exercise the I/O-error arms of `read_available`.
1375    struct FlakyReader {
1376        header: Vec<u8>,
1377        phase: usize,
1378    }
1379
1380    impl Read for FlakyReader {
1381        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1382            self.phase += 1;
1383            match self.phase {
1384                1 => {
1385                    let n = buf.len().min(self.header.len());
1386                    buf[..n].copy_from_slice(&self.header[..n]);
1387                    Ok(n)
1388                }
1389                2 => Err(std::io::Error::new(
1390                    std::io::ErrorKind::Interrupted,
1391                    "eintr",
1392                )),
1393                _ => Err(std::io::Error::other("boom")),
1394            }
1395        }
1396    }
1397
1398    impl Seek for FlakyReader {
1399        fn seek(&mut self, _pos: SeekFrom) -> std::io::Result<u64> {
1400            Ok(0)
1401        }
1402    }
1403
1404    #[test]
1405    fn io_error_during_block_read_propagates() {
1406        let mut header = vec![0u8; 512];
1407        header[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
1408        header[3..11].copy_from_slice(b"MSWIN4.1");
1409        header[12] = 0x02;
1410        header[440..448].copy_from_slice(&0x1000u64.to_le_bytes());
1411        let mut reader = FlakyReader { header, phase: 0 };
1412        let res = BitLockerVolume::read_metadata(&mut reader);
1413        assert!(matches!(res, Err(BdeError::Io(_))));
1414    }
1415
1416    #[test]
1417    fn wrong_password_fails_authentication() {
1418        let (image, _) = build_volume("test-pw");
1419        let res = BitLockerVolume::unlock_with_password(Cursor::new(image), "wrong");
1420        assert!(matches!(
1421            res,
1422            Err(BdeError::AuthenticationFailed { what }) if what == "volume master key"
1423        ));
1424    }
1425
1426    #[test]
1427    fn read_and_seek_traits() {
1428        use std::io::{Read as _, Seek as _};
1429        let (image, plain) = build_volume("test-pw");
1430        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
1431        vol.seek(SeekFrom::Start(0)).unwrap();
1432        let mut buf = [0u8; 256];
1433        vol.read_exact(&mut buf).unwrap();
1434        assert_eq!(&buf[..], &plain[..256]);
1435        assert_eq!(vol.volume_size(), IMAGE_SIZE as u64);
1436    }
1437
1438    #[test]
1439    fn non_bitlocker_reader_errors() {
1440        let r = BitLockerVolume::unlock_with_password(Cursor::new(vec![0u8; 1024]), "x");
1441        assert!(matches!(r, Err(BdeError::NotBitLocker { .. })));
1442    }
1443
1444    #[test]
1445    fn read_metadata_reports_protectors() {
1446        let (image, _) = build_volume("test-pw");
1447        let meta = BitLockerVolume::read_metadata(&mut Cursor::new(image)).unwrap();
1448        assert_eq!(meta.protector_types(), vec![PROTECTION_PASSWORD]);
1449    }
1450}