Skip to main content

offline_protocol_leaf/
store.rs

1//! The storage seam, and the one rule that makes it safe.
2//!
3//! # Persist before emit
4//!
5//! A leaf node MUST have its MLS state durable before it emits a frame whose
6//! production advanced that state. The failure this prevents is not a delivery
7//! hiccup: a device that answers and then loses power before its ratchet state
8//! reaches flash comes back and **reuses an AEAD nonce**, which is a
9//! confidentiality failure in a protocol whose whole claim is the AEAD
10//! boundary.
11//!
12//! This crate does not ask firmware to remember that. Every operation that
13//! advances state writes through this trait and only then returns the bytes to
14//! send, so a store that returns an error produces no frame at all. The
15//! ordering is not documented here and implemented elsewhere; it is the reason
16//! [`LeafDevice`](crate::LeafDevice) hands back frames rather than exposing
17//! the MLS group it seals with.
18//!
19//! # What an implementation owes
20//!
21//! [`LeafStore::store`] must be **durable and atomic per entry**: after it
22//! returns `Ok`, a power cut must leave the new value readable, and it must
23//! never leave a torn one. mls-rs asks the same of its own storage provider,
24//! in the same words, for the same reason. A flash driver that buffers a write
25//! and reports success satisfies the type and breaks the rule.
26//!
27//! # What it bounds, and what it does not
28//!
29//! This crate bounds how much a store *holds*: peers, unspent key packages and
30//! prior-epoch records each have a ceiling. It does not bound how often a store
31//! is *written*, and an inbound key package costs a handful of writes to a part
32//! whose flash has a finite number of them. A device exposed to strangers wants
33//! its pairing window controlled by firmware rather than left open, which is
34//! the same conclusion the authorization obligation reaches from the other
35//! direction.
36//!
37//! # Where the key material should live
38//!
39//! Everything written through this trait is secret: the identity private key,
40//! MLS group state, key package private keys. On a part with secure key
41//! storage this trait is how that storage is reached, which is
42//! [R12](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/security/threat-model.md)
43//! in the threat model. A device that keeps them in general flash yields them
44//! to anyone holding the device.
45
46use alloc::{string::String, vec::Vec};
47use thiserror::Error;
48
49/// Key type for the device's own identity material.
50pub const KEY_TYPE_IDENTITY: &str = "identity";
51/// Key type for MLS group state.
52pub const KEY_TYPE_GROUP_STATE: &str = "group_state";
53/// Key type for MLS prior-epoch records.
54pub const KEY_TYPE_GROUP_EPOCH: &str = "group_epoch";
55/// Key type for key package private material.
56pub const KEY_TYPE_KEY_PACKAGE: &str = "key_package";
57/// Key type for what a peer told us it can parse.
58pub const KEY_TYPE_PEER: &str = "peer";
59/// Key type for the list of peers this device holds records for.
60///
61/// One entry, rewritten as peers are admitted and forgotten. It exists because
62/// this seam offers no way to enumerate what is in it, and a bound nobody can
63/// count is not a bound.
64pub const KEY_TYPE_PEER_INDEX: &str = "peer_index";
65
66/// What a store can fail with.
67#[derive(Debug, Clone, PartialEq, Eq, Error)]
68pub enum StoreError {
69    /// The write did not happen, or cannot be proven to have happened.
70    #[error("store failed: {0}")]
71    Store(String),
72    /// The read failed.
73    #[error("load failed: {0}")]
74    Load(String),
75    /// The delete failed.
76    #[error("delete failed: {0}")]
77    Delete(String),
78    /// The stored bytes are not what this crate wrote.
79    #[error("corrupt record: {0}")]
80    Corrupt(String),
81}
82
83/// Durable storage for a leaf node's secret material and MLS state.
84///
85/// The shape mirrors `MlsStorage` in `offline-protocol-mls`, which is the same
86/// seam on the phone: a two-part `(key_type, key_id)` key, `&self` methods so
87/// one store can be shared by the several places that write through it, and
88/// per-entry atomicity. A device implements it over the part's secure key
89/// storage, its flash filesystem, or an EEPROM.
90///
91/// # Errors
92///
93/// Returning an error is always safe. It aborts the operation before anything
94/// is emitted, which is the whole point of the seam.
95pub trait LeafStore: Send + Sync {
96    /// Writes `data`, replacing any previous value for this key.
97    ///
98    /// Must be durable and atomic per entry: after `Ok`, a power cut leaves
99    /// either the new value or the old one, never a torn record, and a
100    /// subsequent [`LeafStore::load`] returns the new value.
101    fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> Result<(), StoreError>;
102
103    /// Reads a value, or `None` if this key was never written.
104    fn load(&self, key_type: &str, key_id: &str) -> Result<Option<Vec<u8>>, StoreError>;
105
106    /// Removes a value. Removing a key that is not there is not an error.
107    ///
108    /// Every value this crate stores is secret, so an implementation should
109    /// erase rather than unlink.
110    fn delete(&self, key_type: &str, key_id: &str) -> Result<(), StoreError>;
111}
112
113#[cfg(any(test, feature = "std"))]
114mod memory {
115    use super::*;
116    use alloc::collections::BTreeMap;
117    use alloc::string::ToString;
118    use std::sync::Mutex;
119
120    /// A store that keeps everything in memory.
121    ///
122    /// For tests and for bringing a board up before its flash driver works.
123    /// **It is not a leaf node's storage**: it satisfies the durability
124    /// contract only in the sense that there is nothing to lose power.
125    #[derive(Debug, Default)]
126    pub struct MemoryStore {
127        entries: Mutex<BTreeMap<(String, String), Vec<u8>>>,
128    }
129
130    impl MemoryStore {
131        /// Creates an empty store.
132        pub fn new() -> Self {
133            Self::default()
134        }
135
136        /// Number of entries held, for tests that assert what was written.
137        pub fn len(&self) -> usize {
138            self.entries.lock().map(|e| e.len()).unwrap_or(0)
139        }
140
141        /// Whether the store holds nothing.
142        pub fn is_empty(&self) -> bool {
143            self.len() == 0
144        }
145    }
146
147    impl LeafStore for MemoryStore {
148        fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> Result<(), StoreError> {
149            let mut entries = self
150                .entries
151                .lock()
152                .map_err(|e| StoreError::Store(e.to_string()))?;
153            entries.insert((key_type.to_string(), key_id.to_string()), data.to_vec());
154            Ok(())
155        }
156
157        fn load(&self, key_type: &str, key_id: &str) -> Result<Option<Vec<u8>>, StoreError> {
158            let entries = self
159                .entries
160                .lock()
161                .map_err(|e| StoreError::Load(e.to_string()))?;
162            Ok(entries
163                .get(&(key_type.to_string(), key_id.to_string()))
164                .cloned())
165        }
166
167        fn delete(&self, key_type: &str, key_id: &str) -> Result<(), StoreError> {
168            let mut entries = self
169                .entries
170                .lock()
171                .map_err(|e| StoreError::Delete(e.to_string()))?;
172            entries.remove(&(key_type.to_string(), key_id.to_string()));
173            Ok(())
174        }
175    }
176}
177
178#[cfg(any(test, feature = "std"))]
179pub use memory::MemoryStore;