core_crypto_keystore/connection/
mod.rs1mod 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;
10#[cfg(target_os = "unknown")]
11mod jspi_test;
12pub(crate) mod managed_connection;
13mod migrations;
14mod mls;
15#[cfg(target_os = "unknown")]
16mod opfs_vfs;
17#[cfg(target_os = "unknown")]
18mod os_unknown;
19mod transaction;
20mod transaction_lock;
21
22use std::sync::Arc;
23
24use async_lock::{Mutex, MutexGuardArc};
25use rusqlite::Connection;
26#[cfg(feature = "log-queries")]
27use rusqlite::trace::{TraceEvent, TraceEventCodes};
28
29#[cfg(target_os = "unknown")]
30pub use self::idb_migration::{delete_legacy_idb, legacy_idb_exists};
31#[cfg(target_os = "unknown")]
32pub use self::jspi_test::run_in_jspi;
33use self::transaction_lock::TransactionLock;
34pub(crate) use self::{
35 filesystem::Filesystem,
36 managed_connection::{ManagedConnection, SqliteGuard},
37 transaction_lock::TransactionGuard,
38};
39pub use self::{
40 migrations::migrate_db_key_type_to_bytes,
41 mls::{deser, ser},
42};
43use crate::{
44 CryptoKeystoreResult, DatabaseKey, Transaction, connection::migrations::MigrationTarget, unique_arc::UniqueWeak,
45};
46
47#[cfg(feature = "log-queries")]
48fn log_query(event: TraceEvent) {
49 if let TraceEvent::Stmt(_, sql) = event {
50 log::info!("{sql}")
51 }
52}
53
54#[derive(derive_more::Debug)]
57pub struct Database {
58 conn: Arc<Mutex<ManagedConnection>>,
68 pub(crate) filesystem: Mutex<Box<dyn Filesystem>>,
71 #[debug(skip)]
72 pub(crate) transaction: Mutex<Option<UniqueWeak<Transaction>>>,
73 transaction_lock: TransactionLock,
75}
76
77impl Database {
78 #[cfg_attr(not(target_os = "unknown"), expect(clippy::unused_async))]
82 async fn open_internal(
83 path: &str,
84 database_key: &DatabaseKey,
85 ) -> CryptoKeystoreResult<(ManagedConnection, Box<dyn Filesystem>)> {
86 #[cfg(target_os = "unknown")]
87 let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
88
89 #[cfg(not(target_os = "unknown"))]
90 let (conn, filesystem) = {
91 let exists = std::fs::exists(path)?;
92 let mut conn = Connection::open(path)?;
93 if exists {
94 encryption::decrypt(&mut conn, database_key)?;
95 } else {
96 encryption::key(&mut conn, database_key)?;
97 }
98
99 #[cfg(target_os = "ios")]
104 if !path.is_empty() {
105 ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
106 }
107
108 (conn, filesystem::NativeFs)
109 };
110
111 let filesystem = Box::new(filesystem);
112 Ok((conn, filesystem))
113 }
114
115 async fn init(
121 conn: ManagedConnection,
122 filesystem: Box<dyn Filesystem>,
123 migration_target: MigrationTarget,
124 ) -> CryptoKeystoreResult<Self> {
125 let transaction_lock = {
126 let _sqlite = SqliteGuard::lock();
127 TransactionLock::new(conn.path().unwrap_or_default())?
128 };
129 let _guard = transaction_lock.acquire().await?;
132 Self::init_with_lock(conn, filesystem, migration_target, transaction_lock)
133 }
134
135 fn init_with_lock(
137 mut conn: ManagedConnection,
138 filesystem: Box<dyn Filesystem>,
139 migration_target: MigrationTarget,
140 transaction_lock: TransactionLock,
141 ) -> CryptoKeystoreResult<Self> {
142 let _sqlite = SqliteGuard::lock();
143 #[cfg(feature = "log-queries")]
144 conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query))?;
145 conn.pragma_update(None, "foreign_keys", "ON")?;
146
147 if let Some(path) = conn.path()
149 && !path.is_empty()
150 {
151 conn.pragma_update(None, "journal_mode", "wal")?;
153 }
154
155 migrations::run_migrations(&mut conn, migration_target)?;
156
157 let conn = Arc::new(Mutex::new(conn));
158
159 Ok(Self {
160 conn,
161 filesystem: filesystem.into(),
162 transaction: Default::default(),
163 transaction_lock,
164 })
165 }
166
167 pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
173 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
174 Self::init(conn, filesystem, MigrationTarget::Latest)
175 .await
176 .map(Into::into)
177 }
178
179 pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
183 let connection = {
184 let _sqlite = SqliteGuard::lock();
185 ManagedConnection::from(Connection::open_in_memory()?)
186 };
187 Self::init_with_lock(
188 connection,
189 Box::new(filesystem::Nop),
190 MigrationTarget::Composite,
191 TransactionLock::new("")?,
192 )
193 .map(Into::into)
194 }
195
196 #[cfg(all(test, not(target_os = "unknown")))]
204 pub(crate) async fn open_at_schema_version(
205 path: &str,
206 database_key: &DatabaseKey,
207 migration_target: MigrationTarget,
208 ) -> CryptoKeystoreResult<Self> {
209 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
210 Self::init(conn, filesystem, migration_target).await
211 }
212
213 pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
215 let mut guard = self.conn.lock().await;
216 let _sqlite = SqliteGuard::lock();
217 encryption::rekey(&mut guard, new_key)
218 }
219
220 async fn take(self) -> CryptoKeystoreResult<(ManagedConnection, Box<dyn Filesystem>, TransactionGuard)> {
226 let conn = {
231 let _sqlite = SqliteGuard::lock();
232 Arc::into_inner(self.conn)
233 .expect("nobody holds a guard over self.conn")
234 .into_inner()
235 };
236 let guard = self.transaction_lock.acquire().await?;
237 Ok((conn, self.filesystem.into_inner(), guard))
238 }
239
240 pub async fn close(self) -> CryptoKeystoreResult<()> {
242 let (conn, _fs, _guard) = self.take().await?;
243 conn.close().map_err(|(_conn, err)| err)?;
244 Ok(())
245 }
246
247 pub async fn wipe(self) -> CryptoKeystoreResult<()> {
253 let (conn, fs, _guard) = self.take().await?;
254 {
255 let _sqlite = SqliteGuard::lock();
256 conn.execute_batch(
257 "
258 PRAGMA writable_schema = 1;
259 DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
260 PRAGMA writable_schema = 0;
261 VACUUM;
262 ",
263 )?;
264 }
265 let location = conn.path().map(ToOwned::to_owned);
266 conn.close().map_err(|(_conn, err)| err)?;
267 if let Some(path) = location {
268 fs.delete(&path).await?;
270 #[cfg(feature = "cross-process-lock")]
273 file_lock::remove_lock_file(&path).await?;
274 }
275 Ok(())
276 }
277
278 pub(crate) async fn raw_conn(&self) -> MutexGuardArc<ManagedConnection> {
284 self.conn.lock_arc().await
285 }
286
287 pub async fn location(&self) -> Option<String> {
291 self.conn()
292 .await
293 .path()
294 .filter(|s| !s.is_empty())
295 .map(ToString::to_string)
296 }
297
298 #[cfg(not(target_os = "unknown"))]
306 pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
307 self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
308 Ok(())
309 }
310}
311
312#[cfg(all(test, not(target_os = "unknown")))]
313mod export_test {
314 use futures_lite::future;
315
316 use crate::connection::{Database, DatabaseKey};
317
318 #[test]
319 fn can_export_database_copy() {
320 future::block_on(async {
321 let temp_dir = tempfile::tempdir().unwrap();
323 let source_path = temp_dir.path().join("test_export_source.db");
324 let dest_path = temp_dir.path().join("test_export_dest.db");
325
326 std::fs::write(&source_path, super::migrations::test::DB).unwrap();
328
329 let key = DatabaseKey::generate();
331 super::migrations::migrate_db_key_type_to_bytes(
332 source_path.to_str().unwrap(),
333 super::migrations::test::OLD_KEY,
334 &key,
335 )
336 .await
337 .unwrap();
338
339 let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
341
342 let test_data = b"test data for export verification";
344 let test_id = 12345;
345 {
346 db.conn()
348 .await
349 .execute(
350 "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
351 [],
352 )
353 .unwrap();
354
355 db.conn()
357 .await
358 .execute(
359 "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
360 [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
361 )
362 .unwrap();
363 }
364
365 db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
367
368 let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
370
371 {
373 let conn = exported_db.conn().await;
374 let mut stmt = conn
375 .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
376 .unwrap();
377 let mut rows = stmt.query([test_id]).unwrap();
378
379 let row = rows.next().unwrap().expect("Expected row to exist");
380 let read_id: i32 = row.get(0).unwrap();
381 let read_data: Vec<u8> = row.get(1).unwrap();
382
383 assert_eq!(read_id, test_id, "ID should match in exported database");
384 assert_eq!(read_data, test_data, "Data should match in exported database");
385 }
386
387 drop(db);
389 drop(exported_db);
390
391 });
393 }
394}