Skip to main content

vck_common/jvck/
codec.rs

1// SPDX-FileCopyrightText: 2026 JC-Lab <joseph@jc-lab.net>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Pluggable EncryptedMetadata cipher ("metadata codec") + per-replica context.
6//!
7//! `JvckMetadataStore` owns no metadata-cipher policy. Opening is two-phase
8//! ([`JvckMetadataReader`](crate::jvck::store::JvckMetadataReader)):
9//!
10//! 1. **Phase A** parses the plaintext header / geometry without decrypting.
11//! 2. **Phase B** iterates the CRC-valid replicas, building a [`ReplicaCtx`] for
12//!    each and calling [`MetadataCodec::unseal`] until one succeeds — the sample
13//!    decrypts with **its own** algorithm there, with full access to the parsed
14//!    header, the raw encrypted blob, and the replica's vendor-specific data.
15//!
16//! The same codec is retained by the store for ongoing re-seal (`store()` /
17//! `store_state()` during the sweep) and recovery (`load_offset`), so the
18//! abstraction is two-directional ([`seal`](MetadataCodec::seal) +
19//! [`unseal`](MetadataCodec::unseal)).
20//!
21//! The default JVCK suite ([`JvckCbcCodec`]) is AES-256-CBC + HKDF-SHA256 + HMAC;
22//! a vendor keeps the JVCK container (replicas, salt, HMAC layout) and swaps the
23//! inner cipher, selecting it from the header (`vendor_id` / `vendor_version` /
24//! `vendor_reserved`) and/or the vendor-specific data region.
25
26use alloc::boxed::Box;
27
28use crate::jvck::metadata::{
29    self, JvckHeader, JvckSecrets, ENCRYPTED_METADATA_SIZE, METADATA_BLOCK_SIZE,
30    OFF_ENCRYPTED_METADATA, OFF_SALT, OFF_VOLUME_ID, SALT_SIZE,
31};
32use crate::store::SectorIo;
33use crate::types::VolumeState;
34use crate::{VckError, VckResult};
35
36/// What [`MetadataCodec::unseal`] recovers from one replica's EncryptedMetadata.
37pub struct Unsealed {
38    pub encrypted_offset: u64,
39    pub state: VolumeState,
40    pub secrets: JvckSecrets,
41}
42
43/// A single CRC-valid metadata replica handed to [`MetadataCodec::unseal`].
44///
45/// Exposes the parsed plaintext header, the raw 512-byte block (and the inner
46/// encrypted blob / salt / volume id within it), and sector-granular reads of
47/// THIS replica's vendor-specific data region — everything a vendor needs to
48/// decide and run its metadata decryption.
49pub struct ReplicaCtx<'a> {
50    header: &'a JvckHeader,
51    /// Owned so the ctx can be returned from `JvckMetadataReader::replica_ctx`
52    /// while only borrowing the header + io.
53    block: [u8; METADATA_BLOCK_SIZE],
54    io: &'a dyn SectorIo,
55    /// Base LBA of this replica's vendor-specific data region.
56    vendor_base_lba: u64,
57    /// Sectors available in this replica's vendor-specific data region.
58    vendor_sector_count: u64,
59    sector_size: u32,
60    /// Index of this replica (header replicas first, then footer replicas).
61    replica_index: usize,
62}
63
64impl<'a> ReplicaCtx<'a> {
65    /// Construct a context for one replica. Called by the framework
66    /// (`JvckMetadataReader`); samples receive a `&ReplicaCtx`, not build one.
67    pub(crate) fn new(
68        header: &'a JvckHeader,
69        block: [u8; METADATA_BLOCK_SIZE],
70        io: &'a dyn SectorIo,
71        vendor_base_lba: u64,
72        vendor_sector_count: u64,
73        sector_size: u32,
74        replica_index: usize,
75    ) -> Self {
76        Self {
77            header,
78            block,
79            io,
80            vendor_base_lba,
81            vendor_sector_count,
82            sector_size,
83            replica_index,
84        }
85    }
86
87    /// The parsed plaintext header (same for every replica of a volume).
88    pub fn header(&self) -> &JvckHeader {
89        self.header
90    }
91
92    /// The full 512-byte Metadata block (CRC already verified).
93    pub fn block(&self) -> &[u8] {
94        &self.block
95    }
96
97    /// The 128-byte EncryptedMetadata blob within the block.
98    pub fn encrypted_metadata(&self) -> &[u8] {
99        &self.block[OFF_ENCRYPTED_METADATA..OFF_ENCRYPTED_METADATA + ENCRYPTED_METADATA_SIZE]
100    }
101
102    /// The per-write salt (plaintext) used to derive this replica's keys.
103    pub fn salt(&self) -> &[u8] {
104        &self.block[OFF_SALT..OFF_SALT + SALT_SIZE]
105    }
106
107    /// The volume id (plaintext) from the header bytes.
108    pub fn volume_id(&self) -> [u8; 16] {
109        self.block[OFF_VOLUME_ID..OFF_VOLUME_ID + 16]
110            .try_into()
111            .unwrap()
112    }
113
114    /// 0-based index of this replica (header replicas first, then footer).
115    pub fn replica_index(&self) -> usize {
116        self.replica_index
117    }
118
119    /// Sectors available in this replica's vendor-specific data region.
120    pub fn vendor_data_sector_count(&self) -> u64 {
121        self.vendor_sector_count
122    }
123
124    /// Read `buf` (a whole number of sectors) from THIS replica's
125    /// vendor-specific data region, starting at vendor-relative `rel_sector`.
126    pub fn read_vendor_data(&self, rel_sector: u64, buf: &mut [u8]) -> VckResult<()> {
127        let ss = self.sector_size as usize;
128        if ss == 0 || buf.is_empty() || !buf.len().is_multiple_of(ss) {
129            return Err(VckError::InvalidData(
130                "vendor data buffer must be a non-zero multiple of the sector size",
131            ));
132        }
133        let nsec = (buf.len() / ss) as u64;
134        if rel_sector
135            .checked_add(nsec)
136            .is_none_or(|end| end > self.vendor_sector_count)
137        {
138            return Err(VckError::ValidationFailed(
139                "vendor data range exceeds the replica region",
140            ));
141        }
142        self.io.read_sectors(self.vendor_base_lba + rel_sector, buf)
143    }
144}
145
146/// Seals/unseals the EncryptedMetadata blob of a JVCK Metadata block.
147///
148/// `unseal`/`seal` MUST round-trip. The plaintext header fields are written /
149/// parsed by the store + [`JvckHeader`]; a codec owns only the 128-byte
150/// encrypted payload (FVEK + offset + state) and its authentication.
151pub trait MetadataCodec: Send + Sync {
152    /// Authenticate + decrypt the EncryptedMetadata of `ctx`'s replica. A wrong
153    /// `vmk` (or a replica that does not belong to this codec) must error so the
154    /// reader can try the next replica.
155    fn unseal(&self, ctx: &ReplicaCtx<'_>, vmk: &[u8]) -> VckResult<Unsealed>;
156
157    /// Serialize `header` + the sensitive `secrets`/`encrypted_offset`/`state`
158    /// into a 512-byte `out` block (encrypting the inner payload, computing
159    /// auth). `salt` is the per-write random salt.
160    #[allow(clippy::too_many_arguments)]
161    fn seal(
162        &self,
163        header: &JvckHeader,
164        secrets: &JvckSecrets,
165        encrypted_offset: u64,
166        state: VolumeState,
167        salt: &[u8; SALT_SIZE],
168        vmk: &[u8],
169        out: &mut [u8; METADATA_BLOCK_SIZE],
170    ) -> VckResult<()>;
171
172    /// Read only `encrypted_offset` (recovery scan) without retaining the FVEK.
173    /// Default: `unseal` then drop the secrets.
174    fn read_offset(&self, ctx: &ReplicaCtx<'_>, vmk: &[u8]) -> VckResult<u64> {
175        Ok(self.unseal(ctx, vmk)?.encrypted_offset)
176    }
177}
178
179/// Default JVCK suite codec: AES-256-CBC EncryptedMetadata, keys derived via
180/// HKDF-SHA256 (`Volume ID ‖ salt`), authenticated with HMAC-SHA256. Delegates to
181/// the reference functions in [`crate::jvck::metadata`] (which operate on the
182/// full 512-byte block).
183pub struct JvckCbcCodec;
184
185impl MetadataCodec for JvckCbcCodec {
186    fn unseal(&self, ctx: &ReplicaCtx<'_>, vmk: &[u8]) -> VckResult<Unsealed> {
187        let (encrypted_offset, state, secrets) = metadata::decrypt_payload(ctx.block(), vmk)?;
188        Ok(Unsealed {
189            encrypted_offset,
190            state,
191            secrets,
192        })
193    }
194
195    fn seal(
196        &self,
197        header: &JvckHeader,
198        secrets: &JvckSecrets,
199        encrypted_offset: u64,
200        state: VolumeState,
201        salt: &[u8; SALT_SIZE],
202        vmk: &[u8],
203        out: &mut [u8; METADATA_BLOCK_SIZE],
204    ) -> VckResult<()> {
205        header.encode(secrets, encrypted_offset, state, salt, vmk, out)
206    }
207
208    fn read_offset(&self, ctx: &ReplicaCtx<'_>, vmk: &[u8]) -> VckResult<u64> {
209        metadata::read_encrypted_offset(ctx.block(), vmk)
210    }
211}
212
213/// Convenience: the default JVCK codec as a boxed trait object.
214pub fn default_codec() -> Box<dyn MetadataCodec> {
215    Box::new(JvckCbcCodec)
216}