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