Skip to main content

coven_replication/sync/store/authorization/
keyring.rs

1use coven_keys::encryption::EncryptionService;
2use coven_keys::keys::{IdentityKeyAuthority, UserKeypair};
3use coven_protocol::membership::MembershipChain;
4use coven_protocol::objects::{ProtocolObjectContext, ProtocolObjectDomain, StorageError};
5use coven_protocol::wrapped_store_key::{
6    PreparedWrappedStoreKey, WrappedStoreKey, WrappedStoreKeyRef,
7};
8
9use crate::sync::store::membership::MembershipMutationError;
10
11/// Store-key operations bound to one exact Store root and its storage.
12pub struct StoreKeyrings<'storage> {
13    storage: &'storage dyn coven_storage::CloudSyncObjectStorage,
14    root: coven_protocol::store_commit::StoreRootRef,
15}
16
17impl<'storage> StoreKeyrings<'storage> {
18    pub fn new(
19        storage: &'storage dyn coven_storage::CloudSyncObjectStorage,
20        root: coven_protocol::store_commit::StoreRootRef,
21    ) -> Self {
22        Self { storage, root }
23    }
24
25    pub(crate) async fn open(
26        &self,
27        identity: &dyn IdentityKeyAuthority,
28        membership: &MembershipChain,
29    ) -> Result<EncryptionService, MembershipMutationError> {
30        let references = wrapped_key_references(identity, membership)?;
31        self.open_references(identity, &references).await
32    }
33
34    pub async fn open_containing(
35        &self,
36        identity: &UserKeypair,
37        membership: &MembershipChain,
38        required: &WrappedStoreKeyRef,
39    ) -> Result<EncryptionService, MembershipMutationError> {
40        let references = wrapped_key_references(identity, membership)?;
41        if !references.contains(required) {
42            return Err(MembershipMutationError::Crypto(
43                "wrapped-key ref is not activated by the verified membership".to_string(),
44            ));
45        }
46        self.open_references(identity, &references).await
47    }
48
49    pub(crate) async fn open_or(
50        &self,
51        identity: &dyn IdentityKeyAuthority,
52        membership: &MembershipChain,
53        initial: &EncryptionService,
54    ) -> Result<EncryptionService, MembershipMutationError> {
55        let references = wrapped_key_references(identity, membership)?;
56        if references.is_empty() {
57            Ok(initial.clone())
58        } else {
59            self.open_references(identity, &references).await
60        }
61    }
62
63    pub(crate) async fn prepare(
64        &self,
65        recipient: &str,
66        value: WrappedStoreKey,
67    ) -> Result<PreparedWrappedStoreKey, StorageError> {
68        coven_foundation::store_dir::validate_path_token(&value.author_pubkey)?;
69        coven_foundation::store_dir::validate_path_token(recipient)?;
70        if value.generation == 0 {
71            return Err(StorageError::InvalidContent(
72                "wrapped Store-key generation must be positive".to_string(),
73            ));
74        }
75        let bytes = serde_json::to_vec(&value).map_err(StorageError::Json)?;
76        let wrap_hash = coven_protocol::store_commit::ObjectHash::digest(&bytes);
77        let semantic_prefix = format!(
78            "keys/{}/{}/{}/{}",
79            value.author_pubkey, recipient, value.generation, wrap_hash
80        );
81        let context = ProtocolObjectContext::recipient_sealed(
82            self.root.store_root_hash,
83            ProtocolObjectDomain::StoreWrappedKey,
84        );
85        let slot = self
86            .storage
87            .allocate_protocol_slot(&context, &semantic_prefix, ".json")
88            .await?;
89        let object =
90            self.storage
91                .prepare_protocol_object(&context, slot, &semantic_prefix, bytes)?;
92        let reference = WrappedStoreKeyRef {
93            owner_pubkey: value.author_pubkey,
94            recipient_pubkey: recipient.to_string(),
95            generation: value.generation,
96            wrap_hash,
97            object: object.reference().clone(),
98        };
99        let prepared = PreparedWrappedStoreKey { reference, object };
100        prepared.validate()?;
101        Ok(prepared)
102    }
103
104    async fn open_references(
105        &self,
106        identity: &dyn IdentityKeyAuthority,
107        references: &[WrappedStoreKeyRef],
108    ) -> Result<EncryptionService, MembershipMutationError> {
109        let recipient = hex::encode(identity.public_key());
110        let root = &self.root;
111        let store_id = root.store_root_id.to_string();
112        let mut merged: Option<EncryptionService> = None;
113        for reference in references {
114            if reference.recipient_pubkey != recipient {
115                return Err(MembershipMutationError::Crypto(
116                    "activated wrapped-key ref names another recipient".to_string(),
117                ));
118            }
119            let wrapped =
120                load_wrapped_store_key(self.storage, root.store_root_hash, reference).await?;
121            let keyring = wrapped
122                .verify_and_open_keyring(
123                    &store_id,
124                    &recipient,
125                    std::iter::once(reference.owner_pubkey.as_str()),
126                    reference.generation,
127                    identity,
128                )
129                .map_err(MembershipMutationError::WrappedKeyring)?;
130            merged = Some(match merged {
131                Some(existing) => existing
132                    .merged_with(&keyring)
133                    .map_err(MembershipMutationError::Encryption)?,
134                None => keyring,
135            });
136        }
137        merged.ok_or_else(|| {
138            MembershipMutationError::Bucket(coven_protocol::objects::StorageError::NotFound(
139                format!("no activated wrapped Store-key ref for {recipient}"),
140            ))
141        })
142    }
143}
144
145fn wrapped_key_references(
146    identity: &dyn IdentityKeyAuthority,
147    membership: &MembershipChain,
148) -> Result<Vec<WrappedStoreKeyRef>, MembershipMutationError> {
149    membership
150        .wrapped_key_authority_for(&hex::encode(identity.public_key()))
151        .map_err(MembershipMutationError::from)
152}
153
154/// Read and validate one wrapped Store key through the exact reference the
155/// membership names.
156pub(crate) async fn load_wrapped_store_key(
157    storage: &dyn coven_storage::CloudSyncObjectStorage,
158    store_root_hash: coven_protocol::store_commit::ObjectHash,
159    reference: &WrappedStoreKeyRef,
160) -> Result<WrappedStoreKey, coven_protocol::objects::StorageError> {
161    let context = coven_protocol::objects::ProtocolObjectContext::recipient_sealed(
162        store_root_hash,
163        coven_protocol::objects::ProtocolObjectDomain::StoreWrappedKey,
164    );
165    let bytes = storage
166        .read_protocol_object(&context, &reference.object, &reference.semantic_prefix())
167        .await?;
168    let value: WrappedStoreKey =
169        serde_json::from_slice(&bytes).map_err(coven_protocol::objects::StorageError::Json)?;
170    reference.validate_value(&value, &bytes)?;
171    Ok(value)
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::sync::test_helpers::{test_cloud_home, TestStore};
178    use coven_keys::keys::UserKeypair;
179    use coven_protocol::wrapped_store_key::WrappedStoreKey;
180    use coven_storage::CloudSyncObjectStorage as _;
181
182    #[tokio::test]
183    async fn distinct_wraps_at_one_generation_remain_distinct_exact_objects() {
184        let owner = UserKeypair::generate();
185        let recipient = UserKeypair::generate();
186        let recipient_pubkey = hex::encode(recipient.public_key());
187        let db_store_dir = crate::sync::test_helpers::test_store_dir();
188        let db = crate::sync::test_helpers::open_test_db(db_store_dir.clone());
189        let (store, cloud_storage) = TestStore::create_with_connection(
190            &db,
191            db_store_dir.clone(),
192            "wrapped-key-exact-objects",
193            owner.clone(),
194            test_cloud_home(),
195        )
196        .await
197        .expect("create exact wrapped-key Store");
198        let device = store
199            .bind_device_in(&db, db_store_dir.clone(), &owner)
200            .await
201            .expect("bind exact wrapped-key Store");
202        let store_id = store.root().store_root_id.to_string();
203        let first = device
204            .prepare_wrapped_key(
205                &recipient_pubkey,
206                WrappedStoreKey::signed(&store_id, &recipient_pubkey, 3, vec![1; 32], &owner),
207            )
208            .await
209            .expect("prepare first exact wrap");
210        let second = device
211            .prepare_wrapped_key(
212                &recipient_pubkey,
213                WrappedStoreKey::signed(&store_id, &recipient_pubkey, 3, vec![2; 32], &owner),
214            )
215            .await
216            .expect("prepare second exact wrap");
217
218        assert_ne!(first.reference, second.reference);
219        cloud_storage
220            .create_protocol_object(&first.object)
221            .await
222            .expect("create first exact wrap");
223        cloud_storage
224            .create_protocol_object(&second.object)
225            .await
226            .expect("create second exact wrap");
227        assert_eq!(
228            load_wrapped_store_key(
229                &*cloud_storage,
230                store.root().store_root_hash,
231                &first.reference,
232            )
233            .await
234            .expect("load first exact wrap"),
235            first.validate().expect("validate first prepared wrap"),
236        );
237        assert_eq!(
238            load_wrapped_store_key(
239                &*cloud_storage,
240                store.root().store_root_hash,
241                &second.reference,
242            )
243            .await
244            .expect("load second exact wrap"),
245            second.validate().expect("validate second prepared wrap"),
246        );
247    }
248
249    #[tokio::test]
250    async fn exact_wrap_ref_rejects_relocated_identity() {
251        let owner = UserKeypair::generate();
252        let recipient = UserKeypair::generate();
253        let recipient_pubkey = hex::encode(recipient.public_key());
254        let db_store_dir = crate::sync::test_helpers::test_store_dir();
255        let db = crate::sync::test_helpers::open_test_db(db_store_dir.clone());
256        let (store, cloud_storage) = TestStore::create_with_connection(
257            &db,
258            db_store_dir.clone(),
259            "wrapped-key-relocation",
260            owner.clone(),
261            test_cloud_home(),
262        )
263        .await
264        .expect("create relocation Store");
265        let device = store
266            .bind_device_in(&db, db_store_dir.clone(), &owner)
267            .await
268            .expect("bind relocation Store");
269        let prepared = device
270            .prepare_wrapped_key(
271                &recipient_pubkey,
272                WrappedStoreKey::signed(
273                    &store.root().store_root_id.to_string(),
274                    &recipient_pubkey,
275                    1,
276                    vec![3; 32],
277                    &owner,
278                ),
279            )
280            .await
281            .expect("prepare exact wrap");
282        cloud_storage
283            .create_protocol_object(&prepared.object)
284            .await
285            .expect("create exact wrap");
286        let mut relocated = prepared.reference;
287        relocated.recipient_pubkey = hex::encode(UserKeypair::generate().public_key());
288
289        assert!(
290            load_wrapped_store_key(&*cloud_storage, store.root().store_root_hash, &relocated)
291                .await
292                .is_err()
293        );
294    }
295}