Skip to main content

coven_storage/
remote.rs

1//! `CloudSyncObjectStorage` implementation backed by any `CloudHome`.
2//!
3//! Resolves protocol slots and blob locators to exact provider objects. Each
4//! protocol domain declares its protection: Store data uses the home's
5//! [`CloudCipher`], Circle data uses the supplied Circle cipher, and signed
6//! readable or recipient-sealed records pass through unchanged. Blob locators
7//! declare their audience, scope, and readable or opaque path. Exact object
8//! references retain the resulting provider address and stored-byte identity.
9
10use 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/// Protection for payloads assigned to this cipher. `Encrypted` seals under
48/// its keyring; `Plaintext` preserves the bytes. Protocol domains and blob
49/// locators determine which cipher applies.
50#[derive(Clone)]
51pub enum CloudCipher {
52    Encrypted(EncryptionService),
53    Plaintext,
54}
55
56/// `CloudSyncObjectStorage` that delegates raw I/O to a `CloudHome` and handles the path
57/// layout and the at-rest protection (its [`CloudCipher`]).
58pub struct CloudSyncConnection {
59    /// `Arc` because ranged readers retain this provider across awaits.
60    home: Arc<dyn ExactCloudHome>,
61    provider_probes: ProviderProbeStorage,
62    cipher: Arc<RwLock<CloudCipher>>,
63    /// Whether a committed rotation is outstanding — see [`PendingRotation`].
64    /// Shared the same way `cipher` is, so a member removal or a refresh cycle
65    /// that discovers a rotation this device can't adopt blocks every seal path,
66    /// not just the one that discovered it.
67    pending_rotation: Arc<PendingRotation>,
68    /// How blob objects are keyed. Unlike the cipher, the scheme does not rotate
69    /// over a home's life, so it is a plain field with no lock.
70    blob_paths: BlobPathScheme,
71    /// How this installation chunks blobs and how wide its range requests are.
72    blob_chunking: BlobChunking,
73    store_id: String,
74    /// The Store identity used to verify that blob append authority names this
75    /// connection's author in its device registration.
76    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    /// The running total of provider operations issued through this
112    /// connection's home — by this connection and by any other over the same
113    /// home, which on a device join is how the plaintext bootstrap reads and
114    /// the encrypted reads after them land in one total.
115    pub fn provider_requests(
116        &self,
117    ) -> Option<Arc<dyn coven_foundation::stage_timing::ProviderRequests>> {
118        self.home.provider_requests()
119    }
120
121    /// Seal and read blobs with `chunking` instead of [`BlobChunking::DEFAULT`].
122    /// The chunk size applies to blobs this storage seals from now on; already
123    /// stored blobs keep the size their own headers record, so installations
124    /// with different settings read each other's blobs unchanged.
125    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            &registration,
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;