From 7f07ac52ef8d4e577a847bcf089ee8099adfef0c Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 15:00:33 +0200 Subject: [PATCH 01/10] feat(crypto-js): Add store configuration to the `OlmMachine` constructor. The `OlmMachine` constructor now has 2 more optional arguments: `store_name` and `store_passphrase`, to use Indexed DB as the backend to store the Olm machine keys, rather than having an in-memory Olm machine. Node.js is used to test our binding. Indexed DB is absent of Node.js. So, firstly, we are using the `fake-indexeddb` JavaScript library to mimic the same API inside Node.js. And, secondly, we use our own fork of the `indexed_db_futures` Rust crate to provide add support for Node.js too ([PR is here](https://github.com/Alorel/rust-indexed-db/pull/11)). It basically looks in the Node.js global environment if the `indexedDB` getter is present, just like it is already done for `Window` on any browser, or `WorkerGlobalScope` for workers. --- Cargo.lock | 5 ++- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 + bindings/matrix-sdk-crypto-js/package.json | 3 +- bindings/matrix-sdk-crypto-js/src/machine.rs | 44 +++++++++++++++++-- .../tests/machine.test.js | 8 ++++ crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b9692d7b..5916b35fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1931,8 +1931,7 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexed_db_futures" version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26ac735f676c52305becf53264b91cea9866a8de61ccbf464405b377b9cbca9" +source = "git+https://github.com/Hywan/rust-indexed-db?branch=feat-factory-nodejs#5dab67890cea0ab88b967031adc09179a537d77c" dependencies = [ "cfg-if", "js-sys", @@ -2365,6 +2364,7 @@ dependencies = [ "js-sys", "matrix-sdk-common", "matrix-sdk-crypto", + "matrix-sdk-indexeddb", "ruma", "serde_json", "tracing", @@ -2372,6 +2372,7 @@ dependencies = [ "vodozemac", "wasm-bindgen", "wasm-bindgen-futures", + "zeroize", ] [[package]] diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index fa86ac7d2..a75349b79 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -28,6 +28,7 @@ tracing = [] [dependencies] matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" } matrix-sdk-crypto = { version = "0.5.0", path = "../../crates/matrix-sdk-crypto" } +matrix-sdk-indexeddb = { version = "0.1.0", path = "../../crates/matrix-sdk-indexeddb" } ruma = { git = "https://github.com/ruma/ruma", rev = "173eb15147c904d7dc0ee894de6114743926e33e", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" @@ -38,6 +39,7 @@ http = "0.2.6" anyhow = "1.0.58" tracing = { version = "0.1.35", default-features = false, features = ["attributes"] } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } +zeroize = "1.3.0" [dependencies.vodozemac] git = "https://github.com/matrix-org/vodozemac/" diff --git a/bindings/matrix-sdk-crypto-js/package.json b/bindings/matrix-sdk-crypto-js/package.json index 59cce9d60..8de6990e5 100644 --- a/bindings/matrix-sdk-crypto-js/package.json +++ b/bindings/matrix-sdk-crypto-js/package.json @@ -30,7 +30,8 @@ "jest": "^28.1.0", "typedoc": "^0.22.17", "cross-env": "^7.0.3", - "yargs-parser": "~21.0.1" + "yargs-parser": "~21.0.1", + "fake-indexeddb": "^4.0" }, "engines": { "node": ">= 10" diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index d5be9449b..bdc3f9755 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -1,11 +1,13 @@ //! The crypto specific Olm objects. use std::collections::BTreeMap; +use std::sync::Arc; use js_sys::{Array, Map, Promise, Set}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; use serde_json::Value as JsonValue; use wasm_bindgen::prelude::*; +use zeroize::Zeroize; use crate::{ downcast, encryption, @@ -36,14 +38,50 @@ impl OlmMachine { /// that owns this machine. #[wasm_bindgen(constructor)] #[allow(clippy::new_ret_no_self)] - pub fn new(user_id: &identifiers::UserId, device_id: &identifiers::DeviceId) -> Promise { + pub fn new( + user_id: &identifiers::UserId, + device_id: &identifiers::DeviceId, + store_name: Option, + store_passphrase: Option, + ) -> Promise { 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)) => { + let store = Some( + matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase( + &store_name, + &store_passphrase, + ) + .await + .map(Arc::new)?, + ); + + store_passphrase.zeroize(); + + store + } + + _ => None, + }; + Ok(OlmMachine { - inner: matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref()) - .await, + inner: match store { + Some(store) => { + matrix_sdk_crypto::OlmMachine::with_store( + user_id.as_ref(), + device_id.as_ref(), + store, + ) + .await? + } + None => { + matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref()) + .await + } + }, }) }) } diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index d4e35641a..4d5291dc0 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -1,10 +1,18 @@ const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState } = require('../pkg/matrix_sdk_crypto_js'); +require('fake-indexeddb/auto'); describe(OlmMachine.name, () => { test('can be instantiated with the async initializer', async () => { expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'))).toBeInstanceOf(OlmMachine); }); + test('can be instantiated with a store', async () => { + let store_name = 'hello'; + let store_passphrase = 'world'; + + expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase)).toBeInstanceOf(OlmMachine); + }); + const user = new UserId('@alice:example.org'); const device = new DeviceId('foobar'); const room = new RoomId('!baz:matrix.org'); diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 259af8c5c..79f33a924 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -26,7 +26,7 @@ base64 = "0.13.0" dashmap = { version = "5.2.0", optional = true } derive_builder = "0.11.2" futures-util = { version = " 0.3.21", default-features = false, features = ["alloc"], optional = true } -indexed_db_futures = "0.2.3" +indexed_db_futures = { git = "https://github.com/Hywan/rust-indexed-db", branch = "feat-factory-nodejs" } js-sys = { version = "0.3.58" } matrix-sdk-base = { version = "0.5.0", path = "../matrix-sdk-base" } matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true } From d49788366904b6fc5f0a7a5c20b67e0abea0910e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 14:35:10 +0200 Subject: [PATCH 02/10] doc(crypto-js): Add more docmuentation and more error messages. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index bdc3f9755..6fb61f1fb 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -36,6 +36,14 @@ impl OlmMachine { /// `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_name` and `store_passphrase` are both optional, but + /// must be both set to have an effect. If they are both set, the + /// state of the machine will persist in a database named + /// `store_name` where its content is encrypted by the passphrase + /// given by `store_passphrase`. If they are not both set, the + /// created machine machine will keep the encryption keys only in + /// memory, and once the object is dropped, the keys will be lost. #[wasm_bindgen(constructor)] #[allow(clippy::new_ret_no_self)] pub fn new( @@ -64,6 +72,10 @@ impl OlmMachine { store } + (Some(_), None) => return Err(anyhow::Error::msg("The `store_name` has been set, and so, it expects a `store_passphrase`, which is not set; please provide one")), + + (None, Some(_)) => return Err(anyhow::Error::msg("The `store_passphrase` has been set, but it has an effect only if `store_name` is set, but it's not; please provide one")), + _ => None, }; From d947448c648507cb78ba24ca3d862326262fc3b5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 14:46:03 +0200 Subject: [PATCH 03/10] test(crypto-js): Add more test case when store is not configured correctly. --- .../tests/machine.test.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 4d5291dc0..330ebed4a 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -13,6 +13,38 @@ describe(OlmMachine.name, () => { expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase)).toBeInstanceOf(OlmMachine); }); + describe('cannot be instantiated with a store', () => { + test('store name is missing', async () => { + let store_name = null; + let store_passphrase = 'world'; + + let err = null; + + try { + await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase); + } catch (error) { + err = error; + } + + expect(err).toBeDefined(); + }); + + test('store passphrase is missing', async () => { + let store_name = 'hello'; + let store_passphrase = null; + + let err = null; + + try { + await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase); + } catch (error) { + err = error; + } + + expect(err).toBeDefined(); + }); + }); + const user = new UserId('@alice:example.org'); const device = new DeviceId('foobar'); const room = new RoomId('!baz:matrix.org'); From 527c727e4aa0509cafd2b84da9031d18fbaf73e8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 14:58:57 +0200 Subject: [PATCH 04/10] test(crypto-js): Improve test cases. --- .../tests/machine.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 330ebed4a..4fc819e89 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -7,10 +7,29 @@ describe(OlmMachine.name, () => { }); test('can be instantiated with a store', async () => { + // No databases. + expect(await indexedDB.databases()).toHaveLength(0); + let store_name = 'hello'; let store_passphrase = 'world'; + // Creating a new Olm machine. expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase)).toBeInstanceOf(OlmMachine); + + // Oh, there is 2 databases now, prefixed by `store_name`. + let databases = await indexedDB.databases(); + + expect(databases).toHaveLength(2); + expect(databases).toStrictEqual([ + { name: `${store_name}::matrix-sdk-crypto-meta`, version: 1 }, + { name: `${store_name}::matrix-sdk-crypto`, version: 1 }, + ]); + + // Creating a new Olm machine, with the stored state. + expect(await new OlmMachine(new UserId('@foo:bar.org'), new DeviceId('baz'), store_name, store_passphrase)).toBeInstanceOf(OlmMachine); + + // Same number of databases. + expect(await indexedDB.databases()).toHaveLength(2); }); describe('cannot be instantiated with a store', () => { From ef56f769786de4de635155bec0c9a6fdb30cf581 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 15:03:26 +0200 Subject: [PATCH 05/10] doc(crypto-js): Fix a typo. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 6fb61f1fb..6d60c89ef 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -42,8 +42,8 @@ impl OlmMachine { /// state of the machine will persist in a database named /// `store_name` where its content is encrypted by the passphrase /// given by `store_passphrase`. If they are not both set, the - /// created machine machine will keep the encryption keys only in - /// memory, and once the object is dropped, the keys will be lost. + /// created machine will keep the encryption keys only in memory, + /// and once the object is dropped, the keys will be lost. #[wasm_bindgen(constructor)] #[allow(clippy::new_ret_no_self)] pub fn new( From 04634d5f39088ca479242ee2f18cabb18e712494 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 15:04:50 +0200 Subject: [PATCH 06/10] chore(crypto-js): Fix a typo in an error message. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 6d60c89ef..ae7a11379 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -74,7 +74,7 @@ impl OlmMachine { (Some(_), None) => return Err(anyhow::Error::msg("The `store_name` has been set, and so, it expects a `store_passphrase`, which is not set; please provide one")), - (None, Some(_)) => return Err(anyhow::Error::msg("The `store_passphrase` has been set, but it has an effect only if `store_name` is set, but it's not; please provide one")), + (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")), _ => None, }; From 432449b009d5a4bc755bd1bff18bbf0434a67e1b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 15:08:51 +0200 Subject: [PATCH 07/10] chore(crypto-js): Make `cargo fmt` happy. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index ae7a11379..934c27f9b 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -1,7 +1,6 @@ //! The crypto specific Olm objects. -use std::collections::BTreeMap; -use std::sync::Arc; +use std::{collections::BTreeMap, sync::Arc}; use js_sys::{Array, Map, Promise, Set}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; From 9722a4c4152235f83de59a5137812d2a979f9f96 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 16:44:49 +0200 Subject: [PATCH 08/10] chore(crypto-js): Use `IndexeddbCryptoStore` only for `wasm32`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 934c27f9b..a198a9505 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -56,6 +56,13 @@ impl OlmMachine { future_to_promise(async move { let store = match (store_name, store_passphrase) { + // 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 + // practise, it doesn't impact this crate because it's + // always compiled for `wasm32`. + #[cfg(target_arch = "wasm32")] (Some(store_name), Some(mut store_passphrase)) => { let store = Some( matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase( From ef8207fbaa5a21b0b715aabe36dc1d91f3564055 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 17:06:07 +0200 Subject: [PATCH 09/10] chore(crypto-js): Make Clippy happy. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index a198a9505..68eb718d6 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -1,6 +1,6 @@ //! The crypto specific Olm objects. -use std::{collections::BTreeMap, sync::Arc}; +use std::collections::BTreeMap; use js_sys::{Array, Map, Promise, Set}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; @@ -64,6 +64,8 @@ impl OlmMachine { // always compiled for `wasm32`. #[cfg(target_arch = "wasm32")] (Some(store_name), Some(mut store_passphrase)) => { + use std::sync::Arc; + let store = Some( matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase( &store_name, From 85795a92b6a2e1bf6db0b7a1a977660212c57299 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 23 Aug 2022 17:38:32 +0200 Subject: [PATCH 10/10] chore(crypto-js): Make Clippy happy. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 68eb718d6..425c90929 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -6,7 +6,6 @@ use js_sys::{Array, Map, Promise, Set}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; use serde_json::Value as JsonValue; use wasm_bindgen::prelude::*; -use zeroize::Zeroize; use crate::{ downcast, encryption, @@ -65,6 +64,7 @@ impl OlmMachine { #[cfg(target_arch = "wasm32")] (Some(store_name), Some(mut store_passphrase)) => { use std::sync::Arc; + use zeroize::Zeroize; let store = Some( matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_passphrase(