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 /// # Arguments
107 ///
108 /// * `key_type` - Category of the data
109 /// * `key_id` - Unique identifier within the category
110 fn load(&self, key_type: &str, key_id: &str) -> StorageResult<Option<Vec<u8>>>;
111
112 /// Deletes data for the given key type and ID.
113 ///
114 /// Should succeed even if the key doesn't exist.
115 ///
116 /// # Arguments
117 ///
118 /// * `key_type` - Category of the data
119 /// * `key_id` - Unique identifier within the category
120 fn delete(&self, key_type: &str, key_id: &str) -> StorageResult<()>;
121
122 /// Lists all key IDs for a given key type.
123 ///
124 /// This is used for enumerating groups, key packages, etc.
125 ///
126 /// # Arguments
127 ///
128 /// * `key_type` - Category of data to list
129 fn list_keys(&self, key_type: &str) -> StorageResult<Vec<String>>;
130
131 /// Checks if a key exists.
132 ///
133 /// Default implementation uses `load`, but can be overridden for efficiency.
134 fn exists(&self, key_type: &str, key_id: &str) -> StorageResult<bool> {
135 Ok(self.load(key_type, key_id)?.is_some())
136 }
137
138 /// Clears all data of a given key type.
139 ///
140 /// Default implementation lists and deletes each key, but can be overridden
141 /// for efficiency.
142 fn clear_type(&self, key_type: &str) -> StorageResult<()> {
143 for key_id in self.list_keys(key_type)? {
144 self.delete(key_type, &key_id)?;
145 }
146 Ok(())
147 }
148}
149
150/// In-memory storage implementation for testing.
151///
152/// **WARNING**: This implementation is NOT secure and should only be used for testing.
153/// All data is lost when the process exits.
154#[derive(Debug, Default)]
155pub struct InMemoryStorage {
156 data: std::sync::RwLock<std::collections::HashMap<String, Vec<u8>>>,
157}
158
159impl InMemoryStorage {
160 /// Creates a new in-memory storage.
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 fn make_key(key_type: &str, key_id: &str) -> String {
166 format!("{}:{}", key_type, key_id)
167 }
168}
169
170impl MlsStorage for InMemoryStorage {
171 fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> StorageResult<()> {
172 let mut storage = self
173 .data
174 .write()
175 .map_err(|e| StorageError::StoreFailed(e.to_string()))?;
176 storage.insert(Self::make_key(key_type, key_id), data.to_vec());
177 Ok(())
178 }
179
180 fn load(&self, key_type: &str, key_id: &str) -> StorageResult<Option<Vec<u8>>> {
181 let storage = self
182 .data
183 .read()
184 .map_err(|e| StorageError::LoadFailed(e.to_string()))?;
185 Ok(storage.get(&Self::make_key(key_type, key_id)).cloned())
186 }
187
188 fn delete(&self, key_type: &str, key_id: &str) -> StorageResult<()> {
189 let mut storage = self
190 .data
191 .write()
192 .map_err(|e| StorageError::DeleteFailed(e.to_string()))?;
193 storage.remove(&Self::make_key(key_type, key_id));
194 Ok(())
195 }
196
197 fn list_keys(&self, key_type: &str) -> StorageResult<Vec<String>> {
198 let storage = self
199 .data
200 .read()
201 .map_err(|e| StorageError::LoadFailed(e.to_string()))?;
202
203 let prefix = format!("{}:", key_type);
204 let keys: Vec<String> = storage
205 .keys()
206 .filter_map(|k| {
207 if k.starts_with(&prefix) {
208 Some(k[prefix.len()..].to_string())
209 } else {
210 None
211 }
212 })
213 .collect();
214
215 Ok(keys)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_in_memory_storage_basic_operations() {
225 let storage = InMemoryStorage::new();
226
227 // Store and load
228 storage.store("test", "key1", b"value1").unwrap();
229 let loaded = storage.load("test", "key1").unwrap();
230 assert_eq!(loaded, Some(b"value1".to_vec()));
231
232 // Load non-existent
233 let loaded = storage.load("test", "key2").unwrap();
234 assert_eq!(loaded, None);
235
236 // Delete
237 storage.delete("test", "key1").unwrap();
238 let loaded = storage.load("test", "key1").unwrap();
239 assert_eq!(loaded, None);
240 }
241
242 #[test]
243 fn test_in_memory_storage_list_keys() {
244 let storage = InMemoryStorage::new();
245
246 storage.store("type_a", "key1", b"v1").unwrap();
247 storage.store("type_a", "key2", b"v2").unwrap();
248 storage.store("type_b", "key3", b"v3").unwrap();
249
250 let mut keys_a = storage.list_keys("type_a").unwrap();
251 keys_a.sort();
252 assert_eq!(keys_a, vec!["key1", "key2"]);
253
254 let keys_b = storage.list_keys("type_b").unwrap();
255 assert_eq!(keys_b, vec!["key3"]);
256
257 let keys_c = storage.list_keys("type_c").unwrap();
258 assert!(keys_c.is_empty());
259 }
260
261 #[test]
262 fn test_in_memory_storage_exists() {
263 let storage = InMemoryStorage::new();
264
265 assert!(!storage.exists("test", "key1").unwrap());
266
267 storage.store("test", "key1", b"value").unwrap();
268 assert!(storage.exists("test", "key1").unwrap());
269
270 storage.delete("test", "key1").unwrap();
271 assert!(!storage.exists("test", "key1").unwrap());
272 }
273
274 #[test]
275 fn test_in_memory_storage_clear_type() {
276 let storage = InMemoryStorage::new();
277
278 storage.store("type_a", "key1", b"v1").unwrap();
279 storage.store("type_a", "key2", b"v2").unwrap();
280 storage.store("type_b", "key3", b"v3").unwrap();
281
282 storage.clear_type("type_a").unwrap();
283
284 assert!(storage.list_keys("type_a").unwrap().is_empty());
285 assert_eq!(storage.list_keys("type_b").unwrap().len(), 1);
286 }
287}