Skip to main content

coven/
saved_restore.rs

1//! Discover and forget saved recovery choices without exposing their private
2//! keys (§13.1).
3
4use crate::CloudProvider;
5use crate::{BootstrapError, StorageConfig, StoreId};
6use coven_crypto::custody::{Keychain, PersonEntries, PersonRestoreCode};
7use coven_sync::UnreadableSavedCode;
8use std::sync::Arc;
9
10/// A store whose restore code is saved in iCloud Keychain or Block Store,
11/// without secrets.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct SavedRestoreCode {
14    /// The store's name as one of the person's devices last saw it: a label
15    /// only, never used to find the store. A rename made while none of their
16    /// devices had the store open leaves it unchanged (§13.1).
17    pub name: String,
18    /// The store named by the saved code.
19    pub store_id: StoreId,
20    /// The code's provider and account or location, with no credentials.
21    /// `provider()` identifies the provider; `oauth_access()` names its sign-in requirement.
22    pub location: StorageConfig,
23    /// Whether a separate S3 key was present when this entry was listed.
24    pub has_s3_credentials: bool,
25}
26
27/// One store's saved entry, as listing found it (§13.1).
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum SavedRestoreEntry {
30    /// An entry the app can pass to `restore_from_saved_code`.
31    Readable(SavedRestoreCode),
32    /// An entry coven can't restore from. The app offers to forget it.
33    Unreadable {
34        /// The store its keychain account names.
35        store_id: StoreId,
36        /// Why it can't be restored from.
37        reason: UnreadableSavedCode,
38    },
39}
40
41impl SavedRestoreEntry {
42    /// The store this entry is saved under.
43    pub fn store_id(&self) -> StoreId {
44        match self {
45            Self::Readable(saved) => saved.store_id,
46            Self::Unreadable { store_id, .. } => *store_id,
47        }
48    }
49}
50
51pub(crate) fn list(keychain: Arc<Keychain>) -> Result<Vec<SavedRestoreEntry>, BootstrapError> {
52    keychain
53        .person_restore_codes()?
54        .into_iter()
55        .map(|(store, code)| entry(&PersonEntries::new(keychain.clone(), store), code))
56        .collect()
57}
58
59/// Read one store's listed entry. Only a keychain read fails it; an entry
60/// coven can't restore from is `Unreadable`.
61pub(crate) fn entry(
62    person: &PersonEntries,
63    code: PersonRestoreCode,
64) -> Result<SavedRestoreEntry, BootstrapError> {
65    let store_id = person.store();
66    let decoded = match code {
67        PersonRestoreCode::Saved(bytes) => coven_sync::read_saved_code(store_id, bytes.as_bytes()),
68        PersonRestoreCode::Ambiguous => Err(UnreadableSavedCode::Ambiguous),
69    };
70    let (saved, location) = match decoded {
71        Ok(decoded) => decoded,
72        Err(reason) => return Ok(SavedRestoreEntry::Unreadable { store_id, reason }),
73    };
74    let has_s3_credentials =
75        location.provider() == CloudProvider::S3 && person.s3_credentials()?.is_some();
76    Ok(SavedRestoreEntry::Readable(SavedRestoreCode {
77        name: saved.name,
78        store_id,
79        location,
80        has_s3_credentials,
81    }))
82}
83
84pub(crate) fn forget(keychain: Arc<Keychain>, store: StoreId) -> Result<(), BootstrapError> {
85    Ok(PersonEntries::new(keychain, store).forget()?)
86}
87
88#[cfg(test)]
89#[path = "saved_restore_tests.rs"]
90mod tests;