Skip to content

Commit f988106

Browse files
authored
Add an API to notify apps when a megolm key is received
This is useful for applications which want to have another go at decryption when some room keys arrive.
1 parent 7fd1c93 commit f988106

File tree

11 files changed

+265
-30
lines changed

11 files changed

+265
-30
lines changed

Cargo.lock

Lines changed: 21 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# v0.1.0-alpha.6
2+
3+
- Add new accessor `InboundGroupSession.senderKey`.
4+
- Add a new API, `OlmMachine.registerRoomKeyUpdatedCallback`, which
5+
applications can use to listen for received room keys.

bindings/matrix-sdk-crypto-js/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ crate-type = ["cdylib"]
3232
[features]
3333
default = ["tracing", "qrcode"]
3434
qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"]
35-
tracing = ["dep:tracing"]
35+
tracing = []
3636

3737
[dependencies]
3838
anyhow = { workspace = true }
3939
console_error_panic_hook = "0.1.7"
40+
futures-util = "0.3.27"
4041
http = { workspace = true }
4142
js-sys = "0.3.49"
4243
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
@@ -45,7 +46,7 @@ matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-inde
4546
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
4647
ruma = { workspace = true, features = ["js", "rand", "unstable-msc2677"] }
4748
serde_json = { workspace = true }
48-
tracing = { workspace = true, optional = true }
49+
tracing = { workspace = true }
4950
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
5051
vodozemac = { workspace = true, features = ["js"] }
5152
wasm-bindgen = "0.2.83"

bindings/matrix-sdk-crypto-js/src/machine.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
33
use std::{collections::BTreeMap, ops::Deref};
44

5+
use futures_util::StreamExt;
56
use js_sys::{Array, Function, Map, Promise, Set};
67
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
78
use serde_json::{json, Value as JsonValue};
9+
use tracing::warn;
810
use wasm_bindgen::prelude::*;
11+
use wasm_bindgen_futures::{spawn_local, JsFuture};
912

1013
use crate::{
1114
device, encryption,
@@ -15,7 +18,9 @@ use crate::{
1518
olm, requests,
1619
requests::{OutgoingRequest, ToDeviceRequest},
1720
responses::{self, response_from_string},
18-
store, sync_events, types, verification, vodozemac,
21+
store,
22+
store::RoomKeyInfo,
23+
sync_events, types, verification, vodozemac,
1924
};
2025

2126
/// State machine implementation of the Olm/Megolm encryption protocol
@@ -768,6 +773,25 @@ impl OlmMachine {
768773
)?)?)
769774
}
770775

776+
/// Register a callback which will be called whenever there is an update to
777+
/// a room key.
778+
///
779+
/// `callback` should be a function that takes a single argument (an array
780+
/// of {@link RoomKeyInfo}) and returns a Promise.
781+
#[wasm_bindgen(js_name = "registerRoomKeyUpdatedCallback")]
782+
pub async fn register_room_key_updated_callback(&self, callback: Function) {
783+
let stream = self.inner.store().room_keys_received_stream();
784+
785+
// fire up a promise chain which will call `cb` on each result from the stream
786+
spawn_local(async move {
787+
// take a reference to `callback` (which we then pass into the closure), to stop
788+
// the callback being moved into the closure (which would mean we could only
789+
// call the closure once)
790+
let callback_ref = &callback;
791+
stream.for_each(move |item| send_room_key_info_to_callback(callback_ref, item)).await;
792+
});
793+
}
794+
771795
/// Shut down the `OlmMachine`.
772796
///
773797
/// The `OlmMachine` cannot be used after this method has been called.
@@ -776,3 +800,41 @@ impl OlmMachine {
776800
/// connections.
777801
pub fn close(self) {}
778802
}
803+
804+
// helper for register_room_key_received_callback: wraps the key info
805+
// into our own RoomKeyInfo struct, and passes it into the javascript
806+
// function
807+
async fn send_room_key_info_to_callback(
808+
callback: &Function,
809+
room_key_info: Vec<matrix_sdk_crypto::store::RoomKeyInfo>,
810+
) {
811+
let rki: Array = room_key_info.into_iter().map(RoomKeyInfo::from).map(JsValue::from).collect();
812+
match promise_result_to_future(callback.call1(&JsValue::NULL, &rki)).await {
813+
Ok(_) => (),
814+
Err(e) => {
815+
warn!("Error calling room-key-received callback: {:?}", e);
816+
}
817+
}
818+
}
819+
820+
/// Given a result from a javascript function which returns a Promise (or throws
821+
/// an exception before returning one), convert the result to a rust Future
822+
/// which completes with the result of the promise
823+
async fn promise_result_to_future(res: Result<JsValue, JsValue>) -> Result<JsValue, JsValue> {
824+
match res {
825+
Ok(retval) => {
826+
if !retval.has_type::<Promise>() {
827+
panic!("not a promise");
828+
}
829+
let prom: Promise = retval.dyn_into().map_err(|v| {
830+
JsError::new(&format!("function returned a non-Promise value {v:?}"))
831+
})?;
832+
JsFuture::from(prom).await
833+
}
834+
Err(e) => {
835+
// the function threw an exception before it returned the promise. We can just
836+
// return the error as an error result.
837+
Err(e)
838+
}
839+
}
840+
}

bindings/matrix-sdk-crypto-js/src/olm.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use wasm_bindgen::prelude::*;
44

5-
use crate::{identifiers, impl_from_to_inner};
5+
use crate::{identifiers, impl_from_to_inner, vodozemac::Curve25519PublicKey};
66

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

60+
/// The Curve25519 key of the sender of this session, as a
61+
/// [Curve25519PublicKey].
62+
#[wasm_bindgen(getter, js_name = "senderKey")]
63+
pub fn sender_key(&self) -> Curve25519PublicKey {
64+
self.inner.sender_key().into()
65+
}
66+
6067
/// Returns the unique identifier for this session.
6168
#[wasm_bindgen(getter, js_name = "sessionId")]
6269
pub fn session_id(&self) -> String {

bindings/matrix-sdk-crypto-js/src/store.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
33
use wasm_bindgen::prelude::*;
44

5-
use crate::impl_from_to_inner;
5+
use crate::{
6+
encryption::EncryptionAlgorithm, identifiers::RoomId, impl_from_to_inner,
7+
vodozemac::Curve25519PublicKey,
8+
};
69

710
/// A struct containing private cross signing keys that can be backed
811
/// up or uploaded to the secret store.
@@ -34,3 +37,40 @@ impl CrossSigningKeyExport {
3437
self.inner.user_signing_key.clone()
3538
}
3639
}
40+
41+
/// Information on a room key that has been received or imported.
42+
#[wasm_bindgen]
43+
#[derive(Debug)]
44+
pub struct RoomKeyInfo {
45+
pub(crate) inner: matrix_sdk_crypto::store::RoomKeyInfo,
46+
}
47+
48+
impl_from_to_inner!(matrix_sdk_crypto::store::RoomKeyInfo => RoomKeyInfo);
49+
50+
#[wasm_bindgen]
51+
impl RoomKeyInfo {
52+
/// The {@link EncryptionAlgorithm} that this key is used for. Will be one
53+
/// of the `m.megolm.*` algorithms.
54+
#[wasm_bindgen(getter)]
55+
pub fn algorithm(&self) -> EncryptionAlgorithm {
56+
self.inner.algorithm.clone().into()
57+
}
58+
59+
/// The room where the key is used.
60+
#[wasm_bindgen(getter, js_name = "roomId")]
61+
pub fn room_id(&self) -> RoomId {
62+
self.inner.room_id.clone().into()
63+
}
64+
65+
/// The Curve25519 key of the device which initiated the session originally.
66+
#[wasm_bindgen(getter, js_name = "senderKey")]
67+
pub fn sender_key(&self) -> Curve25519PublicKey {
68+
self.inner.sender_key.into()
69+
}
70+
71+
/// The ID of the session that the key is for.
72+
#[wasm_bindgen(getter, js_name = "sessionId")]
73+
pub fn session_id(&self) -> String {
74+
self.inner.session_id.clone()
75+
}
76+
}

bindings/matrix-sdk-crypto-js/tests/machine.test.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,15 +602,15 @@ describe(OlmMachine.name, () => {
602602
});
603603

604604
describe("can export/import room keys", () => {
605-
let m;
606605
let exportedRoomKeys;
607606

608607
test("can export room keys", async () => {
609-
m = await machine();
608+
let m = await machine();
610609
await m.shareRoomKey(room, [new UserId("@bob:example.org")], new EncryptionSettings());
611610

612611
exportedRoomKeys = await m.exportRoomKeys((session) => {
613612
expect(session).toBeInstanceOf(InboundGroupSession);
613+
expect(session.senderKey.toBase64()).toEqual(m.identityKeys.curve25519.toBase64());
614614
expect(session.roomId.toString()).toStrictEqual(room.toString());
615615
expect(session.sessionId).toBeDefined();
616616
expect(session.hasBeenImported()).toStrictEqual(false);
@@ -664,6 +664,7 @@ describe(OlmMachine.name, () => {
664664
expect(total).toStrictEqual(1n);
665665
};
666666

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

669670
expect(result).toMatchObject({
@@ -672,6 +673,18 @@ describe(OlmMachine.name, () => {
672673
keys: expect.any(Object),
673674
});
674675
});
676+
677+
test("importing room keys calls RoomKeyUpdatedCallback", async () => {
678+
const callback = jest.fn();
679+
callback.mockImplementation(() => Promise.resolve(undefined));
680+
let m = await machine();
681+
m.registerRoomKeyUpdatedCallback(callback);
682+
await m.importRoomKeys(exportedRoomKeys, (_, _1) => {});
683+
expect(callback).toHaveBeenCalledTimes(1);
684+
let keyInfoList = callback.mock.calls[0][0];
685+
expect(keyInfoList.length).toEqual(1);
686+
expect(keyInfoList[0].roomId.toString()).toStrictEqual(room.toString());
687+
});
675688
});
676689

677690
describe("can do in-room verification", () => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# v0.7.0
2+
3+
- Add new API `store::Store::room_keys_received_stream` to provide
4+
updates of room keys being received.

crates/matrix-sdk-crypto/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ tracing = { workspace = true, features = ["attributes"] }
5757
vodozemac = { workspace = true }
5858
zeroize = { workspace = true, features = ["zeroize_derive"] }
5959
cfg-if = "1.0"
60+
tokio-stream = { version = "0.1.12", features = ["sync"] }
61+
tokio = { workspace = true, default-features = false, features = ["sync"] }
6062

6163
[target.'cfg(target_arch = "wasm32")'.dependencies]
6264
tokio = { workspace = true }
@@ -68,7 +70,7 @@ tokio = { workspace = true, features = ["time"] }
6870
anyhow = { workspace = true }
6971
assert_matches = "1.5.0"
7072
ctor.workspace = true
71-
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
73+
futures = { version = "0.3.21", features = ["executor"] }
7274
http = { workspace = true }
7375
indoc = "1.0.4"
7476
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }

0 commit comments

Comments
 (0)