Skip to main content

filevault/
volume.rs

1//! Decrypted logical-volume reader: AES-XTS-128 over 512-byte sectors, tweak =
2//! logical sector number, single contiguous segment at the LV physical base.
3
4use std::io::{self, Read, Seek, SeekFrom};
5
6use crate::error::FileVaultError;
7use crate::unlock::VolumeKeys;
8use crate::xts;
9
10/// AES-XTS unit for LV sector decryption.
11const SECTOR_SIZE: usize = 512;
12
13/// A decrypted CoreStorage logical volume over a `Read + Seek` source.
14///
15/// Reads are decrypted on demand: for a logical offset `L`, the physical offset
16/// is `segment_base + (L - segment_logical_base)`, the sector is `L / 512`, and
17/// the tweak is that logical sector number.
18#[derive(Debug)]
19pub struct DecryptedVolume<R: Read + Seek> {
20    reader: R,
21    keys: VolumeKeys,
22    /// Physical byte offset of the LV base (first segment).
23    physical_base: u64,
24    /// Logical volume size in bytes.
25    size: u64,
26    /// Current logical read position (for the `Read`/`Seek` impls).
27    position: u64,
28}
29
30impl<R: Read + Seek> DecryptedVolume<R> {
31    /// Construct a decrypted-volume view for a single contiguous segment.
32    pub(crate) fn new(reader: R, keys: VolumeKeys, physical_base: u64, size: u64) -> Self {
33        DecryptedVolume {
34            reader,
35            keys,
36            physical_base,
37            size,
38            position: 0,
39        }
40    }
41
42    /// The logical volume size in bytes.
43    #[must_use]
44    pub fn size(&self) -> u64 {
45        self.size
46    }
47
48    /// Read and decrypt `buf.len()` bytes at logical `offset`, returning the
49    /// number of bytes read (0 at or past the end of the volume).
50    ///
51    /// Reads are sector-aligned internally: the enclosing 512-byte sectors are
52    /// fetched, decrypted with their logical-sector tweaks, and the requested
53    /// window is copied out.
54    ///
55    /// # Errors
56    /// [`FileVaultError::Io`] if the underlying read/seek fails.
57    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, FileVaultError> {
58        if offset >= self.size || buf.is_empty() {
59            return Ok(0);
60        }
61        let available = self.size - offset;
62        let want = (buf.len() as u64).min(available) as usize;
63
64        let first_sector = offset / SECTOR_SIZE as u64;
65        let end = offset + want as u64;
66        let last_sector = (end - 1) / SECTOR_SIZE as u64;
67        let sector_count = (last_sector - first_sector + 1) as usize;
68
69        let region_len =
70            sector_count
71                .checked_mul(SECTOR_SIZE)
72                .ok_or(FileVaultError::OutOfRange {
73                    what: "read region length",
74                })?;
75        let mut region = vec![0u8; region_len];
76
77        let physical = self.physical_base + first_sector * SECTOR_SIZE as u64;
78        self.reader.seek(SeekFrom::Start(physical))?;
79        // A short read (past end of the backing image) leaves the tail zeroed;
80        // decryption of zero ciphertext yields defined-but-meaningless bytes,
81        // never a panic. We still surface the bytes we could read.
82        read_full_or_eof(&mut self.reader, &mut region)?;
83
84        xts::decrypt_units(
85            &mut region,
86            &self.keys.vmk,
87            &self.keys.tweak_key,
88            SECTOR_SIZE,
89            u128::from(first_sector),
90        );
91
92        let inner = (offset - first_sector * SECTOR_SIZE as u64) as usize;
93        let slice = region
94            .get(inner..inner + want)
95            .ok_or(FileVaultError::OutOfRange {
96                what: "decrypted window",
97            })?;
98        buf.get_mut(..want)
99            .ok_or(FileVaultError::OutOfRange {
100                what: "output buffer window",
101            })?
102            .copy_from_slice(slice);
103        Ok(want)
104    }
105}
106
107/// Read exactly `buf.len()` bytes, or until EOF; a clean EOF short read leaves
108/// the remaining bytes as-is (zero-filled) rather than erroring.
109fn read_full_or_eof<R: Read>(reader: &mut R, buf: &mut [u8]) -> io::Result<()> {
110    let mut filled = 0;
111    while filled < buf.len() {
112        match reader.read(&mut buf[filled..]) {
113            Ok(0) => break,
114            Ok(n) => filled += n,
115            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
116            Err(e) => return Err(e),
117        }
118    }
119    Ok(())
120}
121
122impl<R: Read + Seek> Read for DecryptedVolume<R> {
123    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
124        let n = self.read_at(self.position, buf).map_err(io::Error::other)?;
125        self.position += n as u64;
126        Ok(n)
127    }
128}
129
130impl<R: Read + Seek> Seek for DecryptedVolume<R> {
131    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
132        let new = match pos {
133            SeekFrom::Start(o) => o,
134            SeekFrom::End(o) => add_signed(self.size, o)?,
135            SeekFrom::Current(o) => add_signed(self.position, o)?,
136        };
137        self.position = new;
138        Ok(new)
139    }
140}
141
142/// Apply a signed offset to a base position, erroring on overflow/underflow.
143fn add_signed(base: u64, offset: i64) -> io::Result<u64> {
144    let result = if offset >= 0 {
145        base.checked_add(offset as u64)
146    } else {
147        base.checked_sub(offset.unsigned_abs())
148    };
149    result.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek out of range"))
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use std::io::Cursor;
156
157    fn keys() -> VolumeKeys {
158        VolumeKeys {
159            vmk: [0x55u8; 16],
160            tweak_key: [0x66u8; 16],
161        }
162    }
163
164    /// Build a backing image: `physical_base` bytes of padding, then XTS-encrypted
165    /// sectors whose plaintext is known. Logical sector N uses tweak N.
166    fn build_backing(base: u64, plaintext: &[u8], keys: &VolumeKeys) -> Vec<u8> {
167        let mut cipher = plaintext.to_vec();
168        crate::xts::encrypt_units(&mut cipher, &keys.vmk, &keys.tweak_key, SECTOR_SIZE, 0);
169        let mut image = vec![0u8; base as usize];
170        image.extend_from_slice(&cipher);
171        image
172    }
173
174    #[test]
175    fn read_at_decrypts_aligned_sector() {
176        let k = keys();
177        let plaintext: Vec<u8> = (0..1024u32).map(|i| (i & 0xff) as u8).collect();
178        let image = build_backing(4096, &plaintext, &k);
179        let mut vol =
180            DecryptedVolume::new(Cursor::new(image), keys(), 4096, plaintext.len() as u64);
181        let mut buf = [0u8; 512];
182        assert_eq!(vol.read_at(0, &mut buf).unwrap(), 512);
183        assert_eq!(&buf[..], &plaintext[..512]);
184        assert_eq!(vol.read_at(512, &mut buf).unwrap(), 512);
185        assert_eq!(&buf[..], &plaintext[512..1024]);
186    }
187
188    #[test]
189    fn read_at_handles_unaligned_window() {
190        let k = keys();
191        let plaintext: Vec<u8> = (0..1024u32).map(|i| (i & 0xff) as u8).collect();
192        let image = build_backing(0, &plaintext, &k);
193        let mut vol = DecryptedVolume::new(Cursor::new(image), keys(), 0, plaintext.len() as u64);
194        let mut buf = [0u8; 300];
195        // Offset 600 spans sector 1 and sector 2 boundary crossing.
196        assert_eq!(vol.read_at(600, &mut buf).unwrap(), 300);
197        assert_eq!(&buf[..], &plaintext[600..900]);
198    }
199
200    #[test]
201    fn read_at_past_end_returns_zero() {
202        let mut vol = DecryptedVolume::new(Cursor::new(vec![0u8; 4096]), keys(), 0, 512);
203        let mut buf = [0u8; 16];
204        assert_eq!(vol.read_at(512, &mut buf).unwrap(), 0);
205        assert_eq!(vol.read_at(1000, &mut buf).unwrap(), 0);
206    }
207
208    #[test]
209    fn read_at_truncates_to_volume_end() {
210        let k = keys();
211        let plaintext: Vec<u8> = (0..512u32).map(|i| (i & 0xff) as u8).collect();
212        let image = build_backing(0, &plaintext, &k);
213        let mut vol = DecryptedVolume::new(Cursor::new(image), keys(), 0, 512);
214        let mut buf = [0u8; 512];
215        // Ask for 512 from offset 256 → only 256 available.
216        assert_eq!(vol.read_at(256, &mut buf).unwrap(), 256);
217        assert_eq!(&buf[..256], &plaintext[256..512]);
218    }
219
220    #[test]
221    fn empty_buffer_reads_nothing() {
222        let mut vol = DecryptedVolume::new(Cursor::new(vec![0u8; 4096]), keys(), 0, 512);
223        assert_eq!(vol.read_at(0, &mut []).unwrap(), 0);
224    }
225
226    #[test]
227    fn size_accessor() {
228        let vol = DecryptedVolume::new(Cursor::new(vec![0u8; 16]), keys(), 0, 167_772_160);
229        assert_eq!(vol.size(), 167_772_160);
230    }
231
232    #[test]
233    fn read_and_seek_impls() {
234        let k = keys();
235        let plaintext: Vec<u8> = (0..1024u32).map(|i| (i & 0xff) as u8).collect();
236        let image = build_backing(0, &plaintext, &k);
237        let mut vol = DecryptedVolume::new(Cursor::new(image), keys(), 0, plaintext.len() as u64);
238        let mut buf = [0u8; 512];
239        assert_eq!(std::io::Read::read(&mut vol, &mut buf).unwrap(), 512);
240        assert_eq!(&buf[..], &plaintext[..512]);
241        // Seek to sector 1 and read.
242        assert_eq!(vol.seek(SeekFrom::Start(512)).unwrap(), 512);
243        assert_eq!(std::io::Read::read(&mut vol, &mut buf).unwrap(), 512);
244        assert_eq!(&buf[..], &plaintext[512..1024]);
245        // SeekFrom::End / Current.
246        assert_eq!(vol.seek(SeekFrom::End(0)).unwrap(), 1024);
247        assert_eq!(vol.seek(SeekFrom::Current(-1024)).unwrap(), 0);
248    }
249
250    #[test]
251    fn seek_out_of_range_errors() {
252        let mut vol = DecryptedVolume::new(Cursor::new(vec![0u8; 16]), keys(), 0, 512);
253        assert!(vol.seek(SeekFrom::Current(-1)).is_err());
254        assert!(vol.seek(SeekFrom::End(1)).is_ok());
255    }
256
257    /// A reader that yields one `Interrupted`, then one hard error after N bytes.
258    struct FlakyReader {
259        data: Vec<u8>,
260        pos: usize,
261        interrupted_once: bool,
262    }
263
264    impl Read for FlakyReader {
265        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
266            if !self.interrupted_once {
267                self.interrupted_once = true;
268                return Err(io::Error::from(io::ErrorKind::Interrupted));
269            }
270            if self.pos >= self.data.len() {
271                return Err(io::Error::other("backing read failed"));
272            }
273            let n = (buf.len()).min(self.data.len() - self.pos).min(4);
274            buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
275            self.pos += n;
276            Ok(n)
277        }
278    }
279
280    #[test]
281    fn read_full_or_eof_retries_interrupt_and_propagates_error() {
282        // Interrupted is retried; a subsequent hard error propagates.
283        let mut r = FlakyReader {
284            data: vec![1u8; 8],
285            pos: 0,
286            interrupted_once: false,
287        };
288        let mut buf = [0u8; 16];
289        let err = read_full_or_eof(&mut r, &mut buf).unwrap_err();
290        assert_eq!(err.to_string(), "backing read failed");
291        // The 8 available bytes were read before the error (interrupt was retried).
292        assert_eq!(&buf[..8], &[1u8; 8]);
293    }
294
295    /// A reader that returns clean EOF (Ok(0)) immediately.
296    struct EofReader;
297    impl Read for EofReader {
298        fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
299            Ok(0)
300        }
301    }
302
303    #[test]
304    fn read_full_or_eof_stops_at_clean_eof() {
305        let mut r = EofReader;
306        let mut buf = [0xffu8; 8];
307        read_full_or_eof(&mut r, &mut buf).unwrap();
308        // Untouched (zero bytes read, no error).
309        assert_eq!(buf, [0xffu8; 8]);
310    }
311}