Skip to main content

core_crypto_keystore/connection/
mod.rs

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