From b4cec6e1bbb7a83b3b2462364d0e6bb46f97fa4f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 21 Sep 2022 16:03:10 +0200 Subject: [PATCH 1/7] feat(crypto-js): Implement `OlmMachine.export_room_keys`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 22 +++++++++++- bindings/matrix-sdk-crypto-js/src/olm.rs | 35 ++++++++++++++++++- .../matrix-sdk-crypto-js/tests/device.test.js | 1 - .../tests/machine.test.js | 27 ++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 6a98875c9..225e3d9bc 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; -use js_sys::{Array, Map, Promise, Set}; +use js_sys::{Array, Function, Map, Promise, Set}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; use serde_json::Value as JsonValue; use wasm_bindgen::prelude::*; @@ -626,4 +626,24 @@ impl OlmMachine { .map(|_| JsValue::UNDEFINED)?) })) } + + #[wasm_bindgen(js_name = "exportRoomKeys")] + pub fn export_room_keys(&self, predicate: Function) -> Promise { + let me = self.inner.clone(); + + future_to_promise(async move { + Ok(serde_json::to_string( + &me.export_room_keys(|session| { + let session = session.clone(); + + predicate + .call1(&JsValue::NULL, &olm::InboundGroupSession::from(session).into()) + .expect("Predicate function passed to `export_room_keys` failed") + .as_bool() + .unwrap_or(false) + }) + .await?, + )?) + }) + } } diff --git a/bindings/matrix-sdk-crypto-js/src/olm.rs b/bindings/matrix-sdk-crypto-js/src/olm.rs index 8a68c6bdc..39fbbf57a 100644 --- a/bindings/matrix-sdk-crypto-js/src/olm.rs +++ b/bindings/matrix-sdk-crypto-js/src/olm.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; -use crate::impl_from_to_inner; +use crate::{identifiers, impl_from_to_inner}; /// Struct representing the state of our private cross signing keys, /// it shows which private cross signing keys we have locally stored. @@ -36,3 +36,36 @@ impl CrossSigningStatus { self.inner.has_user_signing } } + +/// Inbound group session. +/// +/// Inbound group sessions are used to exchange room messages between a group of +/// participants. Inbound group sessions are used to decrypt the room messages. +#[wasm_bindgen] +pub struct InboundGroupSession { + inner: matrix_sdk_crypto::olm::InboundGroupSession, +} + +impl_from_to_inner!(matrix_sdk_crypto::olm::InboundGroupSession => InboundGroupSession); + +#[wasm_bindgen] +impl InboundGroupSession { + /// The room where this session is used in. + #[wasm_bindgen(getter, js_name = "roomId")] + pub fn room_id(&self) -> identifiers::RoomId { + self.inner.room_id().to_owned().into() + } + + /// Returns the unique identifier for this session. + #[wasm_bindgen(getter, js_name = "sessionId")] + pub fn session_id(&self) -> String { + self.inner.session_id().to_owned() + } + + /// Has the session been imported from a file or server-side backup? As + /// opposed to being directly received as an `m.room_key` event. + #[wasm_bindgen(js_name = "hasBeenImported")] + pub fn has_been_imported(&self) -> bool { + self.inner.has_been_imported() + } +} diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index d9d4fd609..532624e26 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -28,7 +28,6 @@ const { QrCode, QrCodeScan, } = require('../pkg/matrix_sdk_crypto_js'); -const { LoggerLevel, Tracing } = require('../pkg/matrix_sdk_crypto_js'); const { zip, addMachineToMachine } = require('./helper'); describe('LocalTrust', () => { diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 59db72b52..9e6cd0975 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -5,6 +5,7 @@ const { DeviceKeyId, DeviceLists, EncryptionSettings, + InboundGroupSession, KeysClaimRequest, KeysQueryRequest, KeysUploadRequest, @@ -19,6 +20,7 @@ const { VerificationRequest, VerificationState, } = require('../pkg/matrix_sdk_crypto_js'); +const { addMachineToMachine } = require('./helper'); require('fake-indexeddb/auto'); describe(OlmMachine.name, () => { @@ -491,4 +493,29 @@ describe(OlmMachine.name, () => { expect(isTrusted).toStrictEqual(false); }); + + test('can export room keys', async () => { + const m = await machine(); + await m.shareRoomKey(room, [new UserId('@bob:example.org')], new EncryptionSettings()); + + const exportedRoomKeys = JSON.parse(await m.exportRoomKeys(session => { + expect(session).toBeInstanceOf(InboundGroupSession); + expect(session.roomId.toString()).toStrictEqual(room.toString()); + + return true; + })); + + expect(exportedRoomKeys).toHaveLength(1); + expect(exportedRoomKeys[0]).toMatchObject({ + algorithm: expect.any(String), + room_id: room.toString(), + sender_key: expect.any(String), + session_id: expect.any(String), + session_key: expect.any(String), + sender_claimed_keys: { + ed25519: expect.any(String), + }, + forwarding_curve25519_key_chain: [], + }); + }); }); From 406fc58720dba9aa33e9b0fb23d49ffeb745ee39 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 21 Sep 2022 17:37:23 +0200 Subject: [PATCH 2/7] feat(crypto-js): Implement `OlmMachine.import_room_keys`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 43 +++++++++++++++++++- bindings/matrix-sdk-crypto-js/src/olm.rs | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 225e3d9bc..7ed6c1091 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use js_sys::{Array, Function, Map, Promise, Set}; use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; -use serde_json::Value as JsonValue; +use serde_json::{json, Value as JsonValue}; use wasm_bindgen::prelude::*; use crate::{ @@ -627,6 +627,12 @@ impl OlmMachine { })) } + /// Export the keys that match the given predicate. + /// + /// `predicate` is a closure that will be called for every known + /// `InboundGroupSession`, which represents a room key. If the closure + /// returns `true`, the `InboundGroupSession` will be included in the + /// export, otherwise it won't. #[wasm_bindgen(js_name = "exportRoomKeys")] pub fn export_room_keys(&self, predicate: Function) -> Promise { let me = self.inner.clone(); @@ -646,4 +652,39 @@ impl OlmMachine { )?) }) } + + /// Import the given room keys into our store. + /// + /// `exported_keys` is a list of previously exported keys that should be + /// imported into our store. If we already have a better version of a key, + /// the key will _not_ be imported. + #[wasm_bindgen(js_name = "importRoomKeys")] + pub fn import_room_keys( + &self, + exported_room_keys: &str, + progress_listener: Function, + ) -> Result { + let me = self.inner.clone(); + let exported_room_keys: Vec = + serde_json::from_str(exported_room_keys)?; + + Ok(future_to_promise(async move { + let matrix_sdk_crypto::RoomKeyImportResult { imported_count, total_count, keys } = me + .import_room_keys(exported_room_keys, false, |progress, total| { + let progress: u64 = progress.try_into().unwrap(); + let total: u64 = total.try_into().unwrap(); + + progress_listener + .call2(&JsValue::NULL, &JsValue::from(progress), &JsValue::from(total)) + .expect("Progress listener passed to `import_room_keys` failed"); + }) + .await?; + + Ok(serde_json::to_string(&json!({ + "imported_count": imported_count, + "total_count": total_count, + "keys": keys, + }))?) + })) + } } diff --git a/bindings/matrix-sdk-crypto-js/src/olm.rs b/bindings/matrix-sdk-crypto-js/src/olm.rs index 39fbbf57a..3d5084570 100644 --- a/bindings/matrix-sdk-crypto-js/src/olm.rs +++ b/bindings/matrix-sdk-crypto-js/src/olm.rs @@ -42,6 +42,7 @@ impl CrossSigningStatus { /// Inbound group sessions are used to exchange room messages between a group of /// participants. Inbound group sessions are used to decrypt the room messages. #[wasm_bindgen] +#[derive(Debug)] pub struct InboundGroupSession { inner: matrix_sdk_crypto::olm::InboundGroupSession, } From 5b919dd840391ed023e64d7d255bcad125be40bd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 21 Sep 2022 17:46:23 +0200 Subject: [PATCH 3/7] test(crypto-js): Test `OlmMachine.import_room_keys`. --- .../tests/machine.test.js | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 9e6cd0975..6b68e681b 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -494,28 +494,51 @@ describe(OlmMachine.name, () => { expect(isTrusted).toStrictEqual(false); }); - test('can export room keys', async () => { - const m = await machine(); - await m.shareRoomKey(room, [new UserId('@bob:example.org')], new EncryptionSettings()); + describe('can export/import room keys', () => { + let m; + let exportedRoomKeys; - const exportedRoomKeys = JSON.parse(await m.exportRoomKeys(session => { - expect(session).toBeInstanceOf(InboundGroupSession); - expect(session.roomId.toString()).toStrictEqual(room.toString()); + test('can export room keys', async () => { + m = await machine(); + await m.shareRoomKey(room, [new UserId('@bob:example.org')], new EncryptionSettings()); - return true; - })); + exportedRoomKeys = JSON.parse(await m.exportRoomKeys(session => { + expect(session).toBeInstanceOf(InboundGroupSession); + expect(session.roomId.toString()).toStrictEqual(room.toString()); - expect(exportedRoomKeys).toHaveLength(1); - expect(exportedRoomKeys[0]).toMatchObject({ - algorithm: expect.any(String), - room_id: room.toString(), - sender_key: expect.any(String), - session_id: expect.any(String), - session_key: expect.any(String), - sender_claimed_keys: { - ed25519: expect.any(String), - }, - forwarding_curve25519_key_chain: [], + return true; + })); + + expect(exportedRoomKeys).toHaveLength(1); + expect(exportedRoomKeys[0]).toMatchObject({ + algorithm: expect.any(String), + room_id: room.toString(), + sender_key: expect.any(String), + session_id: expect.any(String), + session_key: expect.any(String), + sender_claimed_keys: { + ed25519: expect.any(String), + }, + forwarding_curve25519_key_chain: [], + }); + }); + + test('can import room keys', async () => { + const progressListener = (progress, total) => { + expect(progress).toBeLessThan(total); + + // Since it's called only once, let's be crazy. + expect(progress).toStrictEqual(0n); + expect(total).toStrictEqual(1n); + }; + + const result = JSON.parse(await m.importRoomKeys(JSON.stringify(exportedRoomKeys), progressListener)); + + expect(result).toMatchObject({ + imported_count: expect.any(Number), + total_count: expect.any(Number), + keys: expect.any(Object), + }); }); }); }); From dee14c4ee4a41e3caf11ff5c9532e3ac497347e3 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 21 Sep 2022 17:50:28 +0200 Subject: [PATCH 4/7] doc(crypto-js): Improve documentation of `OlmMachine.import_room_keys`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 7ed6c1091..0643c46f6 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -658,6 +658,9 @@ impl OlmMachine { /// `exported_keys` is a list of previously exported keys that should be /// imported into our store. If we already have a better version of a key, /// the key will _not_ be imported. + /// + /// `progress_listener` is a closure that takes 2 arguments: `progress` and + /// `total`, and returns nothing. #[wasm_bindgen(js_name = "importRoomKeys")] pub fn import_room_keys( &self, From 6fa04a0ba1fb4417ea7f1e6f26069ff61bf0ce48 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 21 Sep 2022 18:00:15 +0200 Subject: [PATCH 5/7] feat(crypto-js): Implement `OlmMachine.(en|de)crypt_exported_room_keys`. --- bindings/matrix-sdk-crypto-js/src/machine.rs | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 0643c46f6..b13d96686 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -690,4 +690,42 @@ impl OlmMachine { }))?) })) } + + /// Encrypt the list of exported room keys using the given passphrase. + /// + /// `exported_room_keys` is a list of sessions that should be encrypted + /// (it's generally returned by `export_room_keys`). `passphrase` is the + /// passphrase that will be used to encrypt the exported room keys. And + /// `rounds` is the number of rounds that should be used for the key + /// derivation when the passphrase gets turned into an AES key. More rounds + /// are increasingly computationnally intensive and as such help against + /// brute-force attacks. Should be at least `10_000`, while values in the + /// `100_000` ranges should be preferred. + #[wasm_bindgen(js_name = "encryptExportedRoomKeys")] + pub fn encrypt_exported_room_keys( + exported_room_keys: &str, + passphrase: &str, + rounds: u32, + ) -> Result { + let exported_room_keys: Vec = + serde_json::from_str(exported_room_keys)?; + + Ok(matrix_sdk_crypto::encrypt_room_key_export(&exported_room_keys, passphrase, rounds)?) + } + + /// Try to decrypt a reader into a list of exported room keys. + /// + /// `encrypted_exported_room_keys` is the result from + /// `encrypt_exported_room_keys`. `passphrase` is the passphrase that was + /// used when calling `encrypt_exported_room_keys`. + #[wasm_bindgen(js_name = "decryptExportedRoomKeys")] + pub fn decrypt_exported_room_keys( + encrypted_exported_room_keys: &str, + passphrase: &str, + ) -> Result { + Ok(serde_json::to_string(&matrix_sdk_crypto::decrypt_room_key_export( + encrypted_exported_room_keys.as_bytes(), + passphrase, + )?)?) + } } From 62ab263d0ea98e3029b71021aced8f96228cce25 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 22 Sep 2022 11:27:12 +0200 Subject: [PATCH 6/7] test(crypto-js): Test `OlmMachine.(en|de)crypt_exported_room_keys`. --- .../tests/machine.test.js | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 6b68e681b..0d600c084 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -502,15 +502,16 @@ describe(OlmMachine.name, () => { m = await machine(); await m.shareRoomKey(room, [new UserId('@bob:example.org')], new EncryptionSettings()); - exportedRoomKeys = JSON.parse(await m.exportRoomKeys(session => { + exportedRoomKeys = await m.exportRoomKeys(session => { expect(session).toBeInstanceOf(InboundGroupSession); expect(session.roomId.toString()).toStrictEqual(room.toString()); return true; - })); + }); - expect(exportedRoomKeys).toHaveLength(1); - expect(exportedRoomKeys[0]).toMatchObject({ + const roomKeys = JSON.parse(exportedRoomKeys); + expect(roomKeys).toHaveLength(1); + expect(roomKeys[0]).toMatchObject({ algorithm: expect.any(String), room_id: room.toString(), sender_key: expect.any(String), @@ -523,6 +524,28 @@ describe(OlmMachine.name, () => { }); }); + let encryptedExportedRoomKeys; + let encryptionPassphrase = 'Hello, Matrix!'; + + test('can encrypt the exported room keys', () => { + encryptedExportedRoomKeys = OlmMachine.encryptExportedRoomKeys( + exportedRoomKeys, + encryptionPassphrase, + 100_000, + ); + + expect(encryptedExportedRoomKeys).toMatch(/^-----BEGIN MEGOLM SESSION DATA-----/); + }); + + test('can decrypt the exported room keys', () => { + const decryptedExportedRoomKeys = OlmMachine.decryptExportedRoomKeys( + encryptedExportedRoomKeys, + encryptionPassphrase, + ); + + expect(decryptedExportedRoomKeys).toStrictEqual(exportedRoomKeys); + }); + test('can import room keys', async () => { const progressListener = (progress, total) => { expect(progress).toBeLessThan(total); @@ -532,7 +555,7 @@ describe(OlmMachine.name, () => { expect(total).toStrictEqual(1n); }; - const result = JSON.parse(await m.importRoomKeys(JSON.stringify(exportedRoomKeys), progressListener)); + const result = JSON.parse(await m.importRoomKeys(exportedRoomKeys, progressListener)); expect(result).toMatchObject({ imported_count: expect.any(Number), From df50a5446f044caa9af8d3679e74ac8ffcf30eda Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 22 Sep 2022 11:31:28 +0200 Subject: [PATCH 7/7] test(crypto-js): Test `InboundGrounSession.sessionId` and `.hasBeenImported`. --- bindings/matrix-sdk-crypto-js/tests/machine.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 0d600c084..7eda1903c 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -505,6 +505,8 @@ describe(OlmMachine.name, () => { exportedRoomKeys = await m.exportRoomKeys(session => { expect(session).toBeInstanceOf(InboundGroupSession); expect(session.roomId.toString()).toStrictEqual(room.toString()); + expect(session.sessionId).toBeDefined(); + expect(session.hasBeenImported()).toStrictEqual(false); return true; });