1use crate::cid::ToVoidCid;
9use crate::store::ObjectStoreExt;
10use crate::{Result, VoidError};
11use void_crypto::{
12 ContentKey, EncryptedCommit, EncryptedMetadata, EncryptedRepoManifest,
13 EncryptedShard, KeyVault,
14};
15
16#[derive(Debug, Clone)]
21pub struct DecryptedShard(Vec<u8>);
22
23impl DecryptedShard {
24 pub fn decompress(self) -> Result<crate::shard::ShardBody> {
27 crate::shard::decompress_shard_body(self.0)
28 }
29
30 pub fn as_bytes(&self) -> &[u8] {
32 &self.0
33 }
34
35 pub fn into_bytes(self) -> Vec<u8> {
37 self.0
38 }
39
40 pub fn len(&self) -> usize {
42 self.0.len()
43 }
44
45 pub fn is_empty(&self) -> bool {
47 self.0.is_empty()
48 }
49}
50
51#[derive(Debug, Clone)]
57pub struct CommitPlaintext(Vec<u8>);
58
59impl CommitPlaintext {
60 pub fn parse(&self) -> Result<crate::metadata::Commit> {
62 crate::metadata::parse_commit(&self.0)
63 }
64
65 pub fn as_bytes(&self) -> &[u8] {
67 &self.0
68 }
69}
70
71pub struct CommitReader {
77 content_key: ContentKey,
78}
79
80impl CommitReader {
81 pub fn from_share_key(key: void_crypto::ShareKey) -> Self {
86 let crypto_reader = void_crypto::CommitReader::from_share_key(key);
87 Self {
88 content_key: crypto_reader.into_parts(),
89 }
90 }
91
92 pub fn from_content_key(content_key: ContentKey) -> Self {
97 Self { content_key }
98 }
99
100 pub fn open_with_vault(vault: &KeyVault, commit_blob: &EncryptedCommit) -> Result<(CommitPlaintext, Self)> {
107 let (plaintext, crypto_reader) = vault.open_commit(commit_blob)?;
108 let content_key = crypto_reader.into_parts();
109 Ok((CommitPlaintext(plaintext), Self { content_key }))
110 }
111
112 pub fn decrypt_metadata<T>(&self, blob: &EncryptedMetadata) -> Result<T>
114 where
115 T: serde::de::DeserializeOwned,
116 {
117 Ok(blob.decrypt_and_parse(self.content_key.as_bytes())?)
118 }
119
120 pub fn decrypt_shard(
128 &self,
129 blob: &EncryptedShard,
130 wrapped_key: Option<&void_crypto::WrappedKey>,
131 ancestor_keys: &[ContentKey],
132 ) -> Result<DecryptedShard> {
133 use void_crypto::{decrypt, unwrap_shard_key, AAD_SHARD};
134
135 let data = blob.as_bytes();
136
137 if let Some(wk) = wrapped_key {
138 if let Ok(shard_key) = unwrap_shard_key(&self.content_key, wk) {
139 if let Ok(result) = decrypt(&shard_key, data, AAD_SHARD) {
140 return Ok(DecryptedShard(result));
141 }
142 }
143 for ak in ancestor_keys {
144 if let Ok(shard_key) = unwrap_shard_key(ak, wk) {
145 if let Ok(result) = decrypt(&shard_key, data, AAD_SHARD) {
146 return Ok(DecryptedShard(result));
147 }
148 }
149 }
150 }
151
152 if let Ok(result) = decrypt(self.content_key.as_bytes(), data, AAD_SHARD) {
153 return Ok(DecryptedShard(result));
154 }
155
156 for key in ancestor_keys {
157 if let Ok(result) = decrypt(key.as_bytes(), data, AAD_SHARD) {
158 return Ok(DecryptedShard(result));
159 }
160 }
161
162 Err(VoidError::Decryption(
163 "shard decryption failed with all known keys".into(),
164 ))
165 }
166
167 pub fn decrypt_repo_manifest(&self, blob: &EncryptedRepoManifest) -> Result<crate::collab::Manifest> {
169 let bytes = blob.decrypt(self.content_key.as_bytes())?;
170 serde_json::from_slice(&bytes)
171 .map_err(|e| VoidError::Serialization(format!("repo manifest JSON: {e}")))
172 }
173
174 pub fn decrypt_envelope_body(&self, blob: &EncryptedCommit) -> Result<(CommitPlaintext, void_crypto::KeyNonce)> {
179 let crypto_reader = void_crypto::CommitReader::from_content_key(*self.content_key());
180 let (plaintext, nonce) = crypto_reader.decrypt_envelope_body(blob.as_bytes(), void_crypto::AAD_COMMIT)?;
181 Ok((CommitPlaintext(plaintext), nonce))
182 }
183
184 pub fn content_key(&self) -> &ContentKey {
186 &self.content_key
187 }
188}
189
190pub fn decrypt_shard_data(
201 content_key: &ContentKey,
202 ancestor_keys: &[ContentKey],
203 blob: &EncryptedShard,
204 wrapped_key: Option<&void_crypto::WrappedKey>,
205) -> Result<DecryptedShard> {
206 use void_crypto::{decrypt, unwrap_shard_key, AAD_SHARD};
207
208 let data = blob.as_bytes();
209
210 if let Some(wk) = wrapped_key {
211 if let Ok(shard_key) = unwrap_shard_key(content_key, wk) {
212 if let Ok(result) = decrypt(&shard_key, data, AAD_SHARD) {
213 return Ok(DecryptedShard(result));
214 }
215 }
216 for ak in ancestor_keys {
217 if let Ok(shard_key) = unwrap_shard_key(ak, wk) {
218 if let Ok(result) = decrypt(&shard_key, data, AAD_SHARD) {
219 return Ok(DecryptedShard(result));
220 }
221 }
222 }
223 }
224 if let Ok(result) = decrypt(content_key.as_bytes(), data, AAD_SHARD) {
225 return Ok(DecryptedShard(result));
226 }
227 for ak in ancestor_keys {
228 if let Ok(result) = decrypt(ak.as_bytes(), data, AAD_SHARD) {
229 return Ok(DecryptedShard(result));
230 }
231 }
232 Err(VoidError::Decryption(
233 "shard decryption failed with all known keys".into(),
234 ))
235}
236
237pub fn collect_ancestor_content_keys_vault(
246 vault: &KeyVault,
247 store: &impl ObjectStoreExt,
248 commit: &crate::metadata::Commit,
249) -> Vec<ContentKey> {
250 let mut keys = Vec::new();
251 let mut current_parent = commit.parent().cloned();
252 for _ in 0..100 {
253 let parent_cid_bytes = match current_parent {
254 Some(ref p) => p.clone(),
255 None => break,
256 };
257 let parent_cid = match parent_cid_bytes.to_void_cid() {
258 Ok(c) => c,
259 Err(_) => break,
260 };
261 let parent_encrypted: EncryptedCommit = match store.get_blob(&parent_cid) {
262 Ok(d) => d,
263 Err(_) => break,
264 };
265 let (parent_bytes, parent_reader) =
266 match CommitReader::open_with_vault(vault, &parent_encrypted) {
267 Ok(r) => r,
268 Err(_) => break,
269 };
270 keys.push(*parent_reader.content_key());
271 let parent_commit = match parent_bytes.parse() {
272 Ok(c) => c,
273 Err(_) => break,
274 };
275 current_parent = parent_commit.parent().cloned();
276 }
277 keys
278}
279
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::crypto::{generate_key, wrap_shard_key};
285
286 #[test]
287 fn commit_reader_open_envelope() {
288 let root_key = [0x42u8; 32];
289 let vault = KeyVault::new(root_key).unwrap();
290 let plaintext = b"commit data";
291
292 let commit_blob = vault.seal_commit(plaintext).unwrap();
293 let (decrypted, reader) = CommitReader::open_with_vault(&vault, &commit_blob).unwrap();
294 assert_eq!(decrypted.as_bytes(), plaintext);
295 assert_ne!(reader.content_key().as_bytes(), &root_key);
296 }
297
298 #[test]
299 fn commit_reader_decrypt_shard() {
300 let root_key = [0x42u8; 32];
301 let vault = KeyVault::new(root_key).unwrap();
302 let shard_data = b"shard content";
303
304 let commit_blob = vault.seal_commit(b"commit").unwrap();
305 let (_commit, reader) = CommitReader::open_with_vault(&vault, &commit_blob).unwrap();
306
307 let shard_blob = EncryptedShard::encrypt(reader.content_key().as_bytes(), shard_data).unwrap();
308 let decrypted = reader.decrypt_shard(&shard_blob, None, &[]).unwrap();
309 assert_eq!(decrypted.as_bytes(), shard_data);
310 }
311
312 #[test]
313 fn decrypt_shard_data_falls_back_on_bad_wrapped_key() {
314 let content_key = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
315 let ancestor_key = ContentKey::from_hex(&hex::encode([0x99u8; 32])).unwrap();
316 let shard_data = b"standalone shard data";
317
318 let shard_blob = EncryptedShard::encrypt(ancestor_key.as_bytes(), shard_data).unwrap();
319
320 let other_key = ContentKey::from_hex(&hex::encode([0xABu8; 32])).unwrap();
321 let shard_key = generate_key();
322 let bad_wrapped = wrap_shard_key(&other_key, &shard_key).unwrap();
323
324 let decrypted = decrypt_shard_data(
325 &content_key,
326 &[ancestor_key],
327 &shard_blob,
328 Some(&bad_wrapped),
329 )
330 .unwrap();
331 assert_eq!(decrypted.as_bytes(), shard_data);
332 }
333
334}