core_crypto_keystore/connection/
mod.rs1mod encryption;
2mod fetch_from_database;
3mod filesystem;
4#[cfg(target_os = "unknown")]
5mod idb_migration;
6#[cfg(target_os = "ios")]
7mod ios_wal_compat;
8mod migrations;
9mod mls;
10#[cfg(target_os = "unknown")]
11mod os_unknown;
12mod transaction;
13
14use std::sync::Arc;
15
16use async_lock::{Mutex, MutexGuardArc, 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::{
25 migrations::migrate_db_key_type_to_bytes,
26 mls::{deser, ser},
27};
28use crate::{
29 CryptoKeystoreResult, DatabaseKey, Transaction, connection::migrations::MigrationTarget, unique_arc::UniqueWeak,
30};
31
32#[cfg(feature = "log-queries")]
33fn log_query(event: TraceEvent) {
34 if let TraceEvent::Stmt(_, sql) = event {
35 log::info!("{sql}")
36 }
37}
38
39#[derive(derive_more::Debug)]
42pub struct Database {
43 conn: Arc<Mutex<Connection>>,
52 pub(crate) filesystem: Mutex<Box<dyn Filesystem>>,
55 #[debug(skip)]
56 pub(crate) transaction: Mutex<Option<UniqueWeak<Transaction>>>,
57 transaction_semaphore: Arc<Semaphore>,
60}
61
62impl Database {
63 async fn open_internal(
67 path: &str,
68 database_key: &DatabaseKey,
69 ) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
70 #[cfg(target_os = "unknown")]
71 let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
72
73 #[cfg(not(target_os = "unknown"))]
74 let (conn, filesystem) = {
75 let exists = std::fs::exists(path)?;
76 let mut conn = Connection::open(path)?;
77 if exists {
78 encryption::decrypt(&mut conn, database_key)?;
79 } else {
80 encryption::key(&mut conn, database_key)?;
81 }
82
83 #[cfg(target_os = "ios")]
88 if !path.is_empty() {
89 ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
90 }
91
92 (conn, filesystem::NativeFs)
93 };
94
95 let filesystem = Box::new(filesystem);
96 Ok((conn, filesystem))
97 }
98
99 fn init(
105 mut conn: Connection,
106 filesystem: Box<dyn Filesystem>,
107 migration_target: MigrationTarget,
108 ) -> CryptoKeystoreResult<Self> {
109 const ALLOWED_CONCURRENT_TRANSACTIONS_COUNT: usize = 1;
110
111 #[cfg(feature = "log-queries")]
112 conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query));
113
114 if let Some(path) = conn.path()
116 && !path.is_empty()
117 {
118 conn.pragma_update(None, "journal_mode", "wal")?;
120 }
121
122 migrations::run_migrations(&mut conn, migration_target)?;
123
124 let conn = Arc::new(Mutex::new(conn));
125
126 Ok(Self {
127 conn,
128 filesystem: filesystem.into(),
129 transaction: Default::default(),
130 transaction_semaphore: Arc::new(Semaphore::new(ALLOWED_CONCURRENT_TRANSACTIONS_COUNT)),
131 })
132 }
133
134 pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
143 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
144 Self::init(conn, filesystem, MigrationTarget::Latest).map(Into::into)
145 }
146
147 pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
151 let connection = Connection::open_in_memory()?;
152 Self::init(connection, Box::new(filesystem::Nop), MigrationTarget::Latest).map(Into::into)
153 }
154
155 #[cfg(all(test, not(target_os = "unknown")))]
163 pub(crate) async fn open_at_schema_version(
164 path: &str,
165 database_key: &DatabaseKey,
166 migration_target: MigrationTarget,
167 ) -> CryptoKeystoreResult<Self> {
168 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
169 Self::init(conn, filesystem, migration_target)
170 }
171
172 pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
174 let mut guard = self.conn.lock().await;
175 encryption::rekey(&mut guard, new_key)
176 }
177
178 async fn take(self) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
184 let conn = Arc::into_inner(self.conn)
187 .expect("nobody ever clones self.conn")
188 .into_inner();
189 let _semaphore = self.transaction_semaphore.acquire().await;
190 Ok((conn, self.filesystem.into_inner()))
191 }
192
193 pub async fn close(self) -> CryptoKeystoreResult<()> {
195 let (conn, _fs) = self.take().await?;
196 conn.close().map_err(|(_conn, err)| err)?;
197 Ok(())
198 }
199
200 pub async fn wipe(self) -> CryptoKeystoreResult<()> {
206 let (conn, fs) = self.take().await?;
207 conn.execute_batch(
208 "
209 PRAGMA writable_schema = 1;
210 DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
211 PRAGMA writable_schema = 0;
212 VACUUM;
213 ",
214 )?;
215 let location = conn.path().map(ToOwned::to_owned);
216 conn.close().map_err(|(_conn, err)| err)?;
217 if let Some(path) = location {
218 fs.delete(&path).await?;
220 }
221 Ok(())
222 }
223
224 pub(crate) async fn raw_conn(&self) -> MutexGuardArc<Connection> {
230 self.conn.lock_arc().await
231 }
232
233 pub async fn location(&self) -> Option<String> {
237 self.conn()
238 .await
239 .path()
240 .filter(|s| !s.is_empty())
241 .map(ToString::to_string)
242 }
243
244 #[cfg(not(target_os = "unknown"))]
252 pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
253 self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
254 Ok(())
255 }
256}
257
258#[cfg(all(test, not(target_os = "unknown")))]
259mod export_test {
260 use futures_lite::future;
261
262 use crate::connection::{Database, DatabaseKey};
263
264 #[test]
265 fn can_export_database_copy() {
266 future::block_on(async {
267 let temp_dir = tempfile::tempdir().unwrap();
269 let source_path = temp_dir.path().join("test_export_source.db");
270 let dest_path = temp_dir.path().join("test_export_dest.db");
271
272 std::fs::write(&source_path, super::migrations::test::DB).unwrap();
274
275 let key = DatabaseKey::generate();
277 super::migrations::migrate_db_key_type_to_bytes(
278 source_path.to_str().unwrap(),
279 super::migrations::test::OLD_KEY,
280 &key,
281 )
282 .await
283 .unwrap();
284
285 let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
287
288 let test_data = b"test data for export verification";
290 let test_id = 12345;
291 {
292 db.conn()
294 .await
295 .execute(
296 "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
297 [],
298 )
299 .unwrap();
300
301 db.conn()
303 .await
304 .execute(
305 "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
306 [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
307 )
308 .unwrap();
309 }
310
311 db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
313
314 let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
316
317 {
319 let conn = exported_db.conn().await;
320 let mut stmt = conn
321 .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
322 .unwrap();
323 let mut rows = stmt.query([test_id]).unwrap();
324
325 let row = rows.next().unwrap().expect("Expected row to exist");
326 let read_id: i32 = row.get(0).unwrap();
327 let read_data: Vec<u8> = row.get(1).unwrap();
328
329 assert_eq!(read_id, test_id, "ID should match in exported database");
330 assert_eq!(read_data, test_data, "Data should match in exported database");
331 }
332
333 drop(db);
335 drop(exported_db);
336
337 });
339 }
340}