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}