Skip to main content

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}