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