1use super::*;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct CloudKeyringMerge {
5 live_key_count: usize,
6 merged_key_count: usize,
7 merged_generation: u64,
8}
9
10impl CloudKeyringMerge {
11 pub fn live_key_count(&self) -> usize {
12 self.live_key_count
13 }
14
15 pub fn merged_key_count(&self) -> usize {
16 self.merged_key_count
17 }
18
19 pub fn merged_generation(&self) -> u64 {
20 self.merged_generation
21 }
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct AdoptedCloudKeyRotation {
26 fingerprint: String,
27 generation: u64,
28}
29
30#[cfg(any(test, feature = "test-utils"))]
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct CloudKeyringFacts {
33 entries: Vec<(u64, [u8; 32])>,
34 seal_key: [u8; 32],
35 current_generation: u64,
36}
37
38#[cfg(any(test, feature = "test-utils"))]
39impl CloudKeyringFacts {
40 pub(super) fn from_encryption(encryption: &EncryptionService) -> Self {
41 Self {
42 entries: encryption.keyring_entries(),
43 seal_key: encryption.key_bytes(),
44 current_generation: encryption.current_generation(),
45 }
46 }
47
48 pub fn entries(&self) -> &[(u64, [u8; 32])] {
49 &self.entries
50 }
51
52 pub fn seal_key(&self) -> [u8; 32] {
53 self.seal_key
54 }
55
56 pub fn current_generation(&self) -> u64 {
57 self.current_generation
58 }
59}
60
61impl AdoptedCloudKeyRotation {
62 pub fn fingerprint(&self) -> &str {
63 &self.fingerprint
64 }
65
66 pub fn generation(&self) -> u64 {
67 self.generation
68 }
69}
70
71pub trait CloudSyncCipherStateAccess: Send + Sync {
74 fn is_plaintext(&self) -> bool;
75 fn suffix(&self) -> &'static str;
76 fn current_generation(&self) -> Option<u64>;
77 fn current_fingerprint(&self) -> Option<String>;
78 fn open(&self, stored: Vec<u8>, aad_context: &[u8]) -> Result<Vec<u8>, EncryptionError>;
79 fn seal(&self, plaintext: Vec<u8>, aad_context: &[u8]) -> Vec<u8>;
80 #[cfg(any(test, feature = "test-utils"))]
81 fn open_sealed_blob_for_test(
82 &self,
83 stored: &[u8],
84 aad_context: &[u8],
85 ) -> Result<
86 (coven_keys::encryption::KeyFingerprint, Vec<u8>),
87 coven_keys::encryption::EncryptionError,
88 >;
89 fn merged_keyring(
90 &self,
91 new_encryption: &EncryptionService,
92 ) -> Result<CloudKeyringMerge, EncryptionError>;
93 fn merge_key_rotation(
94 &self,
95 new_encryption: &EncryptionService,
96 custody: &dyn coven_keys::keys::MasterKeyCustody,
97 ) -> Result<Option<String>, coven_keys::keys::KeyError>;
98
99 fn adopt_key_rotation(
100 &self,
101 new_encryption: &EncryptionService,
102 custody: &dyn coven_keys::keys::MasterKeyCustody,
103 ) -> Result<AdoptedCloudKeyRotation, coven_keys::keys::KeyError> {
104 let fingerprint = match self.merge_key_rotation(new_encryption, custody)? {
105 Some(fingerprint) => fingerprint,
106 None => self
107 .merged_keyring(new_encryption)
108 .map_err(coven_keys::keys::KeyError::Encryption)
109 .and_then(|status| {
110 if status.live_key_count() != status.merged_key_count() {
111 return Err(coven_keys::keys::KeyError::UnretainedKeyRotation);
112 }
113 self.current_fingerprint()
114 .ok_or_else(|| coven_keys::keys::KeyError::PlaintextCloudKeyRotation)
115 })?,
116 };
117 let generation = self
118 .current_generation()
119 .ok_or_else(|| coven_keys::keys::KeyError::PlaintextCloudKeyRotation)?;
120 Ok(AdoptedCloudKeyRotation {
121 fingerprint,
122 generation,
123 })
124 }
125}
126
127fn merge_into(
135 live: &mut EncryptionService,
136 new_encryption: &EncryptionService,
137 custody: &dyn coven_keys::keys::MasterKeyCustody,
138) -> Result<Option<String>, coven_keys::keys::KeyError> {
139 let merged = live
140 .merged_with(new_encryption)
141 .map_err(coven_keys::keys::KeyError::Encryption)?;
142 if merged.key_count() == live.key_count() {
143 return Ok(None);
144 }
145 custody.persist(&coven_keys::encryption::MasterKeyring::from(merged.clone()))?;
146 *live = merged;
147 Ok(Some(live.fingerprint()))
148}
149
150impl CloudSyncCipherStateAccess for RwLock<CloudCipher> {
151 fn is_plaintext(&self) -> bool {
152 self.read().unwrap().is_plaintext()
153 }
154
155 fn suffix(&self) -> &'static str {
156 self.read().unwrap().suffix()
157 }
158
159 fn current_generation(&self) -> Option<u64> {
160 match &*self.read().unwrap() {
161 CloudCipher::Encrypted(encryption) => Some(encryption.current_generation()),
162 CloudCipher::Plaintext => None,
163 }
164 }
165
166 fn current_fingerprint(&self) -> Option<String> {
167 match &*self.read().unwrap() {
168 CloudCipher::Encrypted(encryption) => Some(encryption.fingerprint()),
169 CloudCipher::Plaintext => None,
170 }
171 }
172
173 fn open(&self, stored: Vec<u8>, aad_context: &[u8]) -> Result<Vec<u8>, EncryptionError> {
174 self.read().unwrap().open(stored, aad_context)
175 }
176
177 fn seal(&self, plaintext: Vec<u8>, aad_context: &[u8]) -> Vec<u8> {
178 self.read().unwrap().seal(plaintext, aad_context)
179 }
180
181 #[cfg(any(test, feature = "test-utils"))]
182 fn open_sealed_blob_for_test(
183 &self,
184 stored: &[u8],
185 aad_context: &[u8],
186 ) -> Result<
187 (coven_keys::encryption::KeyFingerprint, Vec<u8>),
188 coven_keys::encryption::EncryptionError,
189 > {
190 let cipher = self.read().unwrap();
191 let CloudCipher::Encrypted(encryption) = &*cipher else {
192 return Err(coven_keys::encryption::EncryptionError::PlaintextKeyring);
193 };
194 super::blob_io::open_sealed_blob(stored, encryption, aad_context)
195 }
196
197 fn merged_keyring(
198 &self,
199 new_encryption: &EncryptionService,
200 ) -> Result<CloudKeyringMerge, EncryptionError> {
201 let cipher = self.read().unwrap();
202 let CloudCipher::Encrypted(live) = &*cipher else {
203 return Err(EncryptionError::PlaintextKeyring);
204 };
205 let merged = live.merged_with(new_encryption)?;
206 Ok(CloudKeyringMerge {
207 live_key_count: live.key_count(),
208 merged_key_count: merged.key_count(),
209 merged_generation: merged.current_generation(),
210 })
211 }
212
213 fn merge_key_rotation(
214 &self,
215 new_encryption: &EncryptionService,
216 custody: &dyn coven_keys::keys::MasterKeyCustody,
217 ) -> Result<Option<String>, coven_keys::keys::KeyError> {
218 let mut cipher = self.write().unwrap();
219 let CloudCipher::Encrypted(live) = &mut *cipher else {
220 return Err(coven_keys::keys::KeyError::PlaintextCloudKeyRotation);
221 };
222 merge_into(live, new_encryption, custody)
223 }
224}
225
226impl CloudCipher {
227 pub(super) fn current_generation(&self) -> Option<u64> {
228 match self {
229 CloudCipher::Encrypted(encryption) => Some(encryption.current_generation()),
230 CloudCipher::Plaintext => None,
231 }
232 }
233
234 pub fn for_storage(
247 storage: coven_foundation::config::HomeStorage,
248 encryption: Option<EncryptionService>,
249 ) -> Option<Self> {
250 if storage.is_opaque() {
251 encryption.map(CloudCipher::Encrypted)
252 } else {
253 Some(CloudCipher::Plaintext)
254 }
255 }
256
257 pub fn seal(&self, plaintext: Vec<u8>, aad_context: &[u8]) -> Vec<u8> {
261 self.seal_scoped(
264 coven_protocol::blob::BlobScope::Master,
265 plaintext,
266 aad_context,
267 )
268 }
269
270 pub fn open(&self, stored: Vec<u8>, aad_context: &[u8]) -> Result<Vec<u8>, EncryptionError> {
272 self.open_scoped(coven_protocol::blob::BlobScope::Master, stored, aad_context)
273 }
274
275 pub fn seal_scoped(
279 &self,
280 scope: coven_protocol::blob::BlobScope,
281 plaintext: Vec<u8>,
282 aad_context: &[u8],
283 ) -> Vec<u8> {
284 match self {
285 CloudCipher::Encrypted(master) => {
286 ScopedBlobSealing::new(scope, master).seal(plaintext, aad_context)
287 }
288 CloudCipher::Plaintext => plaintext,
289 }
290 }
291
292 pub fn open_scoped(
294 &self,
295 scope: coven_protocol::blob::BlobScope,
296 stored: Vec<u8>,
297 aad_context: &[u8],
298 ) -> Result<Vec<u8>, EncryptionError> {
299 match self {
300 CloudCipher::Encrypted(e) => open_scoped_encrypted(scope, e, &stored, aad_context),
301 CloudCipher::Plaintext => Ok(stored),
302 }
303 }
304
305 pub fn suffix(&self) -> &'static str {
309 match self {
310 CloudCipher::Encrypted(_) => ".enc",
311 CloudCipher::Plaintext => "",
312 }
313 }
314
315 pub fn is_plaintext(&self) -> bool {
317 matches!(self, CloudCipher::Plaintext)
318 }
319
320 pub fn body_len(&self, header: SealedBlobHeader) -> u64 {
325 match self {
326 CloudCipher::Encrypted(_) => KeyTag::LEN as u64 + header.sealed_len(),
327 CloudCipher::Plaintext => header.plaintext_len(),
328 }
329 }
330
331 pub async fn open_body(
337 &self,
338 scope: coven_protocol::blob::BlobScope,
339 file_path: &std::path::Path,
340 aad_context: &[u8],
341 chunk_size: std::num::NonZeroU32,
342 ) -> Result<BlobBody, coven_foundation::atomic_file::FileError> {
343 let plaintext_len = coven_foundation::local_file::file_len(file_path).await?;
344 let header = SealedBlobHeader::new(
345 chunk_size,
346 plaintext_len,
347 &NoncePolicy::DerivedFromContext {
348 context: aad_context.to_vec(),
349 },
350 );
351 let reader = crate::local_file::open_reader(file_path).await?;
352 Ok(match self {
353 CloudCipher::Encrypted(encryption) => {
354 ScopedBlobSealing::new(scope, encryption).into_body(header, reader, aad_context)
355 }
356 CloudCipher::Plaintext => {
357 BlobBody::from_file_with_prefix(self.body_len(header), reader, None, Vec::new())
358 }
359 })
360 }
361
362 pub async fn open_exact_body(
366 &self,
367 scope: coven_protocol::blob::BlobScope,
368 file_path: &std::path::Path,
369 aad_context: &[u8],
370 chunk_size: std::num::NonZeroU32,
371 expected_size: u64,
372 expected_hash: coven_protocol::store_commit::ObjectHash,
373 progress: crate::cloud::PreparationProgress,
374 ) -> Result<BlobBody, coven_foundation::atomic_file::FileError> {
375 let plaintext_len = coven_foundation::local_file::file_len(file_path).await?;
376 if plaintext_len != expected_size {
377 return Err(coven_foundation::atomic_file::FileError::Path {
378 operation: "validate local blob source size",
379 path: file_path.to_path_buf(),
380 source: std::io::Error::new(
381 std::io::ErrorKind::InvalidData,
382 format!("declares {expected_size} bytes but the source has {plaintext_len}"),
383 ),
384 });
385 }
386 let header = SealedBlobHeader::new(
387 chunk_size,
388 plaintext_len,
389 &NoncePolicy::DerivedFromContext {
390 context: aad_context.to_vec(),
391 },
392 );
393 let reader =
394 crate::local_file::open_exact_reader(file_path, expected_size, expected_hash, progress)
395 .await?;
396 Ok(match self {
397 CloudCipher::Encrypted(encryption) => {
398 ScopedBlobSealing::new(scope, encryption).into_body(header, reader, aad_context)
399 }
400 CloudCipher::Plaintext => {
401 BlobBody::from_file_with_prefix(self.body_len(header), reader, None, Vec::new())
402 }
403 })
404 }
405}
406
407pub(crate) fn encryption_for_scope(
414 scope: coven_protocol::blob::BlobScope,
415 master: &EncryptionService,
416) -> EncryptionService {
417 match scope {
418 coven_protocol::blob::BlobScope::Master => master.clone(),
419 coven_protocol::blob::BlobScope::Derived(s) => master.derive_scoped(&s),
420 }
421}
422
423pub fn cloud_aad_context(store_id: &str, cloud_key: &str) -> Vec<u8> {
424 let mut context =
425 Vec::with_capacity(std::mem::size_of::<u64>() * 2 + store_id.len() + cloud_key.len());
426 context.extend_from_slice(&(store_id.len() as u64).to_le_bytes());
427 context.extend_from_slice(store_id.as_bytes());
428 context.extend_from_slice(&(cloud_key.len() as u64).to_le_bytes());
429 context.extend_from_slice(cloud_key.as_bytes());
430 context
431}
432
433pub(crate) fn protocol_object_aad_context(
434 context: &ProtocolObjectContext,
435 semantic_prefix: &str,
436) -> Vec<u8> {
437 let domain = context.domain().aad_label();
438 let mut aad = Vec::with_capacity(
439 context.store_root_hash().as_bytes().len()
440 + std::mem::size_of::<u64>() * 2
441 + domain.len()
442 + semantic_prefix.len(),
443 );
444 aad.extend_from_slice(context.store_root_hash().as_bytes());
445 aad.extend_from_slice(&(domain.len() as u64).to_le_bytes());
446 aad.extend_from_slice(domain);
447 aad.extend_from_slice(&(semantic_prefix.len() as u64).to_le_bytes());
448 aad.extend_from_slice(semantic_prefix.as_bytes());
449 aad
450}
451
452pub(crate) struct ScopedBlobSealing {
457 encryption: EncryptionService,
458 key_tag: Vec<u8>,
459}
460
461impl ScopedBlobSealing {
462 fn new(scope: coven_protocol::blob::BlobScope, master: &EncryptionService) -> Self {
463 Self {
464 encryption: encryption_for_scope(scope, master),
465 key_tag: KeyTag::write(&master.seal_fingerprint()),
466 }
467 }
468
469 fn seal(self, plaintext: Vec<u8>, aad_context: &[u8]) -> Vec<u8> {
470 let mut stored = self.key_tag;
471 stored.extend(self.encryption.encrypt(&plaintext, aad_context));
472 stored
473 }
474
475 fn into_body(
476 self,
477 header: SealedBlobHeader,
478 reader: crate::local_file::PlaintextReader,
479 aad_context: &[u8],
480 ) -> BlobBody {
481 let mut prefix = self.key_tag;
482 prefix.extend_from_slice(&header.to_bytes());
483 BlobBody::from_file_with_prefix(
484 KeyTag::LEN as u64 + header.sealed_len(),
485 reader,
486 Some(
487 self.encryption
488 .blob_sealer(
489 header,
490 &NoncePolicy::DerivedFromContext {
491 context: aad_context.to_vec(),
492 },
493 aad_context,
494 )
495 .expect("a blob header records the derived policy it was built under"),
496 ),
497 prefix,
498 )
499 }
500}
501
502pub(crate) fn opening_encryption_for_scope(
503 scope: coven_protocol::blob::BlobScope,
504 master: &EncryptionService,
505 fingerprint: &[u8; 32],
506) -> Result<EncryptionService, EncryptionError> {
507 match scope {
508 coven_protocol::blob::BlobScope::Master => master.service_for_fingerprint(fingerprint),
509 coven_protocol::blob::BlobScope::Derived(scope_id) => {
510 master.derive_scoped_for_fingerprint(fingerprint, &scope_id)
511 }
512 }
513}
514
515pub(crate) fn open_scoped_encrypted(
516 scope: coven_protocol::blob::BlobScope,
517 master: &EncryptionService,
518 stored: &[u8],
519 aad_context: &[u8],
520) -> Result<Vec<u8>, EncryptionError> {
521 let (fingerprint, ciphertext) = KeyTag::read(stored)?;
522 opening_encryption_for_scope(scope, master, &fingerprint)?.decrypt(ciphertext, aad_context)
523}