Skip to main content

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}