Skip to main content

core_crypto_keystore/connection/
mod.rs

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