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 async fn open_internal(
70 path: &str,
71 database_key: &DatabaseKey,
72 ) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
73 #[cfg(target_os = "unknown")]
74 let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
75
76 #[cfg(not(target_os = "unknown"))]
77 let (conn, filesystem) = {
78 let exists = std::fs::exists(path)?;
79 let mut conn = Connection::open(path)?;
80 if exists {
81 encryption::decrypt(&mut conn, database_key)?;
82 } else {
83 encryption::key(&mut conn, database_key)?;
84 }
85
86 #[cfg(target_os = "ios")]
91 if !path.is_empty() {
92 ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
93 }
94
95 (conn, filesystem::NativeFs)
96 };
97
98 let filesystem = Box::new(filesystem);
99 Ok((conn, filesystem))
100 }
101
102 fn init(
108 mut conn: Connection,
109 filesystem: Box<dyn Filesystem>,
110 migration_target: MigrationTarget,
111 ) -> CryptoKeystoreResult<Self> {
112 #[cfg(feature = "log-queries")]
113 conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query));
114
115 if let Some(path) = conn.path()
117 && !path.is_empty()
118 {
119 conn.pragma_update(None, "journal_mode", "wal")?;
121 }
122
123 migrations::run_migrations(&mut conn, migration_target)?;
124
125 let transaction_lock = TransactionLock::new(conn.path().unwrap_or_default())?;
126 let conn = Arc::new(Mutex::new(conn));
127
128 Ok(Self {
129 conn,
130 filesystem: filesystem.into(),
131 transaction: Default::default(),
132 transaction_lock,
133 })
134 }
135
136 pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
145 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
146 Self::init(conn, filesystem, MigrationTarget::Latest).map(Into::into)
147 }
148
149 pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
153 let connection = Connection::open_in_memory()?;
154 Self::init(connection, Box::new(filesystem::Nop), MigrationTarget::Composite).map(Into::into)
155 }
156
157 #[cfg(all(test, not(target_os = "unknown")))]
165 pub(crate) async fn open_at_schema_version(
166 path: &str,
167 database_key: &DatabaseKey,
168 migration_target: MigrationTarget,
169 ) -> CryptoKeystoreResult<Self> {
170 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
171 Self::init(conn, filesystem, migration_target)
172 }
173
174 pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
176 let mut guard = self.conn.lock().await;
177 encryption::rekey(&mut guard, new_key)
178 }
179
180 async fn take(self) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>, TransactionGuard)> {
186 let conn = Arc::into_inner(self.conn)
189 .expect("nobody ever clones self.conn")
190 .into_inner();
191 let guard = self.transaction_lock.acquire().await?;
192 Ok((conn, self.filesystem.into_inner(), guard))
193 }
194
195 pub async fn close(self) -> CryptoKeystoreResult<()> {
197 let (conn, _fs, _guard) = self.take().await?;
198 conn.close().map_err(|(_conn, err)| err)?;
199 Ok(())
200 }
201
202 pub async fn wipe(self) -> CryptoKeystoreResult<()> {
208 let (conn, fs, _guard) = self.take().await?;
209 conn.execute_batch(
210 "
211 PRAGMA writable_schema = 1;
212 DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
213 PRAGMA writable_schema = 0;
214 VACUUM;
215 ",
216 )?;
217 let location = conn.path().map(ToOwned::to_owned);
218 conn.close().map_err(|(_conn, err)| err)?;
219 if let Some(path) = location {
220 fs.delete(&path).await?;
222 #[cfg(feature = "cross-process-lock")]
225 file_lock::remove_lock_file(&path).await?;
226 }
227 Ok(())
228 }
229
230 pub(crate) async fn raw_conn(&self) -> MutexGuardArc<Connection> {
236 self.conn.lock_arc().await
237 }
238
239 pub async fn location(&self) -> Option<String> {
243 self.conn()
244 .await
245 .path()
246 .filter(|s| !s.is_empty())
247 .map(ToString::to_string)
248 }
249
250 #[cfg(not(target_os = "unknown"))]
258 pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
259 self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
260 Ok(())
261 }
262}
263
264#[cfg(all(test, not(target_os = "unknown")))]
265mod export_test {
266 use futures_lite::future;
267
268 use crate::connection::{Database, DatabaseKey};
269
270 #[test]
271 fn can_export_database_copy() {
272 future::block_on(async {
273 let temp_dir = tempfile::tempdir().unwrap();
275 let source_path = temp_dir.path().join("test_export_source.db");
276 let dest_path = temp_dir.path().join("test_export_dest.db");
277
278 std::fs::write(&source_path, super::migrations::test::DB).unwrap();
280
281 let key = DatabaseKey::generate();
283 super::migrations::migrate_db_key_type_to_bytes(
284 source_path.to_str().unwrap(),
285 super::migrations::test::OLD_KEY,
286 &key,
287 )
288 .await
289 .unwrap();
290
291 let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
293
294 let test_data = b"test data for export verification";
296 let test_id = 12345;
297 {
298 db.conn()
300 .await
301 .execute(
302 "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
303 [],
304 )
305 .unwrap();
306
307 db.conn()
309 .await
310 .execute(
311 "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
312 [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
313 )
314 .unwrap();
315 }
316
317 db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
319
320 let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
322
323 {
325 let conn = exported_db.conn().await;
326 let mut stmt = conn
327 .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
328 .unwrap();
329 let mut rows = stmt.query([test_id]).unwrap();
330
331 let row = rows.next().unwrap().expect("Expected row to exist");
332 let read_id: i32 = row.get(0).unwrap();
333 let read_data: Vec<u8> = row.get(1).unwrap();
334
335 assert_eq!(read_id, test_id, "ID should match in exported database");
336 assert_eq!(read_data, test_data, "Data should match in exported database");
337 }
338
339 drop(db);
341 drop(exported_db);
342
343 });
345 }
346}