core_crypto_keystore/connection/
mod.rs

1use std::fmt;
2use std::ops::Deref;
3
4use sha2::{Digest as _, Sha256};
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7pub mod platform {
8    cfg_if::cfg_if! {
9        if #[cfg(target_family = "wasm")] {
10            mod wasm;
11            pub use wasm::keystore_v_1_0_0;
12            pub use self::wasm::WasmConnection as KeystoreDatabaseConnection;
13            pub use wasm::storage;
14            pub use self::wasm::storage::WasmStorageTransaction as TransactionWrapper;
15
16            /// TODO: this is here only because it's needed for the cryptobox migration test.
17            ///       Once we drop cryptobox migration and the related test, drop this too.
18            pub use wasm::open_and_migrate_pre_v4;
19        } else {
20            mod generic;
21            pub use self::generic::SqlCipherConnection as KeystoreDatabaseConnection;
22            pub use self::generic::TransactionWrapper;
23        }
24    }
25}
26
27pub use self::platform::*;
28use crate::entities::{Entity, EntityFindParams, StringEntityId};
29use std::ops::DerefMut;
30
31use crate::entities::{EntityTransactionExt, UniqueEntity};
32use crate::transaction::KeystoreTransaction;
33use crate::{CryptoKeystoreError, CryptoKeystoreResult};
34use async_lock::{Mutex, MutexGuard, Semaphore};
35use std::sync::Arc;
36
37/// Limit on the length of a blob to be stored in the database.
38///
39/// This limit applies to both SQLCipher-backed stores and WASM.
40/// This limit is conservative on purpose when targeting WASM, as the lower bound that exists is Safari with a limit of 1GB per origin.
41///
42/// See: [SQLite limits](https://www.sqlite.org/limits.html)
43/// See: [IndexedDB limits](https://stackoverflow.com/a/63019999/1934177)
44pub const MAX_BLOB_LEN: usize = 1_000_000_000;
45
46#[cfg(not(target_family = "wasm"))]
47// ? Because of UniFFI async requirements, we need our keystore to be Send as well now
48pub trait DatabaseConnectionRequirements: Sized + Send {}
49#[cfg(target_family = "wasm")]
50// ? On the other hand, things cannot be Send on WASM because of platform restrictions (all things are copied across the FFI)
51pub trait DatabaseConnectionRequirements: Sized {}
52
53/// The key used to encrypt the database.
54#[derive(Clone, Zeroize, ZeroizeOnDrop, derive_more::From)]
55pub struct DatabaseKey([u8; Self::LEN]);
56
57impl DatabaseKey {
58    pub const LEN: usize = 32;
59
60    pub fn generate() -> DatabaseKey {
61        DatabaseKey(rand::random::<[u8; Self::LEN]>())
62    }
63}
64
65impl fmt::Debug for DatabaseKey {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
67        f.write_str("DatabaseKey(hash=")?;
68        for x in Sha256::digest(self).as_slice().iter().take(10) {
69            fmt::LowerHex::fmt(x, f)?
70        }
71        f.write_str("...)")
72    }
73}
74
75impl AsRef<[u8]> for DatabaseKey {
76    fn as_ref(&self) -> &[u8] {
77        &self.0
78    }
79}
80
81impl Deref for DatabaseKey {
82    type Target = [u8];
83
84    fn deref(&self) -> &Self::Target {
85        &self.0
86    }
87}
88
89impl TryFrom<&[u8]> for DatabaseKey {
90    type Error = CryptoKeystoreError;
91
92    fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
93        if buf.len() != Self::LEN {
94            Err(CryptoKeystoreError::InvalidDbKeySize {
95                expected: Self::LEN,
96                actual: buf.len(),
97            })
98        } else {
99            Ok(Self(buf.try_into().unwrap()))
100        }
101    }
102}
103
104#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
105#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
106pub trait DatabaseConnection<'a>: DatabaseConnectionRequirements {
107    type Connection: 'a;
108
109    async fn open(name: &str, key: &DatabaseKey) -> CryptoKeystoreResult<Self>;
110
111    async fn open_in_memory(name: &str, key: &DatabaseKey) -> CryptoKeystoreResult<Self>;
112
113    async fn close(self) -> CryptoKeystoreResult<()>;
114
115    /// Default implementation of wipe
116    async fn wipe(self) -> CryptoKeystoreResult<()> {
117        self.close().await
118    }
119
120    fn check_buffer_size(size: usize) -> CryptoKeystoreResult<()> {
121        #[cfg(not(target_family = "wasm"))]
122        if size > i32::MAX as usize {
123            return Err(CryptoKeystoreError::BlobTooBig);
124        }
125
126        if size >= MAX_BLOB_LEN {
127            return Err(CryptoKeystoreError::BlobTooBig);
128        }
129
130        Ok(())
131    }
132}
133
134#[derive(Debug, Clone)]
135pub struct Connection {
136    pub(crate) conn: Arc<Mutex<KeystoreDatabaseConnection>>,
137    pub(crate) transaction: Arc<Mutex<Option<KeystoreTransaction>>>,
138    transaction_semaphore: Arc<Semaphore>,
139}
140
141const ALLOWED_CONCURRENT_TRANSACTIONS_COUNT: usize = 1;
142
143/// Interface to fetch from the database either from the connection directly or through a
144/// transaaction
145#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
146#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
147pub trait FetchFromDatabase: Send + Sync {
148    async fn find<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(
149        &self,
150        id: &[u8],
151    ) -> CryptoKeystoreResult<Option<E>>;
152
153    async fn find_unique<U: UniqueEntity<ConnectionType = KeystoreDatabaseConnection>>(
154        &self,
155    ) -> CryptoKeystoreResult<U>;
156
157    async fn find_all<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(
158        &self,
159        params: EntityFindParams,
160    ) -> CryptoKeystoreResult<Vec<E>>;
161
162    async fn find_many<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(
163        &self,
164        ids: &[Vec<u8>],
165    ) -> CryptoKeystoreResult<Vec<E>>;
166    async fn count<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(&self) -> CryptoKeystoreResult<usize>;
167}
168
169// SAFETY: this has mutexes and atomics protecting underlying data so this is safe to share between threads
170unsafe impl Send for Connection {}
171// SAFETY: this has mutexes and atomics protecting underlying data so this is safe to share between threads
172unsafe impl Sync for Connection {}
173
174impl Connection {
175    pub async fn open_with_key(name: impl AsRef<str>, key: &DatabaseKey) -> CryptoKeystoreResult<Self> {
176        let conn = KeystoreDatabaseConnection::open(name.as_ref(), key).await?.into();
177        #[allow(clippy::arc_with_non_send_sync)] // see https://github.com/rustwasm/wasm-bindgen/pull/955
178        let conn = Arc::new(conn);
179        Ok(Self {
180            conn,
181            transaction: Default::default(),
182            transaction_semaphore: Arc::new(Semaphore::new(ALLOWED_CONCURRENT_TRANSACTIONS_COUNT)),
183        })
184    }
185
186    pub async fn open_in_memory_with_key(name: impl AsRef<str>, key: &DatabaseKey) -> CryptoKeystoreResult<Self> {
187        let conn = KeystoreDatabaseConnection::open_in_memory(name.as_ref(), key)
188            .await?
189            .into();
190        #[allow(clippy::arc_with_non_send_sync)] // see https://github.com/rustwasm/wasm-bindgen/pull/955
191        let conn = Arc::new(conn);
192        Ok(Self {
193            conn,
194            transaction: Default::default(),
195            transaction_semaphore: Arc::new(Semaphore::new(ALLOWED_CONCURRENT_TRANSACTIONS_COUNT)),
196        })
197    }
198
199    pub async fn borrow_conn(&self) -> CryptoKeystoreResult<MutexGuard<'_, KeystoreDatabaseConnection>> {
200        Ok(self.conn.lock().await)
201    }
202
203    pub async fn migrate_db_key_type_to_bytes(
204        name: &str,
205        old_key: &str,
206        new_key: &DatabaseKey,
207    ) -> CryptoKeystoreResult<()> {
208        KeystoreDatabaseConnection::migrate_db_key_type_to_bytes(name, old_key, new_key).await
209    }
210
211    pub async fn wipe(self) -> CryptoKeystoreResult<()> {
212        if self.transaction.lock().await.is_some() {
213            return Err(CryptoKeystoreError::TransactionInProgress {
214                attempted_operation: "wipe()".to_string(),
215            });
216        }
217        let conn: KeystoreDatabaseConnection = Arc::into_inner(self.conn).unwrap().into_inner();
218        conn.wipe().await?;
219        Ok(())
220    }
221
222    pub async fn can_close(&self) -> bool {
223        // transaction in progress
224        if self.transaction.lock().await.is_some() {
225            return false;
226        }
227        Arc::strong_count(&self.conn) <= 1
228    }
229
230    pub async fn close(self) -> CryptoKeystoreResult<()> {
231        if self.transaction.lock().await.is_some() {
232            return Err(CryptoKeystoreError::TransactionInProgress {
233                attempted_operation: "close()".to_string(),
234            });
235        }
236        let Some(conn) = Arc::into_inner(self.conn) else {
237            return Err(CryptoKeystoreError::CannotClose);
238        };
239        let conn = conn.into_inner();
240        conn.close().await?;
241        Ok(())
242    }
243
244    /// Waits for the current transaction to be committed or rolled back, then starts a new one.
245    pub async fn new_transaction(&self) -> CryptoKeystoreResult<()> {
246        let semaphore = self.transaction_semaphore.acquire_arc().await;
247        let mut transaction_guard = self.transaction.lock().await;
248        *transaction_guard = Some(KeystoreTransaction::new(semaphore).await?);
249        Ok(())
250    }
251
252    pub async fn commit_transaction(&self) -> CryptoKeystoreResult<()> {
253        let mut transaction_guard = self.transaction.lock().await;
254        let Some(transaction) = transaction_guard.as_ref() else {
255            return Err(CryptoKeystoreError::MutatingOperationWithoutTransaction);
256        };
257        transaction.commit(self).await?;
258        *transaction_guard = None;
259        Ok(())
260    }
261
262    pub async fn rollback_transaction(&self) -> CryptoKeystoreResult<()> {
263        let mut transaction_guard = self.transaction.lock().await;
264        if transaction_guard.is_none() {
265            return Err(CryptoKeystoreError::MutatingOperationWithoutTransaction);
266        };
267        *transaction_guard = None;
268        Ok(())
269    }
270
271    pub async fn child_groups<
272        E: Entity<ConnectionType = KeystoreDatabaseConnection> + crate::entities::PersistedMlsGroupExt + Sync,
273    >(
274        &self,
275        entity: E,
276    ) -> CryptoKeystoreResult<Vec<E>> {
277        let mut conn = self.conn.lock().await;
278        let persisted_records = entity.child_groups(conn.deref_mut()).await?;
279
280        let transaction_guard = self.transaction.lock().await;
281        let Some(transaction) = transaction_guard.as_ref() else {
282            return Ok(persisted_records);
283        };
284        transaction.child_groups(entity, persisted_records).await
285    }
286
287    pub async fn save<E: Entity<ConnectionType = KeystoreDatabaseConnection> + Sync + EntityTransactionExt>(
288        &self,
289        entity: E,
290    ) -> CryptoKeystoreResult<E> {
291        let transaction_guard = self.transaction.lock().await;
292        let Some(transaction) = transaction_guard.as_ref() else {
293            return Err(CryptoKeystoreError::MutatingOperationWithoutTransaction);
294        };
295        transaction.save_mut(entity).await
296    }
297
298    pub async fn remove<
299        E: Entity<ConnectionType = KeystoreDatabaseConnection> + EntityTransactionExt,
300        S: AsRef<[u8]>,
301    >(
302        &self,
303        id: S,
304    ) -> CryptoKeystoreResult<()> {
305        let transaction_guard = self.transaction.lock().await;
306        let Some(transaction) = transaction_guard.as_ref() else {
307            return Err(CryptoKeystoreError::MutatingOperationWithoutTransaction);
308        };
309        transaction.remove::<E, S>(id).await
310    }
311
312    pub async fn cred_delete_by_credential(&self, cred: Vec<u8>) -> CryptoKeystoreResult<()> {
313        let transaction_guard = self.transaction.lock().await;
314        let Some(transaction) = transaction_guard.as_ref() else {
315            return Err(CryptoKeystoreError::MutatingOperationWithoutTransaction);
316        };
317        transaction.cred_delete_by_credential(cred).await
318    }
319}
320
321#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
322#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
323impl FetchFromDatabase for Connection {
324    async fn find<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(
325        &self,
326        id: &[u8],
327    ) -> CryptoKeystoreResult<Option<E>> {
328        // If a transaction is in progress...
329        if let Some(transaction) = self.transaction.lock().await.as_ref() {
330            //... and it has information about this entity, ...
331            if let Some(result) = transaction.find::<E>(id).await? {
332                // ... return that result
333                return Ok(result);
334            }
335        }
336
337        // Otherwise get it from the database
338        let mut conn = self.conn.lock().await;
339        E::find_one(&mut conn, &id.into()).await
340    }
341
342    async fn find_unique<U: UniqueEntity>(&self) -> CryptoKeystoreResult<U> {
343        // If a transaction is in progress...
344        if let Some(transaction) = self.transaction.lock().await.as_ref() {
345            //... and it has information about this entity, ...
346            if let Some(result) = transaction.find_unique::<U>().await? {
347                // ... return that result
348                return Ok(result);
349            }
350        }
351        // Otherwise get it from the database
352        let mut conn = self.conn.lock().await;
353        U::find_unique(&mut conn).await
354    }
355
356    async fn find_all<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(
357        &self,
358        params: EntityFindParams,
359    ) -> CryptoKeystoreResult<Vec<E>> {
360        let mut conn = self.conn.lock().await;
361        let persisted_records = E::find_all(&mut conn, params.clone()).await?;
362
363        let transaction_guard = self.transaction.lock().await;
364        let Some(transaction) = transaction_guard.as_ref() else {
365            return Ok(persisted_records);
366        };
367        transaction.find_all(persisted_records, params).await
368    }
369
370    async fn find_many<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(
371        &self,
372        ids: &[Vec<u8>],
373    ) -> CryptoKeystoreResult<Vec<E>> {
374        let entity_ids: Vec<StringEntityId> = ids.iter().map(|id| id.as_slice().into()).collect();
375        let mut conn = self.conn.lock().await;
376        let persisted_records = E::find_many(&mut conn, &entity_ids).await?;
377
378        let transaction_guard = self.transaction.lock().await;
379        let Some(transaction) = transaction_guard.as_ref() else {
380            return Ok(persisted_records);
381        };
382        transaction.find_many(persisted_records, ids).await
383    }
384
385    async fn count<E: Entity<ConnectionType = KeystoreDatabaseConnection>>(&self) -> CryptoKeystoreResult<usize> {
386        if self.transaction.lock().await.is_some() {
387            // Unfortunately, we have to do this because of possible record id overlap
388            // between cache and db.
389            return Ok(self.find_all::<E>(Default::default()).await?.len());
390        };
391        let mut conn = self.conn.lock().await;
392        E::count(&mut conn).await
393    }
394}