Skip to main content

coven/
store_security.rs

1use std::sync::Arc;
2
3use coven_foundation::config::Config;
4#[cfg(test)]
5use coven_foundation::config::HomeStorage;
6use coven_keys::encryption::{EncryptionService, MasterKeyring, SealError};
7use coven_keys::keys::{
8    CloudHomeCredentials, DeviceIdentityCustody, IdentityError, KeyError, MasterKeyCustody,
9    MasterKeyError, StoreKeys, UserKeypair,
10};
11use coven_storage::cloud::ExactCloudHome;
12use coven_storage::{BlobChunking, BlobPathScheme, CloudCipher, CloudSyncConnection};
13
14/// Whether the selected cloud-home storage needs an available master key.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum CloudHomeKeyState {
17    NotRequired,
18    Available,
19    Locked,
20}
21
22pub(crate) struct PreparedCloudHomeKey {
23    state: CloudHomeKeyState,
24    custody: Arc<dyn MasterKeyCustody>,
25    staged: std::sync::Mutex<Option<Arc<coven_keys::keys::StagedMasterKeyCustody>>>,
26}
27
28pub(crate) enum SyncKeyCustody {
29    Current,
30    Prepared(Arc<PreparedCloudHomeKey>),
31}
32
33impl PreparedCloudHomeKey {
34    pub(crate) fn state(&self) -> CloudHomeKeyState {
35        self.state
36    }
37
38    pub(crate) fn commit(&self) -> Result<(), KeyError> {
39        match &*self.staged.lock().expect("lock prepared master key") {
40            Some(staged) => staged.commit(),
41            None => Ok(()),
42        }
43    }
44
45    pub(crate) fn rollback(&self) -> Result<(), KeyError> {
46        match &*self.staged.lock().expect("lock prepared master key") {
47            Some(staged) => staged.rollback(),
48            None => Ok(()),
49        }
50    }
51
52    pub(crate) fn finish(&self) {
53        self.staged.lock().expect("lock prepared master key").take();
54    }
55}
56
57impl MasterKeyCustody for PreparedCloudHomeKey {
58    fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
59        self.custody.unlock()
60    }
61
62    fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
63        self.custody.persist(keyring)
64    }
65
66    fn forget(&self) -> Result<(), KeyError> {
67        self.custody.forget()
68    }
69}
70
71impl Drop for PreparedCloudHomeKey {
72    fn drop(&mut self) {
73        if let Some(staged) = self
74            .staged
75            .get_mut()
76            .expect("lock prepared master key")
77            .take()
78        {
79            if let Err(error) = staged.rollback() {
80                tracing::error!("failed to roll back uncompleted cloud-home master key: {error}");
81            }
82        }
83    }
84}
85
86#[derive(Clone)]
87pub(crate) struct StoreSecurity {
88    keys: StoreKeys,
89    master_keys: Arc<dyn MasterKeyCustody>,
90    identity: Arc<dyn DeviceIdentityCustody>,
91    store_dir: coven_foundation::store_dir::StoreDir,
92}
93
94impl StoreSecurity {
95    pub(crate) fn new(
96        keys: StoreKeys,
97        master_keys: Arc<dyn MasterKeyCustody>,
98        identity: Arc<dyn DeviceIdentityCustody>,
99        store_dir: coven_foundation::store_dir::StoreDir,
100    ) -> Self {
101        Self {
102            keys,
103            master_keys,
104            identity,
105            store_dir,
106        }
107    }
108
109    pub(crate) async fn prepare_sync_components(
110        &self,
111        database: coven_database::StoreDatabase,
112        storage: Arc<CloudSyncConnection>,
113        initialization: coven_replication::sync::cycle::StoreInitialization,
114        key_custody: SyncKeyCustody,
115    ) -> Result<coven_replication::sync::cycle::PreparedSyncComponents, crate::store_sync::SyncError>
116    {
117        let master_keys: Arc<dyn MasterKeyCustody> = match key_custody {
118            SyncKeyCustody::Current => self.master_keys.clone(),
119            SyncKeyCustody::Prepared(master_keys) => master_keys,
120        };
121        let routing_encryption = routing_encryption(&storage, master_keys.as_ref())?;
122        coven_replication::sync::cycle::PreparedSyncComponents::prepare(
123            database,
124            self.store_dir.clone(),
125            storage,
126            self.required_identity()?,
127            initialization,
128            routing_encryption,
129            master_keys,
130        )
131        .await
132        .map_err(crate::store_sync::SyncError::from)
133    }
134
135    pub(crate) async fn load_store(
136        &self,
137        database: coven_database::StoreDatabase,
138        storage: Arc<CloudSyncConnection>,
139    ) -> Result<coven_replication::sync::Store, crate::store_sync::SyncError> {
140        let routing_encryption = routing_encryption(&storage, self.master_keys.as_ref())?;
141        coven_replication::sync::Store::load(
142            database,
143            storage,
144            self.store_dir.clone(),
145            self.required_identity()?,
146            routing_encryption,
147        )
148        .await
149        .map_err(crate::store_sync::SyncError::from)
150    }
151
152    pub(crate) async fn export_activated_device_continuation(
153        &self,
154        database: &coven_database::StoreDatabase,
155    ) -> Result<coven_protocol::recovery::ActivatedContinuation, crate::store_sync::SyncError> {
156        let identity = self.required_identity()?;
157        Ok(database
158            .export_activated_device_continuation(&identity)
159            .await?)
160    }
161
162    pub(crate) fn import_master_key(&self, serialized: &str) -> Result<(), MasterKeyError> {
163        let keyring = MasterKeyring::from_serialized(serialized)?;
164        self.master_keys.persist(&keyring)?;
165        Ok(())
166    }
167
168    pub(crate) fn cloud_home_key_state(
169        &self,
170        storage: coven_foundation::config::HomeStorage,
171    ) -> Result<CloudHomeKeyState, KeyError> {
172        if storage.is_browsable() {
173            return Ok(CloudHomeKeyState::NotRequired);
174        }
175        Ok(match self.master_keys.unlock()? {
176            Some(_) => CloudHomeKeyState::Available,
177            None => CloudHomeKeyState::Locked,
178        })
179    }
180
181    pub(crate) fn forget_master_key(&self) -> Result<(), KeyError> {
182        self.master_keys.forget()
183    }
184
185    pub(crate) fn prepare_cloud_home_key(
186        &self,
187        storage: coven_foundation::config::HomeStorage,
188    ) -> Result<Arc<PreparedCloudHomeKey>, MasterKeyError> {
189        if storage.is_browsable() {
190            return Ok(Arc::new(PreparedCloudHomeKey {
191                state: CloudHomeKeyState::NotRequired,
192                custody: self.master_keys.clone(),
193                staged: std::sync::Mutex::new(None),
194            }));
195        }
196        if self.master_keys.unlock()?.is_some() {
197            return Ok(Arc::new(PreparedCloudHomeKey {
198                state: CloudHomeKeyState::Available,
199                custody: self.master_keys.clone(),
200                staged: std::sync::Mutex::new(None),
201            }));
202        }
203        let staged = coven_keys::keys::StagedMasterKeyCustody::new(
204            self.master_keys.clone(),
205            MasterKeyring::generate(),
206        )?;
207        Ok(Arc::new(PreparedCloudHomeKey {
208            state: CloudHomeKeyState::Available,
209            custody: staged.clone(),
210            staged: std::sync::Mutex::new(Some(staged)),
211        }))
212    }
213
214    pub(crate) fn prepare_imported_cloud_home_key(
215        &self,
216        serialized: &str,
217    ) -> Result<Arc<PreparedCloudHomeKey>, MasterKeyError> {
218        let keyring = MasterKeyring::from_serialized(serialized)?;
219        let staged =
220            coven_keys::keys::StagedMasterKeyCustody::new(self.master_keys.clone(), keyring)?;
221        Ok(Arc::new(PreparedCloudHomeKey {
222            state: CloudHomeKeyState::Available,
223            custody: staged.clone(),
224            staged: std::sync::Mutex::new(Some(staged)),
225        }))
226    }
227
228    pub(crate) fn initialize_identity(&self) -> Result<String, IdentityError> {
229        if self.identity.unlock()?.is_some() {
230            return Err(IdentityError::AlreadyEstablished);
231        }
232        let identity = UserKeypair::generate();
233        self.identity.persist(&identity)?;
234        Ok(coven_keys::keys::public_key_hex(&identity))
235    }
236
237    fn required_identity(&self) -> Result<UserKeypair, KeyError> {
238        coven_keys::keys::require_identity(self.identity.as_ref())
239    }
240
241    pub(crate) fn required_identity_public_key_hex(&self) -> Result<String, KeyError> {
242        Ok(coven_keys::keys::public_key_hex(&self.required_identity()?))
243    }
244
245    pub(crate) fn set_host_secret(&self, name: &str, value: &str) -> Result<(), KeyError> {
246        self.keys.set_host_secret(name, value)
247    }
248
249    pub(crate) fn host_secret(&self, name: &str) -> Result<Option<String>, KeyError> {
250        self.keys.get_host_secret(name)
251    }
252
253    pub(crate) fn delete_host_secret(&self, name: &str) -> Result<(), KeyError> {
254        self.keys.delete_host_secret(name)
255    }
256
257    pub(crate) fn seal_app_data(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
258        let keyring = self.master_keys.unlock()?.ok_or(SealError::Locked)?;
259        Ok(EncryptionService::from(keyring).seal_app_data(plaintext, aad))
260    }
261
262    pub(crate) fn open_app_data(&self, sealed: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
263        let keyring = self.master_keys.unlock()?.ok_or(SealError::Locked)?;
264        EncryptionService::from(keyring).open_app_data(sealed, aad)
265    }
266
267    pub(crate) fn open_cloud_storage(
268        &self,
269        config: &Config,
270        home: Arc<dyn ExactCloudHome>,
271        cipher: Option<CloudCipher>,
272        blob_chunking: BlobChunking,
273    ) -> Result<CloudSyncConnection, coven_storage::cloud::setup::StorageSetupError> {
274        self.open_cloud_storage_with_master_keys(
275            config,
276            home,
277            cipher,
278            blob_chunking,
279            self.master_keys.clone(),
280        )
281    }
282
283    pub(crate) fn open_cloud_storage_with_master_keys(
284        &self,
285        config: &Config,
286        home: Arc<dyn ExactCloudHome>,
287        cipher: Option<CloudCipher>,
288        blob_chunking: BlobChunking,
289        master_keys: Arc<dyn MasterKeyCustody>,
290    ) -> Result<CloudSyncConnection, coven_storage::cloud::setup::StorageSetupError> {
291        let cipher = match cipher {
292            Some(cipher) => cipher,
293            None if config.cloud_home.storage.is_browsable() => CloudCipher::Plaintext,
294            None => {
295                let keyring = master_keys
296                    .unlock()?
297                    .ok_or(coven_storage::cloud::setup::StorageSetupError::NoEncryptionKey)?;
298                CloudCipher::Encrypted(keyring.into())
299            }
300        };
301        let identity = self.required_identity()?;
302        Ok(CloudSyncConnection::new(
303            home,
304            cipher,
305            BlobPathScheme::for_storage(config.cloud_home.storage),
306            config.store_id.clone(),
307            identity,
308        )
309        .with_blob_chunking(blob_chunking))
310    }
311
312    #[cfg(test)]
313    pub(crate) fn cloud_cipher_fingerprint_for_test(
314        &self,
315        storage: HomeStorage,
316    ) -> Result<Option<String>, coven_storage::cloud::setup::StorageSetupError> {
317        if storage.is_browsable() {
318            return Ok(None);
319        }
320        let keyring = self
321            .master_keys
322            .unlock()?
323            .ok_or(coven_storage::cloud::setup::StorageSetupError::NoEncryptionKey)?;
324        Ok(Some(EncryptionService::from(keyring).fingerprint()))
325    }
326
327    pub(crate) fn generate_restore_code(
328        &self,
329        config: &Config,
330        store_root: coven_protocol::store_commit::StoreRootRef,
331        founder_pubkey: String,
332        membership_floor: coven_protocol::membership::MembershipFloor,
333        authority: coven_protocol::recovery::RestoreAuthority,
334    ) -> Result<String, coven_storage::cloud::setup::SetupError> {
335        use coven_domain::restoration::{encode_restore_code, RestoreCode, RESTORE_CODE_VERSION};
336        use coven_storage::cloud::CloudHomeJoinInfo;
337
338        let cloud_provider = config.cloud_home.provider.as_ref().ok_or_else(|| {
339            coven_storage::cloud::setup::SetupError::Configuration(
340                "No cloud provider configured. Set up sync first.".to_string(),
341            )
342        })?;
343        let encryption_key = if config.cloud_home.storage.is_opaque() {
344            Some(
345                self.master_keys
346                    .unlock()?
347                    .ok_or_else(|| {
348                        coven_storage::cloud::setup::SetupError::Configuration(
349                            "No encryption key found".to_string(),
350                        )
351                    })?
352                    .to_serialized(),
353            )
354        } else {
355            None
356        };
357
358        let provider = match cloud_provider {
359            coven_foundation::config::CloudProvider::S3 => {
360                let credentials = self.keys.get_cloud_home_credentials()?.ok_or_else(|| {
361                    coven_storage::cloud::setup::SetupError::Configuration(
362                        "No S3 credentials found in keyring".to_string(),
363                    )
364                })?;
365                let (access_key, secret_key) = match credentials {
366                    CloudHomeCredentials::S3 {
367                        access_key,
368                        secret_key,
369                    } => (access_key, secret_key),
370                    _ => {
371                        return Err(coven_storage::cloud::setup::SetupError::Configuration(
372                            "Expected S3 credentials but found different type".to_string(),
373                        ))
374                    }
375                };
376                CloudHomeJoinInfo::S3 {
377                    bucket: config.cloud_home.s3_bucket.clone().ok_or_else(|| {
378                        coven_storage::cloud::setup::SetupError::Configuration(
379                            "S3 bucket not configured".to_string(),
380                        )
381                    })?,
382                    region: config.cloud_home.s3_region.clone().ok_or_else(|| {
383                        coven_storage::cloud::setup::SetupError::Configuration(
384                            "S3 region not configured".to_string(),
385                        )
386                    })?,
387                    endpoint: config.cloud_home.s3_endpoint.clone(),
388                    key_prefix: config.cloud_home.s3_key_prefix.clone(),
389                    access_key,
390                    secret_key,
391                }
392            }
393            coven_foundation::config::CloudProvider::CloudKit => {
394                if config.cloud_home.cloudkit_owner_name.is_some()
395                    || config.cloud_home.cloudkit_zone_name.is_some()
396                {
397                    return Err(coven_storage::cloud::setup::SetupError::Configuration(
398                        "This store was joined through a CloudKit share; only the store's owner can create a restore code.".to_string(),
399                    ));
400                }
401                CloudHomeJoinInfo::CloudKit
402            }
403            coven_foundation::config::CloudProvider::GoogleDrive => {
404                CloudHomeJoinInfo::GoogleDrive {
405                    folder_id: config
406                        .cloud_home
407                        .google_drive_folder_id
408                        .clone()
409                        .ok_or_else(|| {
410                            coven_storage::cloud::setup::SetupError::Configuration(
411                                "Google Drive folder ID not configured".to_string(),
412                            )
413                        })?,
414                }
415            }
416            coven_foundation::config::CloudProvider::Dropbox => {
417                CloudHomeJoinInfo::Dropbox {
418                    folder_path: config.cloud_home.dropbox_folder_path.clone().ok_or_else(
419                        || {
420                            coven_storage::cloud::setup::SetupError::Configuration(
421                                "Dropbox folder path not configured".to_string(),
422                            )
423                        },
424                    )?,
425                }
426            }
427            coven_foundation::config::CloudProvider::OneDrive => CloudHomeJoinInfo::OneDrive {
428                drive_id: config.cloud_home.onedrive_drive_id.clone().ok_or_else(|| {
429                    coven_storage::cloud::setup::SetupError::Configuration(
430                        "OneDrive drive ID not configured".to_string(),
431                    )
432                })?,
433                folder_id: config
434                    .cloud_home
435                    .onedrive_folder_id
436                    .clone()
437                    .ok_or_else(|| {
438                        coven_storage::cloud::setup::SetupError::Configuration(
439                            "OneDrive folder ID not configured".to_string(),
440                        )
441                    })?,
442            },
443        };
444
445        Ok(encode_restore_code(&RestoreCode {
446            v: RESTORE_CODE_VERSION,
447            sid: config.store_id.clone(),
448            ek: encryption_key,
449            name: config.store_name.clone(),
450            provider,
451            store_root,
452            founder_pubkey,
453            membership_floor,
454            authority,
455        }))
456    }
457}
458
459fn routing_encryption(
460    storage: &CloudSyncConnection,
461    master_keys: &dyn MasterKeyCustody,
462) -> Result<Option<EncryptionService>, crate::store_sync::SyncError> {
463    if storage.is_plaintext() {
464        return Ok(None);
465    }
466    let keyring = master_keys
467        .unlock()?
468        .ok_or(coven_keys::keys::RoutingEncryptionError::NotEstablished)?;
469    Ok(Some(EncryptionService::from(keyring)))
470}
471
472#[cfg(test)]
473#[path = "store_security_tests.rs"]
474mod tests;