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, PROTECTION_PASSWORD, PROTECTION_RECOVERY, VALUE_TYPE_AES_CCM, VALUE_TYPE_STRETCH,
16};
17use crate::method::{EncryptionMethod, SectorCipherKind};
18
19/// Encryption method sentinel: the volume is not encrypted.
20const METHOD_NONE: u16 = 0x0000;
21/// The fixed BitLocker sector size.
22const SECTOR_SIZE: usize = 512;
23/// How many bytes to read at a candidate metadata offset (reserved FVE metadata
24/// blocks are well under this).
25const METADATA_READ_LEN: usize = 64 * 1024;
26
27/// Namespace for opening a BitLocker volume. All state lives in the returned
28/// [`DecryptedVolume`]; this type carries no data itself.
29pub struct BitLockerVolume;
30
31impl BitLockerVolume {
32    /// Parse the volume header and the first valid FVE metadata block from
33    /// `reader`, without unlocking. Used by the forensic analyzer to audit the
34    /// key protectors even when no password is known.
35    ///
36    /// # Errors
37    /// [`BdeError::NotBitLocker`] if the header signature is absent, or
38    /// [`BdeError::NoValidMetadata`] if no `-FVE-FS-` block is found.
39    pub fn read_metadata<R: Read + Seek>(reader: &mut R) -> Result<FveMetadata> {
40        let mut header = [0u8; SECTOR_SIZE];
41        reader.seek(SeekFrom::Start(0))?;
42        read_fill(reader, &mut header)?;
43        let volume_header = VolumeHeader::parse(&header)?;
44
45        let offsets = volume_header.fve_metadata_offsets;
46        for &offset in &offsets {
47            if offset == 0 {
48                continue;
49            }
50            let mut block = vec![0u8; METADATA_READ_LEN];
51            reader.seek(SeekFrom::Start(offset))?;
52            let n = read_available(reader, &mut block)?;
53            block.truncate(n);
54            if let Some(meta) = FveMetadata::parse(&block, volume_header.bytes_per_sector) {
55                return Ok(meta);
56            }
57        }
58        Err(BdeError::NoValidMetadata { offsets })
59    }
60
61    /// Unlock the volume with `password`, returning a plaintext view.
62    ///
63    /// # Errors
64    /// Fails loud on a non-BitLocker image, an unsupported cipher, a missing
65    /// password protector, absent key material, or a wrong password (the AES-CCM
66    /// tag fails to verify).
67    pub fn unlock_with_password<R: Read + Seek>(
68        reader: R,
69        password: &str,
70    ) -> Result<DecryptedVolume<R>> {
71        Self::unlock_with_protector(
72            reader,
73            PROTECTION_PASSWORD,
74            "password",
75            password_hash(password),
76        )
77    }
78
79    /// Unlock the volume with a 48-digit `recovery` password, returning a
80    /// plaintext view. Uses the recovery protector (`0x0800`) and the recovery
81    /// key-derivation ([`crate::crypto::recovery_key_hash`]).
82    ///
83    /// # Errors
84    /// [`BdeError::InvalidRecoveryPassword`] if the recovery password is
85    /// malformed; otherwise the same failures as [`Self::unlock_with_password`]
86    /// (non-BitLocker image, unsupported/unvalidated cipher, no recovery
87    /// protector, absent key material, or a wrong key).
88    pub fn unlock_with_recovery_password<R: Read + Seek>(
89        reader: R,
90        recovery: &str,
91    ) -> Result<DecryptedVolume<R>> {
92        let key_hash = recovery_key_hash(recovery)
93            .map_err(|reason| BdeError::InvalidRecoveryPassword { reason })?;
94        Self::unlock_with_protector(reader, PROTECTION_RECOVERY, "recovery password", key_hash)
95    }
96
97    /// Shared unlock path: parse metadata, gate the cipher method, derive the
98    /// sector cipher from the given protector, and return the plaintext view.
99    fn unlock_with_protector<R: Read + Seek>(
100        mut reader: R,
101        protector_type: u16,
102        protector_name: &'static str,
103        key_hash: [u8; 32],
104    ) -> Result<DecryptedVolume<R>> {
105        let metadata = Self::read_metadata(&mut reader)?;
106
107        // Decode the method into its three axes, then gate on whether we have an
108        // oracle-validated decrypt for it: unrecognized ⇒ Unsupported; recognized
109        // but no oracle (0x8001/0x8003/0x8004/0x8005) ⇒ Unvalidated (refuse, never
110        // decrypt by construction); validated ⇒ build the sector cipher.
111        let raw = metadata.encryption_method;
112        let kind = EncryptionMethod::decode(raw)
113            .ok_or(BdeError::UnsupportedEncryptionMethod { method: raw })?
114            .validated_kind()
115            .ok_or(BdeError::UnvalidatedEncryptionMethod { method: raw })?;
116
117        let cipher = derive_cipher(&metadata, kind, protector_type, protector_name, &key_hash)?;
118        let total_size = reader.seek(SeekFrom::End(0))?;
119
120        Ok(DecryptedVolume {
121            reader,
122            cipher,
123            metadata,
124            total_size,
125            position: 0,
126        })
127    }
128}
129
130/// Derive the sector cipher from the VMK protected by `protector_type`, using
131/// `key_hash` as the stretch input, and build the transform for the
132/// already-validated `kind`. `protector_name` names the protector in the
133/// not-found error.
134fn derive_cipher(
135    metadata: &FveMetadata,
136    kind: SectorCipherKind,
137    protector_type: u16,
138    protector_name: &'static str,
139    key_hash: &[u8; 32],
140) -> Result<SectorCipher> {
141    // 1. Locate the VMK for the requested protector.
142    let vmk = metadata
143        .vmk_entries()
144        .find(|e| e.protection_type() == Some(protector_type))
145        .ok_or_else(|| BdeError::NoUnlockProtector {
146            protector: protector_name,
147            found: metadata.protector_types(),
148        })?;
149
150    // VMK properties are nested entries starting at value-data offset 28.
151    let props = vmk.nested(28);
152    let stretch = props
153        .iter()
154        .find(|e| e.value_type == VALUE_TYPE_STRETCH)
155        .ok_or(BdeError::MissingKeyMaterial {
156            what: "stretch key",
157        })?;
158    let vmk_ccm = props
159        .iter()
160        .find(|e| e.value_type == VALUE_TYPE_AES_CCM)
161        .ok_or(BdeError::MissingKeyMaterial {
162            what: "VMK AES-CCM key",
163        })?;
164
165    // Salt is at stretch value-data offset 4 (after the 4-byte method).
166    let mut salt = [0u8; 16];
167    let salt_src = stretch
168        .data
169        .get(4..20)
170        .ok_or(BdeError::MissingKeyMaterial {
171            what: "stretch salt",
172        })?;
173    salt.copy_from_slice(salt_src);
174
175    // 2. Key hash -> stretched key -> unwrap the VMK.
176    let stretched = stretch_key(key_hash, &salt);
177    let vmk_container =
178        aes_ccm_unwrap(&stretched, &vmk_ccm.data).ok_or(BdeError::AuthenticationFailed {
179            what: "volume master key",
180        })?;
181    let vmk_key = take_key32(&vmk_container, 12, "volume master key")?;
182
183    // 3. FVEK entry -> unwrap with the VMK -> FVEK (first 16 bytes). The
184    //    diffuser kind additionally carries a 16-byte TWEAK key at offset 44.
185    let fvek_entry = metadata
186        .fvek_entry()
187        .ok_or(BdeError::MissingKeyMaterial { what: "FVEK entry" })?;
188    let fvek_container = aes_ccm_unwrap(&vmk_key, &fvek_entry.data)
189        .ok_or(BdeError::AuthenticationFailed { what: "FVEK" })?;
190
191    match kind {
192        SectorCipherKind::Cbc128Diffuser => {
193            let fvek = take_key16(&fvek_container, 12, "FVEK")?;
194            let tweak = take_key16(&fvek_container, 44, "FVEK")?;
195            Ok(SectorCipher::new(fvek, tweak))
196        }
197        SectorCipherKind::Cbc128 => {
198            let fvek = take_key16(&fvek_container, 12, "FVEK")?;
199            Ok(SectorCipher::new_cbc(fvek))
200        }
201        SectorCipherKind::Cbc256 => {
202            let fvek = take_key32(&fvek_container, 12, "FVEK")?;
203            Ok(SectorCipher::new_cbc256(fvek))
204        }
205        SectorCipherKind::Xts128 => {
206            // XTS-128 carries a 32-byte key (two AES-128 keys) at offset 12.
207            let fvek = take_key32(&fvek_container, 12, "FVEK")?;
208            Ok(SectorCipher::new_xts128(fvek))
209        }
210        SectorCipherKind::Xts256 => {
211            // XTS-256 carries a 64-byte key (two AES-256 keys) at offset 12.
212            let fvek = take_key64(&fvek_container, 12, "FVEK")?;
213            Ok(SectorCipher::new_xts256(fvek))
214        }
215    }
216}
217
218fn take_key64(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 64]> {
219    let s = container
220        .get(off..off + 64)
221        .ok_or(BdeError::MalformedKeyContainer {
222            what,
223            got: container.len(),
224            need: off + 64,
225        })?;
226    let mut k = [0u8; 64];
227    k.copy_from_slice(s);
228    Ok(k)
229}
230
231fn take_key32(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 32]> {
232    let s = container
233        .get(off..off + 32)
234        .ok_or(BdeError::MalformedKeyContainer {
235            what,
236            got: container.len(),
237            need: off + 32,
238        })?;
239    let mut k = [0u8; 32];
240    k.copy_from_slice(s);
241    Ok(k)
242}
243
244fn take_key16(container: &[u8], off: usize, what: &'static str) -> Result<[u8; 16]> {
245    let s = container
246        .get(off..off + 16)
247        .ok_or(BdeError::MalformedKeyContainer {
248            what,
249            got: container.len(),
250            need: off + 16,
251        })?;
252    let mut k = [0u8; 16];
253    k.copy_from_slice(s);
254    Ok(k)
255}
256
257/// A plaintext view of an unlocked BitLocker volume.
258pub struct DecryptedVolume<R> {
259    reader: R,
260    cipher: SectorCipher,
261    metadata: FveMetadata,
262    total_size: u64,
263    position: u64,
264}
265
266impl<R: Read + Seek> DecryptedVolume<R> {
267    /// The parsed FVE metadata (cipher, protectors, volume GUID, …).
268    #[must_use]
269    pub fn metadata(&self) -> &FveMetadata {
270        &self.metadata
271    }
272
273    /// The total size of the volume in bytes.
274    #[must_use]
275    pub fn volume_size(&self) -> u64 {
276        self.total_size
277    }
278
279    /// Read decrypted bytes at logical `offset` into `buf`, filling it completely
280    /// (bytes past the end of the volume read back as zero).
281    ///
282    /// # Errors
283    /// Propagates I/O errors from the underlying reader.
284    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
285        let mut done = 0usize;
286        while done < buf.len() {
287            let pos = offset + done as u64;
288            let sector_start = pos - (pos % SECTOR_SIZE as u64);
289            let within = (pos - sector_start) as usize;
290            let plain = self.decrypt_logical_sector(sector_start)?;
291            let take = (SECTOR_SIZE - within).min(buf.len() - done);
292            buf[done..done + take].copy_from_slice(&plain[within..within + take]);
293            done += take;
294        }
295        Ok(())
296    }
297
298    /// Decrypt the logical 512-byte sector starting at `sector_start` (a multiple
299    /// of 512), applying metadata-region blanking, volume-header relocation, and
300    /// the encrypted-volume-size boundary exactly as BitLocker's read path does.
301    fn decrypt_logical_sector(&mut self, sector_start: u64) -> Result<[u8; SECTOR_SIZE]> {
302        // Metadata block regions read back as zero in the decrypted view.
303        let meta_size = u64::from(self.metadata.metadata_size);
304        for &m in &self.metadata.metadata_offsets {
305            if m != 0 && sector_start >= m && sector_start < m + meta_size {
306                return Ok([0u8; SECTOR_SIZE]);
307            }
308        }
309
310        // The first `volume_header_size` bytes are stored, encrypted, elsewhere;
311        // the physical location doubles as the IV sector address.
312        let physical = if sector_start < self.metadata.volume_header_size {
313            self.metadata.volume_header_offset + sector_start
314        } else {
315            sector_start
316        };
317
318        let mut ct = [0u8; SECTOR_SIZE];
319        self.reader.seek(SeekFrom::Start(physical))?;
320        read_available(&mut self.reader, &mut ct)?;
321
322        // Not encrypted: NONE method, or past the still-encrypted region.
323        let evs = self.metadata.encrypted_volume_size;
324        if self.metadata.encryption_method == METHOD_NONE || (evs != 0 && physical >= evs) {
325            return Ok(ct);
326        }
327
328        let plain = self.cipher.decrypt_sector(&ct, physical);
329        let mut out = [0u8; SECTOR_SIZE];
330        let n = plain.len().min(SECTOR_SIZE);
331        out[..n].copy_from_slice(&plain[..n]);
332        Ok(out)
333    }
334}
335
336impl<R: Read + Seek> Read for DecryptedVolume<R> {
337    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
338        if self.position >= self.total_size {
339            return Ok(0);
340        }
341        let remaining = self.total_size - self.position;
342        let n = (buf.len() as u64).min(remaining) as usize;
343        self.read_at(self.position, &mut buf[..n])
344            .map_err(|e| std::io::Error::other(e.to_string()))?;
345        self.position += n as u64;
346        Ok(n)
347    }
348}
349
350impl<R: Read + Seek> Seek for DecryptedVolume<R> {
351    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
352        let new = match pos {
353            SeekFrom::Start(o) => i128::from(o),
354            SeekFrom::End(o) => i128::from(self.total_size) + i128::from(o),
355            SeekFrom::Current(o) => i128::from(self.position) + i128::from(o),
356        };
357        if new < 0 {
358            return Err(std::io::Error::new(
359                std::io::ErrorKind::InvalidInput,
360                "seek before start",
361            ));
362        }
363        self.position = new as u64;
364        Ok(self.position)
365    }
366}
367
368/// Read exactly `buf.len()` bytes, erroring on premature EOF.
369fn read_fill<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
370    reader.read_exact(buf)?;
371    Ok(())
372}
373
374/// Read up to `buf.len()` bytes, zero-filling the remainder on EOF. Returns the
375/// number of real bytes read.
376fn read_available<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
377    let mut filled = 0;
378    while filled < buf.len() {
379        match reader.read(&mut buf[filled..]) {
380            Ok(0) => break,
381            Ok(n) => filled += n,
382            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
383            Err(e) => return Err(e.into()),
384        }
385    }
386    for b in &mut buf[filled..] {
387        *b = 0;
388    }
389    Ok(filled)
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::crypto::{aes_ccm_wrap, password_hash, stretch_key, SectorCipher};
396    use crate::metadata::{
397        ENTRY_TYPE_FVEK, ENTRY_TYPE_VMK, ENTRY_TYPE_VOLUME_HEADER, PROTECTION_PASSWORD,
398    };
399    use std::io::Cursor;
400
401    const RELOCATED_OFFSET: u64 = 0x4000;
402    const META_BLOCK_OFFSET: u64 = 0x1000;
403    const IMAGE_SIZE: usize = 0x5000;
404    // Smaller than the image so a physical offset past it exercises the
405    // still-encrypted-region boundary (bytes beyond are returned raw).
406    const ENCRYPTED_SIZE: u64 = 0x4800;
407
408    fn entry(entry_type: u16, value_type: u16, data: &[u8]) -> Vec<u8> {
409        let size = (8 + data.len()) as u16;
410        let mut v = Vec::new();
411        v.extend_from_slice(&size.to_le_bytes());
412        v.extend_from_slice(&entry_type.to_le_bytes());
413        v.extend_from_slice(&value_type.to_le_bytes());
414        v.extend_from_slice(&1u16.to_le_bytes());
415        v.extend_from_slice(data);
416        v
417    }
418
419    /// Build a minimal synthetic BitLocker To Go volume plus the plaintext of its
420    /// relocated first sector.
421    fn build_volume(password: &str) -> (Vec<u8>, [u8; 512]) {
422        let salt = [0x33u8; 16];
423        let fvek = [0x11u8; 16];
424        let tweak = [0x22u8; 16];
425        let vmk = [0x44u8; 32];
426
427        let mut vmk_container = vec![0u8; 44];
428        vmk_container[12..44].copy_from_slice(&vmk);
429        let stretched = stretch_key(&password_hash(password), &salt);
430        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x55; 12], &vmk_container);
431
432        let mut fvek_container = vec![0u8; 76];
433        fvek_container[12..28].copy_from_slice(&fvek);
434        fvek_container[44..60].copy_from_slice(&tweak);
435        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x66; 12], &fvek_container);
436
437        let mut stretch_data = vec![0u8; 4]; // 4-byte method
438        stretch_data.extend_from_slice(&salt);
439        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
440        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
441
442        let mut vmk_data = vec![0u8; 28];
443        vmk_data[26..28].copy_from_slice(&PROTECTION_PASSWORD.to_le_bytes());
444        vmk_data.extend_from_slice(&stretch_entry);
445        vmk_data.extend_from_slice(&vmk_ccm_entry);
446        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
447
448        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
449
450        let mut vh_data = Vec::new();
451        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
452        vh_data.extend_from_slice(&512u64.to_le_bytes());
453        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
454
455        let mut entries = Vec::new();
456        entries.extend_from_slice(&vh_entry);
457        entries.extend_from_slice(&vmk_entry);
458        entries.extend_from_slice(&fvek_entry);
459        let metadata_size = 48 + entries.len();
460
461        let mut image = vec![0u8; IMAGE_SIZE];
462        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
463        image[3..11].copy_from_slice(b"MSWIN4.1");
464        image[12] = 0x02; // bytes per sector = 512
465        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
466
467        let mb = META_BLOCK_OFFSET as usize;
468        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
469        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
470        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
471        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
472        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
473        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
474        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
475        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8000u16.to_le_bytes());
476        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
477
478        let mut plain = [0u8; 512];
479        for (i, b) in plain.iter_mut().enumerate() {
480            *b = (i as u8) ^ 0x5a;
481        }
482        let cipher = SectorCipher::new(fvek, tweak);
483        let ct = cipher.encrypt_sector(&plain, RELOCATED_OFFSET);
484        let ro = RELOCATED_OFFSET as usize;
485        image[ro..ro + 512].copy_from_slice(&ct);
486
487        (image, plain)
488    }
489
490    use crate::metadata::{VALUE_TYPE_AES_CCM, VALUE_TYPE_STRETCH, VALUE_TYPE_VMK};
491
492    #[test]
493    fn unlock_and_read_synthetic_volume() {
494        // Self-consistency (Tier-3): we author both encoder and decoder here, so
495        // this proves the pipeline is internally consistent, NOT that it matches
496        // BitLocker. The Tier-1 pybde oracle (oracle_bdetogo.rs) is the real proof.
497        let (image, plain) = build_volume("test-pw");
498        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
499        let mut buf = [0u8; 512];
500        vol.read_at(0, &mut buf).unwrap();
501        assert_eq!(buf, plain);
502        assert_eq!(vol.metadata().encryption_method, 0x8000);
503
504        // A metadata-block region reads back as zero in the decrypted view.
505        let mut z = [1u8; 512];
506        vol.read_at(META_BLOCK_OFFSET, &mut z).unwrap();
507        assert_eq!(z, [0u8; 512]);
508
509        // A sector past the relocated volume-header region but still encrypted:
510        // physical == logical, decrypted in place (no panic).
511        let mut e = [0u8; 512];
512        vol.read_at(0x600, &mut e).unwrap();
513
514        // A sector at/after the encrypted-volume-size boundary is returned raw
515        // (the image is zero there).
516        let mut r = [0u8; 512];
517        vol.read_at(ENCRYPTED_SIZE, &mut r).unwrap();
518        assert_eq!(r, [0u8; 512]);
519    }
520
521    /// CBC-encrypt one sector for method 0x8002: IV = ECB(FVEK, LE128(off)),
522    /// then AES-128-CBC encrypt with the FVEK — no diffuser, no tweak. This is a
523    /// test-authored encoder (Tier-3 self-consistency); the Tier-1 proof that the
524    /// *decrypt* matches BitLocker is `core/tests/oracle_bitlocker1.rs`.
525    fn cbc_encrypt_sector(fvek: &[u8; 16], byte_offset: u64, plain: &[u8; 512]) -> [u8; 512] {
526        use aes::cipher::block_padding::NoPadding;
527        use aes::cipher::generic_array::GenericArray;
528        use aes::cipher::{BlockEncrypt, BlockEncryptMut, KeyInit, KeyIvInit};
529        use aes::Aes128;
530
531        let mut iv = [0u8; 16];
532        iv[0..8].copy_from_slice(&byte_offset.to_le_bytes());
533        let mut iv_block = GenericArray::clone_from_slice(&iv);
534        Aes128::new(GenericArray::from_slice(fvek)).encrypt_block(&mut iv_block);
535
536        let mut buf = *plain;
537        cbc::Encryptor::<Aes128>::new(GenericArray::from_slice(fvek), &iv_block)
538            .encrypt_padded_mut::<NoPadding>(&mut buf, 512)
539            .unwrap();
540        buf
541    }
542
543    /// Build a minimal synthetic method-0x8002 (AES-128-CBC, no diffuser) volume
544    /// plus the plaintext of its relocated first sector, wrapping the VMK under
545    /// `protector_type` with `key_hash` as the stretch input. The FVEK container
546    /// holds only a 128-bit FVEK (no TWEAK) — the key layout for a no-diffuser
547    /// volume.
548    fn build_cbc_volume(protector_type: u16, key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
549        let salt = [0x37u8; 16];
550        let fvek = [0x13u8; 16];
551        let vmk = [0x46u8; 32];
552
553        let mut vmk_container = vec![0u8; 44];
554        vmk_container[12..44].copy_from_slice(&vmk);
555        let stretched = stretch_key(&key_hash, &salt);
556        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x57; 12], &vmk_container);
557
558        // No TWEAK for 0x8002: the container carries only the FVEK at offset 12.
559        let mut fvek_container = vec![0u8; 44];
560        fvek_container[12..28].copy_from_slice(&fvek);
561        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x68; 12], &fvek_container);
562
563        let mut stretch_data = vec![0u8; 4];
564        stretch_data.extend_from_slice(&salt);
565        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
566        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
567
568        let mut vmk_data = vec![0u8; 28];
569        vmk_data[26..28].copy_from_slice(&protector_type.to_le_bytes());
570        vmk_data.extend_from_slice(&stretch_entry);
571        vmk_data.extend_from_slice(&vmk_ccm_entry);
572        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
573
574        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
575
576        let mut vh_data = Vec::new();
577        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
578        vh_data.extend_from_slice(&512u64.to_le_bytes());
579        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
580
581        let mut entries = Vec::new();
582        entries.extend_from_slice(&vh_entry);
583        entries.extend_from_slice(&vmk_entry);
584        entries.extend_from_slice(&fvek_entry);
585        let metadata_size = 48 + entries.len();
586
587        let mut image = vec![0u8; IMAGE_SIZE];
588        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
589        image[3..11].copy_from_slice(b"MSWIN4.1");
590        image[12] = 0x02;
591        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
592
593        let mb = META_BLOCK_OFFSET as usize;
594        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
595        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
596        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
597        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
598        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
599        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
600        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
601        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8002u16.to_le_bytes());
602        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
603
604        let mut plain = [0u8; 512];
605        for (i, b) in plain.iter_mut().enumerate() {
606            *b = (i as u8) ^ 0x3c;
607        }
608        let ct = cbc_encrypt_sector(&fvek, RELOCATED_OFFSET, &plain);
609        let ro = RELOCATED_OFFSET as usize;
610        image[ro..ro + 512].copy_from_slice(&ct);
611
612        (image, plain)
613    }
614
615    #[test]
616    fn unlock_and_read_synthetic_cbc_volume() {
617        // Self-consistency (Tier-3): we author both the CBC encoder and the
618        // decoder here, so this proves the 0x8002 pipeline is internally
619        // consistent, NOT that it matches BitLocker. The Tier-1 pybde oracle
620        // (oracle_bitlocker1.rs) is the real proof.
621        let (image, plain) = build_cbc_volume(PROTECTION_PASSWORD, password_hash("cbc-pw"));
622        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "cbc-pw").unwrap();
623        let mut buf = [0u8; 512];
624        vol.read_at(0, &mut buf).unwrap();
625        assert_eq!(buf, plain);
626        assert_eq!(vol.metadata().encryption_method, 0x8002);
627    }
628
629    #[test]
630    fn unlock_and_read_synthetic_recovery_volume() {
631        // Recovery-password unlock over a synthetic 0x8002 volume: the VMK is
632        // wrapped with the recovery-key hash under a recovery protector (0x0800).
633        // Tier-3 self-consistency for the recovery-pw wiring; the real end-to-end
634        // proof is the m8003/vault Tier-1/2 oracles.
635        let rk = "111111-111111-111111-111111-111111-111111-111111-111111";
636        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
637        let (image, plain) = build_cbc_volume(crate::metadata::PROTECTION_RECOVERY, key_hash);
638        let mut vol =
639            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
640        let mut buf = [0u8; 512];
641        vol.read_at(0, &mut buf).unwrap();
642        assert_eq!(buf, plain);
643        assert_eq!(vol.metadata().encryption_method, 0x8002);
644    }
645
646    #[test]
647    fn recovery_unlock_rejects_malformed_password() {
648        let rk = "111111-111111-111111-111111-111111-111111-111111-111111";
649        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
650        let (image, _) = build_cbc_volume(crate::metadata::PROTECTION_RECOVERY, key_hash);
651        let res = BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), "nope");
652        assert!(matches!(res, Err(BdeError::InvalidRecoveryPassword { .. })));
653    }
654
655    #[test]
656    fn no_recovery_protector_errors() {
657        // A password-only volume has no recovery protector to unlock with.
658        let rk = "111111-111111-111111-111111-111111-111111-111111-111111";
659        let (image, _) = build_cbc_volume(PROTECTION_PASSWORD, password_hash("cbc-pw"));
660        let res = BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk);
661        assert!(matches!(res, Err(BdeError::NoUnlockProtector { .. })));
662    }
663
664    /// Build a synthetic method-0x8003 (AES-256-CBC, no diffuser) volume plus the
665    /// plaintext of its relocated first sector. The FVEK container carries a
666    /// 256-bit FVEK at offset 12 and no TWEAK.
667    fn build_cbc256_volume(key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
668        let salt = [0x39u8; 16];
669        let fvek = [0x24u8; 32];
670        let vmk = [0x48u8; 32];
671
672        let mut vmk_container = vec![0u8; 44];
673        vmk_container[12..44].copy_from_slice(&vmk);
674        let stretched = stretch_key(&key_hash, &salt);
675        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x59; 12], &vmk_container);
676
677        // 0x8003: 32-byte FVEK at offset 12, no TWEAK.
678        let mut fvek_container = vec![0u8; 76];
679        fvek_container[12..44].copy_from_slice(&fvek);
680        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6a; 12], &fvek_container);
681
682        let mut stretch_data = vec![0u8; 4];
683        stretch_data.extend_from_slice(&salt);
684        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
685        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
686
687        let mut vmk_data = vec![0u8; 28];
688        vmk_data[26..28].copy_from_slice(&PROTECTION_RECOVERY.to_le_bytes());
689        vmk_data.extend_from_slice(&stretch_entry);
690        vmk_data.extend_from_slice(&vmk_ccm_entry);
691        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
692
693        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
694
695        let mut vh_data = Vec::new();
696        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
697        vh_data.extend_from_slice(&512u64.to_le_bytes());
698        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
699
700        let mut entries = Vec::new();
701        entries.extend_from_slice(&vh_entry);
702        entries.extend_from_slice(&vmk_entry);
703        entries.extend_from_slice(&fvek_entry);
704        let metadata_size = 48 + entries.len();
705
706        let mut image = vec![0u8; IMAGE_SIZE];
707        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
708        image[3..11].copy_from_slice(b"MSWIN4.1");
709        image[12] = 0x02;
710        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
711
712        let mb = META_BLOCK_OFFSET as usize;
713        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
714        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
715        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
716        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
717        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
718        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
719        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
720        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8003u16.to_le_bytes());
721        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
722
723        let mut plain = [0u8; 512];
724        for (i, b) in plain.iter_mut().enumerate() {
725            *b = (i as u8) ^ 0x71;
726        }
727        let ct = SectorCipher::new_cbc256(fvek).encrypt_sector(&plain, RELOCATED_OFFSET);
728        let ro = RELOCATED_OFFSET as usize;
729        image[ro..ro + 512].copy_from_slice(&ct);
730
731        (image, plain)
732    }
733
734    #[test]
735    fn unlock_and_read_synthetic_cbc256_volume() {
736        // Tier-3 self-consistency for the 0x8003 pipeline; the Tier-2 proof is
737        // oracle_m8003.rs.
738        let rk = "222222-222222-222222-222222-222222-222222-222222-222222";
739        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
740        let (image, plain) = build_cbc256_volume(key_hash);
741        let mut vol =
742            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
743        let mut buf = [0u8; 512];
744        vol.read_at(0, &mut buf).unwrap();
745        assert_eq!(buf, plain);
746        assert_eq!(vol.metadata().encryption_method, 0x8003);
747    }
748
749    /// Build a synthetic method-0x8004 (XTS-AES-128) volume plus the plaintext of
750    /// its relocated first sector. The FVEK container carries the 32-byte XTS key
751    /// (two AES-128 keys) at offset 12.
752    fn build_xts128_volume(key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
753        let salt = [0x3bu8; 16];
754        let xts_key = [0x35u8; 32];
755        let vmk = [0x4au8; 32];
756
757        let mut vmk_container = vec![0u8; 44];
758        vmk_container[12..44].copy_from_slice(&vmk);
759        let stretched = stretch_key(&key_hash, &salt);
760        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x5b; 12], &vmk_container);
761
762        // 0x8004: 32-byte XTS key at offset 12, no separate TWEAK entry.
763        let mut fvek_container = vec![0u8; 76];
764        fvek_container[12..44].copy_from_slice(&xts_key);
765        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6c; 12], &fvek_container);
766
767        let mut stretch_data = vec![0u8; 4];
768        stretch_data.extend_from_slice(&salt);
769        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
770        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
771
772        let mut vmk_data = vec![0u8; 28];
773        vmk_data[26..28].copy_from_slice(&PROTECTION_RECOVERY.to_le_bytes());
774        vmk_data.extend_from_slice(&stretch_entry);
775        vmk_data.extend_from_slice(&vmk_ccm_entry);
776        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
777
778        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
779
780        let mut vh_data = Vec::new();
781        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
782        vh_data.extend_from_slice(&512u64.to_le_bytes());
783        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
784
785        let mut entries = Vec::new();
786        entries.extend_from_slice(&vh_entry);
787        entries.extend_from_slice(&vmk_entry);
788        entries.extend_from_slice(&fvek_entry);
789        let metadata_size = 48 + entries.len();
790
791        let mut image = vec![0u8; IMAGE_SIZE];
792        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
793        image[3..11].copy_from_slice(b"MSWIN4.1");
794        image[12] = 0x02;
795        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
796
797        let mb = META_BLOCK_OFFSET as usize;
798        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
799        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
800        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
801        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
802        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
803        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
804        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
805        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8004u16.to_le_bytes());
806        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
807
808        let mut plain = [0u8; 512];
809        for (i, b) in plain.iter_mut().enumerate() {
810            *b = (i as u8) ^ 0x2e;
811        }
812        let ct = SectorCipher::new_xts128(xts_key).encrypt_sector(&plain, RELOCATED_OFFSET);
813        let ro = RELOCATED_OFFSET as usize;
814        image[ro..ro + 512].copy_from_slice(&ct);
815
816        (image, plain)
817    }
818
819    #[test]
820    fn unlock_and_read_synthetic_xts128_volume() {
821        // Tier-3 self-consistency for the 0x8004 pipeline; the Tier-1 proof is
822        // oracle_vault.rs.
823        let rk = "333333-333333-333333-333333-333333-333333-333333-333333";
824        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
825        let (image, plain) = build_xts128_volume(key_hash);
826        let mut vol =
827            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
828        let mut buf = [0u8; 512];
829        vol.read_at(0, &mut buf).unwrap();
830        assert_eq!(buf, plain);
831        assert_eq!(vol.metadata().encryption_method, 0x8004);
832    }
833
834    /// Build a synthetic method-0x8005 (XTS-AES-256) volume plus the plaintext of
835    /// its relocated first sector. The FVEK container carries the 64-byte XTS key
836    /// (two AES-256 keys) at offset 12.
837    fn build_xts256_volume(key_hash: [u8; 32]) -> (Vec<u8>, [u8; 512]) {
838        let salt = [0x3du8; 16];
839        let mut xts_key = [0u8; 64];
840        for (i, b) in xts_key.iter_mut().enumerate() {
841            *b = 0x46u8.wrapping_add(i as u8);
842        }
843        let vmk = [0x4cu8; 32];
844
845        let mut vmk_container = vec![0u8; 44];
846        vmk_container[12..44].copy_from_slice(&vmk);
847        let stretched = stretch_key(&key_hash, &salt);
848        let vmk_ccm = aes_ccm_wrap(&stretched, &[0x5d; 12], &vmk_container);
849
850        // 0x8005: 64-byte XTS key at offset 12.
851        let mut fvek_container = vec![0u8; 76];
852        fvek_container[12..76].copy_from_slice(&xts_key);
853        let fvek_ccm = aes_ccm_wrap(&vmk, &[0x6e; 12], &fvek_container);
854
855        let mut stretch_data = vec![0u8; 4];
856        stretch_data.extend_from_slice(&salt);
857        let stretch_entry = entry(0, VALUE_TYPE_STRETCH, &stretch_data);
858        let vmk_ccm_entry = entry(0, VALUE_TYPE_AES_CCM, &vmk_ccm);
859
860        let mut vmk_data = vec![0u8; 28];
861        vmk_data[26..28].copy_from_slice(&PROTECTION_RECOVERY.to_le_bytes());
862        vmk_data.extend_from_slice(&stretch_entry);
863        vmk_data.extend_from_slice(&vmk_ccm_entry);
864        let vmk_entry = entry(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, &vmk_data);
865
866        let fvek_entry = entry(ENTRY_TYPE_FVEK, VALUE_TYPE_AES_CCM, &fvek_ccm);
867
868        let mut vh_data = Vec::new();
869        vh_data.extend_from_slice(&RELOCATED_OFFSET.to_le_bytes());
870        vh_data.extend_from_slice(&512u64.to_le_bytes());
871        let vh_entry = entry(ENTRY_TYPE_VOLUME_HEADER, ENTRY_TYPE_VOLUME_HEADER, &vh_data);
872
873        let mut entries = Vec::new();
874        entries.extend_from_slice(&vh_entry);
875        entries.extend_from_slice(&vmk_entry);
876        entries.extend_from_slice(&fvek_entry);
877        let metadata_size = 48 + entries.len();
878
879        let mut image = vec![0u8; IMAGE_SIZE];
880        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
881        image[3..11].copy_from_slice(b"MSWIN4.1");
882        image[12] = 0x02;
883        image[440..448].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
884
885        let mb = META_BLOCK_OFFSET as usize;
886        image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
887        image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
888        image[mb + 16..mb + 24].copy_from_slice(&ENCRYPTED_SIZE.to_le_bytes());
889        image[mb + 28..mb + 32].copy_from_slice(&1u32.to_le_bytes());
890        image[mb + 32..mb + 40].copy_from_slice(&META_BLOCK_OFFSET.to_le_bytes());
891        image[mb + 56..mb + 64].copy_from_slice(&RELOCATED_OFFSET.to_le_bytes());
892        image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
893        image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&0x8005u16.to_le_bytes());
894        image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
895
896        let mut plain = [0u8; 512];
897        for (i, b) in plain.iter_mut().enumerate() {
898            *b = (i as u8) ^ 0x17;
899        }
900        let ct = SectorCipher::new_xts256(xts_key).encrypt_sector(&plain, RELOCATED_OFFSET);
901        let ro = RELOCATED_OFFSET as usize;
902        image[ro..ro + 512].copy_from_slice(&ct);
903
904        (image, plain)
905    }
906
907    #[test]
908    fn unlock_and_read_synthetic_xts256_volume() {
909        // Tier-3 self-consistency for the 0x8005 pipeline; the Tier-2 proof is
910        // oracle_m8005.rs.
911        let rk = "444444-444444-444444-444444-444444-444444-444444-444444";
912        let key_hash = crate::crypto::recovery_key_hash(rk).unwrap();
913        let (image, plain) = build_xts256_volume(key_hash);
914        let mut vol =
915            BitLockerVolume::unlock_with_recovery_password(Cursor::new(image), rk).unwrap();
916        let mut buf = [0u8; 512];
917        vol.read_at(0, &mut buf).unwrap();
918        assert_eq!(buf, plain);
919        assert_eq!(vol.metadata().encryption_method, 0x8005);
920    }
921
922    #[test]
923    fn seek_variants_and_eof() {
924        use std::io::{Read as _, Seek as _};
925        let (image, _) = build_volume("test-pw");
926        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
927
928        assert_eq!(vol.seek(SeekFrom::End(0)).unwrap(), IMAGE_SIZE as u64);
929        let mut b = [0u8; 16];
930        assert_eq!(vol.read(&mut b).unwrap(), 0); // read at EOF
931
932        assert_eq!(vol.seek(SeekFrom::Start(10)).unwrap(), 10);
933        assert_eq!(vol.seek(SeekFrom::Current(5)).unwrap(), 15);
934        assert!(vol.seek(SeekFrom::Current(-100)).is_err()); // before start
935    }
936
937    /// A metadata-only image (no key material) with configurable cipher,
938    /// protectors, header offsets, and whether the block is actually present.
939    fn meta_only_image(
940        method: u16,
941        protectors: &[u16],
942        header_offsets: [u64; 3],
943        block_at: Option<usize>,
944    ) -> Vec<u8> {
945        let mut entries = Vec::new();
946        for p in protectors {
947            let mut d = vec![0u8; 28];
948            d[26..28].copy_from_slice(&p.to_le_bytes());
949            entries.extend(entry(0x0002, 0x0008, &d));
950        }
951        let metadata_size = 48 + entries.len();
952        let mut image = vec![0u8; 0x2000];
953        image[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
954        image[3..11].copy_from_slice(b"MSWIN4.1");
955        image[12] = 0x02;
956        image[440..448].copy_from_slice(&header_offsets[0].to_le_bytes());
957        image[448..456].copy_from_slice(&header_offsets[1].to_le_bytes());
958        image[456..464].copy_from_slice(&header_offsets[2].to_le_bytes());
959        if let Some(mb) = block_at {
960            image[mb..mb + 8].copy_from_slice(b"-FVE-FS-");
961            image[mb + 10..mb + 12].copy_from_slice(&2u16.to_le_bytes());
962            image[mb + 32..mb + 40].copy_from_slice(&(mb as u64).to_le_bytes());
963            image[mb + 64..mb + 68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
964            image[mb + 64 + 36..mb + 64 + 38].copy_from_slice(&method.to_le_bytes());
965            image[mb + 64 + 48..mb + 64 + 48 + entries.len()].copy_from_slice(&entries);
966        }
967        image
968    }
969
970    #[test]
971    fn recognized_but_unvalidated_method_refuses() {
972        // Only 0x8001 (CBC-256 + Elephant Diffuser) is still recognized by the
973        // dispatch without a Tier-1/2 oracle — it must refuse (naming the
974        // method), never by-construction decrypt. The refusal is gated before
975        // key derivation. (0x8002–0x8005 are all now oracle-validated.)
976        let img = meta_only_image(0x8001, &[0x2000], [0x1000, 0, 0], Some(0x1000));
977        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
978        assert!(matches!(
979            res,
980            Err(BdeError::UnvalidatedEncryptionMethod { method: 0x8001 })
981        ));
982    }
983
984    #[test]
985    fn unrecognized_method_errors() {
986        // Not a 0x800x BDE cipher at all — a distinct "unsupported" error.
987        let img = meta_only_image(0x1234, &[0x2000], [0x1000, 0, 0], Some(0x1000));
988        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
989        assert!(matches!(
990            res,
991            Err(BdeError::UnsupportedEncryptionMethod { method: 0x1234 })
992        ));
993    }
994
995    #[test]
996    fn no_password_protector_errors() {
997        // Recovery-only volume — no password protector to unlock with.
998        let img = meta_only_image(0x8000, &[0x0800], [0x1000, 0, 0], Some(0x1000));
999        let res = BitLockerVolume::unlock_with_password(Cursor::new(img), "x");
1000        assert!(matches!(
1001            res,
1002            Err(BdeError::NoUnlockProtector { protector, .. }) if protector == "password"
1003        ));
1004    }
1005
1006    #[test]
1007    fn no_valid_metadata_errors() {
1008        // Valid BitLocker header, but the block offsets point to non-FVE bytes.
1009        let img = meta_only_image(0x8000, &[0x2000], [0x1000, 0x1200, 0x1400], None);
1010        let err = BitLockerVolume::read_metadata(&mut Cursor::new(img)).unwrap_err();
1011        assert!(matches!(err, BdeError::NoValidMetadata { .. }));
1012    }
1013
1014    #[test]
1015    fn read_metadata_skips_zero_first_offset() {
1016        // First offset zero (skipped), second valid — covers the `continue`.
1017        let img = meta_only_image(0x8000, &[0x2000], [0, 0x1000, 0], Some(0x1000));
1018        let meta = BitLockerVolume::read_metadata(&mut Cursor::new(img)).unwrap();
1019        assert_eq!(meta.encryption_method, 0x8000);
1020    }
1021
1022    /// A reader that returns a valid header once, then a transient `Interrupted`,
1023    /// then a hard error — to exercise the I/O-error arms of `read_available`.
1024    struct FlakyReader {
1025        header: Vec<u8>,
1026        phase: usize,
1027    }
1028
1029    impl Read for FlakyReader {
1030        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1031            self.phase += 1;
1032            match self.phase {
1033                1 => {
1034                    let n = buf.len().min(self.header.len());
1035                    buf[..n].copy_from_slice(&self.header[..n]);
1036                    Ok(n)
1037                }
1038                2 => Err(std::io::Error::new(
1039                    std::io::ErrorKind::Interrupted,
1040                    "eintr",
1041                )),
1042                _ => Err(std::io::Error::other("boom")),
1043            }
1044        }
1045    }
1046
1047    impl Seek for FlakyReader {
1048        fn seek(&mut self, _pos: SeekFrom) -> std::io::Result<u64> {
1049            Ok(0)
1050        }
1051    }
1052
1053    #[test]
1054    fn io_error_during_block_read_propagates() {
1055        let mut header = vec![0u8; 512];
1056        header[0..3].copy_from_slice(&[0xeb, 0x58, 0x90]);
1057        header[3..11].copy_from_slice(b"MSWIN4.1");
1058        header[12] = 0x02;
1059        header[440..448].copy_from_slice(&0x1000u64.to_le_bytes());
1060        let mut reader = FlakyReader { header, phase: 0 };
1061        let res = BitLockerVolume::read_metadata(&mut reader);
1062        assert!(matches!(res, Err(BdeError::Io(_))));
1063    }
1064
1065    #[test]
1066    fn wrong_password_fails_authentication() {
1067        let (image, _) = build_volume("test-pw");
1068        let res = BitLockerVolume::unlock_with_password(Cursor::new(image), "wrong");
1069        assert!(matches!(
1070            res,
1071            Err(BdeError::AuthenticationFailed { what }) if what == "volume master key"
1072        ));
1073    }
1074
1075    #[test]
1076    fn read_and_seek_traits() {
1077        use std::io::{Read as _, Seek as _};
1078        let (image, plain) = build_volume("test-pw");
1079        let mut vol = BitLockerVolume::unlock_with_password(Cursor::new(image), "test-pw").unwrap();
1080        vol.seek(SeekFrom::Start(0)).unwrap();
1081        let mut buf = [0u8; 256];
1082        vol.read_exact(&mut buf).unwrap();
1083        assert_eq!(&buf[..], &plain[..256]);
1084        assert_eq!(vol.volume_size(), IMAGE_SIZE as u64);
1085    }
1086
1087    #[test]
1088    fn non_bitlocker_reader_errors() {
1089        let r = BitLockerVolume::unlock_with_password(Cursor::new(vec![0u8; 1024]), "x");
1090        assert!(matches!(r, Err(BdeError::NotBitLocker { .. })));
1091    }
1092
1093    #[test]
1094    fn read_metadata_reports_protectors() {
1095        let (image, _) = build_volume("test-pw");
1096        let meta = BitLockerVolume::read_metadata(&mut Cursor::new(image)).unwrap();
1097        assert_eq!(meta.protector_types(), vec![PROTECTION_PASSWORD]);
1098    }
1099}