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}