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;
10mod migrations;
11mod mls;
12#[cfg(target_os = "unknown")]
13mod os_unknown;
14mod transaction;
15mod transaction_lock;
16
17use std::sync::Arc;
18
19use async_lock::{Mutex, MutexGuardArc};
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};
26use self::transaction_lock::TransactionLock;
27pub(crate) use self::{filesystem::Filesystem, transaction_lock::TransactionGuard};
28pub use self::{
29 migrations::migrate_db_key_type_to_bytes,
30 mls::{deser, ser},
31};
32use crate::{
33 CryptoKeystoreResult, DatabaseKey, Transaction, connection::migrations::MigrationTarget, unique_arc::UniqueWeak,
34};
35
36#[cfg(feature = "log-queries")]
37fn log_query(event: TraceEvent) {
38 if let TraceEvent::Stmt(_, sql) = event {
39 log::info!("{sql}")
40 }
41}
42
43#[derive(derive_more::Debug)]
46pub struct Database {
47 conn: Arc<Mutex<Connection>>,
56 pub(crate) filesystem: Mutex<Box<dyn Filesystem>>,
59 #[debug(skip)]
60 pub(crate) transaction: Mutex<Option<UniqueWeak<Transaction>>>,
61 transaction_lock: TransactionLock,
63}
64
65impl Database {
66 #[cfg_attr(not(target_os = "unknown"), expect(clippy::unused_async))]
70 async fn open_internal(
71 path: &str,
72 database_key: &DatabaseKey,
73 ) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
74 #[cfg(target_os = "unknown")]
75 let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
76
77 #[cfg(not(target_os = "unknown"))]
78 let (conn, filesystem) = {
79 let exists = std::fs::exists(path)?;
80 let mut conn = Connection::open(path)?;
81 if exists {
82 encryption::decrypt(&mut conn, database_key)?;
83 } else {
84 encryption::key(&mut conn, database_key)?;
85 }
86
87 #[cfg(target_os = "ios")]
92 if !path.is_empty() {
93 ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
94 }
95
96 (conn, filesystem::NativeFs)
97 };
98
99 let filesystem = Box::new(filesystem);
100 Ok((conn, filesystem))
101 }
102
103 async fn init(
109 conn: Connection,
110 filesystem: Box<dyn Filesystem>,
111 migration_target: MigrationTarget,
112 ) -> CryptoKeystoreResult<Self> {
113 let transaction_lock = TransactionLock::new(conn.path().unwrap_or_default())?;
114 let _guard = transaction_lock.acquire().await?;
117 Self::init_with_lock(conn, filesystem, migration_target, transaction_lock)
118 }
119
120 fn init_with_lock(
122 mut conn: Connection,
123 filesystem: Box<dyn Filesystem>,
124 migration_target: MigrationTarget,
125 transaction_lock: TransactionLock,
126 ) -> CryptoKeystoreResult<Self> {
127 #[cfg(feature = "log-queries")]
128 conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query));
129 conn.pragma_update(None, "foreign_keys", "ON")?;
130
131 if let Some(path) = conn.path()
133 && !path.is_empty()
134 {
135 conn.pragma_update(None, "journal_mode", "wal")?;
137 }
138
139 migrations::run_migrations(&mut conn, migration_target)?;
140
141 let conn = Arc::new(Mutex::new(conn));
142
143 Ok(Self {
144 conn,
145 filesystem: filesystem.into(),
146 transaction: Default::default(),
147 transaction_lock,
148 })
149 }
150
151 pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
160 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
161 Self::init(conn, filesystem, MigrationTarget::Latest)
162 .await
163 .map(Into::into)
164 }
165
166 pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
170 let connection = Connection::open_in_memory()?;
171 Self::init_with_lock(
172 connection,
173 Box::new(filesystem::Nop),
174 MigrationTarget::Composite,
175 TransactionLock::new("")?,
176 )
177 .map(Into::into)
178 }
179
180 #[cfg(all(test, not(target_os = "unknown")))]
188 pub(crate) async fn open_at_schema_version(
189 path: &str,
190 database_key: &DatabaseKey,
191 migration_target: MigrationTarget,
192 ) -> CryptoKeystoreResult<Self> {
193 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
194 Self::init(conn, filesystem, migration_target).await
195 }
196
197 pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
199 let mut guard = self.conn.lock().await;
200 encryption::rekey(&mut guard, new_key)
201 }
202
203 async fn take(self) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>, TransactionGuard)> {
209 let conn = Arc::into_inner(self.conn)
212 .expect("nobody ever clones self.conn")
213 .into_inner();
214 let guard = self.transaction_lock.acquire().await?;
215 Ok((conn, self.filesystem.into_inner(), guard))
216 }
217
218 pub async fn close(self) -> CryptoKeystoreResult<()> {
220 let (conn, _fs, _guard) = self.take().await?;
221 conn.close().map_err(|(_conn, err)| err)?;
222 Ok(())
223 }
224
225 pub async fn wipe(self) -> CryptoKeystoreResult<()> {
231 let (conn, fs, _guard) = self.take().await?;
232 conn.execute_batch(
233 "
234 PRAGMA writable_schema = 1;
235 DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
236 PRAGMA writable_schema = 0;
237 VACUUM;
238 ",
239 )?;
240 let location = conn.path().map(ToOwned::to_owned);
241 conn.close().map_err(|(_conn, err)| err)?;
242 if let Some(path) = location {
243 fs.delete(&path).await?;
245 #[cfg(feature = "cross-process-lock")]
248 file_lock::remove_lock_file(&path).await?;
249 }
250 Ok(())
251 }
252
253 pub(crate) async fn raw_conn(&self) -> MutexGuardArc<Connection> {
259 self.conn.lock_arc().await
260 }
261
262 pub async fn location(&self) -> Option<String> {
266 self.conn()
267 .await
268 .path()
269 .filter(|s| !s.is_empty())
270 .map(ToString::to_string)
271 }
272
273 #[cfg(not(target_os = "unknown"))]
281 pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
282 self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
283 Ok(())
284 }
285}
286
287#[cfg(all(test, not(target_os = "unknown")))]
288mod export_test {
289 use futures_lite::future;
290
291 use crate::connection::{Database, DatabaseKey};
292
293 #[test]
294 fn can_export_database_copy() {
295 future::block_on(async {
296 let temp_dir = tempfile::tempdir().unwrap();
298 let source_path = temp_dir.path().join("test_export_source.db");
299 let dest_path = temp_dir.path().join("test_export_dest.db");
300
301 std::fs::write(&source_path, super::migrations::test::DB).unwrap();
303
304 let key = DatabaseKey::generate();
306 super::migrations::migrate_db_key_type_to_bytes(
307 source_path.to_str().unwrap(),
308 super::migrations::test::OLD_KEY,
309 &key,
310 )
311 .await
312 .unwrap();
313
314 let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
316
317 let test_data = b"test data for export verification";
319 let test_id = 12345;
320 {
321 db.conn()
323 .await
324 .execute(
325 "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
326 [],
327 )
328 .unwrap();
329
330 db.conn()
332 .await
333 .execute(
334 "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
335 [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
336 )
337 .unwrap();
338 }
339
340 db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
342
343 let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
345
346 {
348 let conn = exported_db.conn().await;
349 let mut stmt = conn
350 .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
351 .unwrap();
352 let mut rows = stmt.query([test_id]).unwrap();
353
354 let row = rows.next().unwrap().expect("Expected row to exist");
355 let read_id: i32 = row.get(0).unwrap();
356 let read_data: Vec<u8> = row.get(1).unwrap();
357
358 assert_eq!(read_id, test_id, "ID should match in exported database");
359 assert_eq!(read_data, test_data, "Data should match in exported database");
360 }
361
362 drop(db);
364 drop(exported_db);
365
366 });
368 }
369}