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#[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;