coven/coven.rs
1//! This app's stores on this device: where they live, the keychain their
2//! entries are kept in, and the device's network; and the calls that create,
3//! list, locate, open and remove them (E1).
4
5use crate::{
6 BootstrapError, CovenBuilder, CovenError, DbError, FileError, SavedRestoreEntry,
7 StoreDeletionError, StoreDir, StoreId, StoreLockError,
8};
9use coven_crypto::custody::{KeyError, Keychain, StoreKeychain};
10use coven_foundation::files::{ReplacedBy, StoreDirAccess, StoreDirectories};
11use coven_foundation::network_watch::NetworkSource;
12use std::path::PathBuf;
13use std::sync::Arc;
14
15/// This app's stores on this device (E1): the app directory each has its own
16/// directory under, the OS keychain service their keys and secrets are kept
17/// under, and the device's network, which each open store watches. The app
18/// builds one at startup and holds it.
19#[derive(Clone)]
20pub struct Coven {
21 directories: StoreDirectories,
22 keychain: Arc<Keychain>,
23 network: NetworkSource,
24}
25
26/// A store on this device (E1).
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct StoreInfo {
29 /// The store's identity.
30 pub id: StoreId,
31 /// The current name from this device's committed database state. While
32 /// a replacement is unfinished, the name its new installation loaded.
33 pub name: String,
34 /// The work that must finish before the store opens, if any.
35 pub unfinished: Option<Unfinished>,
36}
37
38/// A closed store's sync status, as `Coven::closed_sync_status` reads it
39/// (E1): the state opening shows before sync starts, and what deleting the
40/// store would lose. A queued write or file waits to sync with `NotSyncing`,
41/// as sync isn't running; `unsent` counts them.
42#[derive(Clone, Debug, PartialEq)]
43pub struct ClosedSyncStatus {
44 /// The sync state and the last synced time.
45 pub sync: crate::SyncStatus,
46 /// The writes and files this device committed and has not uploaded.
47 pub unsent: crate::UnsentWork,
48}
49
50/// Work on a store that must finish before it opens (E1).
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum Unfinished {
53 /// A replacement of this device's installation, which the call it names
54 /// finishes, or `CovenBuilder::resume_replacement` (§19.3).
55 Replacement(ReplacedBy),
56 /// This device is deleting the store and leaving it, its removal
57 /// journaled or about to be: `CovenBuilder::resume_deletion` finishes it
58 /// (§11.6).
59 Deletion,
60}
61
62/// A store on this device as the listing found it (E1).
63#[derive(Debug)]
64pub enum ListedStore {
65 /// Its name and state read.
66 Readable(StoreInfo),
67 /// Its replacement record or name can't be read, for `reason`. The app
68 /// shows it as damaged and can offer `discard_store` for it.
69 Unreadable {
70 /// The store whose record or name can't be read.
71 id: StoreId,
72 /// Why it can't be read.
73 reason: CovenError,
74 },
75}
76
77impl ListedStore {
78 /// The store this entry lists.
79 pub fn id(&self) -> StoreId {
80 match self {
81 Self::Readable(info) => info.id,
82 Self::Unreadable { id, .. } => *id,
83 }
84 }
85}
86
87/// A failure listing the stores. A store that can't be read is not one: it
88/// is listed as `ListedStore::Unreadable`.
89#[derive(Debug, thiserror::Error)]
90pub enum StoreListError {
91 /// The directory listing or an identity settings read failed.
92 #[error(transparent)]
93 File(FileError),
94 /// The runtime is shutting down and dropped the listing's work (E15).
95 #[error(transparent)]
96 Shutdown(#[from] crate::task::Shutdown),
97 /// Listing the stores panicked on the blocking pool: a bug, which the
98 /// caller reports (E15).
99 #[error(transparent)]
100 Bug(#[from] crate::task::Bug),
101 /// Finishing the recorded deletion of `store` failed (E1). The store is
102 /// not listed, and the next listing finishes the deletion.
103 #[error("finishing the deletion of store {store} failed: {source}")]
104 Deletion {
105 /// The store being deleted.
106 store: StoreId,
107 /// Why its deletion stopped.
108 #[source]
109 source: StoreDeletionError,
110 },
111}
112
113impl From<FileError> for StoreListError {
114 fn from(error: FileError) -> Self {
115 match error {
116 FileError::Bug(bug) => Self::Bug(bug),
117 FileError::Shutdown(shutdown) => Self::Shutdown(shutdown),
118 error => Self::File(error),
119 }
120 }
121}
122
123impl crate::task::CarriesBug for StoreListError {
124 fn bug(&self) -> Option<&crate::task::Bug> {
125 match self {
126 Self::Bug(bug) => Some(bug),
127 Self::File(error) => error.bug(),
128 Self::Deletion { source, .. } => source.bug(),
129 Self::Shutdown(_) => None,
130 }
131 }
132}
133
134impl Coven {
135 /// The stores under `app_dir`, their keys and secrets in the OS keychain
136 /// under the service `keyring_service`, watching the platform's network.
137 /// The app creates `app_dir`; coven creates directories only inside it
138 /// and never opens a directory above it, which Android refuses. No key
139 /// is read here. A bug in work nobody waits for, in any store, goes to
140 /// `bugs`, the app's crash reporter (E15). An empty service name, or one
141 /// containing NUL, fails with `KeyError::InvalidServiceName`; an OS
142 /// keychain coven can't reach fails with its `KeyError`.
143 pub fn new(
144 app_dir: PathBuf,
145 keyring_service: &str,
146 bugs: crate::task::BugSinkRef,
147 ) -> Result<Self, KeyError> {
148 Ok(Self {
149 directories: StoreDirectories::new(app_dir, bugs),
150 keychain: Keychain::native(keyring_service)?,
151 network: NetworkSource::Platform,
152 })
153 }
154
155 /// Read the saved storage location without opening the store or its custody.
156 /// None means storage has never been set up; once set up, the location stays.
157 pub async fn storage_config(
158 store_dir: &StoreDir,
159 ) -> Result<Option<crate::StorageConfig>, crate::SyncError> {
160 let store_dir = store_dir.clone();
161 let bugs = store_dir.bugs().clone();
162 crate::task::blocking_completing("read the storage settings", bugs, move || {
163 coven_storage::recorded_config(&store_dir)
164 })
165 .await?
166 .map_err(crate::SyncError::from)
167 }
168
169 /// A closed store's sync state, last synced time and unsent work, read
170 /// without opening it (E1, E5): the state opening shows before sync
171 /// starts. It holds the store's writer lock while it reads, since only
172 /// that lock shows that no handle runs the store's sync: an open handle,
173 /// here or in another process, refuses it with
174 /// `StoreLockError::AlreadyOpen` at once, and the app reads that handle.
175 /// An instance closing in this process is waited for first, and an open
176 /// of the store in this process meanwhile waits for this read. Whether
177 /// this device holds the location's credentials is read from the
178 /// keychain, as opening reads it. A store whose replacement or deletion
179 /// is unfinished is refused as opening refuses it, and one whose device
180 /// entry the keychain doesn't hold fails with `DeviceKeyMissing`.
181 pub async fn closed_sync_status(
182 &self,
183 store_dir: &StoreDir,
184 ) -> Result<ClosedSyncStatus, CovenError> {
185 let lock = store_dir.lock_closed().await?;
186 let location = {
187 let directory = store_dir.clone();
188 let keys = StoreKeychain::new(self.keychain.clone(), store_dir.id());
189 let bugs = store_dir.bugs().clone();
190 crate::task::blocking_completing("read the closed store's location", bugs, move || {
191 let store = directory.id();
192 crate::builder::refuse_unfinished_deletion(&directory, store)?;
193 // A copy whose keychain no longer names its device doesn't
194 // open, so it has no state to show (§19.3).
195 if keys.device_id()? != Some(directory.settings()?.device_id) {
196 return Err(CovenError::DeviceKeyMissing { store });
197 }
198 coven_sync::read_location(&directory, &keys).map_err(CovenError::from)
199 })
200 .await??
201 };
202 let (facts, unsent) = coven_database::closed_sync_facts(store_dir.clone(), lock).await?;
203 let sync = coven_sync::SyncStatus::closed(facts, location)?;
204 Ok(ClosedSyncStatus { sync, unsent })
205 }
206
207 /// Keep SQLite's temporary tables in `directory`, such as the app's cache
208 /// folder, for every store this process opens. An Android app calls this
209 /// before opening its first store: SQLite finds no folder there by itself,
210 /// and temporary tables that outgrow a connection's cache fail without
211 /// one. Elsewhere SQLite uses the system's.
212 pub fn set_temporary_directory(
213 directory: &std::path::Path,
214 ) -> Result<(), crate::TemporaryDirectoryError> {
215 coven_database::set_temporary_directory(directory)
216 }
217
218 /// Stores on this device, ordered by id, with their committed names.
219 /// A store whose replacement record or name can't be read is listed as
220 /// `Unreadable` with why; a listing never substitutes an older name.
221 /// A store whose replacement or deletion is unfinished lists with the
222 /// work that must finish, so the app can offer to finish it; an
223 /// unfinished first installation is not listed. First it finishes every
224 /// deletion from the device that a cut call left under the directory's
225 /// deletion name, which no call holds (§19.3, E1); a failure there fails
226 /// the listing with `StoreListError::Deletion`.
227 pub async fn stores(&self) -> Result<Vec<ListedStore>, StoreListError> {
228 for store in self.directories.recorded_deletions().await? {
229 let directory = self.store_dir(&store);
230 let keys = StoreKeychain::new(self.keychain.clone(), store);
231 match finish_deletion(&directory, keys).await {
232 Ok(()) => tracing::info!(%store, "finished a recorded deletion"),
233 // A call is deleting it now, and finishes it.
234 Err(StoreDeletionError::Lock(StoreLockError::AlreadyOpen(_))) => {
235 tracing::debug!(%store, "a call holds a recorded deletion")
236 }
237 Err(StoreDeletionError::Bug(bug)) => return Err(StoreListError::Bug(bug)),
238 Err(StoreDeletionError::Shutdown(shutdown)) => {
239 return Err(StoreListError::Shutdown(shutdown))
240 }
241 Err(source) => return Err(StoreListError::Deletion { store, source }),
242 }
243 }
244 let mut stores = Vec::new();
245 for id in self.directories.store_ids().await? {
246 stores.push(match self.read_listed(id).await {
247 Ok(info) => ListedStore::Readable(info),
248 Err(error) => {
249 if let Some(bug) = crate::task::CarriesBug::bug(&error) {
250 return Err(StoreListError::Bug(bug.clone()));
251 }
252 match error {
253 CovenError::Shutdown(shutdown)
254 | CovenError::Database(DbError::Shutdown(shutdown)) => {
255 return Err(StoreListError::Shutdown(shutdown));
256 }
257 reason => ListedStore::Unreadable { id, reason },
258 }
259 }
260 });
261 }
262 Ok(stores)
263 }
264
265 async fn read_listed(&self, id: StoreId) -> Result<StoreInfo, CovenError> {
266 let directory = self.store_dir(&id);
267 let (replacement, deletion) = {
268 let directory = directory.clone();
269 let bugs = directory.bugs().clone();
270 crate::task::blocking_completing("read the store's records", bugs, move || {
271 Ok::<_, CovenError>((
272 directory.unfinished_replacement()?,
273 directory.unfinished_deletion().map_err(DbError::from)?,
274 ))
275 })
276 .await??
277 };
278 Ok(match replacement {
279 Some(record) => StoreInfo {
280 id,
281 name: record.name,
282 unfinished: Some(Unfinished::Replacement(record.by)),
283 },
284 None => StoreInfo {
285 id,
286 name: coven_database::read_store_name(directory).await?,
287 unfinished: deletion.map(|_| Unfinished::Deletion),
288 },
289 })
290 }
291
292 /// Address a store without reading settings or acquiring the writer lock.
293 pub fn store_dir(&self, id: &StoreId) -> StoreDir {
294 self.directories.store_dir(id)
295 }
296
297 /// Configure creating, opening, restoring or joining a store.
298 pub fn builder(&self) -> CovenBuilder {
299 CovenBuilder::new(self.directories.clone(), self.keychain.clone(), self.network.clone())
300 }
301
302 /// Deletes a closed store from this device without opening it: this
303 /// device's own keychain entries for it, including every saved host
304 /// secret, then its directory. The person's saved restore code and S3 key
305 /// stay for their next devices; they go only when the
306 /// person leaves the store (§13.1). Unsent writes and files are lost, and
307 /// this device stays registered with the store until it is removed; prefer
308 /// `CovenHandle::delete_store`. Use this for a store that cannot open, or
309 /// to finish a handle's deletion that failed after closing. A store whose
310 /// `reset_device` or `restore_device` stopped past its record is deleted
311 /// whole: both installations, the replacement's directory and record,
312 /// and this device's entries, which by then may name either device.
313 /// Refused while a writer, read-only handle, file stream or a running
314 /// replacement remains; storage is untouched. A handle of it in this
315 /// process that is closing, or whose last clone was dropped, is waited
316 /// for. Its first step renames the store's directory, which hides the
317 /// store and records the deletion: one that stops part way, a crash
318 /// included, is finished by retrying, by the next `stores()`, or by a
319 /// restore or join of the store (§19.3).
320 pub async fn discard_store(&self, store_dir: &StoreDir) -> Result<(), StoreDeletionError> {
321 let keys = StoreKeychain::new(self.keychain.clone(), store_dir.id());
322 finish_deletion(store_dir, keys).await
323 }
324
325 /// The saved entries of this app's keychain service, in iCloud Keychain on
326 /// Apple and in this phone's Block Store on Android, one per store,
327 /// ordered by store id; empty when none are saved. An entry coven can't
328 /// restore from is listed as `Unreadable`, never omitted, so the person
329 /// can forget it. A keychain read failure (the search, an entry's bytes
330 /// or an S3 key's presence) fails the whole call, as does a restore-code
331 /// account with no store id, `KeyError::RestoreCodeStoreId`. A keychain
332 /// with neither iCloud Keychain nor Block Store fails with
333 /// `BootstrapError::SecureStorage(KeyError::Unsupported)`. Pass a readable
334 /// entry to `restore_from_saved_code`, which reads its current code and key.
335 pub async fn saved_restore_codes(&self) -> Result<Vec<SavedRestoreEntry>, BootstrapError> {
336 let keychain = self.keychain.clone();
337 let bugs = self.directories.bugs().clone();
338 crate::task::blocking_completing("list saved restore codes", bugs, move || {
339 crate::saved_restore::list(keychain)
340 })
341 .await?
342 }
343
344 /// Delete the store's saved S3 key, then its restore code: from iCloud
345 /// Keychain, and so from every one of the person's Apple devices, or from
346 /// this phone's Block Store, leaving other phones' own (§13.1). Readable
347 /// and unreadable entries alike; every keychain group's copy goes. The
348 /// store, its storage and every device's copy stay; no store need be open
349 /// and no provider connected. Absent entries succeed; after a failure
350 /// part way the store is still listed, and calling again finishes. A
351 /// later join, restore, reset or member-key replacement saves the entry
352 /// again. A keychain with neither iCloud Keychain nor Block Store fails
353 /// with `BootstrapError::SecureStorage(KeyError::Unsupported)`.
354 pub async fn forget_saved_restore_code(&self, store: StoreId) -> Result<(), BootstrapError> {
355 let keychain = self.keychain.clone();
356 let bugs = self.directories.bugs().clone();
357 crate::task::blocking_completing("forget the saved restore code", bugs, move || {
358 crate::saved_restore::forget(keychain, store)
359 })
360 .await?
361 }
362}
363
364/// Delete the store at `directory` from this device, or finish a deletion a
365/// cut call left recorded, once no instance of it in this process is
366/// closing.
367pub(crate) async fn finish_deletion<E>(directory: &StoreDir, keys: StoreKeychain) -> Result<(), E>
368where
369 E: From<StoreLockError>
370 + From<FileError>
371 + From<KeyError>
372 + From<crate::task::TaskFailed>
373 + crate::task::CarriesBug
374 + Send
375 + 'static,
376{
377 directory.closed().await?;
378 let directory = directory.clone();
379 let bugs = directory.bugs().clone();
380 crate::task::blocking_completing("delete the store", bugs, move || delete(&directory, &keys))
381 .await?
382}
383
384/// Delete the store at `directory` from this device: rename its directory to
385/// its deletion name, which hides the store and records the deletion, then
386/// delete this device's entries for it, then the directory and its lock
387/// files (§19.3, E1). Each step finds the last one's work done, so a
388/// deletion stopped anywhere is finished by deleting again.
389pub(crate) fn delete<E>(directory: &StoreDir, keys: &StoreKeychain) -> Result<(), E>
390where
391 E: From<StoreLockError> + From<FileError> + From<KeyError>,
392{
393 let mut lock = directory.lock_for_deletion()?;
394 if let Some(lock) = &mut lock {
395 lock.unpublish()?;
396 }
397 keys.delete_device_entries()?;
398 if let Some(lock) = lock {
399 lock.remove_directory()?;
400 }
401 Ok(())
402}
403
404#[cfg(any(test, feature = "test-utils"))]
405#[path = "test_utils.rs"]
406mod test_utils;
407
408#[cfg(test)]
409#[path = "coven_tests.rs"]
410mod tests;