Skip to content

Commit cd141c5

Browse files
authored
feat(widget): Receive custom to-device messages in widgets in e2ee rooms
Proper support for receiving to-device messages for widgets. If the widget is in an e2ee room, clear to-device traffic will be excluded. Also filter out internal to-device messages that widgets should not be aware off.
1 parent 9596aa0 commit cd141c5

File tree

6 files changed

+474
-93
lines changed

6 files changed

+474
-93
lines changed

crates/matrix-sdk-crypto/src/olm/account.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,11 @@ impl Account {
15091509
)
15101510
.into());
15111511
}
1512+
1513+
// TODO: we should have access to some decryption settings here
1514+
// (TrustRequirement) and use it to manually reject the decryption.
1515+
// Similar to check_sender_trust_requirement for room events
1516+
15121517
sender_device = Some(device);
15131518
}
15141519

crates/matrix-sdk/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ sso-login = ["local-server"]
5555

5656
uniffi = ["dep:uniffi", "matrix-sdk-base/uniffi", "dep:matrix-sdk-ffi-macros"]
5757

58-
experimental-widgets = ["dep:uuid"]
58+
experimental-widgets = ["dep:uuid", "experimental-send-custom-to-device"]
5959

6060
docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"]
6161

crates/matrix-sdk/src/test_utils/mocks/encryption.rs

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,29 @@
1717
//! tests.
1818
use std::{
1919
collections::BTreeMap,
20+
future::Future,
2021
sync::{atomic::Ordering, Arc, Mutex},
2122
};
2223

24+
use matrix_sdk_test::test_json;
2325
use ruma::{
24-
api::client::keys::upload_signatures::v3::SignedKeys,
26+
api::client::{
27+
keys::upload_signatures::v3::SignedKeys, to_device::send_event_to_device::v3::Messages,
28+
},
2529
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
2630
owned_device_id, owned_user_id,
2731
serde::Raw,
28-
CrossSigningKeyId, DeviceId, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedOneTimeKeyId,
29-
OwnedUserId, UserId,
32+
CrossSigningKeyId, DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OwnedDeviceId,
33+
OwnedOneTimeKeyId, OwnedUserId, UserId,
3034
};
3135
use serde_json::json;
3236
use wiremock::{
3337
matchers::{method, path_regex},
34-
Mock, Request, ResponseTemplate,
38+
Mock, MockGuard, Request, ResponseTemplate,
3539
};
3640

3741
use crate::{
42+
crypto::types::events::room::encrypted::EncryptedToDeviceEvent,
3843
test_utils::{
3944
client::MockClientBuilder,
4045
mocks::{Keys, MatrixMockServer},
@@ -178,6 +183,156 @@ impl MatrixMockServer {
178183
.mount(&self.server)
179184
.await;
180185
}
186+
187+
/// Creates a response handler for mocking encrypted to-device message
188+
/// requests.
189+
///
190+
/// This function creates a response handler that captures encrypted
191+
/// to-device messages sent via the `/sendToDevice` endpoint.
192+
///
193+
/// # Arguments
194+
///
195+
/// * `sender` - The user ID of the message sender
196+
///
197+
/// # Returns
198+
///
199+
/// Returns a tuple containing:
200+
/// - A `MockGuard` the end-point mock is scoped to this guard
201+
/// - A `Future` that resolves to a `Raw<EncryptedToDeviceEvent>>`
202+
/// containing the captured encrypted to-device message.
203+
///
204+
/// # Examples
205+
///
206+
/// ```rust
207+
/// # use ruma::{ device_id, user_id, serde::Raw};
208+
/// # use serde_json::json;
209+
///
210+
/// # use matrix_sdk_test::async_test;
211+
/// # use matrix_sdk::test_utils::mocks::MatrixMockServer;
212+
/// #
213+
/// #[async_test]
214+
/// async fn test_mock_capture_put_to_device() {
215+
/// let server = MatrixMockServer::new().await;
216+
/// server.mock_crypto_endpoints_preset().await;
217+
///
218+
/// let (alice, bob) = server.set_up_alice_and_bob_for_encryption().await;
219+
/// let bob_user_id = bob.user_id().unwrap();
220+
/// let bob_device_id = bob.device_id().unwrap();
221+
///
222+
/// // From the point of view of Alice, Bob now has a device.
223+
/// let alice_bob_device = alice
224+
/// .encryption()
225+
/// .get_device(bob_user_id, bob_device_id)
226+
/// .await
227+
/// .unwrap()
228+
/// .expect("alice sees bob's device");
229+
///
230+
/// let content_raw = Raw::new(&json!({ /*...*/ })).unwrap().cast();
231+
///
232+
/// // Set up the mock to capture encrypted to-device messages
233+
/// let (guard, captured) =
234+
/// server.mock_capture_put_to_device(alice.user_id().unwrap()).await;
235+
///
236+
/// alice
237+
/// .encryption()
238+
/// .encrypt_and_send_raw_to_device(
239+
/// vec![&alice_bob_device],
240+
/// "call.keys",
241+
/// content_raw,
242+
/// )
243+
/// .await
244+
/// .unwrap();
245+
///
246+
/// // this is the captured event as sent by alice!
247+
/// let sent_event = captured.await;
248+
/// drop(guard);
249+
/// }
250+
/// ```
251+
pub async fn mock_capture_put_to_device(
252+
&self,
253+
sender_user_id: &UserId,
254+
) -> (MockGuard, impl Future<Output = Raw<EncryptedToDeviceEvent>>) {
255+
let (tx, rx) = tokio::sync::oneshot::channel();
256+
let tx = Arc::new(Mutex::new(Some(tx)));
257+
258+
let sender = sender_user_id.to_owned();
259+
let guard = Mock::given(method("PUT"))
260+
.and(path_regex(r"^/_matrix/client/.*/sendToDevice/m.room.encrypted/.*"))
261+
.respond_with(move |req: &Request| {
262+
#[derive(Debug, serde::Deserialize)]
263+
struct Parameters {
264+
messages: Messages,
265+
}
266+
267+
let params: Parameters = req.body_json().unwrap();
268+
269+
let (_, device_to_content) = params.messages.first_key_value().unwrap();
270+
let content = device_to_content.first_key_value().unwrap().1;
271+
272+
let event = json!({
273+
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
274+
"sender": sender,
275+
"type": "m.room.encrypted",
276+
"content": content,
277+
});
278+
let event: Raw<EncryptedToDeviceEvent> = serde_json::from_value(event).unwrap();
279+
280+
if let Ok(mut guard) = tx.lock() {
281+
if let Some(tx) = guard.take() {
282+
let _ = tx.send(event);
283+
}
284+
}
285+
286+
ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)
287+
})
288+
// Should be called once
289+
.expect(1)
290+
.named("send_to_device")
291+
.mount_as_scoped(self.server())
292+
.await;
293+
294+
let future =
295+
async move { rx.await.expect("Failed to receive captured value - sender was dropped") };
296+
297+
(guard, future)
298+
}
299+
300+
/// Captures a to-device message when it is sent to the mock server and then
301+
/// injects it into the recipient's sync response.
302+
///
303+
/// This is a utility function that combines capturing an encrypted
304+
/// to-device message and delivering it to the recipient through a sync
305+
/// response. It's useful for testing end-to-end encryption scenarios
306+
/// where you need to verify message delivery and processing.
307+
///
308+
/// # Arguments
309+
///
310+
/// * `sender_user_id` - The user ID of the message sender
311+
/// * `recipient` - The client that will receive the message through sync
312+
///
313+
/// # Returns
314+
///
315+
/// Returns a `Future` that will resolve when the captured event has been
316+
/// fed back down the recipient sync.
317+
pub async fn mock_capture_put_to_device_then_sync_back<'a>(
318+
&'a self,
319+
sender_user_id: &UserId,
320+
recipient: &'a Client,
321+
) -> impl Future<Output = Raw<EncryptedToDeviceEvent>> + 'a {
322+
let (guard, sent_event) = self.mock_capture_put_to_device(sender_user_id).await;
323+
324+
async {
325+
let sent_event = sent_event.await;
326+
drop(guard);
327+
self.mock_sync()
328+
.ok_and_run(recipient, |sync_builder| {
329+
sync_builder.add_to_device_event(sent_event.deserialize_as().unwrap());
330+
})
331+
.await;
332+
333+
sent_event
334+
}
335+
}
181336
}
182337

183338
/// Intercepts a `/keys/query` request and mock its results as returned by an

crates/matrix-sdk/src/widget/matrix.rs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ use tokio::sync::{
4040
broadcast::{error::RecvError, Receiver},
4141
mpsc::{unbounded_channel, UnboundedReceiver},
4242
};
43-
use tracing::error;
43+
use tracing::{error, trace, warn};
4444

4545
use super::{machine::SendEventResponse, StateKeySelector};
4646
use crate::{
47-
event_handler::EventHandlerDropGuard, room::MessagesOptions, sync::RoomUpdate, Error, Result,
48-
Room,
47+
event_handler::EventHandlerDropGuard, room::MessagesOptions, sync::RoomUpdate, Client, Error,
48+
Result, Room,
4949
};
5050

5151
/// Thin wrapper around a [`Room`] that provides functionality relevant for
@@ -235,21 +235,88 @@ impl MatrixDriver {
235235
pub(crate) fn to_device_events(&self) -> EventReceiver<Raw<AnyToDeviceEvent>> {
236236
let (tx, rx) = unbounded_channel();
237237

238+
let room_id = self.room.room_id().to_owned();
238239
let to_device_handle = self.room.client().add_event_handler(
239-
// TODO: encryption support for to-device is not yet supported. Needs an Olm
240-
// EncryptionInfo. The widgetAPI expects a boolean `encrypted` to be added
241-
// (!) to the raw content to know if the to-device message was encrypted or
242-
// not (as per MSC3819).
243-
move |raw: Raw<AnyToDeviceEvent>, _: Option<EncryptionInfo>| {
244-
let _ = tx.send(raw);
245-
async {}
240+
241+
async move |raw: Raw<AnyToDeviceEvent>, encryption_info: Option<EncryptionInfo>, client: Client| {
242+
243+
// Some to-device traffic is used by the SDK for internal machinery.
244+
// They should not be exposed to widgets.
245+
if Self::should_filter_message_to_widget(&raw) {
246+
return;
247+
}
248+
249+
// Encryption can be enabled after the widget has been instantiated,
250+
// we want to keep track of the latest status
251+
let Some(room) = client.get_room(&room_id) else {
252+
warn!("Room {room_id} not found in client.");
253+
return;
254+
};
255+
256+
let room_encrypted = room.latest_encryption_state().await
257+
.map(|s| s.is_encrypted())
258+
// Default consider encrypted
259+
.unwrap_or(true);
260+
if room_encrypted {
261+
// The room is encrypted so the to-device traffic should be too.
262+
if encryption_info.is_none() {
263+
warn!(
264+
?room_id,
265+
"Received to-device event in clear for a widget in an e2e room, dropping."
266+
);
267+
return;
268+
};
269+
270+
// There are no per-room specific decryption settings, so we can just send to the
271+
// widget
272+
let _ = tx.send(raw);
273+
} else {
274+
// forward to the widget
275+
// It is ok to send an encrypted to-device message even if the room is clear.
276+
let _ = tx.send(raw);
277+
}
246278
},
247279
);
248280

249281
let drop_guard = self.room.client().event_handler_drop_guard(to_device_handle);
250282
EventReceiver { rx, _drop_guard: drop_guard }
251283
}
252284

285+
fn should_filter_message_to_widget(raw_message: &Raw<AnyToDeviceEvent>) -> bool {
286+
let Ok(Some(event_type)) = raw_message.get_field::<String>("type") else {
287+
trace!("Invalid to-device message (no type) filtered out by widget driver.");
288+
return true;
289+
};
290+
291+
// Filter out all the internal crypto related traffic.
292+
// The SDK has already zeroized the critical data, but let's not leak any
293+
// information
294+
let filtered = matches!(
295+
event_type.as_str(),
296+
"m.dummy"
297+
| "m.room_key"
298+
| "m.room_key_request"
299+
| "m.forwarded_room_key"
300+
| "m.key.verification.request"
301+
| "m.key.verification.ready"
302+
| "m.key.verification.start"
303+
| "m.key.verification.cancel"
304+
| "m.key.verification.accept"
305+
| "m.key.verification.key"
306+
| "m.key.verification.mac"
307+
| "m.key.verification.done"
308+
| "m.secret.request"
309+
| "m.secret.send"
310+
// drop utd traffic
311+
| "m.room.encrypted"
312+
);
313+
314+
if filtered {
315+
trace!("To-device message of type <{event_type}> filtered out by widget driver.",);
316+
}
317+
filtered
318+
}
319+
253320
/// It will ignore all devices where errors occurred or where the device is
254321
/// not verified or where th user has a has_verification_violation.
255322
pub(crate) async fn send_to_device(

0 commit comments

Comments
 (0)