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 = 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;