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