void_crypto/blob_types.rs
1//! Typed encrypted blob newtypes for compile-time AAD safety.
2//!
3//! Each blob type corresponds to a distinct encrypted object kind with its own
4//! AAD constant. This prevents type confusion at the encryption boundary —
5//! you cannot accidentally encrypt with the wrong AAD or pass an encrypted
6//! index blob where a stash blob is expected.
7//!
8//! Local blob types (not stored in ObjectStore):
9//! - `EncryptedIndex` — workspace index (`AAD_INDEX`)
10//! - `EncryptedStaged` — staged file content (`AAD_STAGED`)
11//! - `EncryptedStash` — stash stack metadata (`AAD_STASH`)
12
13use std::fmt;
14
15use crate::aead;
16use crate::CryptoResult;
17
18// ============================================================================
19// EncryptedBlob trait — common interface for all encrypted blob types
20// ============================================================================
21
22/// Common interface for typed encrypted blob newtypes.
23///
24/// Every blob type wraps `Vec<u8>` ciphertext and provides the same
25/// constructors/accessors. This trait enables generic operations
26/// (e.g., typed `ObjectStore::put_blob` / `get_blob`) without knowing
27/// the concrete blob type.
28pub trait EncryptedBlob: Sized {
29 /// Wrap raw ciphertext bytes.
30 fn from_bytes(bytes: Vec<u8>) -> Self;
31
32 /// Access the underlying ciphertext bytes.
33 fn as_bytes(&self) -> &[u8];
34
35 /// Consume and return the underlying ciphertext bytes.
36 fn into_bytes(self) -> Vec<u8>;
37}
38
39// ============================================================================
40// Macro for encrypted blob newtypes
41// ============================================================================
42
43/// Defines an encrypted blob newtype wrapping `Vec<u8>` with a fixed AAD constant.
44///
45/// Generated types include:
46/// - `from_bytes(bytes: Vec<u8>) -> Self` (wrap raw ciphertext)
47/// - `as_bytes(&self) -> &[u8]` (access ciphertext)
48/// - `into_bytes(self) -> Vec<u8>` (consume and return ciphertext)
49/// - `encrypt(key, plaintext) -> CryptoResult<Self>` (encrypt with correct AAD)
50/// - `decrypt(&self, key) -> CryptoResult<Vec<u8>>` (decrypt with correct AAD)
51/// - `decrypt_and_parse<T>(&self, key) -> CryptoResult<T>` (decrypt + CBOR parse)
52/// - `Debug` impl showing byte length
53macro_rules! define_encrypted_blob {
54 (
55 $(#[$meta:meta])*
56 $name:ident, $aad:expr
57 ) => {
58 $(#[$meta])*
59 pub struct $name(Vec<u8>);
60
61 impl $name {
62 /// Wrap raw ciphertext bytes.
63 pub fn from_bytes(bytes: Vec<u8>) -> Self {
64 Self(bytes)
65 }
66
67 /// Access the underlying ciphertext bytes.
68 pub fn as_bytes(&self) -> &[u8] {
69 &self.0
70 }
71
72 /// Consume and return the underlying ciphertext bytes.
73 pub fn into_bytes(self) -> Vec<u8> {
74 self.0
75 }
76
77 /// Encrypt plaintext with this blob type's AAD.
78 pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> CryptoResult<Self> {
79 let ciphertext = aead::encrypt(key, plaintext, $aad)?;
80 Ok(Self(ciphertext))
81 }
82
83 /// Decrypt this blob with the correct AAD.
84 pub fn decrypt(&self, key: &[u8; 32]) -> CryptoResult<Vec<u8>> {
85 aead::decrypt(key, &self.0, $aad)
86 }
87
88 /// Decrypt and parse a CBOR-encoded type with the correct AAD.
89 pub fn decrypt_and_parse<T>(&self, key: &[u8; 32]) -> CryptoResult<T>
90 where
91 T: serde::de::DeserializeOwned,
92 {
93 aead::decrypt_and_parse(key, &self.0, $aad)
94 }
95 }
96
97 impl fmt::Debug for $name {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "{}({} bytes)", stringify!($name), self.0.len())
100 }
101 }
102
103 impl EncryptedBlob for $name {
104 fn from_bytes(bytes: Vec<u8>) -> Self {
105 Self(bytes)
106 }
107
108 fn as_bytes(&self) -> &[u8] {
109 &self.0
110 }
111
112 fn into_bytes(self) -> Vec<u8> {
113 self.0
114 }
115 }
116 };
117}
118
119// ============================================================================
120// Local encrypted blob types — not stored in ObjectStore
121// ============================================================================
122
123define_encrypted_blob!(
124 /// Encrypted workspace index blob (`AAD_INDEX`).
125 ///
126 /// Contains a CBOR-serialized `WorkspaceIndex` struct encrypted with
127 /// AES-256-GCM. Written to `.void/index/index.bin`.
128 EncryptedIndex, aead::AAD_INDEX
129);
130
131define_encrypted_blob!(
132 /// Encrypted staged file content blob (`AAD_STAGED`).
133 ///
134 /// Contains raw file content encrypted with AES-256-GCM.
135 /// Written to `.void/staged/{hex_hash}`.
136 EncryptedStaged, aead::AAD_STAGED
137);
138
139define_encrypted_blob!(
140 /// Encrypted stash stack metadata blob (`AAD_STASH`).
141 ///
142 /// Contains a CBOR-serialized `StashStack` struct encrypted with
143 /// AES-256-GCM. Written to `.void/stash/meta.bin`.
144 EncryptedStash, aead::AAD_STASH
145);
146
147// ============================================================================
148// ObjectStore-flow encrypted blob types
149// ============================================================================
150
151define_encrypted_blob!(
152 /// Encrypted commit blob (`AAD_COMMIT`).
153 ///
154 /// Contains a VD01 envelope with a CBOR-serialized `Commit` struct.
155 /// Stored in the ObjectStore, addressed by `CommitCid`.
156 EncryptedCommit, aead::AAD_COMMIT
157);
158
159define_encrypted_blob!(
160 /// Encrypted metadata bundle blob (`AAD_METADATA`).
161 ///
162 /// Contains a CBOR-serialized `MetadataBundle` struct encrypted with
163 /// AES-256-GCM. Stored in the ObjectStore, addressed by `MetadataCid`.
164 EncryptedMetadata, aead::AAD_METADATA
165);
166
167define_encrypted_blob!(
168 /// Encrypted content shard blob (`AAD_SHARD`).
169 ///
170 /// Contains a shard header + compressed file content encrypted with
171 /// AES-256-GCM. Stored in the ObjectStore, addressed by `ShardCid`.
172 EncryptedShard, aead::AAD_SHARD
173);
174
175define_encrypted_blob!(
176 /// Encrypted tree manifest blob (`AAD_MANIFEST`).
177 ///
178 /// Contains a CBOR-serialized `TreeManifest` struct encrypted with
179 /// AES-256-GCM. Stored in the ObjectStore, addressed by `ManifestCid`.
180 EncryptedManifest, aead::AAD_MANIFEST
181);
182
183define_encrypted_blob!(
184 /// Encrypted repo manifest blob (`AAD_REPO_MANIFEST`).
185 ///
186 /// Contains JSON-serialized collaboration `Manifest` encrypted with
187 /// AES-256-GCM. Stored in the ObjectStore, addressed by `RepoManifestCid`.
188 EncryptedRepoManifest, aead::AAD_REPO_MANIFEST
189);
190
191// ============================================================================
192// Tests
193// ============================================================================
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn encrypted_index_roundtrip() {
201 let key = [0x42u8; 32];
202 let plaintext = b"index data";
203
204 let blob = EncryptedIndex::encrypt(&key, plaintext).unwrap();
205 let decrypted = blob.decrypt(&key).unwrap();
206 assert_eq!(decrypted, plaintext);
207 }
208
209 #[test]
210 fn encrypted_staged_roundtrip() {
211 let key = [0x42u8; 32];
212 let plaintext = b"staged file content";
213
214 let blob = EncryptedStaged::encrypt(&key, plaintext).unwrap();
215 let decrypted = blob.decrypt(&key).unwrap();
216 assert_eq!(decrypted, plaintext);
217 }
218
219 #[test]
220 fn encrypted_stash_roundtrip() {
221 let key = [0x42u8; 32];
222 let plaintext = b"stash metadata";
223
224 let blob = EncryptedStash::encrypt(&key, plaintext).unwrap();
225 let decrypted = blob.decrypt(&key).unwrap();
226 assert_eq!(decrypted, plaintext);
227 }
228
229 #[test]
230 fn wrong_key_fails() {
231 let key1 = [0x42u8; 32];
232 let key2 = [0x43u8; 32];
233 let plaintext = b"secret";
234
235 let blob = EncryptedIndex::encrypt(&key1, plaintext).unwrap();
236 assert!(blob.decrypt(&key2).is_err());
237 }
238
239 #[test]
240 fn debug_format() {
241 let key = [0x42u8; 32];
242 let blob = EncryptedIndex::encrypt(&key, b"data").unwrap();
243 let debug = format!("{:?}", blob);
244 assert!(debug.contains("EncryptedIndex"));
245 assert!(debug.contains("bytes"));
246 }
247
248 #[test]
249 fn from_bytes_wraps_raw() {
250 let raw = vec![1, 2, 3, 4];
251 let blob = EncryptedStaged::from_bytes(raw.clone());
252 assert_eq!(blob.as_bytes(), &raw);
253 assert_eq!(blob.into_bytes(), raw);
254 }
255
256 #[test]
257 fn decrypt_and_parse_roundtrip() {
258 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
259 struct TestData {
260 value: u64,
261 }
262
263 let key = [0x42u8; 32];
264 let data = TestData { value: 42 };
265 let mut serialized = Vec::new();
266 ciborium::into_writer(&data, &mut serialized).unwrap();
267
268 let blob = EncryptedStash::encrypt(&key, &serialized).unwrap();
269 let parsed: TestData = blob.decrypt_and_parse(&key).unwrap();
270 assert_eq!(parsed, data);
271 }
272}