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 = if storage.is_plaintext() {
122            None
123        } else {
124            let keyring = master_keys
125                .unlock()?
126                .ok_or(coven_keys::keys::RoutingEncryptionError::NotEstablished)?;
127            Some(EncryptionService::from(keyring))
128        };
129        coven_replication::sync::cycle::PreparedSyncComponents::prepare(
130            database,
131            self.store_dir.clone(),
132            storage,
133            self.required_identity()?,
134            initialization,
135            routing_encryption,
136            master_keys,
137        )
138        .await
139        .map_err(crate::store_sync::SyncError::from)
140    }
141
142    pub(crate) async fn load_store(
143        &self,
144        database: coven_database::StoreDatabase,
145        storage: Arc<dyn coven_storage::CloudSyncObjectStorage>,
146    ) -> Result<coven_replication::sync::Store, crate::store_sync::SyncError> {
147        coven_replication::sync::Store::load(
148            database,
149            storage,
150            self.store_dir.clone(),
151            self.required_identity()?,
152        )
153        .await
154        .map_err(crate::store_sync::SyncError::from)
155    }
156
157    pub(crate) async fn export_activated_device_continuation(
158        &self,
159        database: &coven_database::StoreDatabase,
160    ) -> Result<coven_protocol::recovery::ActivatedContinuation, crate::store_sync::SyncError> {
161        let identity = self.required_identity()?;
162        Ok(database
163            .export_activated_device_continuation(&identity)
164            .await?)
165    }
166
167    pub(crate) fn import_master_key(&self, serialized: &str) -> Result<(), MasterKeyError> {
168        let keyring = MasterKeyring::from_serialized(serialized)?;
169        self.master_keys.persist(&keyring)?;
170        Ok(())
171    }
172
173    pub(crate) fn cloud_home_key_state(
174        &self,
175        storage: coven_foundation::config::HomeStorage,
176    ) -> Result<CloudHomeKeyState, KeyError> {
177        if storage.is_browsable() {
178            return Ok(CloudHomeKeyState::NotRequired);
179        }
180        Ok(match self.master_keys.unlock()? {
181            Some(_) => CloudHomeKeyState::Available,
182            None => CloudHomeKeyState::Locked,
183        })
184    }
185
186    pub(crate) fn forget_master_key(&self) -> Result<(), KeyError> {
187        self.master_keys.forget()
188    }
189
190    pub(crate) fn prepare_cloud_home_key(
191        &self,
192        storage: coven_foundation::config::HomeStorage,
193    ) -> Result<Arc<PreparedCloudHomeKey>, MasterKeyError> {
194        if storage.is_browsable() {
195            return Ok(Arc::new(PreparedCloudHomeKey {
196                state: CloudHomeKeyState::NotRequired,
197                custody: self.master_keys.clone(),
198                staged: std::sync::Mutex::new(None),
199            }));
200        }
201        if self.master_keys.unlock()?.is_some() {
202            return Ok(Arc::new(PreparedCloudHomeKey {
203                state: CloudHomeKeyState::Available,
204                custody: self.master_keys.clone(),
205                staged: std::sync::Mutex::new(None),
206            }));
207        }
208        let staged = coven_keys::keys::StagedMasterKeyCustody::new(
209            self.master_keys.clone(),
210            MasterKeyring::generate(),
211        )?;
212        Ok(Arc::new(PreparedCloudHomeKey {
213            state: CloudHomeKeyState::Available,
214            custody: staged.clone(),
215            staged: std::sync::Mutex::new(Some(staged)),
216        }))
217    }
218
219    pub(crate) fn prepare_imported_cloud_home_key(
220        &self,
221        serialized: &str,
222    ) -> Result<Arc<PreparedCloudHomeKey>, MasterKeyError> {
223        let keyring = MasterKeyring::from_serialized(serialized)?;
224        let staged =
225            coven_keys::keys::StagedMasterKeyCustody::new(self.master_keys.clone(), keyring)?;
226        Ok(Arc::new(PreparedCloudHomeKey {
227            state: CloudHomeKeyState::Available,
228            custody: staged.clone(),
229            staged: std::sync::Mutex::new(Some(staged)),
230        }))
231    }
232
233    pub(crate) fn initialize_identity(&self) -> Result<String, IdentityError> {
234        if self.identity.unlock()?.is_some() {
235            return Err(IdentityError::AlreadyEstablished);
236        }
237        let identity = UserKeypair::generate();
238        self.identity.persist(&identity)?;
239        Ok(coven_keys::keys::public_key_hex(&identity))
240    }
241
242    fn required_identity(&self) -> Result<UserKeypair, KeyError> {
243        coven_keys::keys::require_identity(self.identity.as_ref())
244    }
245
246    pub(crate) fn required_identity_public_key_hex(&self) -> Result<String, KeyError> {
247        Ok(coven_keys::keys::public_key_hex(&self.required_identity()?))
248    }
249
250    pub(crate) fn identity_public_key(&self) -> Result<Option<[u8; 32]>, KeyError> {
251        Ok(self
252            .identity
253            .unlock()?
254            .map(|identity| identity.public_key()))
255    }
256
257    pub(crate) fn set_host_secret(&self, name: &str, value: &str) -> Result<(), KeyError> {
258        self.keys.set_host_secret(name, value)
259    }
260
261    pub(crate) fn host_secret(&self, name: &str) -> Result<Option<String>, KeyError> {
262        self.keys.get_host_secret(name)
263    }
264
265    pub(crate) fn delete_host_secret(&self, name: &str) -> Result<(), KeyError> {
266        self.keys.delete_host_secret(name)
267    }
268
269    pub(crate) fn seal_app_data(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
270        let keyring = self.master_keys.unlock()?.ok_or(SealError::Locked)?;
271        Ok(EncryptionService::from(keyring).seal_app_data(plaintext, aad))
272    }
273
274    pub(crate) fn open_app_data(&self, sealed: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
275        let keyring = self.master_keys.unlock()?.ok_or(SealError::Locked)?;
276        EncryptionService::from(keyring).open_app_data(sealed, aad)
277    }
278
279    pub(crate) fn open_cloud_storage(
280        &self,
281        config: &Config,
282        home: Arc<dyn ExactCloudHome>,
283        cipher: Option<CloudCipher>,
284        blob_chunking: BlobChunking,
285    ) -> Result<CloudSyncConnection, coven_storage::cloud::setup::StorageSetupError> {
286        self.open_cloud_storage_with_master_keys(
287            config,
288            home,
289            cipher,
290            blob_chunking,
291            self.master_keys.clone(),
292        )
293    }
294
295    pub(crate) fn open_cloud_storage_with_master_keys(
296        &self,
297        config: &Config,
298        home: Arc<dyn ExactCloudHome>,
299        cipher: Option<CloudCipher>,
300        blob_chunking: BlobChunking,
301        master_keys: Arc<dyn MasterKeyCustody>,
302    ) -> Result<CloudSyncConnection, coven_storage::cloud::setup::StorageSetupError> {
303        let cipher = match cipher {
304            Some(cipher) => cipher,
305            None if config.cloud_home.storage.is_browsable() => CloudCipher::Plaintext,
306            None => {
307                let keyring = master_keys
308                    .unlock()?
309                    .ok_or(coven_storage::cloud::setup::StorageSetupError::NoEncryptionKey)?;
310                CloudCipher::Encrypted(keyring.into())
311            }
312        };
313        let identity = self.required_identity()?;
314        Ok(CloudSyncConnection::new(
315            home,
316            cipher,
317            BlobPathScheme::for_storage(config.cloud_home.storage),
318            config.store_id.clone(),
319            identity,
320        )
321        .with_blob_chunking(blob_chunking))
322    }
323
324    #[cfg(test)]
325    pub(crate) fn cloud_cipher_fingerprint_for_test(
326        &self,
327        storage: HomeStorage,
328    ) -> Result<Option<String>, coven_storage::cloud::setup::StorageSetupError> {
329        if storage.is_browsable() {
330            return Ok(None);
331        }
332        let keyring = self
333            .master_keys
334            .unlock()?
335            .ok_or(coven_storage::cloud::setup::StorageSetupError::NoEncryptionKey)?;
336        Ok(Some(EncryptionService::from(keyring).fingerprint()))
337    }
338
339    pub(crate) fn generate_restore_code(
340        &self,
341        config: &Config,
342        store_root: coven_protocol::store_commit::StoreRootRef,
343        founder_pubkey: String,
344        membership_floor: coven_protocol::membership::MembershipFloor,
345        authority: coven_protocol::recovery::RestoreAuthority,
346    ) -> Result<String, coven_storage::cloud::setup::SetupError> {
347        use coven_domain::restoration::{encode_restore_code, RestoreCode, RESTORE_CODE_VERSION};
348        use coven_storage::cloud::CloudHomeJoinInfo;
349
350        let cloud_provider = config.cloud_home.provider.as_ref().ok_or_else(|| {
351            coven_storage::cloud::setup::SetupError::Configuration(
352                "No cloud provider configured. Set up sync first.".to_string(),
353            )
354        })?;
355        let encryption_key = if config.cloud_home.storage.is_opaque() {
356            Some(
357                self.master_keys
358                    .unlock()?
359                    .ok_or_else(|| {
360                        coven_storage::cloud::setup::SetupError::Configuration(
361                            "No encryption key found".to_string(),
362                        )
363                    })?
364                    .to_serialized(),
365            )
366        } else {
367            None
368        };
369
370        let provider = match cloud_provider {
371            coven_foundation::config::CloudProvider::S3 => {
372                let credentials = self.keys.get_cloud_home_credentials()?.ok_or_else(|| {
373                    coven_storage::cloud::setup::SetupError::Configuration(
374                        "No S3 credentials found in keyring".to_string(),
375                    )
376                })?;
377                let (access_key, secret_key) = match credentials {
378                    CloudHomeCredentials::S3 {
379                        access_key,
380                        secret_key,
381                    } => (access_key, secret_key),
382                    _ => {
383                        return Err(coven_storage::cloud::setup::SetupError::Configuration(
384                            "Expected S3 credentials but found different type".to_string(),
385                        ))
386                    }
387                };
388                CloudHomeJoinInfo::S3 {
389                    bucket: config.cloud_home.s3_bucket.clone().ok_or_else(|| {
390                        coven_storage::cloud::setup::SetupError::Configuration(
391                            "S3 bucket not configured".to_string(),
392                        )
393                    })?,
394                    region: config.cloud_home.s3_region.clone().ok_or_else(|| {
395                        coven_storage::cloud::setup::SetupError::Configuration(
396                            "S3 region not configured".to_string(),
397                        )
398                    })?,
399                    endpoint: config.cloud_home.s3_endpoint.clone(),
400                    key_prefix: config.cloud_home.s3_key_prefix.clone(),
401                    access_key,
402                    secret_key,
403                }
404            }
405            coven_foundation::config::CloudProvider::CloudKit => {
406                if config.cloud_home.cloudkit_owner_name.is_some()
407                    || config.cloud_home.cloudkit_zone_name.is_some()
408                {
409                    return Err(coven_storage::cloud::setup::SetupError::Configuration(
410                        "This store was joined through a CloudKit share; only the store's owner can create a restore code.".to_string(),
411                    ));
412                }
413                CloudHomeJoinInfo::CloudKit
414            }
415            coven_foundation::config::CloudProvider::GoogleDrive => {
416                CloudHomeJoinInfo::GoogleDrive {
417                    folder_id: config
418                        .cloud_home
419                        .google_drive_folder_id
420                        .clone()
421                        .ok_or_else(|| {
422                            coven_storage::cloud::setup::SetupError::Configuration(
423                                "Google Drive folder ID not configured".to_string(),
424                            )
425                        })?,
426                }
427            }
428            coven_foundation::config::CloudProvider::Dropbox => {
429                CloudHomeJoinInfo::Dropbox {
430                    folder_path: config.cloud_home.dropbox_folder_path.clone().ok_or_else(
431                        || {
432                            coven_storage::cloud::setup::SetupError::Configuration(
433                                "Dropbox folder path not configured".to_string(),
434                            )
435                        },
436                    )?,
437                }
438            }
439            coven_foundation::config::CloudProvider::OneDrive => CloudHomeJoinInfo::OneDrive {
440                drive_id: config.cloud_home.onedrive_drive_id.clone().ok_or_else(|| {
441                    coven_storage::cloud::setup::SetupError::Configuration(
442                        "OneDrive drive ID not configured".to_string(),
443                    )
444                })?,
445                folder_id: config
446                    .cloud_home
447                    .onedrive_folder_id
448                    .clone()
449                    .ok_or_else(|| {
450                        coven_storage::cloud::setup::SetupError::Configuration(
451                            "OneDrive folder ID not configured".to_string(),
452                        )
453                    })?,
454            },
455        };
456
457        Ok(encode_restore_code(&RestoreCode {
458            v: RESTORE_CODE_VERSION,
459            sid: config.store_id.clone(),
460            ek: encryption_key,
461            name: config.store_name.clone(),
462            provider,
463            store_root,
464            founder_pubkey,
465            membership_floor,
466            authority,
467        }))
468    }
469}
470
471#[cfg(test)]
472#[path = "store_security_tests.rs"]
473mod tests;