Skip to main content

core_crypto_keystore/connection/
mod.rs

1mod encryption;
2mod fetch_from_database;
3#[cfg(feature = "cross-process-lock")]
4mod file_lock;
5mod filesystem;
6#[cfg(target_os = "unknown")]
7mod idb_migration;
8#[cfg(target_os = "ios")]
9mod ios_wal_compat;
10#[cfg(target_os = "unknown")]
11mod jspi_test;
12pub(crate) mod managed_connection;
13mod migrations;
14mod mls;
15#[cfg(target_os = "unknown")]
16mod opfs_vfs;
17#[cfg(target_os = "unknown")]
18mod os_unknown;
19mod transaction;
20mod transaction_lock;
21
22use std::sync::Arc;
23
24use async_lock::{Mutex, MutexGuardArc};
25use rusqlite::Connection;
26#[cfg(feature = "log-queries")]
27use rusqlite::trace::{TraceEvent, TraceEventCodes};
28
29#[cfg(target_os = "unknown")]
30pub use self::idb_migration::{delete_legacy_idb, legacy_idb_exists};
31#[cfg(target_os = "unknown")]
32pub use self::jspi_test::run_in_jspi;
33use self::transaction_lock::TransactionLock;
34pub(crate) use self::{
35    filesystem::Filesystem,
36    managed_connection::{ManagedConnection, SqliteGuard},
37    transaction_lock::TransactionGuard,
38};
39pub use self::{
40    migrations::migrate_db_key_type_to_bytes,
41    mls::{deser, ser},
42};
43use crate::{
44    CryptoKeystoreResult, DatabaseKey, Transaction, connection::migrations::MigrationTarget, unique_arc::UniqueWeak,
45};
46
47#[cfg(feature = "log-queries")]
48fn log_query(event: TraceEvent) {
49    if let TraceEvent::Stmt(_, sql) = event {
50        log::info!("{sql}")
51    }
52}
53
54// Intentionally not `Clone`; outer users should wrap this entire thing in an `Arc` (or `Arc<Mutex<Option<Self>>>`
55// etc) as required for their desired semantics.
56#[derive(derive_more::Debug)]
57pub struct Database {
58    // internal connection; mutexed in order to ensure unique access
59    // and provide `Sync`. `Arc` allows us to hand out lifetime-free
60    // lock guards to the connection.
61    //
62    // Note: it is important for the correctness of `Self::take` that
63    // nobody ever actually clones this `Arc`, other than through a lock guard
64    // which is gone by the time `take` runs. For now I don't believe it's
65    // worth the effort of making a `UniqueArc` work here, but if this proves
66    // to be a problem, we might make that effort in the future.
67    conn: Arc<Mutex<ManagedConnection>>,
68    // handler with which to delete the database;
69    // mutexed to provide `Sync`
70    pub(crate) filesystem: Mutex<Box<dyn Filesystem>>,
71    #[debug(skip)]
72    pub(crate) transaction: Mutex<Option<UniqueWeak<Transaction>>>,
73    // ensures at most one transaction is in flight against this database at a time
74    transaction_lock: TransactionLock,
75}
76
77impl Database {
78    /// Open an encrypted `Database` at the provided location.
79    ///
80    /// This function is the internal implementation for [`Self::open`]; that method should be generally preferred.
81    #[cfg_attr(not(target_os = "unknown"), expect(clippy::unused_async))]
82    async fn open_internal(
83        path: &str,
84        database_key: &DatabaseKey,
85    ) -> CryptoKeystoreResult<(ManagedConnection, Box<dyn Filesystem>)> {
86        #[cfg(target_os = "unknown")]
87        let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
88
89        #[cfg(not(target_os = "unknown"))]
90        let (conn, filesystem) = {
91            let exists = std::fs::exists(path)?;
92            let mut conn = Connection::open(path)?;
93            if exists {
94                encryption::decrypt(&mut conn, database_key)?;
95            } else {
96                encryption::key(&mut conn, database_key)?;
97            }
98
99            // ? iOS WAL journaling fix; see details here: https://github.com/sqlcipher/sqlcipher/issues/255
100            // Use the caller-provided path here rather than `Connection::path()`, which SQLite
101            // canonicalizes. The iOS WAL compatibility salt is keyed by the path, so changing a
102            // relative path into an absolute one would make existing databases use the wrong salt.
103            #[cfg(target_os = "ios")]
104            if !path.is_empty() {
105                ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
106            }
107
108            (conn, filesystem::NativeFs)
109        };
110
111        let filesystem = Box::new(filesystem);
112        Ok((conn, filesystem))
113    }
114
115    /// Set up the database from a connection
116    ///
117    /// The connection must already be configured for encryption if appropriate.
118    ///
119    /// Sets appropriate pragmas and performs migrations and general initialization work.
120    async fn init(
121        conn: ManagedConnection,
122        filesystem: Box<dyn Filesystem>,
123        migration_target: MigrationTarget,
124    ) -> CryptoKeystoreResult<Self> {
125        let transaction_lock = {
126            let _sqlite = SqliteGuard::lock();
127            TransactionLock::new(conn.path().unwrap_or_default())?
128        };
129        // SQL migrations and their meta migrations commit separately. Keep other processes
130        // out for the entire initialization, including reading the current schema version.
131        let _guard = transaction_lock.acquire().await?;
132        Self::init_with_lock(conn, filesystem, migration_target, transaction_lock)
133    }
134
135    /// Initialize while holding `transaction_lock`, or with a private in-memory connection.
136    fn init_with_lock(
137        mut conn: ManagedConnection,
138        filesystem: Box<dyn Filesystem>,
139        migration_target: MigrationTarget,
140        transaction_lock: TransactionLock,
141    ) -> CryptoKeystoreResult<Self> {
142        let _sqlite = SqliteGuard::lock();
143        #[cfg(feature = "log-queries")]
144        conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query))?;
145        conn.pragma_update(None, "foreign_keys", "ON")?;
146
147        // path is an empty string for in-memory databases
148        if let Some(path) = conn.path()
149            && !path.is_empty()
150        {
151            // Enable WAL journaling mode when not in memory
152            conn.pragma_update(None, "journal_mode", "wal")?;
153        }
154
155        migrations::run_migrations(&mut conn, migration_target)?;
156
157        let conn = Arc::new(Mutex::new(conn));
158
159        Ok(Self {
160            conn,
161            filesystem: filesystem.into(),
162            transaction: Default::default(),
163            transaction_lock,
164        })
165    }
166
167    /// Open an encrypted Sqlite `Database` at the provided location.
168    ///
169    /// The database is encrypted via sqlcipher. When compiled with `target_os = "unknown"`,
170    /// it is stored in OPFS via JSPI, and WAL uses exclusive locking and an in-memory index.
171    /// Otherwise, it is stored at a path in the local filesystem.
172    pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
173        let (conn, filesystem) = Self::open_internal(path, database_key).await?;
174        Self::init(conn, filesystem, MigrationTarget::Latest)
175            .await
176            .map(Into::into)
177    }
178
179    /// Open an in-memory `Database`.
180    ///
181    /// In-memory databases are never encrypted.
182    pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
183        let connection = {
184            let _sqlite = SqliteGuard::lock();
185            ManagedConnection::from(Connection::open_in_memory()?)
186        };
187        Self::init_with_lock(
188            connection,
189            Box::new(filesystem::Nop),
190            MigrationTarget::Composite,
191            TransactionLock::new("")?,
192        )
193        .map(Into::into)
194    }
195
196    /// Open an encrypted `Database` at the provided location.
197    ///
198    /// Acts as `open`, but only migrates to the specified schema version.
199    ///
200    /// Note: this is known to work because `Self::open_internal` will only ever perform
201    /// a partial migration when `target_os = "unknown"`, where this function is not defined.
202    /// Use caution when adjusting the cfg flags here!
203    #[cfg(all(test, not(target_os = "unknown")))]
204    pub(crate) async fn open_at_schema_version(
205        path: &str,
206        database_key: &DatabaseKey,
207        migration_target: MigrationTarget,
208    ) -> CryptoKeystoreResult<Self> {
209        let (conn, filesystem) = Self::open_internal(path, database_key).await?;
210        Self::init(conn, filesystem, migration_target).await
211    }
212
213    /// Change the encryption key for this database.
214    pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
215        let mut guard = self.conn.lock().await;
216        let _sqlite = SqliteGuard::lock();
217        encryption::rekey(&mut guard, new_key)
218    }
219
220    /// Wait for any running transaction to finish, then take the connection out of this database,
221    /// preventing this from being used again.
222    ///
223    /// The returned guard keeps other processes out for as long as the caller holds it, so that
224    /// teardown is not interleaved with somebody else's transaction.
225    async fn take(self) -> CryptoKeystoreResult<(ManagedConnection, Box<dyn Filesystem>, TransactionGuard)> {
226        // Nobody ever clones `self.conn`; the Arc is only so we can have a lifetime-free guard over
227        // the interior mutex. Such a guard holds a clone, though, and on Wasm a dropped transaction
228        // defers its rollback together with its guard. Acquiring `SqliteGuard` runs that deferred
229        // cleanup and so releases the clone. After that, the strong count is 1.
230        let conn = {
231            let _sqlite = SqliteGuard::lock();
232            Arc::into_inner(self.conn)
233                .expect("nobody holds a guard over self.conn")
234                .into_inner()
235        };
236        let guard = self.transaction_lock.acquire().await?;
237        Ok((conn, self.filesystem.into_inner(), guard))
238    }
239
240    // Close this database connection
241    pub async fn close(self) -> CryptoKeystoreResult<()> {
242        let (conn, _fs, _guard) = self.take().await?;
243        conn.close().map_err(|(_conn, err)| err)?;
244        Ok(())
245    }
246
247    /// Close and remove this database.
248    ///
249    /// This deletes the database, including its encryption key.
250    /// Future opens will always succeed with any arbitrary encryption key; they will
251    /// simply open an empty database.
252    pub async fn wipe(self) -> CryptoKeystoreResult<()> {
253        let (conn, fs, _guard) = self.take().await?;
254        {
255            let _sqlite = SqliteGuard::lock();
256            conn.execute_batch(
257                "
258            PRAGMA writable_schema = 1;
259            DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
260            PRAGMA writable_schema = 0;
261            VACUUM;
262        ",
263            )?;
264        }
265        let location = conn.path().map(ToOwned::to_owned);
266        conn.close().map_err(|(_conn, err)| err)?;
267        if let Some(path) = location {
268            // not in-memory
269            fs.delete(&path).await?;
270            // `_guard` is still alive, so we unlink the lock file while still holding it; that
271            // keeps a peer from acquiring the fresh lock file before the database is really gone.
272            #[cfg(feature = "cross-process-lock")]
273            file_lock::remove_lock_file(&path).await?;
274        }
275        Ok(())
276    }
277
278    /// Get a reference to this database's connection without checking if a transaction is in flight.
279    ///
280    /// **CAUTION**: this will block until the in-flight transaction completes, if one exists.
281    ///
282    /// Most users should prefer [`Self::conn`].
283    pub(crate) async fn raw_conn(&self) -> MutexGuardArc<ManagedConnection> {
284        self.conn.lock_arc().await
285    }
286
287    /// Get the location of the database.
288    ///
289    /// Returns None if the database is in-memory.
290    pub async fn location(&self) -> Option<String> {
291        self.conn()
292            .await
293            .path()
294            .filter(|s| !s.is_empty())
295            .map(ToString::to_string)
296    }
297
298    /// Export a copy of the database to the specified path using VACUUM INTO.
299    ///
300    /// This creates a fully vacuumed and optimized copy of the database.
301    /// The copy will be encrypted with the same key as the source database.
302    ///
303    /// # Arguments
304    /// * `destination_path` - The file path where the database copy should be created
305    #[cfg(not(target_os = "unknown"))]
306    pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
307        self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
308        Ok(())
309    }
310}
311
312#[cfg(all(test, not(target_os = "unknown")))]
313mod export_test {
314    use futures_lite::future;
315
316    use crate::connection::{Database, DatabaseKey};
317
318    #[test]
319    fn can_export_database_copy() {
320        future::block_on(async {
321            // Create temporary directory
322            let temp_dir = tempfile::tempdir().unwrap();
323            let source_path = temp_dir.path().join("test_export_source.db");
324            let dest_path = temp_dir.path().join("test_export_dest.db");
325
326            // Write test database
327            std::fs::write(&source_path, super::migrations::test::DB).unwrap();
328
329            // Migrate the database to use the new key format
330            let key = DatabaseKey::generate();
331            super::migrations::migrate_db_key_type_to_bytes(
332                source_path.to_str().unwrap(),
333                super::migrations::test::OLD_KEY,
334                &key,
335            )
336            .await
337            .unwrap();
338
339            // Open the database
340            let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
341
342            // Insert test data into a test table
343            let test_data = b"test data for export verification";
344            let test_id = 12345;
345            {
346                // Create a test table
347                db.conn()
348                    .await
349                    .execute(
350                        "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
351                        [],
352                    )
353                    .unwrap();
354
355                // Insert test data
356                db.conn()
357                    .await
358                    .execute(
359                        "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
360                        [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
361                    )
362                    .unwrap();
363            }
364
365            // Export the database
366            db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
367
368            // Verify the exported database can be opened with the same key
369            let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
370
371            // Read the data from the exported database
372            {
373                let conn = exported_db.conn().await;
374                let mut stmt = conn
375                    .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
376                    .unwrap();
377                let mut rows = stmt.query([test_id]).unwrap();
378
379                let row = rows.next().unwrap().expect("Expected row to exist");
380                let read_id: i32 = row.get(0).unwrap();
381                let read_data: Vec<u8> = row.get(1).unwrap();
382
383                assert_eq!(read_id, test_id, "ID should match in exported database");
384                assert_eq!(read_data, test_data, "Data should match in exported database");
385            }
386
387            // Close databases before cleanup
388            drop(db);
389            drop(exported_db);
390
391            // temp_dir is automatically cleaned up when it goes out of scope
392        });
393    }
394}