Skip to main content

void_crypto/
reader.rs

1//! CommitReader: encapsulates the commit→metadata→shard key derivation chain.
2//!
3//! Makes it structurally impossible to use the wrong key for metadata/shard
4//! decryption. Replaces the manual `decrypt_commit` → `derive_scoped_key` →
5//! `decrypt_blob_*_key_fallback` pattern.
6//!
7//! # Security
8//!
9//! `CommitReader` holds the per-commit content key. Methods use the content key
10//! for metadata/shard decryption. Wrapped shard keys are unwrapped with the
11//! content key or ancestor keys.
12
13use crate::envelope::decrypt_envelope;
14use crate::kdf::{derive_scoped_key, ContentKey, KeyNonce};
15use crate::{CryptoError, CryptoResult};
16
17/// Encapsulates the key derivation chain for reading a commit's objects.
18///
19/// Created by decrypting a commit blob, which extracts the envelope nonce
20/// and derives the content key. The content key is then used to decrypt
21/// metadata and shards.
22///
23/// # Security
24///
25/// The `content_key` is a per-commit derived key, not the root key.
26pub struct CommitReader {
27    content_key: ContentKey,
28}
29
30impl CommitReader {
31    /// Decrypt a commit blob and create a `CommitReader` for its child objects.
32    ///
33    /// Requires VD01 envelope format. The content key is derived from the
34    /// envelope nonce.
35    ///
36    /// # Security
37    ///
38    /// This is one of only two operations that require the root key
39    /// (the other being `seal_commit` on `KeyVault`).
40    pub fn open(root_key: &[u8; 32], commit_blob: &[u8]) -> CryptoResult<(Vec<u8>, Self)> {
41        let (plaintext, nonce) =
42            decrypt_envelope(root_key, commit_blob, crate::aead::AAD_COMMIT)?;
43
44        let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
45        let content_key = ContentKey::new(derive_scoped_key(root_key, &scope)?);
46
47        Ok((
48            plaintext,
49            Self { content_key },
50        ))
51    }
52
53    /// Decrypt a metadata bundle blob using the content key.
54    pub fn decrypt_metadata<T>(&self, blob: &[u8]) -> CryptoResult<T>
55    where
56        T: serde::de::DeserializeOwned,
57    {
58        crate::aead::decrypt_and_parse::<T>(
59            self.content_key.as_bytes(),
60            blob,
61            crate::aead::AAD_METADATA,
62        )
63    }
64
65    /// Decrypt a metadata bundle blob into raw bytes.
66    pub fn decrypt_metadata_raw(&self, blob: &[u8]) -> CryptoResult<Vec<u8>> {
67        crate::aead::decrypt(
68            self.content_key.as_bytes(),
69            blob,
70            crate::aead::AAD_METADATA,
71        )
72    }
73
74    /// Decrypt a shard blob. Handles wrapped keys and ancestor key fallback.
75    ///
76    /// Fallback chain:
77    /// 1. If `wrapped_key` is `Some`, try unwrapping with content_key, then each ancestor key
78    /// 2. Error if all attempts fail
79    pub fn decrypt_shard(
80        &self,
81        blob: &[u8],
82        wrapped_key: Option<&crate::WrappedKey>,
83        ancestor_keys: &[ContentKey],
84    ) -> CryptoResult<Vec<u8>> {
85        use crate::aead::{decrypt, unwrap_shard_key, AAD_SHARD};
86
87        // Try wrapped key path
88        if let Some(wk) = wrapped_key {
89            if let Ok(shard_key) = unwrap_shard_key(&self.content_key, wk) {
90                if let Ok(result) = decrypt(&shard_key, blob, AAD_SHARD) {
91                    return Ok(result);
92                }
93            }
94            for ak in ancestor_keys {
95                if let Ok(shard_key) = unwrap_shard_key(ak, wk) {
96                    if let Ok(result) = decrypt(&shard_key, blob, AAD_SHARD) {
97                        return Ok(result);
98                    }
99                }
100            }
101        }
102
103        // Try content_key directly (for shards encrypted directly with content key)
104        if let Ok(result) = decrypt(self.content_key.as_bytes(), blob, AAD_SHARD) {
105            return Ok(result);
106        }
107
108        // Try ancestor keys directly
109        for key in ancestor_keys {
110            if let Ok(result) = decrypt(key.as_bytes(), blob, AAD_SHARD) {
111                return Ok(result);
112            }
113        }
114
115        Err(CryptoError::Decryption(
116            "shard decryption failed with all known keys".into(),
117        ))
118    }
119
120    /// Decrypt a repo manifest blob (collaboration manifest JSON).
121    pub fn decrypt_repo_manifest(&self, blob: &[u8]) -> CryptoResult<Vec<u8>> {
122        crate::aead::decrypt(
123            self.content_key.as_bytes(),
124            blob,
125            crate::aead::AAD_REPO_MANIFEST,
126        )
127    }
128
129    /// Get the content key (derived from envelope nonce).
130    pub fn content_key(&self) -> &ContentKey {
131        &self.content_key
132    }
133
134    /// Create a CommitReader from a content key directly (for scoped-key clone).
135    ///
136    /// Used when cloning from a published repo with `--content-key`. The content
137    /// key is the only key available — no root key exists in scoped mode.
138    pub fn from_content_key(content_key: ContentKey) -> Self {
139        Self { content_key }
140    }
141
142    /// Create a reader from a share key (for share-based unseal).
143    ///
144    /// In share-based unseal the derived key serves directly as the
145    /// content key — there is no commit envelope involved.
146    pub fn from_share_key(key: crate::kdf::ShareKey) -> Self {
147        Self {
148            content_key: ContentKey::from_raw(*key.as_bytes()),
149        }
150    }
151
152    /// Decrypt a VD01 envelope body using the content key.
153    ///
154    /// Validates the VD01 header, extracts the nonce, and decrypts the
155    /// payload using the content key directly (no key derivation — the
156    /// content key is already the correct decryption key).
157    ///
158    /// Returns the decrypted plaintext and the envelope nonce.
159    ///
160    /// Used by share-based unseal where the share key IS the content key.
161    pub fn decrypt_envelope_body(
162        &self,
163        blob: &[u8],
164        aad: &[u8],
165    ) -> CryptoResult<(Vec<u8>, KeyNonce)> {
166        const HEADER_SIZE: usize = 4 + KeyNonce::SIZE;
167        if blob.len() <= HEADER_SIZE || !blob.starts_with(crate::envelope::MAGIC_V1) {
168            return Err(CryptoError::Decryption(
169                "invalid envelope: missing or invalid VD01 header".into(),
170            ));
171        }
172        let nonce = KeyNonce::from_bytes(&blob[4..HEADER_SIZE])
173            .ok_or_else(|| CryptoError::Decryption("invalid envelope nonce length".into()))?;
174        let plaintext = crate::aead::decrypt(self.content_key.as_bytes(), &blob[HEADER_SIZE..], aad)?;
175        Ok((plaintext, nonce))
176    }
177
178    /// Consume the reader, returning the content key.
179    ///
180    /// Used by `void-core`'s `CommitReader::open_with_vault()` to construct
181    /// the core-layer reader from a vault-opened crypto-layer reader.
182    pub fn into_parts(self) -> ContentKey {
183        self.content_key
184    }
185}
186
187// ---------------------------------------------------------------------------
188// decrypt_shard_data: unified shard decryption with wrapped key support
189// ---------------------------------------------------------------------------
190
191/// Decrypt a shard blob with wrapped key support.
192///
193/// Fallback chain:
194/// 1. If `wrapped_key` is `Some`, try unwrapping with content_key, then each ancestor key
195/// 2. Try content_key directly
196/// 3. Try each ancestor key directly
197pub fn decrypt_shard_data(
198    content_key: &ContentKey,
199    ancestor_keys: &[ContentKey],
200    blob: &[u8],
201    wrapped_key: Option<&crate::WrappedKey>,
202) -> CryptoResult<Vec<u8>> {
203    use crate::aead::{decrypt, unwrap_shard_key, AAD_SHARD};
204
205    if let Some(wk) = wrapped_key {
206        if let Ok(shard_key) = unwrap_shard_key(content_key, wk) {
207            if let Ok(result) = decrypt(&shard_key, blob, AAD_SHARD) {
208                return Ok(result);
209            }
210        }
211        for ak in ancestor_keys {
212            if let Ok(shard_key) = unwrap_shard_key(ak, wk) {
213                if let Ok(result) = decrypt(&shard_key, blob, AAD_SHARD) {
214                    return Ok(result);
215                }
216            }
217        }
218    }
219    if let Ok(result) = decrypt(content_key.as_bytes(), blob, AAD_SHARD) {
220        return Ok(result);
221    }
222    for ak in ancestor_keys {
223        if let Ok(result) = decrypt(ak.as_bytes(), blob, AAD_SHARD) {
224            return Ok(result);
225        }
226    }
227    Err(CryptoError::Decryption(
228        "shard decryption failed with all known keys".into(),
229    ))
230}
231
232// ---------------------------------------------------------------------------
233// decrypt_object: universal decrypt with envelope auto-detection
234// ---------------------------------------------------------------------------
235
236/// Decrypt a blob with VD01 envelope format.
237pub fn decrypt_object(key: &[u8; 32], blob: &[u8], aad: &[u8]) -> CryptoResult<Vec<u8>> {
238    decrypt_envelope(key, blob, aad).map(|(plaintext, _nonce)| plaintext)
239}
240
241/// Decrypt a blob to raw bytes with VD01 envelope format.
242pub fn decrypt_object_raw(
243    key: &[u8; 32],
244    blob: &[u8],
245    aad: &[u8],
246) -> CryptoResult<Vec<u8>> {
247    const HEADER_SIZE: usize = 4 + KeyNonce::SIZE;
248    if blob.len() > HEADER_SIZE && blob.starts_with(b"VD01") {
249        let nonce = KeyNonce::from_bytes(&blob[4..HEADER_SIZE])
250            .ok_or_else(|| CryptoError::Decryption("invalid envelope nonce length".into()))?;
251        let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
252        let derived_key = derive_scoped_key(key, &scope)?;
253        return crate::aead::decrypt(&derived_key, &blob[HEADER_SIZE..], aad);
254    }
255    Err(CryptoError::Decryption(
256        "missing VD01 envelope header".into(),
257    ))
258}
259
260/// Decrypt a blob and parse a CBOR-encoded type with VD01 envelope format.
261pub fn decrypt_object_parse<T>(key: &[u8; 32], blob: &[u8], aad: &[u8]) -> CryptoResult<T>
262where
263    T: serde::de::DeserializeOwned,
264{
265    let plaintext = decrypt_object_raw(key, blob, aad)?;
266    ciborium::from_reader(&plaintext[..])
267        .map_err(|e| CryptoError::Serialization(format!("CBOR deserialization failed: {e}")))
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::{encrypt, encrypt_with_envelope, generate_key_nonce, wrap_shard_key, AAD_COMMIT, AAD_SHARD};
274
275    #[test]
276    fn commit_reader_open_envelope() {
277        let root_key = [0x42u8; 32];
278        let nonce = generate_key_nonce();
279        let plaintext = b"commit data";
280
281        let commit_blob =
282            encrypt_with_envelope(&root_key, &nonce, plaintext, AAD_COMMIT).unwrap();
283
284        let (decrypted, reader) = CommitReader::open(&root_key, &commit_blob).unwrap();
285        assert_eq!(decrypted, plaintext);
286        assert_ne!(reader.content_key().as_bytes(), &root_key);
287    }
288
289    #[test]
290    fn commit_reader_decrypt_shard() {
291        let root_key = [0x42u8; 32];
292        let nonce = generate_key_nonce();
293        let commit_data = b"commit";
294        let shard_data = b"shard content";
295
296        let commit_blob =
297            encrypt_with_envelope(&root_key, &nonce, commit_data, AAD_COMMIT).unwrap();
298        let (_commit, reader) = CommitReader::open(&root_key, &commit_blob).unwrap();
299
300        let shard_blob =
301            encrypt(reader.content_key().as_bytes(), shard_data, AAD_SHARD).unwrap();
302        let decrypted = reader.decrypt_shard(&shard_blob, None, &[]).unwrap();
303        assert_eq!(decrypted, shard_data);
304    }
305
306    #[test]
307    fn commit_reader_shard_with_wrapped_key() {
308        let root_key = [0x42u8; 32];
309        let nonce = generate_key_nonce();
310        let commit_data = b"commit";
311        let shard_data = b"shard with wrapped key";
312
313        let commit_blob =
314            encrypt_with_envelope(&root_key, &nonce, commit_data, AAD_COMMIT).unwrap();
315        let (_commit, reader) = CommitReader::open(&root_key, &commit_blob).unwrap();
316
317        // Generate a random shard key, wrap it under content_key, encrypt shard with it
318        let shard_key = crate::generate_key();
319        let wrapped = wrap_shard_key(reader.content_key(), &shard_key).unwrap();
320        let shard_blob = encrypt(&shard_key, shard_data, AAD_SHARD).unwrap();
321
322        let decrypted = reader.decrypt_shard(&shard_blob, Some(&wrapped), &[]).unwrap();
323        assert_eq!(decrypted, shard_data);
324    }
325
326    #[test]
327    fn decrypt_object_envelope() {
328        let root_key = [0x42u8; 32];
329        let nonce = generate_key_nonce();
330        let plaintext = b"envelope data";
331
332        let blob = encrypt_with_envelope(&root_key, &nonce, plaintext, AAD_COMMIT).unwrap();
333        let decrypted = decrypt_object(&root_key, &blob, AAD_COMMIT).unwrap();
334        assert_eq!(decrypted, plaintext);
335    }
336
337    #[test]
338    fn decrypt_object_raw_roundtrip() {
339        let root_key = [0x42u8; 32];
340        let nonce = generate_key_nonce();
341        let plaintext = b"raw data";
342
343        let blob = encrypt_with_envelope(&root_key, &nonce, plaintext, AAD_SHARD).unwrap();
344        let raw = decrypt_object_raw(&root_key, &blob, AAD_SHARD).unwrap();
345        assert_eq!(&raw, plaintext);
346    }
347}