Skip to content

Commit

Permalink
Add an API to notify apps when a megolm key is received
Browse files Browse the repository at this point in the history
This is useful for applications which want to have another go at decryption
when some room keys arrive.
  • Loading branch information
richvdh authored Mar 30, 2023
1 parent 7fd1c93 commit f988106
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 30 deletions.
40 changes: 21 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions bindings/matrix-sdk-crypto-js/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# v0.1.0-alpha.6

- Add new accessor `InboundGroupSession.senderKey`.
- Add a new API, `OlmMachine.registerRoomKeyUpdatedCallback`, which
applications can use to listen for received room keys.
5 changes: 3 additions & 2 deletions bindings/matrix-sdk-crypto-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ crate-type = ["cdylib"]
[features]
default = ["tracing", "qrcode"]
qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"]
tracing = ["dep:tracing"]
tracing = []

[dependencies]
anyhow = { workspace = true }
console_error_panic_hook = "0.1.7"
futures-util = "0.3.27"
http = { workspace = true }
js-sys = "0.3.49"
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
Expand All @@ -45,7 +46,7 @@ matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-inde
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
ruma = { workspace = true, features = ["js", "rand", "unstable-msc2677"] }
serde_json = { workspace = true }
tracing = { workspace = true, optional = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
vodozemac = { workspace = true, features = ["js"] }
wasm-bindgen = "0.2.83"
Expand Down
64 changes: 63 additions & 1 deletion bindings/matrix-sdk-crypto-js/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
use std::{collections::BTreeMap, ops::Deref};

use futures_util::StreamExt;
use js_sys::{Array, Function, Map, Promise, Set};
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use serde_json::{json, Value as JsonValue};
use tracing::warn;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{spawn_local, JsFuture};

use crate::{
device, encryption,
Expand All @@ -15,7 +18,9 @@ use crate::{
olm, requests,
requests::{OutgoingRequest, ToDeviceRequest},
responses::{self, response_from_string},
store, sync_events, types, verification, vodozemac,
store,
store::RoomKeyInfo,
sync_events, types, verification, vodozemac,
};

/// State machine implementation of the Olm/Megolm encryption protocol
Expand Down Expand Up @@ -768,6 +773,25 @@ impl OlmMachine {
)?)?)
}

/// Register a callback which will be called whenever there is an update to
/// a room key.
///
/// `callback` should be a function that takes a single argument (an array
/// of {@link RoomKeyInfo}) and returns a Promise.
#[wasm_bindgen(js_name = "registerRoomKeyUpdatedCallback")]
pub async fn register_room_key_updated_callback(&self, callback: Function) {
let stream = self.inner.store().room_keys_received_stream();

// fire up a promise chain which will call `cb` on each result from the stream
spawn_local(async move {
// take a reference to `callback` (which we then pass into the closure), to stop
// the callback being moved into the closure (which would mean we could only
// call the closure once)
let callback_ref = &callback;
stream.for_each(move |item| send_room_key_info_to_callback(callback_ref, item)).await;
});
}

/// Shut down the `OlmMachine`.
///
/// The `OlmMachine` cannot be used after this method has been called.
Expand All @@ -776,3 +800,41 @@ impl OlmMachine {
/// connections.
pub fn close(self) {}
}

// helper for register_room_key_received_callback: wraps the key info
// into our own RoomKeyInfo struct, and passes it into the javascript
// function
async fn send_room_key_info_to_callback(
callback: &Function,
room_key_info: Vec<matrix_sdk_crypto::store::RoomKeyInfo>,
) {
let rki: Array = room_key_info.into_iter().map(RoomKeyInfo::from).map(JsValue::from).collect();
match promise_result_to_future(callback.call1(&JsValue::NULL, &rki)).await {
Ok(_) => (),
Err(e) => {
warn!("Error calling room-key-received callback: {:?}", e);
}
}
}

/// Given a result from a javascript function which returns a Promise (or throws
/// an exception before returning one), convert the result to a rust Future
/// which completes with the result of the promise
async fn promise_result_to_future(res: Result<JsValue, JsValue>) -> Result<JsValue, JsValue> {
match res {
Ok(retval) => {
if !retval.has_type::<Promise>() {
panic!("not a promise");
}
let prom: Promise = retval.dyn_into().map_err(|v| {
JsError::new(&format!("function returned a non-Promise value {v:?}"))
})?;
JsFuture::from(prom).await
}
Err(e) => {
// the function threw an exception before it returned the promise. We can just
// return the error as an error result.
Err(e)
}
}
}
9 changes: 8 additions & 1 deletion bindings/matrix-sdk-crypto-js/src/olm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use wasm_bindgen::prelude::*;

use crate::{identifiers, impl_from_to_inner};
use crate::{identifiers, impl_from_to_inner, vodozemac::Curve25519PublicKey};

/// Struct representing the state of our private cross signing keys,
/// it shows which private cross signing keys we have locally stored.
Expand Down Expand Up @@ -57,6 +57,13 @@ impl InboundGroupSession {
self.inner.room_id().to_owned().into()
}

/// The Curve25519 key of the sender of this session, as a
/// [Curve25519PublicKey].
#[wasm_bindgen(getter, js_name = "senderKey")]
pub fn sender_key(&self) -> Curve25519PublicKey {
self.inner.sender_key().into()
}

/// Returns the unique identifier for this session.
#[wasm_bindgen(getter, js_name = "sessionId")]
pub fn session_id(&self) -> String {
Expand Down
42 changes: 41 additions & 1 deletion bindings/matrix-sdk-crypto-js/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
use wasm_bindgen::prelude::*;

use crate::impl_from_to_inner;
use crate::{
encryption::EncryptionAlgorithm, identifiers::RoomId, impl_from_to_inner,
vodozemac::Curve25519PublicKey,
};

/// A struct containing private cross signing keys that can be backed
/// up or uploaded to the secret store.
Expand Down Expand Up @@ -34,3 +37,40 @@ impl CrossSigningKeyExport {
self.inner.user_signing_key.clone()
}
}

/// Information on a room key that has been received or imported.
#[wasm_bindgen]
#[derive(Debug)]
pub struct RoomKeyInfo {
pub(crate) inner: matrix_sdk_crypto::store::RoomKeyInfo,
}

impl_from_to_inner!(matrix_sdk_crypto::store::RoomKeyInfo => RoomKeyInfo);

#[wasm_bindgen]
impl RoomKeyInfo {
/// The {@link EncryptionAlgorithm} that this key is used for. Will be one
/// of the `m.megolm.*` algorithms.
#[wasm_bindgen(getter)]
pub fn algorithm(&self) -> EncryptionAlgorithm {
self.inner.algorithm.clone().into()
}

/// The room where the key is used.
#[wasm_bindgen(getter, js_name = "roomId")]
pub fn room_id(&self) -> RoomId {
self.inner.room_id.clone().into()
}

/// The Curve25519 key of the device which initiated the session originally.
#[wasm_bindgen(getter, js_name = "senderKey")]
pub fn sender_key(&self) -> Curve25519PublicKey {
self.inner.sender_key.into()
}

/// The ID of the session that the key is for.
#[wasm_bindgen(getter, js_name = "sessionId")]
pub fn session_id(&self) -> String {
self.inner.session_id.clone()
}
}
17 changes: 15 additions & 2 deletions bindings/matrix-sdk-crypto-js/tests/machine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,15 +602,15 @@ describe(OlmMachine.name, () => {
});

describe("can export/import room keys", () => {
let m;
let exportedRoomKeys;

test("can export room keys", async () => {
m = await machine();
let 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.senderKey.toBase64()).toEqual(m.identityKeys.curve25519.toBase64());
expect(session.roomId.toString()).toStrictEqual(room.toString());
expect(session.sessionId).toBeDefined();
expect(session.hasBeenImported()).toStrictEqual(false);
Expand Down Expand Up @@ -664,6 +664,7 @@ describe(OlmMachine.name, () => {
expect(total).toStrictEqual(1n);
};

let m = await machine();
const result = JSON.parse(await m.importRoomKeys(exportedRoomKeys, progressListener));

expect(result).toMatchObject({
Expand All @@ -672,6 +673,18 @@ describe(OlmMachine.name, () => {
keys: expect.any(Object),
});
});

test("importing room keys calls RoomKeyUpdatedCallback", async () => {
const callback = jest.fn();
callback.mockImplementation(() => Promise.resolve(undefined));
let m = await machine();
m.registerRoomKeyUpdatedCallback(callback);
await m.importRoomKeys(exportedRoomKeys, (_, _1) => {});
expect(callback).toHaveBeenCalledTimes(1);
let keyInfoList = callback.mock.calls[0][0];
expect(keyInfoList.length).toEqual(1);
expect(keyInfoList[0].roomId.toString()).toStrictEqual(room.toString());
});
});

describe("can do in-room verification", () => {
Expand Down
4 changes: 4 additions & 0 deletions crates/matrix-sdk-crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# v0.7.0

- Add new API `store::Store::room_keys_received_stream` to provide
updates of room keys being received.
4 changes: 3 additions & 1 deletion crates/matrix-sdk-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ tracing = { workspace = true, features = ["attributes"] }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
cfg-if = "1.0"
tokio-stream = { version = "0.1.12", features = ["sync"] }
tokio = { workspace = true, default-features = false, features = ["sync"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { workspace = true }
Expand All @@ -68,7 +70,7 @@ tokio = { workspace = true, features = ["time"] }
anyhow = { workspace = true }
assert_matches = "1.5.0"
ctor.workspace = true
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
futures = { version = "0.3.21", features = ["executor"] }
http = { workspace = true }
indoc = "1.0.4"
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
Expand Down
Loading

0 comments on commit f988106

Please sign in to comment.