coven_replication/sync/store/authorization/
keyring.rs1use 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
11pub 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
154pub(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}