Skip to main content

vck_common/jvck/
store.rs

1// SPDX-FileCopyrightText: 2026 JC-Lab <joseph@jc-lab.net>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Volume-backed JVCK metadata store implementing `EncryptedOffsetStore`.
6//!
7//! Works for both OS and data volumes by reading/writing the header/footer
8//! replica regions through a `SectorIo`.
9//!
10//! Replica layout (all sizes are sector-aligned; `metadata_size` must be a
11//! multiple of `sector_size`):
12//! - Header replica `i`: region starts at `i * replica_sectors`; the 512-byte
13//!   Metadata block occupies the **first** sector of the region (`[Metadata][vendor]`).
14//! - Footer replica `j`: occupies the last `use_footer` regions of the volume;
15//!   the Metadata block occupies the **last** sector of the region
16//!   (`[vendor][Metadata]`), so the final footer replica's Metadata is the very
17//!   last sector of the volume and can be found by a single read.
18
19use alloc::boxed::Box;
20use alloc::vec::Vec;
21use core::sync::atomic::{AtomicU16, AtomicU64, Ordering};
22
23use zeroize::Zeroizing;
24
25use crate::{
26    jvck::{
27        codec::{JvckCbcCodec, MetadataCodec, ReplicaCtx, Unsealed},
28        metadata::{self, JvckHeader, JvckSecrets, METADATA_BLOCK_SIZE},
29        options::JvckMetadataOptions,
30    },
31    store::{EncryptedOffsetStore, SectorIo},
32    types::{EncryptedOffset, VolumeState},
33    VckError, VckResult,
34};
35
36/// Computed geometry of the encryption target region.
37#[derive(Debug, Clone, Copy)]
38pub struct Geometry {
39    /// First absolute LBA of the data (encryptable) region.
40    pub offset_sector: u64,
41    /// Number of sectors to encrypt (metadata regions excluded).
42    pub data_sectors: u64,
43    pub sector_size: u32,
44}
45
46pub struct JvckMetadataStore<S: SectorIo> {
47    io: S,
48    options: JvckMetadataOptions,
49    vmk: Zeroizing<Vec<u8>>,
50    geometry: Geometry,
51    volume_sectors: u64,
52    /// Immutable plaintext template (counts, sizes, volume id). Re-encoded with
53    /// `secrets` + the live `encrypted_offset` on every metadata write.
54    header: JvckHeader,
55    /// FVEK material, kept (zeroize-on-drop) so `store()` can re-encode the
56    /// EncryptedMetadata blob when `encrypted_offset` advances.
57    secrets: JvckSecrets,
58    /// Current on-disk encrypted_offset (data-region relative). Written together
59    /// with `state` on every metadata re-encode.
60    offset: AtomicU64,
61    /// Current on-disk sweep direction (`VolumeState` as u16).
62    state: AtomicU16,
63    /// EncryptedMetadata seal/unseal policy. Supplied by the caller (the sample
64    /// chooses it, the default being `JvckCbcCodec`), so the store hardcodes no
65    /// metadata cipher.
66    codec: Box<dyn MetadataCodec>,
67}
68
69/// Sectors-per-replica for the given layout.
70///
71/// The 512-byte Metadata block always occupies exactly one sector, and the
72/// vendor-specific area is `floor((metadata_size - sector_size) / sector_size)`
73/// sectors. The total is therefore `floor(metadata_size / sector_size)`: when
74/// `metadata_size` is not a multiple of `sector_size` the remainder is dropped
75/// so a replica region never exceeds `metadata_size`.
76fn replica_sectors(metadata_size: u32, sector_size: u32) -> u64 {
77    (metadata_size / sector_size) as u64
78}
79
80/// Absolute LBAs of every replica's Metadata block (header first, then footer).
81fn metadata_sector_lbas(
82    volume_sectors: u64,
83    replica_sectors: u64,
84    use_header: u32,
85    use_footer: u32,
86) -> Vec<u64> {
87    let mut lbas = Vec::with_capacity((use_header + use_footer) as usize);
88    // Header: Metadata in the first sector of each region.
89    for i in 0..use_header as u64 {
90        lbas.push(i * replica_sectors);
91    }
92    // Footer: Metadata in the last sector of each region.
93    let footer_start = volume_sectors - use_footer as u64 * replica_sectors;
94    for j in 0..use_footer as u64 {
95        let region_start = footer_start + j * replica_sectors;
96        lbas.push(region_start + replica_sectors - 1);
97    }
98    lbas
99}
100
101/// Base LBA of `replica_index`'s vendor-specific data region (header replicas
102/// first, then footer). `None` if the index is out of range.
103fn vendor_data_base_lba_at(
104    volume_sectors: u64,
105    rs: u64,
106    use_header: u32,
107    use_footer: u32,
108    replica_index: usize,
109) -> Option<u64> {
110    let uh = use_header as usize;
111    let uf = use_footer as usize;
112    if replica_index < uh {
113        // Header replica: Metadata is the first sector; vendor data follows.
114        Some(replica_index as u64 * rs + 1)
115    } else if replica_index < uh + uf {
116        let j = (replica_index - uh) as u64;
117        let footer_start = volume_sectors - uf as u64 * rs;
118        // Footer replica: Metadata is the last sector; vendor data precedes it.
119        Some(footer_start + j * rs)
120    } else {
121        None
122    }
123}
124
125fn compute_geometry(
126    sector_size: u32,
127    volume_sectors: u64,
128    options: &JvckMetadataOptions,
129) -> VckResult<Geometry> {
130    if (sector_size as usize) < METADATA_BLOCK_SIZE {
131        return Err(VckError::Unsupported("sector size smaller than 512"));
132    }
133    // `metadata_size` need not be a multiple of `sector_size`: the replica region
134    // is floored to whole sectors (`floor(metadata_size / sector_size)`), so it
135    // never exceeds `metadata_size`. It must still hold at least the one Metadata
136    // sector.
137    let rs = replica_sectors(options.metadata_size, sector_size);
138    if rs == 0 {
139        return Err(VckError::ValidationFailed(
140            "metadata_size smaller than one sector",
141        ));
142    }
143    let consumed = (options.use_header + options.use_footer) as u64 * rs;
144    if consumed >= volume_sectors {
145        return Err(VckError::ValidationFailed(
146            "volume too small to hold metadata replicas",
147        ));
148    }
149    Ok(Geometry {
150        offset_sector: options.use_header as u64 * rs,
151        data_sectors: volume_sectors - consumed,
152        sector_size,
153    })
154}
155
156fn read_block<S: SectorIo>(
157    io: &S,
158    sector_size: u32,
159    lba: u64,
160) -> VckResult<[u8; METADATA_BLOCK_SIZE]> {
161    let mut sector = alloc::vec![0u8; sector_size as usize];
162    io.read_sectors(lba, &mut sector)?;
163    let mut block = [0u8; METADATA_BLOCK_SIZE];
164    block.copy_from_slice(&sector[..METADATA_BLOCK_SIZE]);
165    Ok(block)
166}
167
168fn write_block<S: SectorIo>(
169    io: &S,
170    sector_size: u32,
171    lba: u64,
172    block: &[u8; METADATA_BLOCK_SIZE],
173) -> VckResult<()> {
174    // The Metadata block sits alone in its sector (layout is sector-aligned),
175    // so zeroing the remainder of the sector cannot clobber vendor data.
176    let mut sector = alloc::vec![0u8; sector_size as usize];
177    sector[..METADATA_BLOCK_SIZE].copy_from_slice(block);
178    io.write_sectors(lba, &sector)
179}
180
181impl<S: SectorIo> JvckMetadataStore<S> {
182    /// Open an existing JVCK volume with the **default JVCK suite** codec
183    /// ([`JvckCbcCodec`]) in one shot: `JvckMetadataReader::open` then
184    /// `into_store`. Convenience for the common case; a caller that selects a
185    /// vendor codec (from the header / vendor data) uses [`JvckMetadataReader`] +
186    /// [`into_store`](JvckMetadataReader::into_store) directly.
187    pub fn open(io: S, vmk: &[u8]) -> VckResult<Self> {
188        JvckMetadataReader::open(io)?.into_store(vmk, |ctx| {
189            let codec: Box<dyn MetadataCodec> = Box::new(JvckCbcCodec);
190            let unsealed = codec.unseal(ctx, vmk)?;
191            Ok((codec, unsealed))
192        })
193    }
194
195    /// Initialize a brand new JVCK volume (first-time encryption): lay out the
196    /// replicas per `options` and write seed metadata (`encrypted_offset = 0`).
197    ///
198    /// The kernel driver no longer creates metadata (the user-space SDK does
199    /// that over an extended-DASD volume handle); this remains the in-tree
200    /// reference encoder used by host tests and tooling.
201    pub fn create(
202        io: S,
203        vmk: &[u8],
204        options: JvckMetadataOptions,
205        fvek_key1: [u8; 32],
206        fvek_key2: [u8; 32],
207        volume_id: [u8; 16],
208        codec: Box<dyn MetadataCodec>,
209    ) -> VckResult<Self> {
210        options.validate()?;
211        let sector_size = io.sector_size();
212        let volume_sectors = io.total_sectors();
213        let geometry = compute_geometry(sector_size, volume_sectors, &options)?;
214
215        let header = JvckHeader {
216            vendor_id: 0,
217            metadata_version: 1,
218            vendor_version: 0,
219            metadata_size: options.metadata_size,
220            sector_size,
221            header_replica_count: options.use_header as u8,
222            footer_replica_count: options.use_footer as u8,
223            volume_id,
224            vendor_reserved: [0u8; metadata::VENDOR_RESERVED_SIZE],
225        };
226        let secrets = JvckSecrets {
227            fvek_key1,
228            fvek_key2,
229        };
230
231        let store = Self {
232            io,
233            options,
234            vmk: Zeroizing::new(vmk.to_vec()),
235            geometry,
236            volume_sectors,
237            header,
238            secrets,
239            offset: AtomicU64::new(0),
240            state: AtomicU16::new(VolumeState::Encrypt.as_u16()),
241            codec,
242        };
243        store.write_all_replicas()?;
244        Ok(store)
245    }
246
247    /// Read every replica and return the most up-to-date `encrypted_offset`.
248    ///
249    /// Uses the cached layout (the on-disk layout is immutable once written, so
250    /// re-bootstrapping per call is unnecessary). Recovery policy: among valid
251    /// replicas, pick the largest `encrypted_offset`. Only the offset is
252    /// recovered; the FVEK decrypted along the way is zeroized immediately.
253    pub fn load_offset(&self) -> VckResult<u64> {
254        let sector_size = self.geometry.sector_size;
255        let rs = replica_sectors(self.options.metadata_size, sector_size);
256        let lbas = metadata_sector_lbas(
257            self.volume_sectors,
258            rs,
259            self.options.use_header,
260            self.options.use_footer,
261        );
262
263        let mut best: Option<u64> = None;
264        for (idx, lba) in lbas.iter().enumerate() {
265            let block = match read_block(&self.io, sector_size, *lba) {
266                Ok(block) => block,
267                Err(_) => continue,
268            };
269            // Only hand CRC-valid replicas to the codec.
270            if metadata::verify_crc(&block).is_err() {
271                continue;
272            }
273            let vendor_base = vendor_data_base_lba_at(
274                self.volume_sectors,
275                rs,
276                self.options.use_header,
277                self.options.use_footer,
278                idx,
279            )
280            .unwrap_or(0);
281            let ctx = ReplicaCtx::new(
282                &self.header,
283                block,
284                &self.io as &dyn SectorIo,
285                vendor_base,
286                rs.saturating_sub(1),
287                sector_size,
288                idx,
289            );
290            if let Ok(offset) = self.codec.read_offset(&ctx, &self.vmk) {
291                best = Some(best.map_or(offset, |b| b.max(offset)));
292            }
293        }
294        best.ok_or(VckError::NotFound("no valid JVCK metadata replica"))
295    }
296
297    /// FVEK key halves recovered at open/create. Kept zeroize-on-drop in the
298    /// store; copied out here only to build the volume cipher.
299    pub fn fvek_keys(&self) -> (&[u8; 32], &[u8; 32]) {
300        (&self.secrets.fvek_key1, &self.secrets.fvek_key2)
301    }
302
303    /// The plaintext Volume ID (HKDF salt) from the metadata header.
304    pub fn volume_id(&self) -> [u8; 16] {
305        self.header.volume_id
306    }
307
308    fn write_all_replicas(&self) -> VckResult<()> {
309        // Fresh per-write salt so the AES-CBC key/IV are never reused across
310        // re-encodes (the EncryptedMetadata plaintext is mostly constant).
311        let mut salt = [0u8; metadata::SALT_SIZE];
312        crate::rng::fill_random(&mut salt)?;
313        let encrypted_offset = self.offset.load(Ordering::Relaxed);
314        let state = VolumeState::from_u16(self.state.load(Ordering::Relaxed));
315        let mut block = [0u8; METADATA_BLOCK_SIZE];
316        self.codec.seal(
317            &self.header,
318            &self.secrets,
319            encrypted_offset,
320            state,
321            &salt,
322            &self.vmk,
323            &mut block,
324        )?;
325        let rs = replica_sectors(self.options.metadata_size, self.geometry.sector_size);
326        for lba in metadata_sector_lbas(
327            self.volume_sectors,
328            rs,
329            self.options.use_header,
330            self.options.use_footer,
331        ) {
332            write_block(&self.io, self.geometry.sector_size, lba, &block)?;
333        }
334        Ok(())
335    }
336
337    pub fn offset_sector(&self) -> u64 {
338        self.geometry.offset_sector
339    }
340
341    pub fn data_sector_count(&self) -> u64 {
342        self.geometry.data_sectors
343    }
344
345    pub fn sector_size(&self) -> u32 {
346        self.geometry.sector_size
347    }
348
349    pub fn footer_replica_count(&self) -> u32 {
350        self.options.use_footer
351    }
352
353    pub fn metadata_size(&self) -> u32 {
354        self.options.metadata_size
355    }
356
357    /// The parsed plaintext header (vendor_id, vendor_version, vendor_reserved,
358    /// volume_id, sizes). A vendor suite selects its crypto from this *whole*
359    /// metadata, not just `vendor_id`.
360    pub fn header(&self) -> &JvckHeader {
361        &self.header
362    }
363
364    // --- Vendor specific DATA region (outside the 512-byte Metadata block) ---
365    //
366    // Each replica region is `replica_sectors` long and holds the Metadata block
367    // in exactly one sector (first for header replicas, last for footer
368    // replicas). The remaining `replica_sectors - 1` sectors are free for vendor
369    // use; the API below reads/writes them at sector granularity.
370
371    /// Number of replica regions (header replicas first, then footer replicas).
372    pub fn replica_count(&self) -> usize {
373        (self.options.use_header + self.options.use_footer) as usize
374    }
375
376    /// Vendor-data sectors available per replica (replica region minus the one
377    /// Metadata sector).
378    pub fn vendor_data_sector_count(&self) -> u64 {
379        replica_sectors(self.options.metadata_size, self.geometry.sector_size).saturating_sub(1)
380    }
381
382    /// Absolute base LBA of `replica_index`'s vendor-data region.
383    fn vendor_data_base_lba(&self, replica_index: usize) -> Option<u64> {
384        let rs = replica_sectors(self.options.metadata_size, self.geometry.sector_size);
385        vendor_data_base_lba_at(
386            self.volume_sectors,
387            rs,
388            self.options.use_header,
389            self.options.use_footer,
390            replica_index,
391        )
392    }
393
394    fn vendor_data_lba_checked(
395        &self,
396        replica_index: usize,
397        rel_sector: u64,
398        len: usize,
399    ) -> VckResult<u64> {
400        let ss = self.geometry.sector_size as usize;
401        if ss == 0 || len == 0 || !len.is_multiple_of(ss) {
402            return Err(VckError::InvalidData(
403                "vendor data buffer must be a non-zero multiple of the sector size",
404            ));
405        }
406        let nsec = (len / ss) as u64;
407        let base = self
408            .vendor_data_base_lba(replica_index)
409            .ok_or(VckError::NotFound("vendor data replica index out of range"))?;
410        let count = self.vendor_data_sector_count();
411        if rel_sector.checked_add(nsec).is_none_or(|end| end > count) {
412            return Err(VckError::ValidationFailed(
413                "vendor data range exceeds the replica region",
414            ));
415        }
416        Ok(base + rel_sector)
417    }
418
419    /// Read `buf` (a whole number of sectors) from replica `replica_index`'s
420    /// vendor-data region, starting at vendor-relative sector `rel_sector`.
421    pub fn read_vendor_data(
422        &self,
423        replica_index: usize,
424        rel_sector: u64,
425        buf: &mut [u8],
426    ) -> VckResult<()> {
427        let lba = self.vendor_data_lba_checked(replica_index, rel_sector, buf.len())?;
428        self.io.read_sectors(lba, buf)
429    }
430
431    /// Write `buf` (a whole number of sectors) into replica `replica_index`'s
432    /// vendor-data region, starting at vendor-relative sector `rel_sector`.
433    pub fn write_vendor_data(
434        &self,
435        replica_index: usize,
436        rel_sector: u64,
437        buf: &[u8],
438    ) -> VckResult<()> {
439        let lba = self.vendor_data_lba_checked(replica_index, rel_sector, buf.len())?;
440        self.io.write_sectors(lba, buf)
441    }
442
443    /// Write `buf` into the vendor-data region of **every** replica (same
444    /// `rel_sector` in each), so the vendor data stays consistent across replicas
445    /// — mirroring how the Metadata block is mirrored to all replicas.
446    ///
447    /// Sequential and best-effort: a mid-loop failure leaves earlier replicas
448    /// updated and returns the error (the caller may retry; the layout is fixed
449    /// so a retry targets the same LBAs). Validates the buffer against the first
450    /// replica before writing any.
451    pub fn write_vendor_data_all(&self, rel_sector: u64, buf: &[u8]) -> VckResult<()> {
452        // Validate once up front (range/alignment is identical for every replica).
453        self.vendor_data_lba_checked(0, rel_sector, buf.len())?;
454        for replica_index in 0..self.replica_count() {
455            self.write_vendor_data(replica_index, rel_sector, buf)?;
456        }
457        Ok(())
458    }
459
460    /// The 192-byte Vendor Specific Reserved area from the parsed header.
461    pub fn vendor_reserved(&self) -> &[u8; metadata::VENDOR_RESERVED_SIZE] {
462        &self.header.vendor_reserved
463    }
464
465    /// Set the Vendor Specific Reserved area and re-seal **all** replicas, so
466    /// every replica's Metadata block carries the new value (the reserved area is
467    /// inside the sealed 512-byte block, mirrored to all replicas on each write).
468    ///
469    /// Takes `&mut self`: call while the store is uniquely owned (e.g. in
470    /// `on_attach` before wrapping it in `Arc<dyn EncryptedOffsetStore>`), since
471    /// the trait-object form used at runtime cannot reach this concrete method.
472    pub fn set_vendor_reserved(
473        &mut self,
474        vendor_reserved: &[u8; metadata::VENDOR_RESERVED_SIZE],
475    ) -> VckResult<()> {
476        self.header.vendor_reserved = *vendor_reserved;
477        self.write_all_replicas()
478    }
479}
480
481impl<S: SectorIo> EncryptedOffsetStore for JvckMetadataStore<S>
482where
483    S: Send + Sync + 'static,
484{
485    fn load(&self) -> VckResult<EncryptedOffset> {
486        Ok(EncryptedOffset {
487            sector: self.load_offset()?,
488            total_sectors: self.geometry.data_sectors,
489        })
490    }
491
492    fn store(&self, offset: &EncryptedOffset) -> VckResult<()> {
493        self.offset.store(offset.sector, Ordering::Relaxed);
494        self.write_all_replicas()
495    }
496
497    fn flush(&self) -> VckResult<()> {
498        // Writes go straight through the synchronous SectorIo; nothing to flush.
499        Ok(())
500    }
501
502    fn load_state(&self) -> VckResult<VolumeState> {
503        Ok(VolumeState::from_u16(self.state.load(Ordering::Relaxed)))
504    }
505
506    fn store_state(&self, state: VolumeState) -> VckResult<()> {
507        // Persist the direction immediately (re-encode all replicas with the
508        // current offset + new state) so a reboot resumes the right direction.
509        self.state.store(state.as_u16(), Ordering::Relaxed);
510        self.write_all_replicas()
511    }
512}
513
514/// Phase-A opener for a JVCK volume.
515///
516/// `open` reads only the **plaintext** header (signature, layout, vendor fields)
517/// — no metadata cipher, no VMK — so the caller can inspect it (and the
518/// vendor-specific data via [`read_vendor_data`](Self::read_vendor_data)) and
519/// pick a [`MetadataCodec`]. [`into_store`](Self::into_store) then iterates the
520/// CRC-valid replicas, building a [`ReplicaCtx`] for each and calling
521/// `codec.unseal` until one succeeds — the sample decrypts with its own
522/// algorithm there. This is the two-phase form of [`JvckMetadataStore::open`].
523pub struct JvckMetadataReader<S: SectorIo> {
524    io: S,
525    options: JvckMetadataOptions,
526    geometry: Geometry,
527    volume_sectors: u64,
528    header: JvckHeader,
529}
530
531impl<S: SectorIo> JvckMetadataReader<S> {
532    /// Phase A: locate a CRC-valid replica (last sector first — always a footer
533    /// Metadata block — then sector 0 for header layouts) and parse its plaintext
534    /// header + layout. No decryption; the VMK is not needed here. Returns
535    /// `NotFound` if no JVCK signature is present anywhere.
536    pub fn open(io: S) -> VckResult<Self> {
537        let sector_size = io.sector_size();
538        if (sector_size as usize) < METADATA_BLOCK_SIZE {
539            return Err(VckError::Unsupported("sector size smaller than 512"));
540        }
541        let volume_sectors = io.total_sectors();
542        if volume_sectors == 0 {
543            return Err(VckError::NotFound("empty volume"));
544        }
545        for lba in [volume_sectors - 1, 0] {
546            let block = read_block(&io, sector_size, lba)?;
547            if metadata::verify_crc(&block).is_err() {
548                continue;
549            }
550            let header = JvckHeader::parse(&block)?;
551            let options = JvckMetadataOptions {
552                use_header: header.header_replica_count as u32,
553                use_footer: header.footer_replica_count as u32,
554                metadata_size: header.metadata_size,
555            };
556            let geometry = compute_geometry(sector_size, volume_sectors, &options)?;
557            return Ok(Self {
558                io,
559                options,
560                geometry,
561                volume_sectors,
562                header,
563            });
564        }
565        Err(VckError::NotFound("no JVCK metadata present"))
566    }
567
568    /// The parsed plaintext header (vendor_id, vendor_version, vendor_reserved,
569    /// volume_id, sizes). Use it to select the [`MetadataCodec`].
570    pub fn header(&self) -> &JvckHeader {
571        &self.header
572    }
573
574    /// Computed encryption-target geometry (offset/data sectors).
575    pub fn geometry(&self) -> Geometry {
576        self.geometry
577    }
578
579    /// Number of replica regions (header replicas first, then footer replicas).
580    pub fn replica_count(&self) -> usize {
581        (self.options.use_header + self.options.use_footer) as usize
582    }
583
584    /// Build a [`ReplicaCtx`] for `replica_index` (header replicas first, then
585    /// footer). Reads that replica's Metadata block and verifies its CRC; the
586    /// returned ctx exposes the header, the raw block / encrypted blob, and that
587    /// replica's vendor-specific data (via [`ReplicaCtx::read_vendor_data`]). A
588    /// vendor uses this to inspect / unseal a specific replica directly, instead
589    /// of relying on the `into_store` iteration order. Errors if the index is out
590    /// of range or the replica's CRC is invalid.
591    pub fn replica_ctx(&self, replica_index: usize) -> VckResult<ReplicaCtx<'_>> {
592        let sector_size = self.geometry.sector_size;
593        let rs = replica_sectors(self.options.metadata_size, sector_size);
594        let lbas = metadata_sector_lbas(
595            self.volume_sectors,
596            rs,
597            self.options.use_header,
598            self.options.use_footer,
599        );
600        let lba = *lbas
601            .get(replica_index)
602            .ok_or(VckError::NotFound("replica index out of range"))?;
603        let block = read_block(&self.io, sector_size, lba)?;
604        metadata::verify_crc(&block)?;
605        let vendor_base = vendor_data_base_lba_at(
606            self.volume_sectors,
607            rs,
608            self.options.use_header,
609            self.options.use_footer,
610            replica_index,
611        )
612        .unwrap_or(0);
613        Ok(ReplicaCtx::new(
614            &self.header,
615            block,
616            &self.io as &dyn SectorIo,
617            vendor_base,
618            rs.saturating_sub(1),
619            sector_size,
620            replica_index,
621        ))
622    }
623
624    /// Read `buf` (a whole number of sectors) from replica `replica_index`'s
625    /// vendor-specific data region, before decryption — to inform codec
626    /// selection. `rel_sector` is vendor-region relative.
627    pub fn read_vendor_data(
628        &self,
629        replica_index: usize,
630        rel_sector: u64,
631        buf: &mut [u8],
632    ) -> VckResult<()> {
633        let sector_size = self.geometry.sector_size;
634        let ss = sector_size as usize;
635        if ss == 0 || buf.is_empty() || !buf.len().is_multiple_of(ss) {
636            return Err(VckError::InvalidData(
637                "vendor data buffer must be a non-zero multiple of the sector size",
638            ));
639        }
640        let rs = replica_sectors(self.options.metadata_size, sector_size);
641        let base = vendor_data_base_lba_at(
642            self.volume_sectors,
643            rs,
644            self.options.use_header,
645            self.options.use_footer,
646            replica_index,
647        )
648        .ok_or(VckError::NotFound("vendor data replica index out of range"))?;
649        let nsec = (buf.len() / ss) as u64;
650        if rel_sector
651            .checked_add(nsec)
652            .is_none_or(|end| end > rs.saturating_sub(1))
653        {
654            return Err(VckError::ValidationFailed(
655                "vendor data range exceeds the replica region",
656            ));
657        }
658        self.io.read_sectors(base + rel_sector, buf)
659    }
660
661    /// Phase B: iterate the CRC-valid replicas, calling `select` on each (with a
662    /// [`ReplicaCtx`]) until it returns `Ok` — that replica is adopted. `select`
663    /// chooses the metadata codec AND unseals, returning **both** as
664    /// `(codec, unsealed)`: a CRC-valid replica may still carry bad encrypted data
665    /// (e.g. a stale `encrypted_offset`) or bad vendor-specific data, so the
666    /// caller returns `Err` to skip it and try the next replica. The default
667    /// selector is `|ctx| { let c = Box::new(JvckCbcCodec); let u = c.unseal(ctx,
668    /// vmk)?; Ok((c, u)) }`.
669    ///
670    /// The store retains the returned codec for re-seal (`store`/`store_state`)
671    /// and recovery (`load_offset`). Returns the last `select` error if none
672    /// succeed.
673    pub fn into_store<F>(self, vmk: &[u8], mut select: F) -> VckResult<JvckMetadataStore<S>>
674    where
675        F: FnMut(&ReplicaCtx<'_>) -> VckResult<(Box<dyn MetadataCodec>, Unsealed)>,
676    {
677        let sector_size = self.geometry.sector_size;
678        let rs = replica_sectors(self.options.metadata_size, sector_size);
679        let lbas = metadata_sector_lbas(
680            self.volume_sectors,
681            rs,
682            self.options.use_header,
683            self.options.use_footer,
684        );
685
686        let mut chosen: Option<(Box<dyn MetadataCodec>, Unsealed)> = None;
687        let mut last_err: Option<VckError> = None;
688        for (idx, lba) in lbas.iter().enumerate() {
689            let block = match read_block(&self.io, sector_size, *lba) {
690                Ok(b) => b,
691                Err(e) => {
692                    last_err = Some(e);
693                    continue;
694                }
695            };
696            // Only hand CRC-valid replicas to the selector; it does the rest
697            // (choose codec + unseal + any per-replica / vendor-data validation).
698            if metadata::verify_crc(&block).is_err() {
699                continue;
700            }
701            let vendor_base = vendor_data_base_lba_at(
702                self.volume_sectors,
703                rs,
704                self.options.use_header,
705                self.options.use_footer,
706                idx,
707            )
708            .unwrap_or(0);
709            let ctx = ReplicaCtx::new(
710                &self.header,
711                block,
712                &self.io as &dyn SectorIo,
713                vendor_base,
714                rs.saturating_sub(1),
715                sector_size,
716                idx,
717            );
718            match select(&ctx) {
719                Ok(pair) => {
720                    chosen = Some(pair);
721                    break;
722                }
723                Err(e) => {
724                    last_err = Some(e);
725                    continue;
726                }
727            }
728        }
729        let (codec, unsealed) = chosen.ok_or_else(|| {
730            last_err.unwrap_or(VckError::NotFound(
731                "no JVCK metadata replica could be unsealed",
732            ))
733        })?;
734
735        let store = JvckMetadataStore {
736            io: self.io,
737            options: self.options,
738            vmk: Zeroizing::new(vmk.to_vec()),
739            geometry: self.geometry,
740            volume_sectors: self.volume_sectors,
741            header: self.header,
742            secrets: unsealed.secrets,
743            offset: AtomicU64::new(0),
744            state: AtomicU16::new(unsealed.state.as_u16()),
745            codec,
746        };
747        // Track the recovered (max) offset so a later store_state() re-encodes
748        // with the correct progress, not 0.
749        let recovered = store.load_offset().unwrap_or(unsealed.encrypted_offset);
750        store.offset.store(recovered, Ordering::Relaxed);
751        Ok(store)
752    }
753}
754
755// --- UEFI Block IO backed SectorIo + convenience constructor ---
756//
757// Provided here (under the `uefi` feature) because the loader only needs plain
758// sector reads of the target volume to recover FVEK/encrypted_offset; the
759// Block IO *hooking* engine lives in `lib/loader`.
760#[cfg(feature = "uefi")]
761pub use uefi_io::{locate_block_io_volume, open_volume_footer_uefi, UefiBlockIoVolume};
762
763#[cfg(feature = "uefi")]
764mod uefi_io {
765    use super::*;
766    use crate::types::{guid_from_windows_bytes, Guid};
767    use alloc::format;
768    use uefi::boot::{self, open_protocol_exclusive, SearchType};
769    use uefi::proto::media::block::BlockIO;
770    use uefi::proto::media::partition::PartitionInfo;
771
772    /// `SectorIo` backed by `EFI_BLOCK_IO_PROTOCOL` for a located volume.
773    ///
774    /// Read-only: the loader only needs to read footer metadata replicas to
775    /// recover the FVEK / encrypted_offset (the transparent decryption hook
776    /// lives in `lib/loader`).
777    pub struct UefiBlockIoVolume {
778        block_io: uefi::boot::ScopedProtocol<BlockIO>,
779        media_id: u32,
780        sector_size: u32,
781        total_sectors: u64,
782    }
783
784    // The loader is single-threaded; `ScopedProtocol` holds raw firmware
785    // pointers that are only ever touched from the boot thread. `SectorIo`
786    // requires `Send + Sync`, so assert it here.
787    unsafe impl Send for UefiBlockIoVolume {}
788    unsafe impl Sync for UefiBlockIoVolume {}
789
790    impl SectorIo for UefiBlockIoVolume {
791        fn sector_size(&self) -> u32 {
792            self.sector_size
793        }
794        fn total_sectors(&self) -> u64 {
795            self.total_sectors
796        }
797        fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> VckResult<()> {
798            self.block_io
799                .read_blocks(self.media_id, lba, buf)
800                .map_err(|e| VckError::Io(format!("BlockIO.ReadBlocks(lba={lba}) failed: {e:?}")))
801        }
802        fn write_sectors(&self, _lba: u64, _buf: &[u8]) -> VckResult<()> {
803            Err(VckError::Unsupported("loader Block IO volume is read-only"))
804        }
805    }
806
807    /// Locate the volume by GPT unique `partition_guid` and open its Block IO as
808    /// a read-only [`SectorIo`]. Does NOT decrypt anything — the caller (a sample
809    /// loader) reads/decrypts the metadata itself (e.g. via
810    /// [`JvckMetadataStore::open`]), choosing its own metadata cipher.
811    pub fn locate_block_io_volume(partition_guid: Guid) -> VckResult<UefiBlockIoVolume> {
812        let handles = boot::locate_handle_buffer(SearchType::from_proto::<BlockIO>())
813            .map_err(|e| VckError::Io(format!("locate BlockIO handles failed: {e:?}")))?;
814
815        for &handle in handles.iter() {
816            // Match by GPT unique partition GUID via the PartitionInfo protocol.
817            // PartitionInfo is produced on the partition (logical) handles only.
818            let matched = match open_protocol_exclusive::<PartitionInfo>(handle) {
819                Ok(pinfo) => match pinfo.gpt_partition_entry() {
820                    Some(gpt) => {
821                        guid_from_windows_bytes(gpt.unique_partition_guid.to_bytes())
822                            == partition_guid
823                    }
824                    None => false,
825                },
826                Err(_) => false,
827            };
828            if !matched {
829                continue;
830            }
831
832            let block_io = open_protocol_exclusive::<BlockIO>(handle)
833                .map_err(|e| VckError::Io(format!("open BlockIO failed: {e:?}")))?;
834            let media = block_io.media();
835            if !media.is_media_present() {
836                return Err(VckError::Io(
837                    "matched partition has no media present".into(),
838                ));
839            }
840            let sector_size = media.block_size();
841            let media_id = media.media_id();
842            let total_sectors = media.last_block().saturating_add(1);
843            return Ok(UefiBlockIoVolume {
844                block_io,
845                media_id,
846                sector_size,
847                total_sectors,
848            });
849        }
850
851        Err(VckError::NotFound(
852            "no Block IO partition matched the target GUID",
853        ))
854    }
855
856    /// Locate the volume by GPT unique `partition_guid`, open its Block IO, and
857    /// build a store from the footer metadata using `vmk` and the **default JVCK
858    /// codec**. Convenience wrapper over [`locate_block_io_volume`] +
859    /// [`JvckMetadataStore::open`]. A loader selecting a vendor codec uses
860    /// `locate_block_io_volume` + `JvckMetadataReader` directly.
861    pub fn open_volume_footer_uefi(
862        partition_guid: Guid,
863        vmk: &[u8],
864    ) -> VckResult<JvckMetadataStore<UefiBlockIoVolume>> {
865        let io = locate_block_io_volume(partition_guid)?;
866        JvckMetadataStore::open(io, vmk)
867    }
868}
869
870#[cfg(test)]
871mod tests {
872    use super::*;
873    use crate::jvck::codec::default_codec;
874    use std::sync::Mutex;
875
876    /// Deterministic randomness source for tests (no real entropy needed).
877    struct TestRng;
878    impl crate::rng::RandomSource for TestRng {
879        fn fill(&self, buf: &mut [u8]) -> VckResult<()> {
880            for (i, b) in buf.iter_mut().enumerate() {
881                *b = (i as u8).wrapping_mul(7).wrapping_add(1);
882            }
883            Ok(())
884        }
885    }
886    static TEST_RNG: TestRng = TestRng;
887    /// Install the test RNG (idempotent — `set_random_source` is call-once).
888    fn ensure_rng() {
889        crate::rng::set_random_source(&TEST_RNG);
890    }
891
892    /// In-memory `SectorIo` for tests.
893    struct MemVolume {
894        sector_size: u32,
895        data: Mutex<Vec<u8>>,
896    }
897
898    impl MemVolume {
899        fn new(sector_size: u32, sectors: u64) -> Self {
900            Self {
901                sector_size,
902                data: Mutex::new(alloc::vec![0u8; (sectors * sector_size as u64) as usize]),
903            }
904        }
905    }
906
907    impl SectorIo for MemVolume {
908        fn sector_size(&self) -> u32 {
909            self.sector_size
910        }
911        fn total_sectors(&self) -> u64 {
912            self.data.lock().unwrap().len() as u64 / self.sector_size as u64
913        }
914        fn read_sectors(&self, lba: u64, buf: &mut [u8]) -> VckResult<()> {
915            let data = self.data.lock().unwrap();
916            let start = (lba * self.sector_size as u64) as usize;
917            buf.copy_from_slice(&data[start..start + buf.len()]);
918            Ok(())
919        }
920        fn write_sectors(&self, lba: u64, buf: &[u8]) -> VckResult<()> {
921            let mut data = self.data.lock().unwrap();
922            let start = (lba * self.sector_size as u64) as usize;
923            data[start..start + buf.len()].copy_from_slice(buf);
924            Ok(())
925        }
926    }
927
928    const VMK: &[u8] = b"unit-test-volume-master-key";
929    const MD_SIZE: u32 = 128 * 1024; // 256 sectors @ 512
930
931    fn footer_only_options() -> JvckMetadataOptions {
932        JvckMetadataOptions {
933            use_header: 0,
934            use_footer: 2,
935            metadata_size: MD_SIZE,
936        }
937    }
938
939    #[test]
940    fn create_then_load_geometry() {
941        ensure_rng();
942        // 1024 sectors: 2 footer replicas (512) + 512 data sectors.
943        let io = MemVolume::new(512, 1024);
944        let store = JvckMetadataStore::create(
945            io,
946            VMK,
947            footer_only_options(),
948            [1; 32],
949            [2; 32],
950            [9; 16],
951            default_codec(),
952        )
953        .unwrap();
954        assert_eq!(store.offset_sector(), 0);
955        assert_eq!(store.data_sector_count(), 512);
956        assert_eq!(store.footer_replica_count(), 2);
957
958        assert_eq!(store.load_offset().unwrap(), 0);
959        assert_eq!(store.fvek_keys().0, &[1u8; 32]);
960        assert_eq!(store.volume_id(), [9; 16]);
961    }
962
963    #[test]
964    fn header_plus_footer_geometry() {
965        ensure_rng();
966        // use_header=1, use_footer=2 -> 3*256 = 768 reserved, 1280 volume.
967        let io = MemVolume::new(512, 1280);
968        let opts = JvckMetadataOptions {
969            use_header: 1,
970            use_footer: 2,
971            metadata_size: MD_SIZE,
972        };
973        let store =
974            JvckMetadataStore::create(io, VMK, opts, [3; 32], [4; 32], [7; 16], default_codec())
975                .unwrap();
976        assert_eq!(store.offset_sector(), 256);
977        assert_eq!(store.data_sector_count(), 512);
978    }
979
980    #[test]
981    fn store_then_load_offset_roundtrip() {
982        ensure_rng();
983        let io = MemVolume::new(512, 1024);
984        let store = JvckMetadataStore::create(
985            io,
986            VMK,
987            footer_only_options(),
988            [1; 32],
989            [2; 32],
990            [9; 16],
991            default_codec(),
992        )
993        .unwrap();
994
995        store
996            .store(&EncryptedOffset {
997                sector: 1234,
998                total_sectors: 512,
999            })
1000            .unwrap();
1001        let loaded = store.load().unwrap();
1002        assert_eq!(loaded.sector, 1234);
1003        assert_eq!(loaded.total_sectors, 512);
1004    }
1005
1006    #[test]
1007    fn reopen_finds_existing_metadata() {
1008        ensure_rng();
1009        let io = MemVolume::new(512, 1024);
1010        let store = JvckMetadataStore::create(
1011            io,
1012            VMK,
1013            footer_only_options(),
1014            [5; 32],
1015            [6; 32],
1016            [8; 16],
1017            default_codec(),
1018        )
1019        .unwrap();
1020        store
1021            .store(&EncryptedOffset {
1022                sector: 777,
1023                total_sectors: 512,
1024            })
1025            .unwrap();
1026        // Move the backing volume out and reopen via JvckMetadataStore::open.
1027        let io = store.io;
1028        let reopened = JvckMetadataStore::open(io, VMK).unwrap();
1029        assert_eq!(reopened.offset_sector(), 0);
1030        assert_eq!(reopened.data_sector_count(), 512);
1031        assert_eq!(reopened.load_offset().unwrap(), 777);
1032    }
1033
1034    #[test]
1035    fn recovery_picks_largest_offset() {
1036        ensure_rng();
1037        let io = MemVolume::new(512, 1024);
1038        let store = JvckMetadataStore::create(
1039            io,
1040            VMK,
1041            footer_only_options(),
1042            [1; 32],
1043            [2; 32],
1044            [9; 16],
1045            default_codec(),
1046        )
1047        .unwrap();
1048        // All replicas at 500.
1049        store
1050            .store(&EncryptedOffset {
1051                sector: 500,
1052                total_sectors: 512,
1053            })
1054            .unwrap();
1055
1056        // Corrupt the last footer replica (very last sector) to a stale 300.
1057        let mut block = [0u8; METADATA_BLOCK_SIZE];
1058        store
1059            .header
1060            .encode(
1061                &store.secrets,
1062                300,
1063                VolumeState::Encrypt,
1064                &[0u8; metadata::SALT_SIZE],
1065                VMK,
1066                &mut block,
1067            )
1068            .unwrap();
1069        write_block(&store.io, 512, store.volume_sectors - 1, &block).unwrap();
1070
1071        // load_offset must still report 500 (the other valid replica).
1072        assert_eq!(store.load_offset().unwrap(), 500);
1073    }
1074
1075    #[test]
1076    fn state_persists_across_reopen() {
1077        ensure_rng();
1078        let io = MemVolume::new(512, 1024);
1079        let store = JvckMetadataStore::create(
1080            io,
1081            VMK,
1082            footer_only_options(),
1083            [1; 32],
1084            [2; 32],
1085            [9; 16],
1086            default_codec(),
1087        )
1088        .unwrap();
1089        // Fresh volumes default to Encrypt.
1090        assert_eq!(store.load_state().unwrap(), VolumeState::Encrypt);
1091
1092        store
1093            .store(&EncryptedOffset {
1094                sector: 100,
1095                total_sectors: 512,
1096            })
1097            .unwrap();
1098        store.store_state(VolumeState::Decrypt).unwrap();
1099
1100        // Reopen: both the offset and the persisted direction survive.
1101        let io = store.io;
1102        let reopened = JvckMetadataStore::open(io, VMK).unwrap();
1103        assert_eq!(reopened.load_state().unwrap(), VolumeState::Decrypt);
1104        assert_eq!(reopened.load_offset().unwrap(), 100);
1105    }
1106
1107    #[test]
1108    fn vendor_data_read_write_roundtrip() {
1109        ensure_rng();
1110        // footer-only: 2 replicas of 256 sectors -> 255 vendor-data sectors each.
1111        let io = MemVolume::new(512, 1024);
1112        let store = JvckMetadataStore::create(
1113            io,
1114            VMK,
1115            footer_only_options(),
1116            [1; 32],
1117            [2; 32],
1118            [9; 16],
1119            default_codec(),
1120        )
1121        .unwrap();
1122        assert_eq!(store.replica_count(), 2);
1123        assert_eq!(store.vendor_data_sector_count(), 255);
1124
1125        let data = alloc::vec![0xCDu8; 512];
1126        store.write_vendor_data(0, 3, &data).unwrap();
1127        let mut back = alloc::vec![0u8; 512];
1128        store.read_vendor_data(0, 3, &mut back).unwrap();
1129        assert_eq!(back, data);
1130
1131        // Out-of-range sector / replica index are rejected.
1132        assert!(store.write_vendor_data(0, 255, &data).is_err());
1133        assert!(store.write_vendor_data(2, 0, &data).is_err());
1134        // Non-sector-aligned length is rejected.
1135        assert!(store.read_vendor_data(0, 0, &mut [0u8; 100]).is_err());
1136
1137        // Vendor-data writes must not clobber the Metadata sector.
1138        assert_eq!(store.load_offset().unwrap(), 0);
1139    }
1140
1141    #[test]
1142    fn write_vendor_data_all_mirrors_every_replica() {
1143        ensure_rng();
1144        let io = MemVolume::new(512, 1024);
1145        let store = JvckMetadataStore::create(
1146            io,
1147            VMK,
1148            footer_only_options(),
1149            [1; 32],
1150            [2; 32],
1151            [9; 16],
1152            default_codec(),
1153        )
1154        .unwrap();
1155        assert_eq!(store.replica_count(), 2);
1156
1157        let data = alloc::vec![0x5Au8; 1024]; // 2 sectors
1158        store.write_vendor_data_all(7, &data).unwrap();
1159
1160        // Every replica's vendor region carries the same bytes.
1161        for replica in 0..store.replica_count() {
1162            let mut back = alloc::vec![0u8; 1024];
1163            store.read_vendor_data(replica, 7, &mut back).unwrap();
1164            assert_eq!(back, data, "replica {replica} vendor data mismatch");
1165        }
1166        // Validation still applies (out-of-range rejected before any write).
1167        assert!(store.write_vendor_data_all(255, &data).is_err());
1168        // Metadata is intact.
1169        assert_eq!(store.load_offset().unwrap(), 0);
1170    }
1171
1172    #[test]
1173    fn set_vendor_reserved_persists_to_all_replicas() {
1174        ensure_rng();
1175        let io = MemVolume::new(512, 1024);
1176        let mut store = JvckMetadataStore::create(
1177            io,
1178            VMK,
1179            footer_only_options(),
1180            [1; 32],
1181            [2; 32],
1182            [9; 16],
1183            default_codec(),
1184        )
1185        .unwrap();
1186        assert_eq!(
1187            store.vendor_reserved(),
1188            &[0u8; metadata::VENDOR_RESERVED_SIZE]
1189        );
1190
1191        let vr = [0xABu8; metadata::VENDOR_RESERVED_SIZE];
1192        store.set_vendor_reserved(&vr).unwrap();
1193        assert_eq!(store.vendor_reserved(), &vr);
1194
1195        // Reopen and confirm every replica's header carries the new reserved area.
1196        let io = store.io;
1197        let reader = JvckMetadataReader::open(io).unwrap();
1198        assert_eq!(reader.header().vendor_reserved, vr);
1199        for replica in 0..reader.replica_count() {
1200            let ctx = reader.replica_ctx(replica).unwrap();
1201            assert_eq!(ctx.header().vendor_reserved, vr, "replica {replica}");
1202        }
1203    }
1204
1205    #[test]
1206    fn reader_replica_ctx_exposes_block_and_vendor_data() {
1207        ensure_rng();
1208        let io = MemVolume::new(512, 1024);
1209        let store = JvckMetadataStore::create(
1210            io,
1211            VMK,
1212            footer_only_options(),
1213            [1; 32],
1214            [2; 32],
1215            [9; 16],
1216            default_codec(),
1217        )
1218        .unwrap();
1219        let marker = alloc::vec![0xE7u8; 512];
1220        store.write_vendor_data_all(0, &marker).unwrap();
1221
1222        let io = store.io;
1223        let reader = JvckMetadataReader::open(io).unwrap();
1224        assert_eq!(reader.replica_count(), 2);
1225
1226        // A specific replica's ctx: CRC-valid block + JVCK signature + its vendor data.
1227        let ctx = reader.replica_ctx(1).unwrap();
1228        assert_eq!(ctx.replica_index(), 1);
1229        assert_eq!(&ctx.block()[..4], b"JVCK");
1230        assert_eq!(
1231            ctx.encrypted_metadata().len(),
1232            metadata::ENCRYPTED_METADATA_SIZE
1233        );
1234        let mut vd = alloc::vec![0u8; 512];
1235        ctx.read_vendor_data(0, &mut vd).unwrap();
1236        assert_eq!(vd, marker);
1237
1238        // Out-of-range index is rejected.
1239        assert!(reader.replica_ctx(2).is_err());
1240    }
1241
1242    #[test]
1243    fn open_empty_volume_fails() {
1244        let io = MemVolume::new(512, 1024);
1245        assert!(matches!(
1246            JvckMetadataStore::open(io, VMK),
1247            Err(VckError::NotFound(_))
1248        ));
1249    }
1250
1251    #[test]
1252    fn metadata_size_not_multiple_of_sector_is_floored() {
1253        ensure_rng();
1254        // 4096-byte sectors with a metadata_size that is NOT a multiple of the
1255        // sector size: 128 KiB + 100 bytes. The replica region floors to
1256        // floor(131172 / 4096) = 32 sectors (the trailing 100 bytes are dropped).
1257        let sector_size = 4096u32;
1258        let md_size = 128 * 1024 + 100;
1259        let opts = JvckMetadataOptions {
1260            use_header: 0,
1261            use_footer: 2,
1262            metadata_size: md_size,
1263        };
1264        let expected_rs = (md_size / sector_size) as u64; // 32
1265        assert_eq!(expected_rs, 32);
1266
1267        // 2 footer replicas (64 sectors) + 64 data sectors.
1268        let io = MemVolume::new(sector_size, 128);
1269        let store =
1270            JvckMetadataStore::create(io, VMK, opts, [1; 32], [2; 32], [9; 16], default_codec())
1271                .unwrap();
1272        assert_eq!(store.data_sector_count(), 128 - 2 * expected_rs);
1273        assert_eq!(store.sector_size(), sector_size);
1274
1275        // The footer Metadata is the last sector and round-trips through reopen.
1276        store
1277            .store(&EncryptedOffset {
1278                sector: 7,
1279                total_sectors: store.data_sector_count(),
1280            })
1281            .unwrap();
1282        let reopened = JvckMetadataStore::open(store.io, VMK).unwrap();
1283        assert_eq!(reopened.metadata_size(), md_size);
1284        assert_eq!(reopened.load_offset().unwrap(), 7);
1285    }
1286}