Skip to main content

coven_protocol/
objects.rs

1//! Exact storage access for signed protocol objects and stored blob bodies.
2//!
3//! Every remote object is addressed by an [`ExactObjectRef`]. The logical key
4//! supplies domain separation and the physical locator selects the one provider
5//! object whose stored size and hash the signed reference authenticates. Prefix
6//! enumeration and provider names never select protocol authority.
7use std::num::NonZeroU64;
8use std::path::Path;
9
10use serde::{Deserialize, Deserializer, Serialize};
11
12use crate::membership::AuthorHead;
13use crate::store_commit::{ObjectHash, StoreDeviceRegistration, StoreProtocolError};
14
15/// Opaque provider revision for an exact mutable object.
16///
17/// The provider assigns the value and interprets it during conditional
18/// replacement. Coven only requires that it is present and retains it beside
19/// the exact bytes observed at that revision.
20#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
21#[serde(transparent)]
22pub struct ExactObjectVersion(String);
23
24impl ExactObjectVersion {
25    pub fn from_provider(value: String) -> Result<Self, StorageError> {
26        if value.is_empty() {
27            return Err(StorageError::Configuration(
28                "cloud object version token is empty".to_string(),
29            ));
30        }
31        Ok(Self(value))
32    }
33
34    pub fn as_provider(&self) -> &str {
35        &self.0
36    }
37}
38
39impl<'de> Deserialize<'de> for ExactObjectVersion {
40    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41    where
42        D: Deserializer<'de>,
43    {
44        let value = String::deserialize(deserializer)?;
45        if value.is_empty() {
46            return Err(serde::de::Error::custom(
47                "cloud object version token is empty",
48            ));
49        }
50        Ok(Self(value))
51    }
52}
53
54mod domains;
55mod provider_binding;
56mod rotation;
57
58pub use domains::{
59    CircleProtocolObjectDomain, ProtectedObjectDomain, ProtocolObjectDomain,
60    RecipientSealedProtocolObjectDomain, SignedStoreProtocolObjectDomain,
61    StoreEncryptedProtocolObjectDomain,
62};
63pub use provider_binding::*;
64#[cfg(any(test, feature = "test-utils"))]
65pub use rotation::LocalRotation;
66pub use rotation::RotationPending;
67#[cfg(any(test, feature = "test-utils"))]
68pub use rotation::RotationPendingState;
69pub use rotation::{RotationGate, RotationGateError, ROTATION_GATE_STATE_KEY};
70
71/// Authenticated storage context for one immutable semantic object.
72///
73/// Store protection cannot be paired with a Circle-encrypted domain:
74///
75/// ```compile_fail
76/// use crate::storage::{ProtocolObjectContext, ProtocolObjectDomain};
77/// use crate::ObjectHash;
78///
79/// let root = ObjectHash::digest(b"store");
80/// let _ = ProtocolObjectContext::signed_plaintext(root, ProtocolObjectDomain::CircleMetadata);
81/// ```
82///
83/// Circle protection cannot be paired with a Store domain:
84///
85/// ```compile_fail
86/// use crate::storage::{ProtocolObjectContext, ProtocolObjectDomain};
87/// use crate::{EncryptionService, ObjectHash};
88///
89/// let root = ObjectHash::digest(b"store");
90/// let encryption = EncryptionService::from_key([7; 32]);
91/// let _ = ProtocolObjectContext::circle(
92///     root,
93///     ProtocolObjectDomain::StoreCommit,
94///     encryption,
95/// );
96/// ```
97pub struct ProtocolObjectContext {
98    store_root_hash: ObjectHash,
99    domain: ProtectedObjectDomain,
100    protection: ProtocolObjectProtection,
101}
102
103#[derive(Clone)]
104pub enum ProtocolObjectProtection {
105    StoreEncrypted,
106    SignedPlaintext,
107    Circle(coven_keys::encryption::EncryptionService),
108    RecipientSealed,
109}
110
111impl ProtocolObjectContext {
112    pub fn store_encrypted(
113        store_root_hash: ObjectHash,
114        domain: StoreEncryptedProtocolObjectDomain,
115    ) -> Self {
116        Self {
117            store_root_hash,
118            domain: domain.0,
119            protection: ProtocolObjectProtection::StoreEncrypted,
120        }
121    }
122
123    pub fn signed_plaintext(
124        store_root_hash: ObjectHash,
125        domain: SignedStoreProtocolObjectDomain,
126    ) -> Self {
127        Self {
128            store_root_hash,
129            domain: domain.0,
130            protection: ProtocolObjectProtection::SignedPlaintext,
131        }
132    }
133
134    pub fn circle(
135        store_root_hash: ObjectHash,
136        domain: CircleProtocolObjectDomain,
137        encryption: coven_keys::encryption::EncryptionService,
138    ) -> Self {
139        Self {
140            store_root_hash,
141            domain: domain.0,
142            protection: ProtocolObjectProtection::Circle(encryption),
143        }
144    }
145
146    pub fn recipient_sealed(
147        store_root_hash: ObjectHash,
148        domain: RecipientSealedProtocolObjectDomain,
149    ) -> Self {
150        Self {
151            store_root_hash,
152            domain: domain.0,
153            protection: ProtocolObjectProtection::RecipientSealed,
154        }
155    }
156
157    pub fn store_root_hash(&self) -> ObjectHash {
158        self.store_root_hash
159    }
160
161    pub fn domain(&self) -> ProtectedObjectDomain {
162        self.domain
163    }
164
165    pub fn protection(&self) -> &ProtocolObjectProtection {
166        &self.protection
167    }
168
169    pub fn validate_path(&self, semantic_prefix: &str) -> Result<(), StorageError> {
170        let metadata = self.domain.metadata();
171        if semantic_prefix.contains("/copies/") || !metadata.path.accepts(semantic_prefix) {
172            return Err(StorageError::Parse(format!(
173                "object domain {:?} does not accept semantic path {semantic_prefix:?}",
174                self.domain
175            )));
176        }
177        Ok(())
178    }
179
180    pub fn validate_extension(&self, extension: &str) -> Result<(), StorageError> {
181        if extension != self.domain.extension() {
182            return Err(StorageError::Parse(format!(
183                "object domain {:?} does not accept extension {extension:?}",
184                self.domain
185            )));
186        }
187        Ok(())
188    }
189
190    pub fn validate_reference(
191        &self,
192        object: &ExactObjectRef,
193        semantic_prefix: &str,
194    ) -> Result<(), StorageError> {
195        self.validate_slot(object.slot(), semantic_prefix)
196    }
197
198    /// The semantic path `slot` names, when the slot names an object of this
199    /// context's domain.
200    ///
201    /// A slot's logical key is its semantic path plus the domain's extension,
202    /// so the path is recoverable from the key alone. Returns `None` for a slot
203    /// this domain would not have written — which is how a caller that learned
204    /// of a slot by listing a provider prefix, rather than by following a
205    /// signed reference, discards what does not belong to it.
206    pub fn semantic_prefix_of<'slot>(&self, slot: &'slot ObjectSlot) -> Option<&'slot str> {
207        let semantic_prefix = slot.logical_key().strip_suffix(self.domain.extension())?;
208        self.validate_slot(slot, semantic_prefix)
209            .ok()
210            .map(|()| semantic_prefix)
211    }
212
213    pub fn validate_slot(
214        &self,
215        slot: &ObjectSlot,
216        semantic_prefix: &str,
217    ) -> Result<(), StorageError> {
218        self.validate_path(semantic_prefix)?;
219        let expected = format!("{semantic_prefix}{}", self.domain.extension());
220        if slot.logical_key() != expected {
221            return Err(StorageError::Parse(format!(
222                "protocol object {:?} does not match semantic path {semantic_prefix:?}",
223                slot.logical_key()
224            )));
225        }
226        Ok(())
227    }
228}
229
230/// Protection selected by the audience authority that prepares a blob spool.
231#[derive(Clone)]
232pub enum BlobSpoolProtection {
233    Opaque(coven_keys::encryption::EncryptionService),
234    Browsable,
235}
236
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238pub enum BlobSpoolWrite {
239    Created,
240    Reused,
241}
242
243#[derive(Clone, Copy)]
244pub struct BlobWriteAuthority<'a> {
245    pub reference: &'a crate::store_commit::StoreDeviceRegistrationRef,
246    pub registration: &'a crate::store_commit::StoreDeviceRegistration,
247}
248
249impl<'a> BlobWriteAuthority<'a> {
250    pub fn new(registration: &'a crate::store_commit::ReferencedStoreDeviceRegistration) -> Self {
251        Self {
252            reference: registration.reference(),
253            registration: registration.value(),
254        }
255    }
256}
257
258/// Exact stored representation of one immutable object.
259#[derive(
260    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
261)]
262#[serde(deny_unknown_fields)]
263pub struct ExactObjectRef {
264    slot: ObjectSlot,
265    stored_size: u64,
266    stored_hash: ObjectHash,
267}
268
269impl ExactObjectRef {
270    pub fn new(slot: ObjectSlot, stored_size: u64, stored_hash: ObjectHash) -> Self {
271        Self {
272            slot,
273            stored_size,
274            stored_hash,
275        }
276    }
277
278    pub fn slot(&self) -> &ObjectSlot {
279        &self.slot
280    }
281
282    pub fn stored_size(&self) -> u64 {
283        self.stored_size
284    }
285
286    pub fn stored_hash(&self) -> ObjectHash {
287        self.stored_hash
288    }
289
290    pub fn verify(&self, bytes: &[u8]) -> Result<(), StorageError> {
291        if bytes.len() as u64 != self.stored_size || ObjectHash::digest(bytes) != self.stored_hash {
292            return Err(StorageError::InvalidContent(format!(
293                "exact object {} does not match stored size/hash",
294                self.slot.logical_key()
295            )));
296        }
297        Ok(())
298    }
299
300    /// Check independently computed file facts against the stored identity.
301    /// Reading the file and computing its facts is the filesystem owner's
302    /// operation; this value only compares.
303    pub fn verify_stored_facts(
304        &self,
305        path: &Path,
306        size: u64,
307        hash: ObjectHash,
308    ) -> Result<(), StorageError> {
309        if size != self.stored_size || hash != self.stored_hash {
310            return Err(StorageError::InvalidContent(format!(
311                "exact file {} does not match stored identity for {}",
312                path.display(),
313                self.slot.logical_key()
314            )));
315        }
316        Ok(())
317    }
318}
319
320/// Immutable stored bytes and the exact reference derived from them.
321#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
322#[serde(deny_unknown_fields)]
323pub struct PreparedExactObject {
324    reference: ExactObjectRef,
325    stored_bytes: Vec<u8>,
326}
327
328impl<'de> serde::Deserialize<'de> for PreparedExactObject {
329    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
330    where
331        D: serde::Deserializer<'de>,
332    {
333        #[derive(serde::Deserialize)]
334        #[serde(deny_unknown_fields)]
335        struct Fields {
336            reference: ExactObjectRef,
337            stored_bytes: Vec<u8>,
338        }
339
340        let fields = Fields::deserialize(deserializer)?;
341        Self::new(fields.reference, fields.stored_bytes).map_err(serde::de::Error::custom)
342    }
343}
344
345impl PreparedExactObject {
346    pub fn new(reference: ExactObjectRef, stored_bytes: Vec<u8>) -> Result<Self, StorageError> {
347        reference.verify(&stored_bytes)?;
348        Ok(Self {
349            reference,
350            stored_bytes,
351        })
352    }
353
354    pub fn reference(&self) -> &ExactObjectRef {
355        &self.reference
356    }
357
358    pub fn stored_bytes(&self) -> &[u8] {
359        &self.stored_bytes
360    }
361}
362
363/// Error type for storage operations.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum StorageBackendFailure {
366    Authentication,
367    PermissionDenied,
368    ContainerNotFound,
369    RegionMismatch,
370    QuotaExceeded,
371    Configuration,
372    Transport,
373    Internal,
374}
375
376#[derive(Debug, thiserror::Error)]
377pub enum StorageError {
378    #[error("storage operation failed: {0}")]
379    Storage(String),
380    #[error("storage backend {kind:?} failure while {operation}: {source}")]
381    Backend {
382        kind: StorageBackendFailure,
383        operation: String,
384        #[source]
385        source: Box<dyn std::error::Error + Send + Sync>,
386    },
387    #[error("{operation}; storage cleanup failed: {cleanup}")]
388    CleanupFailed {
389        #[source]
390        operation: Box<StorageError>,
391        cleanup: Box<StorageError>,
392    },
393    #[error("{operation}; exact response settlement failed: {settlement}")]
394    UnresolvedOutcome {
395        #[source]
396        operation: Box<StorageError>,
397        settlement: Box<StorageError>,
398    },
399    #[error("storage configuration is invalid: {0}")]
400    Configuration(String),
401    #[error("storage object parse failed: {0}")]
402    Parse(String),
403    #[error("storage object JSON failed: {0}")]
404    Json(#[from] serde_json::Error),
405    #[error("storage key custody failed: {0}")]
406    Key(#[from] coven_keys::keys::KeyError),
407    #[error("provider probe journal is invalid: {0}")]
408    ProviderProbeJournal(#[from] crate::provider::ProviderProbeJournalError),
409    #[error("Store protocol object is invalid: {0}")]
410    StoreProtocol(#[source] Box<crate::store_commit::StoreProtocolError>),
411    #[error("stored blob reference is invalid: {0}")]
412    BlobLocator(#[from] crate::blob::locator::BlobLocatorError),
413    #[error("storage worker failed while {operation}: {source}")]
414    Blocking {
415        operation: &'static str,
416        #[source]
417        source: coven_foundation::blocking::BlockingTaskError,
418    },
419    #[error("storage URL is invalid: {0}")]
420    Url(#[from] url::ParseError),
421    #[error("object not found: {0}")]
422    NotFound(String),
423    #[error("storage object already exists: {0}")]
424    AlreadyExists(String),
425    #[error("reserved storage slot contains different bytes: {0}")]
426    SlotCollision(String),
427    /// A retained prepared object opened to bytes other than its durable
428    /// journal records.
429    #[error("prepared exact object differs from its durable bytes: {0}")]
430    PreparedObjectMismatch(String),
431    #[error("decryption failed for {context}: {source}")]
432    Decryption {
433        context: String,
434        #[source]
435        source: coven_keys::encryption::EncryptionError,
436    },
437    #[error("remote blob content is invalid: {0}")]
438    InvalidContent(String),
439    #[error("local blob filesystem failed: {0}")]
440    LocalFilesystem(#[from] coven_foundation::atomic_file::FileError),
441    #[error("storage I/O failed: {0}")]
442    Io(#[from] std::io::Error),
443    #[error("publishing a new local file failed: {0}")]
444    CommitNewFile(#[from] coven_foundation::local_file::CommitNewFileError),
445    #[error("unsafe blob path: {0}")]
446    UnsafeBlobPath(#[from] coven_foundation::store_dir::PathTokenError),
447    /// This device has not adopted a store-key rotation the cloud already
448    /// committed; see [`RotationPending`].
449    #[error("{0}")]
450    RotationPending(#[from] RotationPending),
451}
452
453impl StorageError {
454    pub fn backend(
455        kind: StorageBackendFailure,
456        operation: impl Into<String>,
457        source: impl std::error::Error + Send + Sync + 'static,
458    ) -> Self {
459        Self::Backend {
460            kind,
461            operation: operation.into(),
462            source: Box::new(source),
463        }
464    }
465
466    pub fn is_transport(&self) -> bool {
467        match self {
468            Self::Storage(_)
469            | Self::Backend {
470                kind: StorageBackendFailure::Transport,
471                ..
472            } => true,
473            Self::CleanupFailed { operation, .. } | Self::UnresolvedOutcome { operation, .. } => {
474                operation.is_transport()
475            }
476            _ => false,
477        }
478    }
479
480    pub fn backend_failure(&self) -> Option<StorageBackendFailure> {
481        match self {
482            Self::Storage(_) => Some(StorageBackendFailure::Transport),
483            Self::Backend { kind, .. } => Some(*kind),
484            Self::CleanupFailed { operation, .. } | Self::UnresolvedOutcome { operation, .. } => {
485                operation.backend_failure()
486            }
487            Self::Configuration(_) => Some(StorageBackendFailure::Configuration),
488            _ => None,
489        }
490    }
491
492    pub fn cleanup_causes(&self) -> Option<(&StorageError, &StorageError)> {
493        match self {
494            Self::CleanupFailed { operation, cleanup } => Some((operation, cleanup)),
495            _ => None,
496        }
497    }
498}
499
500impl From<crate::store_commit::StoreProtocolError> for StorageError {
501    fn from(source: crate::store_commit::StoreProtocolError) -> Self {
502        Self::StoreProtocol(Box::new(source))
503    }
504}
505
506/// Provider-specific physical address for a caller-reserved immutable slot.
507#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
508#[serde(
509    tag = "kind",
510    content = "value",
511    rename_all = "snake_case",
512    deny_unknown_fields
513)]
514pub enum PhysicalObjectLocator {
515    LogicalKey,
516    Opaque(String),
517}
518
519/// Exact logical and physical location persisted before an immutable write.
520#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
521#[serde(deny_unknown_fields)]
522pub struct ObjectSlot {
523    logical_key: String,
524    physical: PhysicalObjectLocator,
525}
526
527impl<'de> Deserialize<'de> for ObjectSlot {
528    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
529    where
530        D: Deserializer<'de>,
531    {
532        #[derive(Deserialize)]
533        #[serde(deny_unknown_fields)]
534        struct Fields {
535            logical_key: String,
536            physical: PhysicalObjectLocator,
537        }
538
539        let fields = Fields::deserialize(deserializer)?;
540        Self::new(fields.logical_key, fields.physical).map_err(serde::de::Error::custom)
541    }
542}
543
544impl ObjectSlot {
545    pub fn logical(logical_key: String) -> Result<Self, StorageError> {
546        Self::new(logical_key, PhysicalObjectLocator::LogicalKey)
547    }
548
549    pub fn opaque(logical_key: String, provider_id: String) -> Result<Self, StorageError> {
550        Self::new(logical_key, PhysicalObjectLocator::Opaque(provider_id))
551    }
552
553    fn new(logical_key: String, physical: PhysicalObjectLocator) -> Result<Self, StorageError> {
554        let slot = Self {
555            logical_key,
556            physical,
557        };
558        slot.validate()?;
559        Ok(slot)
560    }
561
562    pub fn validate(&self) -> Result<(), StorageError> {
563        if self.logical_key.is_empty() {
564            return Err(StorageError::Configuration(
565                "object slot logical key is empty".to_string(),
566            ));
567        }
568        if matches!(&self.physical, PhysicalObjectLocator::Opaque(value) if value.is_empty()) {
569            return Err(StorageError::Configuration(
570                "object slot provider locator is empty".to_string(),
571            ));
572        }
573        Ok(())
574    }
575
576    pub fn logical_key(&self) -> &str {
577        &self.logical_key
578    }
579
580    pub fn physical(&self) -> &PhysicalObjectLocator {
581        &self.physical
582    }
583
584    /// Reject this slot when the provider requires the logical key to be the
585    /// physical object locator.
586    pub fn require_logical_key_for(&self, provider: &str) -> Result<(), StorageError> {
587        self.validate()?;
588        if self.physical != PhysicalObjectLocator::LogicalKey {
589            return Err(StorageError::Configuration(format!(
590                "{provider} slot for {} must use its logical key",
591                self.logical_key
592            )));
593        }
594        Ok(())
595    }
596}
597
598#[derive(Clone, Debug)]
599pub struct VerifiedObject<T> {
600    pub value: T,
601    pub bytes: Vec<u8>,
602    pub semantic_hash: ObjectHash,
603    pub object: ExactObjectRef,
604}
605
606#[derive(Debug, thiserror::Error)]
607pub enum StoreObjectError {
608    #[error("{0}")]
609    Storage(
610        #[from]
611        #[source]
612        StorageError,
613    ),
614    #[error("Store object {key:?} is invalid for semantic object {semantic_prefix:?}: {source}")]
615    InvalidObject {
616        semantic_prefix: String,
617        key: String,
618        #[source]
619        source: Box<StoreProtocolError>,
620    },
621}
622
623/// Decode the JSON body of one protocol object. Bytes that do not parse as `T`
624/// are malformed for the slot they were read from.
625pub fn decode_protocol_object<T: serde::de::DeserializeOwned>(
626    bytes: &[u8],
627) -> Result<T, StoreProtocolError> {
628    serde_json::from_slice(bytes).map_err(StoreProtocolError::from)
629}
630
631/// Reject an object that names a different Store root than the one it was read
632/// under.
633pub fn verify_store_root(
634    expected: ObjectHash,
635    actual: ObjectHash,
636) -> Result<(), StoreProtocolError> {
637    if actual != expected {
638        return Err(StoreProtocolError::StoreRootMismatch { expected, actual });
639    }
640    Ok(())
641}
642
643pub fn verify_membership_head_reference(
644    head: &AuthorHead,
645    expected_coord: &crate::membership::MembershipCoord,
646    expected_head_hash: ObjectHash,
647    registration: &StoreDeviceRegistration,
648) -> Result<(), StoreProtocolError> {
649    if head.entry_coord() != *expected_coord
650        || head.head_hash() != expected_head_hash
651        || registration.author_pubkey != expected_coord.author_pubkey
652        || !head.verify(registration)
653    {
654        return Err(StoreProtocolError::Malformed(
655            "exact membership head differs from its reference or certified author".to_string(),
656        ));
657    }
658    Ok(())
659}
660
661/// One loaded protocol object: its typed value, its canonical plaintext, and
662/// the bytes that go to storage.
663///
664/// `bytes` and `prepared` are not the same thing wherever the object is
665/// encrypted: `bytes` is the canonical semantic value retained by durable
666/// validation, while `prepared` holds the exact provider representation. The
667/// object's reference lives on `prepared` alone.
668#[derive(Debug, Clone)]
669pub struct ExactProtocolObject<T> {
670    pub value: T,
671    pub bytes: Vec<u8>,
672    pub prepared: PreparedExactObject,
673}
674
675pub struct PreparedProtocolObject<T> {
676    pub value: T,
677    pub prepared: PreparedExactObject,
678}
679
680#[cfg(test)]
681mod tests;