Skip to main content

vck_common/
types.rs

1// SPDX-FileCopyrightText: 2026 JC-Lab <joseph@jc-lab.net>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use alloc::boxed::Box;
6use alloc::string::String;
7
8use serde::{Deserialize, Serialize};
9
10/// Volume / partition GUID. Stored as a 16-byte UUID.
11///
12/// NOTE: GPT partition GUIDs and UEFI `EFI_GUID` use a mixed-endian layout for
13/// the first three fields. Conversions to/from `uefi::Guid` must account for
14/// this; see [`guid_from_windows_bytes`].
15pub type Guid = uuid::Uuid;
16
17/// Build a [`Guid`] from the 16 raw bytes of a Windows `GUID` / GPT
18/// `PARTITION_INFORMATION_GPT.PartitionId` / `EFI_GUID` as they appear in
19/// memory.
20///
21/// Those structures store the first three fields (`Data1: u32`, `Data2: u16`,
22/// `Data3: u16`) little-endian and the trailing 8 bytes (`Data4`) as-is. The
23/// `uuid` crate's canonical (RFC 4122) byte order is big-endian for those
24/// fields, so the raw bytes must be read with `from_bytes_le` for the resulting
25/// `Guid` to match the canonical string the Go app writes into `vck.json`
26/// (which formats `Data1`/`Data2`/`Data3` as integers).
27pub fn guid_from_windows_bytes(bytes: [u8; 16]) -> Guid {
28    Guid::from_bytes_le(bytes)
29}
30
31/// Persisted sweep direction for a volume.
32///
33/// Stored in the EncryptedMetadata `state` field (offset 24, u16) so that after
34/// a reboot the driver resumes the sweep in the correct direction instead of
35/// always re-encrypting. `Encrypt` is `0`, so a zero `state` field (the default
36/// for a freshly-created volume) means "encrypting".
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum VolumeState {
39    Encrypt,
40    Decrypt,
41}
42
43impl VolumeState {
44    pub fn as_u16(self) -> u16 {
45        match self {
46            VolumeState::Encrypt => 0,
47            VolumeState::Decrypt => 1,
48        }
49    }
50
51    /// Parse from the on-disk u16; unknown values fall back to `Encrypt`.
52    pub fn from_u16(v: u16) -> Self {
53        match v {
54            1 => VolumeState::Decrypt,
55            _ => VolumeState::Encrypt,
56        }
57    }
58}
59
60/// Progressive-encryption progress state.
61///
62/// All sector numbers are **relative to the data region** (`offset_sector`):
63/// `0` is the first encryptable sector and header/footer metadata regions are
64/// not counted. The filter maps an absolute LBA to `rel = lba - offset_sector`
65/// before comparing against these values.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct EncryptedOffset {
68    /// Sectors before this (data-region relative) are already encrypted.
69    pub sector: u64,
70    /// Total number of sectors to encrypt (metadata regions excluded).
71    pub total_sectors: u64,
72}
73
74impl EncryptedOffset {
75    /// `sector` is a data-region relative sector number.
76    pub fn is_encrypted(&self, sector: u64) -> bool {
77        sector < self.sector
78    }
79
80    pub fn is_fully_encrypted(&self) -> bool {
81        self.sector >= self.total_sectors
82    }
83}
84
85/// A contiguous run of sectors `[start, start + count)`.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct SectorRange {
88    pub start: u64,
89    pub count: u64,
90}
91
92impl SectorRange {
93    pub fn end(&self) -> u64 {
94        self.start + self.count
95    }
96
97    pub fn contains(&self, sector: u64) -> bool {
98        sector >= self.start && sector < self.end()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn encrypted_offset_boundaries() {
108        let off = EncryptedOffset {
109            sector: 10,
110            total_sectors: 100,
111        };
112        assert!(off.is_encrypted(9));
113        assert!(!off.is_encrypted(10));
114        assert!(!off.is_fully_encrypted());
115
116        let done = EncryptedOffset {
117            sector: 100,
118            total_sectors: 100,
119        };
120        assert!(done.is_fully_encrypted());
121    }
122
123    #[test]
124    fn guid_from_windows_bytes_matches_canonical() {
125        // Windows GUID in-memory bytes for {12345678-9abc-def0-1122-334455667788}:
126        // Data1=0x12345678 LE, Data2=0x9abc LE, Data3=0xdef0 LE, Data4 as-is.
127        let win = [
128            0x78, 0x56, 0x34, 0x12, // Data1 LE
129            0xbc, 0x9a, // Data2 LE
130            0xf0, 0xde, // Data3 LE
131            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Data4
132        ];
133        let guid = guid_from_windows_bytes(win);
134        let canonical = uuid::Uuid::parse_str("12345678-9abc-def0-1122-334455667788").unwrap();
135        assert_eq!(guid, canonical);
136    }
137
138    #[test]
139    fn sector_range_contains() {
140        let r = SectorRange { start: 5, count: 3 };
141        assert_eq!(r.end(), 8);
142        assert!(r.contains(5));
143        assert!(r.contains(7));
144        assert!(!r.contains(8));
145        assert!(!r.contains(4));
146    }
147}
148
149/// A full-volume sector cipher.
150///
151/// All sector numbers are **data-region relative** (`rel = lba - offset_sector`);
152/// callers MUST map absolute LBAs before invoking these methods and MUST NOT
153/// call them for sectors inside header/footer metadata regions.
154pub trait VolumeCipher: Send + Sync {
155    /// Encrypt one sector in place.
156    fn encrypt_sector(&self, rel_sector: u64, sector: &mut [u8]);
157    /// Decrypt one sector in place.
158    fn decrypt_sector(&self, rel_sector: u64, sector: &mut [u8]);
159    /// Encrypt a contiguous buffer of `sector_size`-byte sectors starting at
160    /// data-region-relative sector `first_rel_sector`.
161    fn encrypt_area(&self, buf: &mut [u8], sector_size: usize, first_rel_sector: u64);
162    /// Decrypt a contiguous buffer (inverse of
163    /// [`encrypt_area`](Self::encrypt_area)).
164    fn decrypt_area(&self, buf: &mut [u8], sector_size: usize, first_rel_sector: u64);
165    /// Explicitly zeroize key material before the cipher is dropped.
166    ///
167    /// Called by the framework at the end of each I/O burst and sweep batch
168    /// immediately before `drop`. The default no-op is safe for ciphers that
169    /// hold key schedules in ordinary memory; override to release/zeroize
170    /// protected key material (RAM-encryption use case).
171    fn destroy(&mut self) {}
172}
173
174/// A factory that produces a short-lived [`VolumeCipher`] for each I/O burst
175/// or sweep batch and lets the cipher be destroyed promptly afterward.
176///
177/// This indirection lets implementors manage key-material lifetime
178/// independently of the volume lifetime.  A RAM-encryption implementation
179/// derives the actual AES key from a hardware-protected secret on each
180/// [`get_cipher`](Self::get_cipher) call and zeroizes it (via
181/// [`VolumeCipher::destroy`]) as soon as the burst completes.
182pub trait VolumeCipherSupplier: Send + Sync {
183    /// Acquire a cipher for one I/O burst or sweep batch.
184    ///
185    /// Returns `None` for a provisional (not-yet-keyed) volume.
186    /// The caller MUST invoke [`VolumeCipher::destroy`] and then `drop` the
187    /// returned value promptly after the burst completes.
188    fn get_cipher(&self) -> Option<Box<dyn VolumeCipher>>;
189}
190
191/// Identifies an attached volume and how to reach its raw sectors.
192#[derive(Debug, Clone)]
193pub struct VolumeId {
194    /// GPT partition unique GUID (matched against the handover `partition_guid`).
195    pub partition_guid: Guid,
196    /// NT device path, e.g. `\Device\HarddiskVolume3`. Used for raw footer
197    /// metadata read/write by the kernel `SectorIo` implementation.
198    pub device_path: String,
199}