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;