Skip to main content

nrc_mls_sqlite_storage/
lib.rs

1//! SQLite-based storage implementation for Nostr MLS.
2//!
3//! This module provides a SQLite-based storage implementation for the Nostr MLS (Messaging Layer Security)
4//! crate. It implements the [`NostrMlsStorageProvider`] trait, allowing it to be used within the Nostr MLS context.
5//!
6//! SQLite-based storage is persistent and will be saved to a file. It's useful for production applications
7//! where data persistence is required.
8
9#![forbid(unsafe_code)]
10#![warn(missing_docs)]
11#![warn(rustdoc::bare_urls)]
12
13use std::path::Path;
14use std::sync::{Arc, Mutex};
15
16use nrc_mls_storage::{Backend, NostrMlsStorageProvider};
17use openmls_sqlite_storage::{Codec, SqliteStorageProvider};
18use rusqlite::Connection;
19use serde::de::DeserializeOwned;
20use serde::Serialize;
21
22mod db;
23pub mod error;
24mod groups;
25mod messages;
26mod migrations;
27mod welcomes;
28
29use self::error::Error;
30
31// Define a type alias for the specific SqliteStorageProvider we're using
32type MlsStorage = SqliteStorageProvider<JsonCodec, Connection>;
33
34// TODO: make this private?
35/// A codec for JSON serialization and deserialization.
36#[derive(Default)]
37pub struct JsonCodec;
38
39impl Codec for JsonCodec {
40    type Error = serde_json::Error;
41
42    #[inline]
43    fn to_vec<T: Serialize>(value: &T) -> Result<Vec<u8>, Self::Error> {
44        serde_json::to_vec(value)
45    }
46
47    #[inline]
48    fn from_slice<T>(slice: &[u8]) -> Result<T, Self::Error>
49    where
50        T: DeserializeOwned,
51    {
52        serde_json::from_slice(slice)
53    }
54}
55
56/// A SQLite-based storage implementation for Nostr MLS.
57///
58/// This struct implements the NostrMlsStorageProvider trait for SQLite databases.
59/// It directly interfaces with a SQLite database for storing MLS data.
60pub struct NostrMlsSqliteStorage {
61    /// The OpenMLS storage implementation
62    openmls_storage: MlsStorage,
63    /// The SQLite connection
64    db_connection: Arc<Mutex<Connection>>,
65}
66
67impl NostrMlsSqliteStorage {
68    /// Creates a new [`NostrMlsSqliteStorage`] with the provided file path.
69    ///
70    /// # Arguments
71    ///
72    /// * `file_path` - Path to the SQLite database file.
73    ///
74    /// # Returns
75    ///
76    /// A Result containing a new instance of [`NostrMlsSqliteStorage`] or an error.
77    pub fn new<P>(file_path: P) -> Result<Self, Error>
78    where
79        P: AsRef<Path>,
80    {
81        // Ensure parent directory exists
82        if let Some(parent) = file_path.as_ref().parent() {
83            std::fs::create_dir_all(parent)?;
84        }
85
86        // Create or open the SQLite database
87        let mls_connection: Connection = Connection::open(&file_path)?;
88
89        // Enable foreign keys
90        mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
91
92        // Create OpenMLS storage
93        let mut openmls_storage: MlsStorage = SqliteStorageProvider::new(mls_connection);
94
95        // Initialize the OpenMLS storage
96        openmls_storage.initialize()?;
97
98        // Create a new connection for the Nostr MLS storage
99        let mut nostr_mls_connection = Connection::open(&file_path)?;
100
101        // Enable foreign keys
102        nostr_mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
103
104        // Apply migrations
105        migrations::run_migrations(&mut nostr_mls_connection)?;
106
107        Ok(Self {
108            openmls_storage,
109            db_connection: Arc::new(Mutex::new(nostr_mls_connection)),
110        })
111    }
112
113    /// Creates a new in-memory [`NostrMlsSqliteStorage`] for testing purposes.
114    ///
115    /// # Returns
116    ///
117    /// A Result containing a new in-memory instance of [`NostrMlsSqliteStorage`] or an error.
118    #[cfg(test)]
119    pub fn new_in_memory() -> Result<Self, Error> {
120        // Create an in-memory SQLite database
121        let mls_connection = Connection::open_in_memory()?;
122
123        // Enable foreign keys
124        mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
125
126        // Create OpenMLS storage
127        let mut openmls_storage: MlsStorage = SqliteStorageProvider::new(mls_connection);
128
129        // Initialize the OpenMLS storage
130        openmls_storage.initialize()?;
131
132        // For in-memory databases, we need to share the connection
133        // to keep the database alive, so we will clone the connection
134        // and let OpenMLS use a new handle
135        let mut nostr_mls_connection: Connection = Connection::open_in_memory()?;
136
137        // Enable foreign keys
138        nostr_mls_connection.execute_batch("PRAGMA foreign_keys = ON;")?;
139
140        // Setup the schema in this connection as well
141        migrations::run_migrations(&mut nostr_mls_connection)?;
142
143        Ok(Self {
144            openmls_storage,
145            db_connection: Arc::new(Mutex::new(nostr_mls_connection)),
146        })
147    }
148}
149
150/// Implementation of [`NostrMlsStorageProvider`] for SQLite-based storage.
151impl NostrMlsStorageProvider for NostrMlsSqliteStorage {
152    type OpenMlsStorageProvider = MlsStorage;
153
154    /// Returns the backend type.
155    ///
156    /// # Returns
157    ///
158    /// [`Backend::SQLite`] indicating this is a SQLite-based storage implementation.
159    fn backend(&self) -> Backend {
160        Backend::SQLite
161    }
162
163    /// Get a reference to the openmls storage provider.
164    ///
165    /// This method provides access to the underlying OpenMLS storage provider.
166    /// This is primarily useful for internal operations and testing.
167    ///
168    /// # Returns
169    ///
170    /// A reference to the openmls storage implementation.
171    fn openmls_storage(&self) -> &Self::OpenMlsStorageProvider {
172        &self.openmls_storage
173    }
174
175    /// Get a mutable reference to the openmls storage provider.
176    ///
177    /// This method provides mutable access to the underlying OpenMLS storage provider.
178    /// This is primarily useful for internal operations and testing.
179    ///
180    /// # Returns
181    ///
182    /// A mutable reference to the openmls storage implementation.
183    fn openmls_storage_mut(&mut self) -> &mut Self::OpenMlsStorageProvider {
184        &mut self.openmls_storage
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use std::collections::BTreeSet;
191
192    use openmls::group::GroupId;
193    use tempfile::tempdir;
194
195    use super::*;
196
197    #[test]
198    fn test_new_in_memory() {
199        let storage = NostrMlsSqliteStorage::new_in_memory();
200        assert!(storage.is_ok());
201        let storage = storage.unwrap();
202        assert_eq!(storage.backend(), Backend::SQLite);
203    }
204
205    #[test]
206    fn test_backend_type() {
207        let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
208        assert_eq!(storage.backend(), Backend::SQLite);
209        assert!(storage.backend().is_persistent());
210    }
211
212    #[test]
213    fn test_file_based_storage() {
214        let temp_dir = tempdir().unwrap();
215        let db_path = temp_dir.path().join("test_db.sqlite");
216
217        // Create a new storage
218        let storage = NostrMlsSqliteStorage::new(&db_path);
219        assert!(storage.is_ok());
220
221        // Verify file exists
222        assert!(db_path.exists());
223
224        // Create a second instance that connects to the same file
225        let storage2 = NostrMlsSqliteStorage::new(&db_path);
226        assert!(storage2.is_ok());
227
228        // Clean up
229        drop(storage);
230        drop(storage2);
231        temp_dir.close().unwrap();
232    }
233
234    #[test]
235    fn test_openmls_storage_access() {
236        let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
237
238        // Test that we can get a reference to the openmls storage
239        let _openmls_storage = storage.openmls_storage();
240
241        // Test mutable accessor
242        let mut mutable_storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
243        let _mutable_ref = mutable_storage.openmls_storage_mut();
244    }
245
246    #[test]
247    fn test_database_tables() {
248        let temp_dir = tempdir().unwrap();
249        let db_path = temp_dir.path().join("migration_test.sqlite");
250
251        // Create a new SQLite database
252        let storage = NostrMlsSqliteStorage::new(&db_path).unwrap();
253
254        // Verify the database has been properly initialized with migrations
255        {
256            let conn_guard = storage.db_connection.lock().unwrap();
257
258            // Check if the tables exist
259            let mut stmt = conn_guard
260                .prepare("SELECT name FROM sqlite_master WHERE type='table'")
261                .unwrap();
262            let table_names: Vec<String> = stmt
263                .query_map([], |row| row.get(0))
264                .unwrap()
265                .map(|r| r.unwrap())
266                .collect();
267
268            // Check for essential tables
269            assert!(table_names.contains(&"groups".to_string()));
270            assert!(table_names.contains(&"messages".to_string()));
271            assert!(table_names.contains(&"welcomes".to_string()));
272            assert!(table_names.contains(&"processed_messages".to_string()));
273            assert!(table_names.contains(&"processed_welcomes".to_string()));
274            assert!(table_names.contains(&"group_relays".to_string()));
275            assert!(table_names.contains(&"group_exporter_secrets".to_string()));
276        } // conn_guard is dropped here when it goes out of scope
277
278        // Drop explicitly to release all resources
279        drop(storage);
280        temp_dir.close().unwrap();
281    }
282
283    #[test]
284    fn test_group_exporter_secrets() {
285        use nrc_mls_storage::groups::types::{Group, GroupExporterSecret, GroupState};
286        use nrc_mls_storage::groups::GroupStorage;
287
288        // Create an in-memory SQLite database
289        let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
290
291        // Create a test group
292        let mls_group_id = GroupId::from_slice(vec![1, 2, 3, 4].as_slice());
293        let group = Group {
294            mls_group_id: mls_group_id.clone(),
295            nostr_group_id: [0u8; 32],
296            name: "Test Group".to_string(),
297            description: "A test group for exporter secrets".to_string(),
298            admin_pubkeys: BTreeSet::new(),
299            last_message_id: None,
300            last_message_at: None,
301            epoch: 0,
302            state: GroupState::Active,
303            image_url: None,
304            image_key: None,
305            image_nonce: None,
306        };
307
308        // Save the group
309        storage.save_group(group.clone()).unwrap();
310
311        // Create test group exporter secrets for different epochs
312        let secret_epoch_0 = GroupExporterSecret {
313            mls_group_id: mls_group_id.clone(),
314            epoch: 0,
315            secret: [0u8; 32],
316        };
317
318        let secret_epoch_1 = GroupExporterSecret {
319            mls_group_id: mls_group_id.clone(),
320            epoch: 1,
321            secret: [0u8; 32],
322        };
323
324        // Save the exporter secrets
325        storage
326            .save_group_exporter_secret(secret_epoch_0.clone())
327            .unwrap();
328        storage
329            .save_group_exporter_secret(secret_epoch_1.clone())
330            .unwrap();
331
332        // Test retrieving exporter secrets
333        let retrieved_secret_0 = storage.get_group_exporter_secret(&mls_group_id, 0).unwrap();
334        assert!(retrieved_secret_0.is_some());
335        let retrieved_secret_0 = retrieved_secret_0.unwrap();
336        assert_eq!(retrieved_secret_0, secret_epoch_0);
337
338        let retrieved_secret_1 = storage.get_group_exporter_secret(&mls_group_id, 1).unwrap();
339        assert!(retrieved_secret_1.is_some());
340        let retrieved_secret_1 = retrieved_secret_1.unwrap();
341        assert_eq!(retrieved_secret_1, secret_epoch_1);
342
343        // Test non-existent epoch
344        let non_existent_epoch = storage
345            .get_group_exporter_secret(&mls_group_id, 999)
346            .unwrap();
347        assert!(non_existent_epoch.is_none());
348
349        // Test non-existent group
350        let non_existent_group_id = GroupId::from_slice(&[9, 9, 9, 9]);
351        let result = storage.get_group_exporter_secret(&non_existent_group_id, 0);
352        assert!(result.is_err());
353
354        // Test overwriting an existing secret
355        let updated_secret_0 = GroupExporterSecret {
356            mls_group_id: mls_group_id.clone(),
357            epoch: 0,
358            secret: [0u8; 32],
359        };
360        storage
361            .save_group_exporter_secret(updated_secret_0.clone())
362            .unwrap();
363
364        let retrieved_updated_secret = storage
365            .get_group_exporter_secret(&mls_group_id, 0)
366            .unwrap()
367            .unwrap();
368        assert_eq!(retrieved_updated_secret, updated_secret_0);
369
370        // Test trying to save a secret for a non-existent group
371        let invalid_secret = GroupExporterSecret {
372            mls_group_id: non_existent_group_id.clone(),
373            epoch: 0,
374            secret: [0u8; 32],
375        };
376        let result = storage.save_group_exporter_secret(invalid_secret);
377        assert!(result.is_err());
378    }
379}