1use async_trait::async_trait;
11use std::path::Path;
12use std::sync::{Arc, RwLock};
13
14use super::provider_probe::ProviderProbeStorage;
15use super::CloudSyncObjectStorage;
16use crate::cloud::{BlobBody, CloudFileReadError, CloudHomeError, ExactCloudHome};
17use coven_keys::encryption::{
18 EncryptionError, EncryptionService, KeyTag, NoncePolicy, SealedBlobHeader,
19 SEALED_BLOB_HEADER_LEN,
20};
21use coven_keys::keys::UserKeypair;
22use coven_protocol::objects::ObjectSlot;
23#[cfg(test)]
24use coven_protocol::objects::ProtocolObjectDomain;
25use coven_protocol::objects::{
26 ExactObjectRef, PreparedExactObject, ProtocolObjectContext, ProtocolObjectProtection,
27 ResolvedProviderBinding, RotationGate, RotationPending, StorageError,
28};
29use coven_protocol::store_commit::ObjectHash;
30
31mod blob_io;
32mod cipher;
33mod rotation;
34mod storage_impl;
35
36#[cfg(any(test, feature = "test-utils"))]
37pub use blob_io::open_sealed_blob;
38pub use blob_io::BlobChunking;
39pub use blob_io::{BlobPathScheme, BlobRangeReader};
40#[cfg(any(test, feature = "test-utils"))]
41pub use cipher::CloudKeyringFacts;
42pub use cipher::{
43 cloud_aad_context, AdoptedCloudKeyRotation, CloudKeyringMerge, CloudSyncCipherStateAccess,
44};
45pub use rotation::{CloudSyncRotationStateAccess, PendingRotation, RotationStateError};
46
47#[derive(Clone)]
51pub enum CloudCipher {
52 Encrypted(EncryptionService),
53 Plaintext,
54}
55
56pub struct CloudSyncConnection {
59 home: Arc<dyn ExactCloudHome>,
61 provider_probes: ProviderProbeStorage,
62 cipher: Arc<RwLock<CloudCipher>>,
63 pending_rotation: Arc<PendingRotation>,
68 blob_paths: BlobPathScheme,
71 blob_chunking: BlobChunking,
73 store_id: String,
74 keypair: UserKeypair,
77}
78
79fn map_cloud_file_read_error(error: CloudFileReadError) -> StorageError {
80 match error {
81 CloudFileReadError::Source(error) => StorageError::from(error),
82 CloudFileReadError::SourceCleanup { source, cleanup } => StorageError::CleanupFailed {
83 operation: Box::new(StorageError::from(source)),
84 cleanup: Box::new(StorageError::LocalFilesystem(cleanup)),
85 },
86 CloudFileReadError::Local(error) => StorageError::LocalFilesystem(error),
87 }
88}
89
90impl CloudSyncConnection {
91 pub fn new(
92 home: Arc<dyn ExactCloudHome>,
93 cipher: CloudCipher,
94 blob_paths: BlobPathScheme,
95 store_id: impl Into<String>,
96 keypair: UserKeypair,
97 ) -> Self {
98 let provider_probes = ProviderProbeStorage::new(home.clone());
99 CloudSyncConnection {
100 home,
101 provider_probes,
102 cipher: Arc::new(RwLock::new(cipher)),
103 pending_rotation: Arc::new(PendingRotation::none()),
104 blob_paths,
105 blob_chunking: BlobChunking::DEFAULT,
106 store_id: store_id.into(),
107 keypair,
108 }
109 }
110
111 pub fn provider_requests(
116 &self,
117 ) -> Option<Arc<dyn coven_foundation::stage_timing::ProviderRequests>> {
118 self.home.provider_requests()
119 }
120
121 pub fn with_blob_chunking(mut self, chunking: BlobChunking) -> Self {
126 self.blob_chunking = chunking;
127 self
128 }
129
130 pub fn blob_path_scheme(&self) -> BlobPathScheme {
131 self.blob_paths
132 }
133
134 pub fn store_id(&self) -> &str {
135 &self.store_id
136 }
137
138 pub async fn probe(&self) -> Result<(), CloudHomeError> {
139 self.home.probe().await
140 }
141
142 fn validate_blob_locator_home(
143 &self,
144 locator: &coven_protocol::blob::locator::BlobLocator,
145 ) -> Result<(), StorageError> {
146 let valid = matches!(
147 (locator, self.blob_paths, self.cipher.is_plaintext()),
148 (
149 coven_protocol::blob::locator::BlobLocator::Opaque { .. },
150 BlobPathScheme::Hashed,
151 false
152 ) | (
153 coven_protocol::blob::locator::BlobLocator::Browsable { .. },
154 BlobPathScheme::Plain,
155 true
156 )
157 );
158 if !valid {
159 return Err(StorageError::InvalidContent(
160 "blob locator protection does not match the cloud home's fixed storage mode"
161 .to_string(),
162 ));
163 }
164 Ok(())
165 }
166
167 async fn validate_blob_append_authority(
168 &self,
169 locator: &coven_protocol::blob::locator::BlobLocator,
170 authority: &coven_protocol::objects::BlobWriteAuthority<'_>,
171 ) -> Result<(), StorageError> {
172 authority
173 .reference
174 .verify_registration(authority.registration)?;
175 if locator.uploader() != authority.reference {
176 return Err(StorageError::InvalidContent(format!(
177 "blob locator uploader {:?} differs from its exact write authority",
178 locator.uploader()
179 )));
180 }
181 if authority.registration.author_pubkey != hex::encode(self.keypair.public_key()) {
182 return Err(StorageError::InvalidContent(
183 "blob write authority is not this device's identity key".to_string(),
184 ));
185 }
186 let live = self
187 .home
188 .provider_binding()
189 .await
190 .map_err(StorageError::from)?;
191 if live.device != authority.registration.provider {
192 return Err(StorageError::InvalidContent(
193 "blob write authority differs from the authenticated provider principal"
194 .to_string(),
195 ));
196 }
197 Ok(())
198 }
199
200 pub fn uses_identity(&self, identity: &UserKeypair) -> bool {
201 self.keypair.public_key() == identity.public_key()
202 }
203
204 #[cfg(any(test, feature = "test-utils"))]
205 pub fn connection_for_test_identity(&self, identity: UserKeypair) -> Self {
206 Self::new(
207 self.home.clone(),
208 self.cipher.read().unwrap().clone(),
209 self.blob_paths,
210 self.store_id.clone(),
211 identity,
212 )
213 .with_blob_chunking(self.blob_chunking)
214 }
215
216 #[cfg(any(test, feature = "test-utils"))]
217 pub fn connection_for_test_identity_and_home(
218 &self,
219 identity: UserKeypair,
220 home: Arc<dyn ExactCloudHome>,
221 ) -> Self {
222 Self::new(
223 home,
224 self.cipher.read().unwrap().clone(),
225 self.blob_paths,
226 self.store_id.clone(),
227 identity,
228 )
229 .with_blob_chunking(self.blob_chunking)
230 }
231
232 pub fn is_plaintext(&self) -> bool {
233 self.cipher.read().unwrap().is_plaintext()
234 }
235
236 fn cipher_suffix(&self) -> &'static str {
237 self.cipher.read().unwrap().suffix()
238 }
239
240 fn open_stored_data(
241 &self,
242 stored: Vec<u8>,
243 aad_context: &[u8],
244 ) -> Result<Vec<u8>, EncryptionError> {
245 self.cipher.read().unwrap().open(stored, aad_context)
246 }
247
248 fn seal_stored_data(
249 &self,
250 plaintext: Vec<u8>,
251 aad_context: &[u8],
252 ) -> Result<Vec<u8>, StorageError> {
253 let cipher = self.cipher.read().unwrap();
254 self.pending_rotation.check(cipher.current_generation())?;
255 Ok(cipher.seal(plaintext, aad_context))
256 }
257
258 fn seal_protocol_data(
259 &self,
260 context: &ProtocolObjectContext,
261 plaintext: Vec<u8>,
262 aad_context: &[u8],
263 ) -> Result<Vec<u8>, StorageError> {
264 match context.protection() {
265 ProtocolObjectProtection::StoreEncrypted => {
266 self.seal_stored_data(plaintext, aad_context)
267 }
268 ProtocolObjectProtection::SignedPlaintext
269 | ProtocolObjectProtection::RecipientSealed => {
270 Ok(CloudCipher::Plaintext.seal(plaintext, aad_context))
271 }
272 ProtocolObjectProtection::Circle(encryption) => {
273 Ok(CloudCipher::Encrypted(encryption.clone()).seal(plaintext, aad_context))
274 }
275 }
276 }
277
278 async fn verify_and_open_protocol_data(
279 &self,
280 operation: &'static str,
281 context: &ProtocolObjectContext,
282 object: ExactObjectRef,
283 stored: Vec<u8>,
284 aad_context: Vec<u8>,
285 ) -> Result<Vec<u8>, StorageError> {
286 let cipher = match context.protection() {
287 ProtocolObjectProtection::StoreEncrypted => self.cipher.read().unwrap().clone(),
288 ProtocolObjectProtection::SignedPlaintext => CloudCipher::Plaintext,
289 ProtocolObjectProtection::Circle(encryption) => {
290 CloudCipher::Encrypted(encryption.clone())
291 }
292 ProtocolObjectProtection::RecipientSealed => CloudCipher::Plaintext,
293 };
294 run_storage_cpu(
295 operation,
296 Box::new(move || {
297 object.verify(&stored)?;
298 cipher
299 .open(stored, &aad_context)
300 .map_err(|source| StorageError::Decryption {
301 context: format!("protocol object {}", object.slot().logical_key()),
302 source,
303 })
304 }),
305 )
306 .await
307 }
308
309 async fn identify_and_open_protocol_data(
310 &self,
311 context: &ProtocolObjectContext,
312 slot: ObjectSlot,
313 stored: Vec<u8>,
314 aad_context: Vec<u8>,
315 ) -> Result<(Vec<u8>, PreparedExactObject), StorageError> {
316 let cipher = match context.protection() {
317 ProtocolObjectProtection::StoreEncrypted => self.cipher.read().unwrap().clone(),
318 ProtocolObjectProtection::SignedPlaintext => CloudCipher::Plaintext,
319 ProtocolObjectProtection::Circle(encryption) => {
320 CloudCipher::Encrypted(encryption.clone())
321 }
322 ProtocolObjectProtection::RecipientSealed => CloudCipher::Plaintext,
323 };
324 run_storage_cpu(
325 "identify and open protocol slot",
326 Box::new(move || {
327 let object = ExactObjectRef::new(
328 slot.clone(),
329 stored.len() as u64,
330 ObjectHash::digest(&stored),
331 );
332 let prepared = PreparedExactObject::new(object, stored.clone())?;
333 let opened = cipher.open(stored, &aad_context).map_err(|source| {
334 StorageError::Decryption {
335 context: format!("protocol object {}", slot.logical_key()),
336 source,
337 }
338 })?;
339 Ok((opened, prepared))
340 }),
341 )
342 .await
343 }
344
345 #[cfg(test)]
346 async fn blob_write_registration(
347 &self,
348 label: &str,
349 ) -> coven_protocol::store_commit::ReferencedStoreDeviceRegistration {
350 use coven_protocol::store_commit::{
351 DeviceStreamAnchor, StoreCreationId, StoreDeviceRegistration,
352 StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef, StoreRootRef,
353 };
354
355 let root_bytes = format!("{label} Store root").into_bytes();
356 let root = StoreRootRef {
357 store_root_id: ObjectHash::digest(format!("{label} root id").as_bytes()),
358 store_root_hash: ObjectHash::digest(&root_bytes),
359 object: ExactObjectRef::new(
360 ObjectSlot::logical(format!("store-v1/store-protocol-root/{label}.json")).unwrap(),
361 root_bytes.len() as u64,
362 ObjectHash::digest(&root_bytes),
363 ),
364 };
365 let anchor_slot = |stream: &str| {
366 ObjectSlot::logical(format!(
367 "store-v1/test-device-streams/{label}/{stream}.json"
368 ))
369 .unwrap()
370 };
371 let provider = CloudSyncObjectStorage::provider_binding(self)
372 .await
373 .unwrap()
374 .device;
375 let registration = StoreDeviceRegistration::signed(
376 root,
377 StoreDeviceRegistrationOrigin::Founder {
378 creation_id: StoreCreationId::from_nonce(label),
379 },
380 provider,
381 DeviceStreamAnchor::StoreAcknowledgements {
382 first_slot: anchor_slot("acknowledgements"),
383 },
384 &self.keypair,
385 )
386 .unwrap();
387 let bytes = registration.to_bytes();
388 let reference = StoreDeviceRegistrationRef::from_registration(
389 ®istration,
390 ExactObjectRef::new(
391 ObjectSlot::logical(format!(
392 "store-v1/devices/{}/registration.json",
393 registration.device_id
394 ))
395 .unwrap(),
396 bytes.len() as u64,
397 ObjectHash::digest(&bytes),
398 ),
399 );
400 coven_protocol::store_commit::ReferencedStoreDeviceRegistration::verified(
401 reference,
402 registration,
403 )
404 .expect("construct test blob write registration")
405 }
406
407 #[cfg(any(test, feature = "test-utils"))]
408 pub fn keyring_facts_for_test(&self) -> Option<CloudKeyringFacts> {
409 match &*self.cipher.read().unwrap() {
410 CloudCipher::Encrypted(encryption) => {
411 Some(CloudKeyringFacts::from_encryption(encryption))
412 }
413 CloudCipher::Plaintext => None,
414 }
415 }
416
417 #[cfg(any(test, feature = "test-utils"))]
418 pub fn adopt_key_rotation_for_test(
419 &self,
420 encryption: &EncryptionService,
421 custody: &dyn coven_keys::keys::MasterKeyCustody,
422 ) -> Result<String, coven_keys::keys::KeyError> {
423 CloudSyncCipherStateAccess::adopt_key_rotation(self, encryption, custody)
424 .map(|adopted| adopted.fingerprint().to_string())
425 }
426
427 #[cfg(any(test, feature = "test-utils"))]
428 pub fn mark_rotation_committed_for_test(
429 &self,
430 generation: u64,
431 ) -> Result<(), RotationStateError> {
432 self.pending_rotation.mark_committed(generation)
433 }
434
435 #[cfg(any(test, feature = "test-utils"))]
436 pub fn pending_rotation_generation_for_test(&self) -> Option<u64> {
437 self.pending_rotation.pending_generation()
438 }
439
440 #[cfg(any(test, feature = "test-utils"))]
441 pub fn clear_rotation_gate_for_test(&self) {
442 self.pending_rotation.install_durable_gate(None);
443 }
444}
445
446impl CloudSyncCipherStateAccess for CloudSyncConnection {
447 fn is_plaintext(&self) -> bool {
448 self.cipher.is_plaintext()
449 }
450
451 fn suffix(&self) -> &'static str {
452 self.cipher.suffix()
453 }
454
455 fn current_generation(&self) -> Option<u64> {
456 self.cipher.current_generation()
457 }
458
459 fn current_fingerprint(&self) -> Option<String> {
460 self.cipher.current_fingerprint()
461 }
462
463 fn open(&self, stored: Vec<u8>, aad_context: &[u8]) -> Result<Vec<u8>, EncryptionError> {
464 self.cipher.open(stored, aad_context)
465 }
466
467 fn seal(&self, plaintext: Vec<u8>, aad_context: &[u8]) -> Vec<u8> {
468 self.cipher.seal(plaintext, aad_context)
469 }
470
471 #[cfg(any(test, feature = "test-utils"))]
472 fn open_sealed_blob_for_test(
473 &self,
474 stored: &[u8],
475 aad_context: &[u8],
476 ) -> Result<
477 (coven_keys::encryption::KeyFingerprint, Vec<u8>),
478 coven_keys::encryption::EncryptionError,
479 > {
480 self.cipher.open_sealed_blob_for_test(stored, aad_context)
481 }
482
483 fn merged_keyring(
484 &self,
485 new_encryption: &EncryptionService,
486 ) -> Result<CloudKeyringMerge, EncryptionError> {
487 self.cipher.merged_keyring(new_encryption)
488 }
489
490 fn merge_key_rotation(
491 &self,
492 new_encryption: &EncryptionService,
493 custody: &dyn coven_keys::keys::MasterKeyCustody,
494 ) -> Result<Option<String>, coven_keys::keys::KeyError> {
495 self.cipher.merge_key_rotation(new_encryption, custody)
496 }
497}
498
499impl CloudSyncRotationStateAccess for CloudSyncConnection {
500 fn mark_candidate(
501 &self,
502 generation: u64,
503 mutation: ObjectHash,
504 ) -> Result<(), RotationStateError> {
505 self.pending_rotation.mark_candidate(generation, mutation)
506 }
507
508 fn mark_committed_mutation(
509 &self,
510 generation: u64,
511 mutation: ObjectHash,
512 ) -> Result<(), RotationStateError> {
513 self.pending_rotation
514 .mark_committed_mutation(generation, mutation)
515 }
516
517 fn gate(&self) -> Option<RotationGate> {
518 self.pending_rotation.gate()
519 }
520
521 fn install_durable_gate(&self, gate: Option<RotationGate>) {
522 self.pending_rotation.install_durable_gate(gate);
523 }
524
525 fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending> {
526 self.pending_rotation.check(live_generation)
527 }
528}
529
530async fn run_storage_cpu<T>(
531 operation: &'static str,
532 work: Box<dyn FnOnce() -> Result<T, StorageError> + Send>,
533) -> Result<T, StorageError>
534where
535 T: Send + 'static,
536{
537 coven_foundation::blocking::run(work)
538 .await
539 .map_err(|source| StorageError::Blocking { operation, source })?
540}
541
542async fn read_source_exact(
543 source: &mut crate::local_file::PlaintextReader,
544 len: usize,
545 locator_hash: ObjectHash,
546) -> Result<Vec<u8>, StorageError> {
547 let mut bytes = Vec::with_capacity(len);
548 while bytes.len() < len {
549 let chunk = source.next_chunk(len - bytes.len()).await?;
550 if chunk.is_empty() {
551 return Err(StorageError::InvalidContent(format!(
552 "blob {locator_hash} stored body ended after {} of {len} required bytes",
553 bytes.len()
554 )));
555 }
556 bytes.extend_from_slice(&chunk);
557 }
558 Ok(bytes)
559}
560
561#[cfg(test)]
562mod tests;