Skip to main content

core_crypto_keystore/connection/
mod.rs

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