core_crypto_keystore/connection/
mod.rs1mod 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 keystore_transaction;
10mod migrations;
11#[cfg(target_os = "unknown")]
12mod os_unknown;
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::KeystoreTransaction,
27};
28
29#[cfg(feature = "log-queries")]
30fn log_query(event: TraceEvent) {
31 if let TraceEvent::Stmt(_, sql) = event {
32 log::info!("{sql}")
33 }
34}
35
36#[derive(Debug)]
39pub struct Database {
40 conn: Mutex<Connection>,
43 pub(crate) filesystem: Mutex<Box<dyn Filesystem>>,
46 pub(crate) transaction: Mutex<Option<KeystoreTransaction>>,
47 transaction_semaphore: Arc<Semaphore>,
50}
51
52impl Database {
53 async fn open_internal(
57 path: &str,
58 database_key: &DatabaseKey,
59 ) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
60 #[cfg(target_os = "unknown")]
61 let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
62
63 #[cfg(not(target_os = "unknown"))]
64 let (conn, filesystem) = {
65 let exists = std::fs::exists(path)?;
66 let mut conn = Connection::open(path)?;
67 if exists {
68 encryption::decrypt(&mut conn, database_key)?;
69 } else {
70 encryption::key(&mut conn, database_key)?;
71 }
72
73 #[cfg(target_os = "ios")]
78 if !path.is_empty() {
79 ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
80 }
81
82 (conn, filesystem::NativeFs)
83 };
84
85 let filesystem = Box::new(filesystem);
86 Ok((conn, filesystem))
87 }
88
89 fn init(
95 mut conn: Connection,
96 filesystem: Box<dyn Filesystem>,
97 migration_target: MigrationTarget,
98 ) -> CryptoKeystoreResult<Self> {
99 const ALLOWED_CONCURRENT_TRANSACTIONS_COUNT: usize = 1;
100
101 #[cfg(feature = "log-queries")]
102 conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query));
103
104 if let Some(path) = conn.path()
106 && !path.is_empty()
107 {
108 conn.pragma_update(None, "journal_mode", "wal")?;
110 }
111
112 migrations::run_migrations(&mut conn, migration_target)?;
113 let conn = conn.into();
114
115 Ok(Self {
116 conn,
117 filesystem: filesystem.into(),
118 transaction: Default::default(),
119 transaction_semaphore: Arc::new(Semaphore::new(ALLOWED_CONCURRENT_TRANSACTIONS_COUNT)),
120 })
121 }
122
123 pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
132 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
133 Self::init(conn, filesystem, MigrationTarget::Latest).map(Into::into)
134 }
135
136 pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
140 let connection = Connection::open_in_memory()?;
141 Self::init(connection, Box::new(filesystem::Nop), MigrationTarget::Latest).map(Into::into)
142 }
143
144 #[cfg(all(test, not(target_os = "unknown")))]
152 pub(crate) async fn open_at_schema_version(
153 path: &str,
154 database_key: &DatabaseKey,
155 migration_target: MigrationTarget,
156 ) -> CryptoKeystoreResult<Self> {
157 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
158 Self::init(conn, filesystem, migration_target)
159 }
160
161 pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
163 let mut guard = self.conn.lock().await;
164 encryption::rekey(&mut guard, new_key)
165 }
166
167 async fn take(self) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
170 let _semaphore = self.transaction_semaphore.acquire().await;
171 Ok((self.conn.into_inner(), self.filesystem.into_inner()))
172 }
173
174 pub async fn close(self) -> CryptoKeystoreResult<()> {
176 let (conn, _fs) = self.take().await?;
177 conn.close().map_err(|(_conn, err)| err)?;
178 Ok(())
179 }
180
181 pub async fn wipe(self) -> CryptoKeystoreResult<()> {
187 let (conn, fs) = self.take().await?;
188 conn.execute_batch(
189 "
190 PRAGMA writable_schema = 1;
191 DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
192 PRAGMA writable_schema = 0;
193 VACUUM;
194 ",
195 )?;
196 let location = conn.path().map(ToOwned::to_owned);
197 conn.close().map_err(|(_conn, err)| err)?;
198 if let Some(path) = location {
199 fs.delete(&path).await?;
201 }
202 Ok(())
203 }
204
205 pub(crate) async fn conn(&self) -> MutexGuard<'_, Connection> {
207 self.conn.lock().await
208 }
209
210 pub async fn location(&self) -> Option<String> {
214 self.conn()
215 .await
216 .path()
217 .filter(|s| !s.is_empty())
218 .map(ToString::to_string)
219 }
220
221 #[cfg(not(target_os = "unknown"))]
229 pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
230 self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
231 Ok(())
232 }
233}
234
235#[cfg(all(test, not(target_os = "unknown")))]
236mod export_test {
237 use futures_lite::future;
238
239 use crate::connection::{Database, DatabaseKey};
240
241 #[test]
242 fn can_export_database_copy() {
243 future::block_on(async {
244 let temp_dir = tempfile::tempdir().unwrap();
246 let source_path = temp_dir.path().join("test_export_source.db");
247 let dest_path = temp_dir.path().join("test_export_dest.db");
248
249 std::fs::write(&source_path, super::migrations::test::DB).unwrap();
251
252 let key = DatabaseKey::generate();
254 super::migrations::migrate_db_key_type_to_bytes(
255 source_path.to_str().unwrap(),
256 super::migrations::test::OLD_KEY,
257 &key,
258 )
259 .await
260 .unwrap();
261
262 let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
264
265 let test_data = b"test data for export verification";
267 let test_id = 12345;
268 {
269 db.conn()
271 .await
272 .execute(
273 "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
274 [],
275 )
276 .unwrap();
277
278 db.conn()
280 .await
281 .execute(
282 "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
283 [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
284 )
285 .unwrap();
286 }
287
288 db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
290
291 let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
293
294 {
296 let conn = exported_db.conn().await;
297 let mut stmt = conn
298 .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
299 .unwrap();
300 let mut rows = stmt.query([test_id]).unwrap();
301
302 let row = rows.next().unwrap().expect("Expected row to exist");
303 let read_id: i32 = row.get(0).unwrap();
304 let read_data: Vec<u8> = row.get(1).unwrap();
305
306 assert_eq!(read_id, test_id, "ID should match in exported database");
307 assert_eq!(read_data, test_data, "Data should match in exported database");
308 }
309
310 drop(db);
312 drop(exported_db);
313
314 });
316 }
317}