diff --git a/CHANGELOG.md b/CHANGELOG.md index ded7d56dc..a4a8ffd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# UNRELEASED + +- Add a `StoreHandle` class which can be used to hold a connection to a + crypto store, and thus improve performance when doing multiple operations + on the store. + ([#76](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/76)) + # matrix-sdk-crypto-wasm v3.5.0 - Update matrix-rust-sdk version, providing several changes including a fix diff --git a/src/machine.rs b/src/machine.rs index 69309ec60..ee4e780a8 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -5,8 +5,8 @@ use std::{collections::BTreeMap, ops::Deref, time::Duration}; use futures_util::{pin_mut, StreamExt}; use js_sys::{Array, Function, JsString, Map, Promise, Set}; use matrix_sdk_common::ruma::{ - self, events::secret::request::SecretName, serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, - UInt, + self, events::secret::request::SecretName, serde::Raw, DeviceKeyAlgorithm, OwnedDeviceId, + OwnedTransactionId, OwnedUserId, UInt, }; use matrix_sdk_crypto::{ backups::MegolmV1BackupKey, @@ -32,7 +32,7 @@ use crate::{ requests::{outgoing_request_to_js_value, CrossSigningBootstrapRequests, ToDeviceRequest}, responses::{self, response_from_string}, store, - store::RoomKeyInfo, + store::{RoomKeyInfo, StoreHandle}, sync_events, types::{self, RoomKeyImportResult, SignatureVerification}, verification, vodozemac, @@ -78,72 +78,59 @@ impl OlmMachine { /// `OlmMachine` gets dropped. /// /// * `store_passphrase` - The passphrase that should be used to encrypt the - /// IndexedDB based - pub fn initialize( + /// IndexedDB-based store. + pub async fn initialize( user_id: &identifiers::UserId, device_id: &identifiers::DeviceId, store_name: Option, store_passphrase: Option, - ) -> Promise { + ) -> Result { let user_id = user_id.inner.clone(); let device_id = device_id.inner.clone(); - future_to_promise(async move { - let store = match (store_name, store_passphrase) { - (Some(store_name), Some(mut store_passphrase)) => { - use zeroize::Zeroize; - - let store = Some( - matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase( - &store_name, - &store_passphrase, - ) - .await?, - ); - - store_passphrase.zeroize(); - - store - } - - (Some(store_name), None) => Some( - matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_name(&store_name).await?, - ), + let store_handle = StoreHandle::open(store_name, store_passphrase) + .await + .map_err(|e| JsError::from(&*e))?; + Self::init_helper(user_id, device_id, store_handle).await + } - (None, Some(_)) => { - return Err(anyhow::Error::msg( - "The `store_passphrase` has been set, but it has an effect only if \ - `store_name` is set, which is not; please provide one", - )) - } + /// Create a new `OlmMachine` backed by an existing store. + /// + /// # Arguments + /// + /// * `user_id` - represents the unique ID of the user that owns this + /// machine. + /// + /// * `device_id` - represents the unique ID of the device + /// that owns this machine. + /// + /// * `store_handle` - the connection to the crypto store to be used for + /// this machine. + pub async fn init_from_store( + user_id: &identifiers::UserId, + device_id: &identifiers::DeviceId, + store_handle: &StoreHandle, + ) -> Result { + let user_id = user_id.inner.clone(); + let device_id = device_id.inner.clone(); + Self::init_helper(user_id, device_id, store_handle.clone()).await + } - (None, None) => None, - }; - - Ok(OlmMachine { - inner: match store { - // We need this `#[cfg]` because `IndexeddbCryptoStore` - // implements `CryptoStore` only on `target_arch = - // "wasm32"`. Without that, we could have a compilation - // error when checking the entire workspace. In practice, - // it doesn't impact this crate because it's always - // compiled for `wasm32`. - #[cfg(target_arch = "wasm32")] - Some(store) => { - matrix_sdk_crypto::OlmMachine::with_store( - user_id.as_ref(), - device_id.as_ref(), - store, - ) - .await? - } - _ => { - matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref()) - .await - } - }, - }) - }) + async fn init_helper( + user_id: OwnedUserId, + device_id: OwnedDeviceId, + store_handle: StoreHandle, + ) -> Result { + Ok(OlmMachine { + inner: matrix_sdk_crypto::OlmMachine::with_store( + user_id.as_ref(), + device_id.as_ref(), + store_handle, + ) + .await + .map_err(JsError::from)?, + } + .into()) } /// The unique user ID that owns this `OlmMachine` instance. diff --git a/src/store.rs b/src/store.rs index b4c6dddcb..39e98db62 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,5 +1,8 @@ //! Store types. +use std::sync::Arc; + +use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore, MemoryStore}; use wasm_bindgen::prelude::*; use crate::{ @@ -7,6 +10,99 @@ use crate::{ vodozemac::Curve25519PublicKey, }; +/// A struct containing an open connection to a CryptoStore. +/// +/// Opening the CryptoStore can take some time, due to the PBKDF calculation +/// involved, so if multiple operations are being done on the same store, it is +/// more efficient to open it once. +#[wasm_bindgen] +#[derive(Clone, Debug)] +pub struct StoreHandle { + pub(crate) store: Arc, +} + +#[wasm_bindgen] +impl StoreHandle { + /// Open a crypto store. + /// + /// The created store will be based on IndexedDB if a `store_name` is + /// provided; otherwise it will be based on a memory store and once the + /// objects is dropped, the keys will be lost. + /// + /// # Arguments + /// + /// + /// * `store_name` - The name that should be used to open the IndexedDB + /// based database. If this isn't provided, a memory-only store will be + /// used. *Note* the memory-only store will lose your E2EE keys when the + /// `StoreHandle` gets dropped. + /// + /// * `store_passphrase` - The passphrase that should be used to encrypt the + /// store, for IndexedDB-based stores + #[wasm_bindgen(js_name = "open")] + pub async fn open_for_js( + store_name: Option, + store_passphrase: Option, + ) -> Result { + Ok(StoreHandle::open(store_name, store_passphrase) + .await + .map_err(|e| JsError::from(&*e))? + .into()) + } + + pub(crate) async fn open( + store_name: Option, + store_passphrase: Option, + ) -> Result { + let store = match store_name { + Some(store_name) => Self::open_indexeddb(&store_name, store_passphrase).await?, + + None => { + if store_passphrase.is_some() { + return Err(anyhow::Error::msg( + "The `store_passphrase` has been set, but it has an effect only if \ + `store_name` is set, which is not; please provide one", + )); + } + + MemoryStore::new().into_crypto_store() + } + }; + + Ok(Self { store }) + } + + async fn open_indexeddb( + store_name: &String, + store_passphrase: Option, + ) -> Result, matrix_sdk_indexeddb::IndexeddbCryptoStoreError> { + let store = match store_passphrase { + Some(mut store_passphrase) => { + use zeroize::Zeroize; + + let store = matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase( + &store_name, + &store_passphrase, + ) + .await?; + + store_passphrase.zeroize(); + store + } + + None => matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_name(&store_name).await?, + }; + + Ok(store.into_crypto_store()) + } +} + +impl IntoCryptoStore for StoreHandle { + fn into_crypto_store(self) -> Arc { + self.store.clone() + } +} + /// A struct containing private cross signing keys that can be backed /// up or uploaded to the secret store. #[wasm_bindgen] diff --git a/tests/machine.test.ts b/tests/machine.test.ts index 751016226..e72e13c80 100644 --- a/tests/machine.test.ts +++ b/tests/machine.test.ts @@ -1,34 +1,35 @@ import { + BackupDecryptionKey, CrossSigningStatus, DecryptedRoomEvent, + DecryptionErrorCode, DeviceId, DeviceKeyId, DeviceLists, EncryptionSettings, EventId, + getVersions, InboundGroupSession, + KeysBackupRequest, + KeysClaimRequest, KeysQueryRequest, KeysUploadRequest, MaybeSignature, + MegolmDecryptionError, OlmMachine, OwnUserIdentity, RequestType, RoomId, RoomMessageRequest, ShieldColor, + SignatureState, SignatureUploadRequest, + StoreHandle, ToDeviceRequest, UserId, UserIdentity, VerificationRequest, Versions, - getVersions, - SignatureState, - BackupDecryptionKey, - MegolmDecryptionError, - DecryptionErrorCode, - KeysClaimRequest, - KeysBackupRequest, } from "../pkg/matrix_sdk_crypto_wasm"; import "fake-indexeddb/auto"; @@ -53,15 +54,28 @@ describe("Versions", () => { }); }); +jest.setTimeout(15000); + describe(OlmMachine.name, () => { test("can be instantiated with the async initializer", async () => { expect(await OlmMachine.initialize(new UserId("@foo:bar.org"), new DeviceId("baz"))).toBeInstanceOf(OlmMachine); }); - test("can be instantiated with a store", async () => { + test("can be instantiated with a StoreHandle", async () => { let storeName = "hello"; let storePassphrase = "world"; + let storeHandle = await StoreHandle.open(storeName, storePassphrase); + expect( + await OlmMachine.init_from_store(new UserId("@foo:bar.org"), new DeviceId("baz"), storeHandle), + ).toBeInstanceOf(OlmMachine); + storeHandle.free(); + }); + + test("can be instantiated with store credentials", async () => { + let storeName = "hello2"; + let storePassphrase = "world"; + const byStoreName = (db: IDBDatabaseInfo) => db.name!.startsWith(storeName); // No databases.