core_crypto_keystore/connection/
mod.rs1mod encryption;
2mod entity_extension_methods;
3mod fetch_from_database;
4#[cfg(feature = "cross-process-lock")]
5mod file_lock;
6mod filesystem;
7#[cfg(target_os = "unknown")]
8mod idb_migration;
9#[cfg(target_os = "ios")]
10mod ios_wal_compat;
11mod migrations;
12#[cfg(target_os = "unknown")]
13mod os_unknown;
14mod transaction;
15mod transaction_lock;
16
17use std::sync::Arc;
18
19use async_lock::{Mutex, MutexGuard};
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};
26pub use self::migrations::migrate_db_key_type_to_bytes;
27use self::transaction_lock::TransactionLock;
28pub(crate) use self::{filesystem::Filesystem, transaction_lock::TransactionGuard};
29use crate::{
30 CryptoKeystoreResult, DatabaseKey, connection::migrations::MigrationTarget, transaction::Transaction,
31 unique_arc::UniqueWeak,
32};
33
34#[cfg(feature = "log-queries")]
35fn log_query(event: TraceEvent) {
36 if let TraceEvent::Stmt(_, sql) = event {
37 log::info!("{sql}")
38 }
39}
40
41#[derive(derive_more::Debug)]
44pub struct Database {
45 conn: Mutex<Connection>,
48 pub(crate) filesystem: Mutex<Box<dyn Filesystem>>,
51 #[debug(skip)]
52 pub(crate) transaction: Mutex<Option<UniqueWeak<Transaction>>>,
53 transaction_lock: TransactionLock,
55}
56
57impl Database {
58 async fn open_internal(
62 path: &str,
63 database_key: &DatabaseKey,
64 ) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>)> {
65 #[cfg(target_os = "unknown")]
66 let (conn, filesystem) = { os_unknown::open(path, database_key).await? };
67
68 #[cfg(not(target_os = "unknown"))]
69 let (conn, filesystem) = {
70 let exists = std::fs::exists(path)?;
71 let mut conn = Connection::open(path)?;
72 if exists {
73 encryption::decrypt(&mut conn, database_key)?;
74 } else {
75 encryption::key(&mut conn, database_key)?;
76 }
77
78 #[cfg(target_os = "ios")]
83 if !path.is_empty() {
84 ios_wal_compat::handle_ios_wal_compat(&conn, path)?;
85 }
86
87 (conn, filesystem::NativeFs)
88 };
89
90 let filesystem = Box::new(filesystem);
91 Ok((conn, filesystem))
92 }
93
94 fn init(
100 mut conn: Connection,
101 filesystem: Box<dyn Filesystem>,
102 migration_target: MigrationTarget,
103 ) -> CryptoKeystoreResult<Self> {
104 #[cfg(feature = "log-queries")]
105 conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(log_query));
106
107 if let Some(path) = conn.path()
109 && !path.is_empty()
110 {
111 conn.pragma_update(None, "journal_mode", "wal")?;
113 }
114
115 migrations::run_migrations(&mut conn, migration_target)?;
116
117 let transaction_lock = TransactionLock::new(conn.path().unwrap_or_default())?;
118 let conn = conn.into();
119
120 Ok(Self {
121 conn,
122 filesystem: filesystem.into(),
123 transaction: Default::default(),
124 transaction_lock,
125 })
126 }
127
128 pub async fn open(path: &str, database_key: &DatabaseKey) -> CryptoKeystoreResult<Arc<Self>> {
137 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
138 Self::init(conn, filesystem, MigrationTarget::Latest).map(Into::into)
139 }
140
141 pub fn open_in_memory() -> CryptoKeystoreResult<Arc<Self>> {
145 let connection = Connection::open_in_memory()?;
146 Self::init(connection, Box::new(filesystem::Nop), MigrationTarget::Latest).map(Into::into)
147 }
148
149 #[cfg(all(test, not(target_os = "unknown")))]
157 pub(crate) async fn open_at_schema_version(
158 path: &str,
159 database_key: &DatabaseKey,
160 migration_target: MigrationTarget,
161 ) -> CryptoKeystoreResult<Self> {
162 let (conn, filesystem) = Self::open_internal(path, database_key).await?;
163 Self::init(conn, filesystem, migration_target)
164 }
165
166 pub async fn update_key(&self, new_key: &DatabaseKey) -> CryptoKeystoreResult<()> {
168 let mut guard = self.conn.lock().await;
169 encryption::rekey(&mut guard, new_key)
170 }
171
172 async fn take(self) -> CryptoKeystoreResult<(Connection, Box<dyn Filesystem>, TransactionGuard)> {
178 let guard = self.transaction_lock.acquire().await?;
179 Ok((self.conn.into_inner(), self.filesystem.into_inner(), guard))
180 }
181
182 pub async fn close(self) -> CryptoKeystoreResult<()> {
184 let (conn, _fs, _guard) = self.take().await?;
185 conn.close().map_err(|(_conn, err)| err)?;
186 Ok(())
187 }
188
189 pub async fn wipe(self) -> CryptoKeystoreResult<()> {
195 let (conn, fs, _guard) = self.take().await?;
196 conn.execute_batch(
197 "
198 PRAGMA writable_schema = 1;
199 DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
200 PRAGMA writable_schema = 0;
201 VACUUM;
202 ",
203 )?;
204 let location = conn.path().map(ToOwned::to_owned);
205 conn.close().map_err(|(_conn, err)| err)?;
206 if let Some(path) = location {
207 fs.delete(&path).await?;
209 #[cfg(feature = "cross-process-lock")]
212 file_lock::remove_lock_file(&path).await?;
213 }
214 Ok(())
215 }
216
217 pub(crate) async fn conn(&self) -> MutexGuard<'_, Connection> {
219 self.conn.lock().await
220 }
221
222 pub async fn location(&self) -> Option<String> {
226 self.conn()
227 .await
228 .path()
229 .filter(|s| !s.is_empty())
230 .map(ToString::to_string)
231 }
232
233 #[cfg(not(target_os = "unknown"))]
241 pub async fn export_copy(&self, destination_path: &str) -> CryptoKeystoreResult<()> {
242 self.conn().await.execute("VACUUM INTO ?1", [destination_path])?;
243 Ok(())
244 }
245}
246
247#[cfg(all(test, not(target_os = "unknown")))]
248mod export_test {
249 use futures_lite::future;
250
251 use crate::connection::{Database, DatabaseKey};
252
253 #[test]
254 fn can_export_database_copy() {
255 future::block_on(async {
256 let temp_dir = tempfile::tempdir().unwrap();
258 let source_path = temp_dir.path().join("test_export_source.db");
259 let dest_path = temp_dir.path().join("test_export_dest.db");
260
261 std::fs::write(&source_path, super::migrations::test::DB).unwrap();
263
264 let key = DatabaseKey::generate();
266 super::migrations::migrate_db_key_type_to_bytes(
267 source_path.to_str().unwrap(),
268 super::migrations::test::OLD_KEY,
269 &key,
270 )
271 .await
272 .unwrap();
273
274 let db = Database::open(source_path.to_str().unwrap(), &key).await.unwrap();
276
277 let test_data = b"test data for export verification";
279 let test_id = 12345;
280 {
281 db.conn()
283 .await
284 .execute(
285 "CREATE TABLE IF NOT EXISTS test_export_data (id INTEGER PRIMARY KEY, data BLOB)",
286 [],
287 )
288 .unwrap();
289
290 db.conn()
292 .await
293 .execute(
294 "INSERT INTO test_export_data (id, data) VALUES (?1, ?2)",
295 [&test_id as &dyn rusqlite::ToSql, &test_data.as_slice()],
296 )
297 .unwrap();
298 }
299
300 db.export_copy(dest_path.to_str().unwrap()).await.unwrap();
302
303 let exported_db = Database::open(dest_path.to_str().unwrap(), &key).await.unwrap();
305
306 {
308 let conn = exported_db.conn().await;
309 let mut stmt = conn
310 .prepare("SELECT id, data FROM test_export_data WHERE id = ?1")
311 .unwrap();
312 let mut rows = stmt.query([test_id]).unwrap();
313
314 let row = rows.next().unwrap().expect("Expected row to exist");
315 let read_id: i32 = row.get(0).unwrap();
316 let read_data: Vec<u8> = row.get(1).unwrap();
317
318 assert_eq!(read_id, test_id, "ID should match in exported database");
319 assert_eq!(read_data, test_data, "Data should match in exported database");
320 }
321
322 drop(db);
324 drop(exported_db);
325
326 });
328 }
329}