-
Notifications
You must be signed in to change notification settings - Fork 358
feat(widget): Receive custom to-device messages in widgets in e2ee rooms #5116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 23 commits
f14ecc9
0c4a895
c4cbcf9
fc4a97e
125aec6
d9137e9
53aeeb7
290f963
10a9688
7dd01ee
566d935
041e50d
27bc725
d705261
88d1fa5
d6819d0
41e0af9
2cbbceb
228a142
7a938ae
2faa3b2
481ba3f
d9ab76e
618bf31
71c02ab
a9aa3f4
51523e1
c714793
9e802a8
7cc8f01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,21 +17,25 @@ | |
| //! tests. | ||
| use std::{ | ||
| collections::BTreeMap, | ||
| future::Future, | ||
| sync::{atomic::Ordering, Arc, Mutex}, | ||
| }; | ||
|
|
||
| use matrix_sdk_test::test_json; | ||
| use ruma::{ | ||
| api::client::keys::upload_signatures::v3::SignedKeys, | ||
| api::client::{ | ||
| keys::upload_signatures::v3::SignedKeys, to_device::send_event_to_device::v3::Messages, | ||
| }, | ||
| encryption::{CrossSigningKey, DeviceKeys, OneTimeKey}, | ||
| owned_device_id, owned_user_id, | ||
| serde::Raw, | ||
| CrossSigningKeyId, DeviceId, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedOneTimeKeyId, | ||
| OwnedUserId, UserId, | ||
| CrossSigningKeyId, DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OwnedDeviceId, | ||
| OwnedOneTimeKeyId, OwnedUserId, UserId, | ||
| }; | ||
| use serde_json::json; | ||
| use serde_json::{json, Value}; | ||
| use wiremock::{ | ||
| matchers::{method, path_regex}, | ||
| Mock, Request, ResponseTemplate, | ||
| Mock, MockGuard, Request, ResponseTemplate, | ||
| }; | ||
|
|
||
| use crate::{ | ||
|
|
@@ -178,6 +182,70 @@ impl MatrixMockServer { | |
| .mount(&self.server) | ||
| .await; | ||
| } | ||
|
|
||
| /// Creates a response handler for mocking encrypted to-device message | ||
| /// requests. | ||
| /// | ||
| /// This function creates a response handler that captures encrypted | ||
| /// to-device messages sent via the `/sendToDevice` endpoint. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `sender` - The user ID of the message sender | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// Returns a tuple containing: | ||
| /// - A `MockGuard` the end-point mock is scoped to this guard | ||
| /// - A `Future` that resolves to a `Value` containing the captured | ||
poljar marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// encrypted to-device message. | ||
| pub async fn mock_capture_put_to_device( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a neat function, could we add an example to the doc so:
|
||
| &self, | ||
| sender_user_id: &UserId, | ||
| ) -> (MockGuard, impl Future<Output = Value>) { | ||
| let (tx, rx) = tokio::sync::oneshot::channel(); | ||
| let tx = Arc::new(Mutex::new(Some(tx))); | ||
|
|
||
| let sender = sender_user_id.to_owned(); | ||
| let guard = Mock::given(method("PUT")) | ||
| .and(path_regex(r"^/_matrix/client/.*/sendToDevice/m.room.encrypted/.*")) | ||
| .respond_with(move |req: &Request| { | ||
| #[derive(Debug, serde::Deserialize)] | ||
| struct Parameters { | ||
| messages: Messages, | ||
| } | ||
|
|
||
| let params: Parameters = req.body_json().unwrap(); | ||
|
|
||
| let (_, device_to_content) = params.messages.first_key_value().unwrap(); | ||
| let content = device_to_content.first_key_value().unwrap().1; | ||
|
|
||
| let event = json!({ | ||
| "origin_server_ts": MilliSecondsSinceUnixEpoch::now(), | ||
| "sender": sender, | ||
| "type": "m.room.encrypted", | ||
| "content": content, | ||
| }); | ||
|
|
||
| if let Ok(mut guard) = tx.lock() { | ||
| if let Some(tx) = guard.take() { | ||
| let _ = tx.send(event); | ||
| } | ||
| } | ||
|
|
||
| ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY) | ||
| }) | ||
| // Should be called once | ||
| .expect(1) | ||
| .named("send_to_device") | ||
| .mount_as_scoped(self.server()) | ||
| .await; | ||
|
|
||
| let future = | ||
| async move { rx.await.expect("Failed to receive captured value - sender was dropped") }; | ||
|
|
||
| (guard, future) | ||
| } | ||
| } | ||
|
|
||
| /// Intercepts a `/keys/query` request and mock its results as returned by an | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,12 +40,12 @@ use tokio::sync::{ | |
| broadcast::{error::RecvError, Receiver}, | ||
| mpsc::{unbounded_channel, UnboundedReceiver}, | ||
| }; | ||
| use tracing::error; | ||
| use tracing::{error, trace, warn}; | ||
|
|
||
| use super::{machine::SendEventResponse, StateKeySelector}; | ||
| use crate::{ | ||
| event_handler::EventHandlerDropGuard, room::MessagesOptions, sync::RoomUpdate, Error, Result, | ||
| Room, | ||
| event_handler::EventHandlerDropGuard, room::MessagesOptions, sync::RoomUpdate, Client, Error, | ||
| Result, Room, | ||
| }; | ||
|
|
||
| /// Thin wrapper around a [`Room`] that provides functionality relevant for | ||
|
|
@@ -235,21 +235,86 @@ impl MatrixDriver { | |
| pub(crate) fn to_device_events(&self) -> EventReceiver<Raw<AnyToDeviceEvent>> { | ||
| let (tx, rx) = unbounded_channel(); | ||
|
|
||
| let room_id = self.room.room_id().to_owned(); | ||
| let to_device_handle = self.room.client().add_event_handler( | ||
| // TODO: encryption support for to-device is not yet supported. Needs an Olm | ||
| // EncryptionInfo. The widgetAPI expects a boolean `encrypted` to be added | ||
| // (!) to the raw content to know if the to-device message was encrypted or | ||
| // not (as per MSC3819). | ||
| move |raw: Raw<AnyToDeviceEvent>, _: Option<EncryptionInfo>| { | ||
| let _ = tx.send(raw); | ||
| async {} | ||
|
|
||
| async move |raw: Raw<AnyToDeviceEvent>, encryption_info: Option<EncryptionInfo>, client: Client| { | ||
|
|
||
| // Some to-device traffic is used by the sdk for internal machinery. | ||
BillCarsonFr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // They should not be exposed to widgets. | ||
| if Self::should_filter_message_to_widget(&raw) { | ||
| trace!("Internal or UTD to-device message filtered out by widget driver."); | ||
|
||
| return; | ||
| } | ||
|
|
||
| // Encryption can be enabled after the widget has been instantiated, | ||
| // we want to keep track of the latest status | ||
| let Some(room) = client.get_room(&room_id) else { | ||
| warn!("Room {} not found in client.", room_id); | ||
BillCarsonFr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
| }; | ||
|
|
||
| let room_encrypted = room.latest_encryption_state().await | ||
| .map(|s| s.is_encrypted()) | ||
| // Default consider encrypted | ||
| .unwrap_or(true); | ||
BillCarsonFr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if room_encrypted { | ||
| // The room is encrypted so the to-device traffic should be too. | ||
| if encryption_info.is_none() { | ||
| warn!( | ||
| ?room_id, | ||
| "Received to-device event in clear for a widget in an e2e room, dropping." | ||
| ); | ||
| return; | ||
| }; | ||
|
|
||
| // There no per-room specific decryption setting, so we can just send to the | ||
| // widget | ||
BillCarsonFr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let _ = tx.send(raw); | ||
| } else { | ||
| // forward to the widget | ||
| // It is ok to send an encrypted to-device message even if the room is clear. | ||
| let _ = tx.send(raw); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| let drop_guard = self.room.client().event_handler_drop_guard(to_device_handle); | ||
| EventReceiver { rx, _drop_guard: drop_guard } | ||
| } | ||
|
|
||
| fn should_filter_message_to_widget(raw_message: &Raw<AnyToDeviceEvent>) -> bool { | ||
| let Ok(Some(event_type)) = raw_message.get_field::<String>("type") else { | ||
| return true; | ||
| }; | ||
|
|
||
| if event_type == "m.room.encrypted" { | ||
| // Unable to decrypt, | ||
| return true; | ||
| } | ||
BillCarsonFr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Filter out all the internal crypto related traffic. | ||
| // The SDK has already zeroized the critical data, but let's not leak any | ||
| // information | ||
| matches!( | ||
| event_type.as_str(), | ||
| "m.dummy" | ||
| | "m.room_key" | ||
| | "m.room_key_request" | ||
| | "m.forwarded_room_key" | ||
| | "m.key.verification.request" | ||
| | "m.key.verification.ready" | ||
| | "m.key.verification.start" | ||
| | "m.key.verification.cancel" | ||
| | "m.key.verification.accept" | ||
| | "m.key.verification.key" | ||
| | "m.key.verification.mac" | ||
| | "m.key.verification.done" | ||
| | "m.secret.request" | ||
| | "m.secret.send" | ||
| ) | ||
| } | ||
|
|
||
| /// It will ignore all devices where errors occurred or where the device is | ||
| /// not verified or where th user has a has_verification_violation. | ||
| pub(crate) async fn send_to_device( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.