Skip to main content

coven_crypto/custody/
keychain_test_utils.rs

1use super::*;
2use crate::custody::block_store::BlockStore;
3use crate::custody::block_store_test_utils::MemoryBlockStore;
4use crate::custody::person_entries::restore_code_store;
5use crate::custody::KeychainError;
6use std::collections::BTreeMap;
7use std::path::Path;
8use zeroize::Zeroizing;
9
10/// The first bytes of a keychain's saved device-only entries: their magic
11/// and version.
12const SAVED_DEVICE_ENTRIES_MAGIC: &[u8] = b"CVDE\x01";
13
14/// The synced entries iCloud Keychain carries to every device of one person:
15/// each in-memory keychain of the person holds a share, which only a keychain
16/// makes, its first device's for itself and each other device's from one it
17/// already has.
18#[derive(Clone)]
19pub(super) struct SyncedEntries(Arc<Mutex<SyncedGroups>>);
20
21impl SyncedEntries {
22    fn groups(&self) -> std::sync::MutexGuard<'_, SyncedGroups> {
23        self.0.lock().expect("in-memory synced keychain entries lock is poisoned")
24    }
25}
26
27/// The keychain groups an app can read. Apple keeps one copy of an entry per
28/// group: a write naming no group adds to the default group, or, when that
29/// group already holds the entry, updates every group's copy; a read naming
30/// none gives the first copy, and a deletion naming none deletes them all.
31#[derive(Default)]
32pub(super) struct SyncedGroups {
33    default: BTreeMap<String, SecretBytes>,
34    another: BTreeMap<String, SecretBytes>,
35}
36
37impl SyncedGroups {
38    fn read(&self, account: &str) -> Option<SecretBytes> {
39        self.copies(account).into_iter().next()
40    }
41
42    fn write(&mut self, account: &str, bytes: &[u8]) {
43        if self.default.contains_key(account) {
44            if let Some(copy) = self.another.get_mut(account) {
45                *copy = SecretBytes::new(bytes.to_vec());
46            }
47        }
48        self.default.insert(account.to_owned(), SecretBytes::new(bytes.to_vec()));
49    }
50
51    fn delete(&mut self, account: &str) {
52        self.default.remove(account);
53        self.another.remove(account);
54    }
55
56    fn copies(&self, account: &str) -> Vec<SecretBytes> {
57        [&self.default, &self.another]
58            .into_iter()
59            .filter_map(|group| group.get(account))
60            .map(|bytes| SecretBytes::new(bytes.as_bytes().to_vec()))
61            .collect()
62    }
63
64    fn replace(&mut self, account: &str, bytes: &[u8]) {
65        let bytes = SecretBytes::new(bytes.to_vec());
66        if self.default.contains_key(account) || !self.another.contains_key(account) {
67            self.default.insert(account.to_owned(), bytes);
68            self.another.remove(account);
69        } else {
70            self.another.insert(account.to_owned(), bytes);
71        }
72    }
73
74    fn all(&self) -> Vec<(String, SecretBytes)> {
75        [&self.default, &self.another]
76            .into_iter()
77            .flatten()
78            .map(|(account, bytes)| (account.clone(), SecretBytes::new(bytes.as_bytes().to_vec())))
79            .collect()
80    }
81}
82
83/// Where an in-memory keychain keeps the person's entries.
84pub(super) enum MemoryPerson {
85    /// iCloud Keychain: shared with every keychain made by `another_device`.
86    ICloud(SyncedEntries),
87    /// Block Store: this phone's own, given to phones set up from it.
88    BlockStore(BlockStore<MemoryBlockStore>),
89}
90
91pub(super) struct MemoryEntries {
92    /// This device's own entries.
93    device: BTreeMap<String, SecretBytes>,
94    /// The person's entries.
95    person: MemoryPerson,
96    fail_after: std::collections::VecDeque<usize>,
97    /// Refuse this device's calls on the person's entries while set.
98    refuse_person: bool,
99    /// Panic in the next read, write, delete or list, as a bug in the
100    /// keychain's code would.
101    panic_next: bool,
102}
103
104/// Panic if a test asked the next read, write, delete or list to, before it
105/// changes anything and with the entries' lock released, so the entries
106/// stay usable after it.
107fn panic_if_asked(backend: &Mutex<MemoryEntries>) {
108    let asked = std::mem::take(
109        &mut backend.lock().expect("in-memory keychain entries lock is poisoned").panic_next,
110    );
111    if asked {
112        panic!("the in-memory keychain's operation panicked");
113    }
114}
115
116impl MemoryEntries {
117    fn check(&mut self, scope: EntryScope) -> Result<(), KeyError> {
118        let person = scope == EntryScope::Person && self.refuse_person;
119        if person || self.fail_after.front() == Some(&0) {
120            if !person {
121                self.fail_after.pop_front();
122            }
123            return Err(KeychainError::from(keyring_core::Error::NoStorageAccess(Box::new(
124                std::io::Error::new(
125                    std::io::ErrorKind::PermissionDenied,
126                    "in-memory keychain refused the operation",
127                ),
128            )))
129            .into());
130        }
131        if let Some(remaining) = self.fail_after.front_mut() {
132            *remaining -= 1;
133        }
134        Ok(())
135    }
136
137    /// Run `device` on this device's entries, or `person` on the person's.
138    fn change<T>(
139        &mut self,
140        scope: EntryScope,
141        device: impl FnOnce(&mut BTreeMap<String, SecretBytes>) -> T,
142        person: impl FnOnce(&mut MemoryPerson) -> Result<T, KeyError>,
143    ) -> Result<T, KeyError> {
144        self.check(scope)?;
145        match scope {
146            EntryScope::DeviceOnly => Ok(device(&mut self.device)),
147            EntryScope::Person => person(&mut self.person),
148        }
149    }
150
151    fn person<T>(
152        &mut self,
153        change: impl FnOnce(&mut MemoryPerson) -> Result<T, KeyError>,
154    ) -> Result<T, KeyError> {
155        self.check(EntryScope::Person)?;
156        change(&mut self.person)
157    }
158}
159
160impl KeychainBackend for Mutex<MemoryEntries> {
161    fn supports_person_entries(&self) -> bool {
162        true
163    }
164
165    fn person_backup_follows_writes(&self) -> bool {
166        matches!(
167            self.lock().expect("in-memory keychain entries lock is poisoned").person,
168            MemoryPerson::BlockStore(_)
169        )
170    }
171
172    fn person_value_limit(&self, service: &str, account: &str) -> Option<usize> {
173        match &self.lock().expect("in-memory keychain entries lock is poisoned").person {
174            MemoryPerson::ICloud(_) => None,
175            MemoryPerson::BlockStore(_) => {
176                Some(BlockStore::<MemoryBlockStore>::value_limit(service, account))
177            }
178        }
179    }
180
181    fn read(
182        &self,
183        scope: EntryScope,
184        service: &str,
185        account: &str,
186    ) -> Result<Option<SecretBytes>, KeyError> {
187        panic_if_asked(self);
188        self.lock().expect("in-memory keychain entries lock is poisoned").change(
189            scope,
190            |entries| entries.get(account).map(|bytes| SecretBytes::new(bytes.as_bytes().to_vec())),
191            |person| match person {
192                MemoryPerson::ICloud(entries) => Ok(entries.groups().read(account)),
193                MemoryPerson::BlockStore(store) => store.read(service, account),
194            },
195        )
196    }
197
198    fn write(
199        &self,
200        scope: EntryScope,
201        service: &str,
202        account: &str,
203        bytes: &[u8],
204    ) -> Result<(), KeyError> {
205        panic_if_asked(self);
206        self.lock().expect("in-memory keychain entries lock is poisoned").change(
207            scope,
208            |entries| {
209                entries.insert(account.to_owned(), SecretBytes::new(bytes.to_vec()));
210            },
211            |person| match person {
212                MemoryPerson::ICloud(entries) => {
213                    entries.groups().write(account, bytes);
214                    Ok(())
215                }
216                MemoryPerson::BlockStore(store) => store.write(service, account, bytes),
217            },
218        )
219    }
220
221    fn delete(&self, scope: EntryScope, service: &str, account: &str) -> Result<(), KeyError> {
222        panic_if_asked(self);
223        self.lock().expect("in-memory keychain entries lock is poisoned").change(
224            scope,
225            |entries| {
226                entries.remove(account);
227            },
228            |person| match person {
229                MemoryPerson::ICloud(entries) => {
230                    entries.groups().delete(account);
231                    Ok(())
232                }
233                MemoryPerson::BlockStore(store) => store.delete(service, account),
234            },
235        )
236    }
237
238    fn person_restore_codes(&self, service: &str) -> Result<Vec<(StoreId, SecretBytes)>, KeyError> {
239        panic_if_asked(self);
240        self.lock().expect("in-memory keychain entries lock is poisoned").person(|person| {
241            match person {
242                MemoryPerson::ICloud(entries) => {
243                    let mut codes = Vec::new();
244                    for (account, bytes) in entries.groups().all() {
245                        if let Some(store) = restore_code_store(&account)? {
246                            codes.push((store, bytes));
247                        }
248                    }
249                    Ok(codes)
250                }
251                MemoryPerson::BlockStore(store) => store.restore_codes(service),
252            }
253        })
254    }
255
256    fn person_copies(&self, service: &str, account: &str) -> Result<Vec<SecretBytes>, KeyError> {
257        panic_if_asked(self);
258        self.lock().expect("in-memory keychain entries lock is poisoned").person(|person| {
259            match person {
260                MemoryPerson::ICloud(entries) => Ok(entries.groups().copies(account)),
261                MemoryPerson::BlockStore(store) => {
262                    Ok(store.read(service, account)?.into_iter().collect())
263                }
264            }
265        })
266    }
267
268    fn replace_person_entry(
269        &self,
270        service: &str,
271        account: &str,
272        bytes: &[u8],
273    ) -> Result<(), KeyError> {
274        panic_if_asked(self);
275        self.lock().expect("in-memory keychain entries lock is poisoned").person(|person| {
276            match person {
277                MemoryPerson::ICloud(entries) => {
278                    entries.groups().replace(account, bytes);
279                    Ok(())
280                }
281                MemoryPerson::BlockStore(store) => store.write(service, account, bytes),
282            }
283        })
284    }
285}
286
287impl Keychain {
288    /// An isolated in-memory keychain with separate device-only entries and
289    /// person entries. The person's entries behave as iCloud Keychain keeps
290    /// them, on every test platform, without touching the OS.
291    pub fn in_memory(name: impl Into<String>) -> Result<Arc<Self>, KeyError> {
292        let entries = SyncedEntries(Arc::new(Mutex::new(SyncedGroups::default())));
293        Self::memory(name.into(), MemoryPerson::ICloud(entries))
294    }
295
296    /// An isolated in-memory keychain whose person entries behave as Block
297    /// Store keeps them on an Android phone: this phone's own, with Block
298    /// Store's limits, given only to a phone set up from it.
299    pub fn in_memory_block_store(name: impl Into<String>) -> Result<Arc<Self>, KeyError> {
300        Self::memory(
301            name.into(),
302            MemoryPerson::BlockStore(BlockStore::new(MemoryBlockStore::new())),
303        )
304    }
305
306    /// An isolated in-memory keychain, as `in_memory` makes, whose
307    /// device-only entries are the ones `save_device_entries` saved at
308    /// `path`: the same device again, in another process. Its person entries
309    /// start empty. Bytes `save_device_entries` didn't write fail with
310    /// `KeyError::Material`.
311    pub fn in_memory_with_saved_device_entries(
312        name: impl Into<String>,
313        path: &Path,
314    ) -> Result<Arc<Self>, KeyError> {
315        let bytes = Zeroizing::new(coven_foundation::files::read_existing(path)?);
316        let device = decode_device_entries(&bytes)?;
317        let keychain = Self::in_memory(name)?;
318        keychain.with_memory(|memory| memory.device = device);
319        Ok(keychain)
320    }
321
322    /// Save this keychain's device-only entries at `path`, replacing what is
323    /// there: each store's device id, store keys and member keys, and every
324    /// other entry that never leaves the device. The person's entries stay
325    /// out. Panics if this is a native keychain.
326    pub fn save_device_entries(&self, path: &Path) -> Result<(), KeyError> {
327        let bytes = self.with_memory(|memory| encode_device_entries(&memory.device));
328        Ok(coven_foundation::files::write_atomic(path, &bytes)?)
329    }
330
331    /// Another device of the same person: its own empty device-only entries.
332    /// With iCloud Keychain it has the same person entries, as iCloud
333    /// Keychain carries them between the person's devices; with Block Store
334    /// its own, empty. Failures injected into either keychain refuse only its
335    /// own calls. Panics if this is a native keychain.
336    pub fn another_device(&self) -> Arc<Self> {
337        let person = match self.synced_entries() {
338            Some(entries) => MemoryPerson::ICloud(entries),
339            None => MemoryPerson::BlockStore(BlockStore::new(MemoryBlockStore::new())),
340        };
341        Self::memory(self.name.clone(), person).expect("registered service name is valid")
342    }
343
344    /// A share of the person's iCloud Keychain entries, for another of the
345    /// person's devices; none with Block Store, whose entries are each
346    /// phone's own.
347    fn synced_entries(&self) -> Option<SyncedEntries> {
348        self.with_memory(|memory| match &memory.person {
349            MemoryPerson::ICloud(entries) => Some(entries.clone()),
350            MemoryPerson::BlockStore(_) => None,
351        })
352    }
353
354    /// A new phone set up from this one by direct transfer: no device-only
355    /// entries, and a copy of every Block Store entry. Requires an in-memory
356    /// Block Store keychain.
357    pub fn set_up_new_device(&self) -> Arc<Self> {
358        let store = self.memory_block_store(|store| store.set_up_new_phone());
359        Self::memory(self.name.clone(), MemoryPerson::BlockStore(BlockStore::new(store)))
360            .expect("registered service name is valid")
361    }
362
363    /// A new phone set up from this phone's last cloud backup: no device-only
364    /// entries, and the Block Store entries that backup holds. Requires an
365    /// in-memory Block Store keychain.
366    pub fn restore_new_device_from_cloud(&self) -> Arc<Self> {
367        let store = self.memory_block_store(|store| store.restore_new_phone_from_cloud());
368        Self::memory(self.name.clone(), MemoryPerson::BlockStore(BlockStore::new(store)))
369            .expect("registered service name is valid")
370    }
371
372    /// Google's periodic cloud backup of this phone's Block Store: the entries
373    /// stored to be backed up. Requires an in-memory Block Store keychain.
374    pub fn back_up_to_cloud(&self) {
375        self.memory_block_store(MemoryBlockStore::back_up_to_cloud);
376    }
377
378    /// Whether this phone's cloud backup would be end-to-end encrypted: it has
379    /// a screen lock. Requires an in-memory Block Store keychain.
380    pub fn set_end_to_end_encryption(&self, available: bool) {
381        self.memory_block_store(|store| store.set_end_to_end_encryption(available));
382    }
383
384    fn memory_block_store<T>(&self, f: impl FnOnce(&MemoryBlockStore) -> T) -> T {
385        self.with_memory(|memory| match &memory.person {
386            MemoryPerson::BlockStore(store) => f(store.api()),
387            MemoryPerson::ICloud(_) => panic!("this in-memory keychain keeps no Block Store"),
388        })
389    }
390
391    fn memory(name: String, person: MemoryPerson) -> Result<Arc<Self>, KeyError> {
392        validate_service(&name)?;
393        Ok(Arc::new(Self {
394            name,
395            backend: Box::new(Mutex::new(MemoryEntries {
396                device: BTreeMap::new(),
397                person,
398                fail_after: std::collections::VecDeque::new(),
399                refuse_person: false,
400                panic_next: false,
401            })),
402            host_secrets: Mutex::new(()),
403        }))
404    }
405
406    /// Run `change` on the in-memory keychain's entries. Panics if this is
407    /// a native keychain.
408    fn with_memory<T>(&self, change: impl FnOnce(&mut MemoryEntries) -> T) -> T {
409        let backend: &dyn std::any::Any = self.backend.as_ref();
410        let memory: &Mutex<MemoryEntries> =
411            backend.downcast_ref().expect("failure injection requires an in-memory keychain");
412        change(&mut memory.lock().expect("in-memory keychain entries lock is poisoned"))
413    }
414
415    /// Run `change` on this device's own entries, as a test inspects or
416    /// moves them.
417    #[cfg(test)]
418    pub(super) fn with_device_entries<T>(
419        &self,
420        change: impl FnOnce(&mut BTreeMap<String, SecretBytes>) -> T,
421    ) -> T {
422        self.with_memory(|memory| change(&mut memory.device))
423    }
424
425    /// Make the fake refuse the next read, write, delete or list before changing state.
426    /// Panics if this is a native keychain.
427    pub fn fail_next_operation(&self) {
428        self.fail_operations_after(&[0]);
429    }
430
431    /// Make the fake panic in the next read, write, delete or list before
432    /// changing state, as a bug in the keychain's code would. Panics if
433    /// this is a native keychain.
434    pub fn panic_next_operation(&self) {
435        self.with_memory(|memory| memory.panic_next = true);
436    }
437
438    /// Run `run` while every call on this keychain's entries waits, as a
439    /// keychain slow to answer would: for a test that acts while a call
440    /// waits on the keychain. Panics if this is a native keychain.
441    pub fn with_operations_held<T>(&self, run: impl FnOnce() -> T) -> T {
442        self.with_memory(|_entries| run())
443    }
444
445    /// Refuse successive operations after each given number of successful calls.
446    /// For example `[2, 0]` refuses the third call and its immediate rollback.
447    /// Replaces any existing failure schedule; requires an in-memory keychain.
448    pub fn fail_operations_after(&self, successes: &[usize]) {
449        self.with_memory(|memory| memory.fail_after = successes.iter().copied().collect());
450    }
451
452    /// Save a person entry's copy in a second keychain group, as an app that
453    /// moved to a new group can find its old copy beside the new one.
454    /// Requires an in-memory iCloud keychain.
455    pub(crate) fn write_in_another_group(&self, account: &str, bytes: &[u8]) {
456        self.with_memory(|memory| match &memory.person {
457            MemoryPerson::ICloud(entries) => {
458                entries
459                    .groups()
460                    .another
461                    .insert(account.to_owned(), SecretBytes::new(bytes.to_vec()));
462            }
463            MemoryPerson::BlockStore(_) => panic!("Block Store has no keychain groups"),
464        });
465    }
466
467    /// While `refuse` holds, refuse every call this device makes on a person
468    /// entry before changing state, letting its device-only calls through.
469    /// Requires an in-memory keychain.
470    pub fn refuse_person_entry_operations(&self, refuse: bool) {
471        self.with_memory(|memory| memory.refuse_person = refuse);
472    }
473}
474
475/// The magic, the number of entries, then each entry's account and bytes,
476/// each after its length.
477fn encode_device_entries(entries: &BTreeMap<String, SecretBytes>) -> Zeroizing<Vec<u8>> {
478    let fields = entries.iter().flat_map(|(account, bytes)| [account.as_bytes(), bytes.as_bytes()]);
479    // Allocate once: growing a secret Vec would abandon an unerased copy.
480    let length = fields.clone().map(|field| 8 + field.len()).sum::<usize>();
481    let mut encoded =
482        Zeroizing::new(Vec::with_capacity(SAVED_DEVICE_ENTRIES_MAGIC.len() + 8 + length));
483    encoded.extend_from_slice(SAVED_DEVICE_ENTRIES_MAGIC);
484    encoded.extend_from_slice(&(entries.len() as u64).to_le_bytes());
485    for field in fields {
486        encoded.extend_from_slice(&(field.len() as u64).to_le_bytes());
487        encoded.extend_from_slice(field);
488    }
489    encoded
490}
491
492/// Read what `encode_device_entries` wrote.
493fn decode_device_entries(
494    mut encoded: &[u8],
495) -> Result<BTreeMap<String, SecretBytes>, MaterialError> {
496    fn field<'a>(encoded: &mut &'a [u8]) -> Result<&'a [u8], MaterialError> {
497        let length = usize::try_from(wire::number(encoded)?).or(Err(MaterialError::Encoding))?;
498        wire::take(encoded, length)
499    }
500    wire::prefix(&mut encoded, SAVED_DEVICE_ENTRIES_MAGIC)?;
501    let mut entries = BTreeMap::new();
502    for _ in 0..wire::number(&mut encoded)? {
503        let account =
504            String::from_utf8(field(&mut encoded)?.to_vec()).or(Err(MaterialError::Encoding))?;
505        let bytes = SecretBytes::new(field(&mut encoded)?.to_vec());
506        if entries.insert(account, bytes).is_some() {
507            return Err(MaterialError::Encoding);
508        }
509    }
510    wire::end(encoded)?;
511    Ok(entries)
512}