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> {
262 self.seal_scoped(
266 coven_protocol::blob::BlobScope::Master,
267 plaintext,
268 aad_context,
269 )
270 }
271
272 pub fn open(&self, stored: Vec<u8>, aad_context: &[u8]) -> Result<Vec<u8>, EncryptionError> {
274 self.open_scoped(coven_protocol::blob::BlobScope::Master, stored, aad_context)
275 }
276
277 pub fn seal_scoped(
281 &self,
282 scope: coven_protocol::blob::BlobScope,
283 plaintext: Vec<u8>,
284 aad_context: &[u8],
285 ) -> Vec<u8> {
286 match self {
287 CloudCipher::Encrypted(master) => {
288 ScopedBlobSealing::new(scope, master).seal(plaintext, aad_context)
289 }
290 CloudCipher::Plaintext => plaintext,
291 }
292 }
293
294 pub fn open_scoped(
296 &self,
297 scope: coven_protocol::blob::BlobScope,
298 stored: Vec<u8>,
299 aad_context: &[u8],
300 ) -> Result<Vec<u8>, EncryptionError> {
301 match self {
302 CloudCipher::Encrypted(e) => open_scoped_encrypted(scope, e, &stored, aad_context),
303 CloudCipher::Plaintext => Ok(stored),
304 }
305 }
306
307 pub fn suffix(&self) -> &'static str {
311 match self {
312 CloudCipher::Encrypted(_) => ".enc",
313 CloudCipher::Plaintext => "",
314 }
315 }
316
317 pub fn is_plaintext(&self) -> bool {
319 matches!(self, CloudCipher::Plaintext)
320 }
321
322 pub fn body_len(&self, header: SealedBlobHeader) -> u64 {
327 match self {
328 CloudCipher::Encrypted(_) => KeyTag::LEN as u64 + header.sealed_len(),
329 CloudCipher::Plaintext => header.plaintext_len(),
330 }
331 }
332
333 pub async fn open_body(
339 &self,
340 scope: coven_protocol::blob::BlobScope,
341 file_path: &std::path::Path,
342 aad_context: &[u8],
343 chunk_size: std::num::NonZeroU32,
344 ) -> Result<BlobBody, coven_foundation::atomic_file::FileError> {
345 let plaintext_len = coven_foundation::local_file::file_len(file_path).await?;
346 let header = SealedBlobHeader::new(
347 chunk_size,
348 plaintext_len,
349 &NoncePolicy::DerivedFromContext {
350 context: aad_context.to_vec(),
351 },
352 );
353 let reader = crate::local_file::open_reader(file_path).await?;
354 Ok(match self {
355 CloudCipher::Encrypted(encryption) => {
356 ScopedBlobSealing::new(scope, encryption).into_body(header, reader, aad_context)
357 }
358 CloudCipher::Plaintext => {
359 BlobBody::from_file_with_prefix(self.body_len(header), reader, None, Vec::new())
360 }
361 })
362 }
363
364 pub async fn open_exact_body(
368 &self,
369 scope: coven_protocol::blob::BlobScope,
370 file_path: &std::path::Path,
371 aad_context: &[u8],
372 chunk_size: std::num::NonZeroU32,
373 expected_size: u64,
374 expected_hash: coven_protocol::store_commit::ObjectHash,
375 progress: crate::cloud::PreparationProgress,
376 ) -> Result<BlobBody, coven_foundation::atomic_file::FileError> {
377 let plaintext_len = coven_foundation::local_file::file_len(file_path).await?;
378 if plaintext_len != expected_size {
379 return Err(coven_foundation::atomic_file::FileError::Path {
380 operation: "validate local blob source size",
381 path: file_path.to_path_buf(),
382 source: std::io::Error::new(
383 std::io::ErrorKind::InvalidData,
384 format!("declares {expected_size} bytes but the source has {plaintext_len}"),
385 ),
386 });
387 }
388 let header = SealedBlobHeader::new(
389 chunk_size,
390 plaintext_len,
391 &NoncePolicy::DerivedFromContext {
392 context: aad_context.to_vec(),
393 },
394 );
395 let reader =
396 crate::local_file::open_exact_reader(file_path, expected_size, expected_hash, progress)
397 .await?;
398 Ok(match self {
399 CloudCipher::Encrypted(encryption) => {
400 ScopedBlobSealing::new(scope, encryption).into_body(header, reader, aad_context)
401 }
402 CloudCipher::Plaintext => {
403 BlobBody::from_file_with_prefix(self.body_len(header), reader, None, Vec::new())
404 }
405 })
406 }
407}
408
409pub(crate) fn encryption_for_scope(
416 scope: coven_protocol::blob::BlobScope,
417 master: &EncryptionService,
418) -> EncryptionService {
419 match scope {
420 coven_protocol::blob::BlobScope::Master => master.clone(),
421 coven_protocol::blob::BlobScope::Derived(s) => master.derive_scoped(&s),
422 }
423}
424
425pub fn cloud_aad_context(store_id: &str, cloud_key: &str) -> Vec<u8> {
426 let mut context =
427 Vec::with_capacity(std::mem::size_of::<u64>() * 2 + store_id.len() + cloud_key.len());
428 context.extend_from_slice(&(store_id.len() as u64).to_le_bytes());
429 context.extend_from_slice(store_id.as_bytes());
430 context.extend_from_slice(&(cloud_key.len() as u64).to_le_bytes());
431 context.extend_from_slice(cloud_key.as_bytes());
432 context
433}
434
435pub(crate) fn protocol_object_aad_context(
436 context: &ProtocolObjectContext,
437 semantic_prefix: &str,
438) -> Vec<u8> {
439 let domain = context.domain().aad_label();
440 let mut aad = Vec::with_capacity(
441 context.store_root_hash().as_bytes().len()
442 + std::mem::size_of::<u64>() * 2
443 + domain.len()
444 + semantic_prefix.len(),
445 );
446 aad.extend_from_slice(context.store_root_hash().as_bytes());
447 aad.extend_from_slice(&(domain.len() as u64).to_le_bytes());
448 aad.extend_from_slice(domain);
449 aad.extend_from_slice(&(semantic_prefix.len() as u64).to_le_bytes());
450 aad.extend_from_slice(semantic_prefix.as_bytes());
451 aad
452}
453
454pub(crate) struct ScopedBlobSealing {
459 encryption: EncryptionService,
460 key_tag: Vec<u8>,
461}
462
463impl ScopedBlobSealing {
464 fn new(scope: coven_protocol::blob::BlobScope, master: &EncryptionService) -> Self {
465 Self {
466 encryption: encryption_for_scope(scope, master),
467 key_tag: KeyTag::write(&master.seal_fingerprint()),
468 }
469 }
470
471 fn seal(self, plaintext: Vec<u8>, aad_context: &[u8]) -> Vec<u8> {
472 let mut stored = self.key_tag;
473 stored.extend(self.encryption.encrypt(&plaintext, aad_context));
474 stored
475 }
476
477 fn into_body(
478 self,
479 header: SealedBlobHeader,
480 reader: crate::local_file::PlaintextReader,
481 aad_context: &[u8],
482 ) -> BlobBody {
483 let mut prefix = self.key_tag;
484 prefix.extend_from_slice(&header.to_bytes());
485 BlobBody::from_file_with_prefix(
486 KeyTag::LEN as u64 + header.sealed_len(),
487 reader,
488 Some(
489 self.encryption
490 .blob_sealer(
491 header,
492 &NoncePolicy::DerivedFromContext {
493 context: aad_context.to_vec(),
494 },
495 aad_context,
496 )
497 .expect("a blob header records the derived policy it was built under"),
498 ),
499 prefix,
500 )
501 }
502}
503
504pub(crate) fn opening_encryption_for_scope(
505 scope: coven_protocol::blob::BlobScope,
506 master: &EncryptionService,
507 fingerprint: &[u8; 32],
508) -> Result<EncryptionService, EncryptionError> {
509 match scope {
510 coven_protocol::blob::BlobScope::Master => master.service_for_fingerprint(fingerprint),
511 coven_protocol::blob::BlobScope::Derived(scope_id) => {
512 master.derive_scoped_for_fingerprint(fingerprint, &scope_id)
513 }
514 }
515}
516
517pub(crate) fn open_scoped_encrypted(
518 scope: coven_protocol::blob::BlobScope,
519 master: &EncryptionService,
520 stored: &[u8],
521 aad_context: &[u8],
522) -> Result<Vec<u8>, EncryptionError> {
523 let (fingerprint, ciphertext) = KeyTag::read(stored)?;
524 opening_encryption_for_scope(scope, master, &fingerprint)?.decrypt(ciphertext, aad_context)
525}