Skip to content

Commit

Permalink
Create new StoreHandle class to save repeatedly opening stores (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
richvdh authored Jan 8, 2024
1 parent 28c052b commit e8dd66c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 68 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
107 changes: 47 additions & 60 deletions src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<String>,
store_passphrase: Option<String>,
) -> Promise {
) -> Result<JsValue, JsValue> {
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<JsValue, JsValue> {
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<JsValue, JsValue> {
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.
Expand Down
96 changes: 96 additions & 0 deletions src/store.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,108 @@
//! Store types.
use std::sync::Arc;

use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore, MemoryStore};
use wasm_bindgen::prelude::*;

use crate::{
encryption::EncryptionAlgorithm, identifiers::RoomId, impl_from_to_inner,
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<DynCryptoStore>,
}

#[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<String>,
store_passphrase: Option<String>,
) -> Result<JsValue, JsValue> {
Ok(StoreHandle::open(store_name, store_passphrase)
.await
.map_err(|e| JsError::from(&*e))?
.into())
}

pub(crate) async fn open(
store_name: Option<String>,
store_passphrase: Option<String>,
) -> Result<StoreHandle, anyhow::Error> {
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<String>,
) -> Result<Arc<DynCryptoStore>, 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<DynCryptoStore> {
self.store.clone()
}
}

/// A struct containing private cross signing keys that can be backed
/// up or uploaded to the secret store.
#[wasm_bindgen]
Expand Down
30 changes: 22 additions & 8 deletions tests/machine.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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.
Expand Down

0 comments on commit e8dd66c

Please sign in to comment.