diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 6a98875c9..b13d96686 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -2,9 +2,9 @@ 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 serde_json::{json, Value as JsonValue}; use wasm_bindgen::prelude::*; use crate::{ @@ -626,4 +626,106 @@ impl OlmMachine { .map(|_| JsValue::UNDEFINED)?) })) } + + /// 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(); + + 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?, + )?) + }) + } + + /// 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. + /// + /// `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, + 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, + }))?) + })) + } + + /// 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, + )?)?) + } } diff --git a/bindings/matrix-sdk-crypto-js/src/olm.rs b/bindings/matrix-sdk-crypto-js/src/olm.rs index 8a68c6bdc..3d5084570 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,37 @@ 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] +#[derive(Debug)] +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..7eda1903c 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,77 @@ describe(OlmMachine.name, () => { expect(isTrusted).toStrictEqual(false); }); + + describe('can export/import room keys', () => { + let m; + let exportedRoomKeys; + + test('can export room keys', async () => { + m = await machine(); + await m.shareRoomKey(room, [new UserId('@bob:example.org')], new EncryptionSettings()); + + 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; + }); + + 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), + session_id: expect.any(String), + session_key: expect.any(String), + sender_claimed_keys: { + ed25519: expect.any(String), + }, + forwarding_curve25519_key_chain: [], + }); + }); + + 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); + + // 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(exportedRoomKeys, progressListener)); + + expect(result).toMatchObject({ + imported_count: expect.any(Number), + total_count: expect.any(Number), + keys: expect.any(Object), + }); + }); + }); });