offline_protocol_mls/storage.rs
1//! Storage trait definitions for MLS key material.
2//!
3//! Apps implement the [`MlsStorage`] trait to provide platform-native secure storage
4//! (e.g., iOS Keychain, Android Keystore, EncryptedSharedPreferences).
5
6use thiserror::Error;
7
8/// Errors that can occur during storage operations.
9// Adding a variant to a public error enum is a breaking change without
10// this attribute; downstream crates must carry a wildcard arm.
11#[non_exhaustive]
12#[derive(Debug, Error)]
13pub enum StorageError {
14 /// Failed to store data.
15 #[error("Failed to store data: {0}")]
16 StoreFailed(String),
17
18 /// Failed to load data.
19 #[error("Failed to load data: {0}")]
20 LoadFailed(String),
21
22 /// Failed to delete data.
23 #[error("Failed to delete data: {0}")]
24 DeleteFailed(String),
25
26 /// Key not found in storage.
27 #[error("Key not found: {0}")]
28 KeyNotFound(String),
29
30 /// Data is corrupted or invalid.
31 #[error("Corrupted data: {0}")]
32 CorruptedData(String),
33
34 /// Storage is not available.
35 #[error("Storage unavailable: {0}")]
36 Unavailable(String),
37}
38
39/// Result type for storage operations.
40pub type StorageResult<T> = std::result::Result<T, StorageError>;
41
42/// Trait for MLS key material storage.
43///
44/// Apps must implement this trait to provide secure storage for MLS cryptographic
45/// material. The implementation should use platform-native secure storage:
46///
47/// - **iOS**: Keychain Services
48/// - **Android**: EncryptedSharedPreferences or Keystore
49/// - **Desktop**: OS-specific secure storage or encrypted file storage
50///
51/// # Key Types
52///
53/// The storage uses a two-part key system: `(key_type, key_id)` where:
54/// - `key_type`: Category of data (e.g., "identity", "key_package", "group_state")
55/// - `key_id`: Unique identifier within that category
56///
57/// # Thread Safety
58///
59/// Implementations must be thread-safe (`Send + Sync`).
60///
61/// # Example
62///
63/// ```ignore
64/// struct KeychainStorage;
65///
66/// impl MlsStorage for KeychainStorage {
67/// fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> StorageResult<()> {
68/// // Store in iOS Keychain
69/// keychain_store(&format!("{}:{}", key_type, key_id), data)?;
70/// Ok(())
71/// }
72/// // ... other methods
73/// }
74/// ```
75pub trait MlsStorage: Send + Sync {
76 /// Stores data with the given key type and ID.
77 ///
78 /// If data already exists for this key, it should be overwritten.
79 /// Implementations should commit atomically for each `(key_type, key_id)` entry:
80 /// after `Ok(())`, a subsequent `load()` from any thread/process must return
81 /// either the old full value or the new full value, never a torn write.
82 /// Implementations should also provide best-effort crash durability so power
83 /// loss does not silently roll back acknowledged critical state writes.
84 ///
85 /// Durability is *ordering*-critical, not just best practice: the SDK treats
86 /// a returned `Ok(())` as persisted and immediately writes state that depends
87 /// on it. The per-install protocol-state record key is the sharp case — the
88 /// moment it stores, sealed records start landing in the (separate,
89 /// app-container) protocol-state store. A backend that acknowledges a write
90 /// it has only staged in memory (Android's `SharedPreferences.apply()`, an
91 /// unflushed file handle) can crash into a state where those records exist
92 /// but their key never did, and they are then dropped as unauthentic.
93 ///
94 /// # Arguments
95 ///
96 /// * `key_type` - Category of the data (e.g., "identity", "group_state")
97 /// * `key_id` - Unique identifier within the category
98 /// * `data` - Binary data to store
99 fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> StorageResult<()>;
100
101 /// Loads data for the given key type and ID.
102 ///
103 /// Returns `Ok(None)` if the key doesn't exist.
104 /// Returns `Err` only for actual failures (corruption, access denied, etc.).
105 ///
106 /// # Which error
107 ///
108 /// Reserve [`StorageError::CorruptedData`] for a record that is present
109 /// and can never be read by any later launch: its bytes are damaged, or
110 /// it does not authenticate. A failure that a later launch might not hit
111 /// (a locked keystore, an I/O error, a device not yet unlocked after
112 /// boot) is [`StorageError::LoadFailed`].
113 ///
114 /// The distinction is acted on, not only logged. The engine answers
115 /// `CorruptedData` on the protocol-state record key by minting a new one,
116 /// which makes every protocol-state record sealed under the old key
117 /// unreadable for good; it answers `LoadFailed` by leaving the key alone
118 /// and not persisting sensitive state for that session. A transient fault
119 /// reported as `CorruptedData` therefore destroys recoverable state.
120 ///
121 /// # Arguments
122 ///
123 /// * `key_type` - Category of the data
124 /// * `key_id` - Unique identifier within the category
125 fn load(&self, key_type: &str, key_id: &str) -> StorageResult<Option<Vec<u8>>>;
126
127 /// Deletes data for the given key type and ID.
128 ///
129 /// Should succeed even if the key doesn't exist.
130 ///
131 /// # Arguments
132 ///
133 /// * `key_type` - Category of the data
134 /// * `key_id` - Unique identifier within the category
135 fn delete(&self, key_type: &str, key_id: &str) -> StorageResult<()>;
136
137 /// Lists all key IDs for a given key type.
138 ///
139 /// This is used for enumerating groups, key packages, etc.
140 ///
141 /// # Arguments
142 ///
143 /// * `key_type` - Category of data to list
144 fn list_keys(&self, key_type: &str) -> StorageResult<Vec<String>>;
145
146 /// Checks if a key exists.
147 ///
148 /// Default implementation uses `load`, but can be overridden for efficiency.
149 fn exists(&self, key_type: &str, key_id: &str) -> StorageResult<bool> {
150 Ok(self.load(key_type, key_id)?.is_some())
151 }
152
153 /// Clears all data of a given key type.
154 ///
155 /// Default implementation lists and deletes each key, but can be overridden
156 /// for efficiency.
157 fn clear_type(&self, key_type: &str) -> StorageResult<()> {
158 for key_id in self.list_keys(key_type)? {
159 self.delete(key_type, &key_id)?;
160 }
161 Ok(())
162 }
163}
164
165/// In-memory storage implementation for testing.
166///
167/// **WARNING**: This implementation is NOT secure and should only be used for testing.
168/// All data is lost when the process exits.
169#[derive(Debug, Default)]
170pub struct InMemoryStorage {
171 data: std::sync::RwLock<std::collections::HashMap<String, Vec<u8>>>,
172}
173
174impl InMemoryStorage {
175 /// Creates a new in-memory storage.
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 fn make_key(key_type: &str, key_id: &str) -> String {
181 format!("{}:{}", key_type, key_id)
182 }
183}
184
185impl MlsStorage for InMemoryStorage {
186 fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> StorageResult<()> {
187 let mut storage = self
188 .data
189 .write()
190 .map_err(|e| StorageError::StoreFailed(e.to_string()))?;
191 storage.insert(Self::make_key(key_type, key_id), data.to_vec());
192 Ok(())
193 }
194
195 fn load(&self, key_type: &str, key_id: &str) -> StorageResult<Option<Vec<u8>>> {
196 let storage = self
197 .data
198 .read()
199 .map_err(|e| StorageError::LoadFailed(e.to_string()))?;
200 Ok(storage.get(&Self::make_key(key_type, key_id)).cloned())
201 }
202
203 fn delete(&self, key_type: &str, key_id: &str) -> StorageResult<()> {
204 let mut storage = self
205 .data
206 .write()
207 .map_err(|e| StorageError::DeleteFailed(e.to_string()))?;
208 storage.remove(&Self::make_key(key_type, key_id));
209 Ok(())
210 }
211
212 fn list_keys(&self, key_type: &str) -> StorageResult<Vec<String>> {
213 let storage = self
214 .data
215 .read()
216 .map_err(|e| StorageError::LoadFailed(e.to_string()))?;
217
218 let prefix = format!("{}:", key_type);
219 let keys: Vec<String> = storage
220 .keys()
221 .filter_map(|k| {
222 if k.starts_with(&prefix) {
223 Some(k[prefix.len()..].to_string())
224 } else {
225 None
226 }
227 })
228 .collect();
229
230 Ok(keys)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_in_memory_storage_basic_operations() {
240 let storage = InMemoryStorage::new();
241
242 // Store and load
243 storage.store("test", "key1", b"value1").unwrap();
244 let loaded = storage.load("test", "key1").unwrap();
245 assert_eq!(loaded, Some(b"value1".to_vec()));
246
247 // Load non-existent
248 let loaded = storage.load("test", "key2").unwrap();
249 assert_eq!(loaded, None);
250
251 // Delete
252 storage.delete("test", "key1").unwrap();
253 let loaded = storage.load("test", "key1").unwrap();
254 assert_eq!(loaded, None);
255 }
256
257 #[test]
258 fn test_in_memory_storage_list_keys() {
259 let storage = InMemoryStorage::new();
260
261 storage.store("type_a", "key1", b"v1").unwrap();
262 storage.store("type_a", "key2", b"v2").unwrap();
263 storage.store("type_b", "key3", b"v3").unwrap();
264
265 let mut keys_a = storage.list_keys("type_a").unwrap();
266 keys_a.sort();
267 assert_eq!(keys_a, vec!["key1", "key2"]);
268
269 let keys_b = storage.list_keys("type_b").unwrap();
270 assert_eq!(keys_b, vec!["key3"]);
271
272 let keys_c = storage.list_keys("type_c").unwrap();
273 assert!(keys_c.is_empty());
274 }
275
276 #[test]
277 fn test_in_memory_storage_exists() {
278 let storage = InMemoryStorage::new();
279
280 assert!(!storage.exists("test", "key1").unwrap());
281
282 storage.store("test", "key1", b"value").unwrap();
283 assert!(storage.exists("test", "key1").unwrap());
284
285 storage.delete("test", "key1").unwrap();
286 assert!(!storage.exists("test", "key1").unwrap());
287 }
288
289 #[test]
290 fn test_in_memory_storage_clear_type() {
291 let storage = InMemoryStorage::new();
292
293 storage.store("type_a", "key1", b"v1").unwrap();
294 storage.store("type_a", "key2", b"v2").unwrap();
295 storage.store("type_b", "key3", b"v3").unwrap();
296
297 storage.clear_type("type_a").unwrap();
298
299 assert!(storage.list_keys("type_a").unwrap().is_empty());
300 assert_eq!(storage.list_keys("type_b").unwrap().len(), 1);
301 }
302}