From f333da898a9978abe3175015c4d650c153d17e7c Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 26 May 2025 14:37:40 +0200 Subject: [PATCH 1/2] feat(widgetDriver): add to-device encryption support --- .../src/widget/machine/driver_req.rs | 1 + crates/matrix-sdk/src/widget/matrix.rs | 188 +++++++++++++++++- 2 files changed, 181 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/widget/machine/driver_req.rs b/crates/matrix-sdk/src/widget/machine/driver_req.rs index 9188077f409..88c61910901 100644 --- a/crates/matrix-sdk/src/widget/machine/driver_req.rs +++ b/crates/matrix-sdk/src/widget/machine/driver_req.rs @@ -273,6 +273,7 @@ impl FromMatrixDriverResponse for SendEventResponse { /// Ask the client to send a to-device message that corresponds to the given /// description. +/// see [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)) as a response. #[derive(Clone, Debug, Deserialize)] pub(crate) struct SendToDeviceRequest { /// The type of the to-device message. diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 86744c0406b..93e43863f25 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -17,6 +17,7 @@ use std::collections::BTreeMap; +use futures_util::future::join_all; use matrix_sdk_base::deserialized_responses::{EncryptionInfo, RawAnySyncOrStrippedState}; use ruma::{ api::client::{ @@ -197,10 +198,11 @@ impl MatrixDriver { async {} }); let drop_guard_msg_like = self.room.client().event_handler_drop_guard(handle_msg_like); - + let _room_id = room_id; + let _tx = tx; // Get only all state events from the state section of the sync. let handle_state = self.room.add_event_handler(move |raw: Raw| { - let _ = tx.send(attach_room_id(raw.cast_ref(), &room_id)); + let _ = _tx.send(attach_room_id(raw.cast_ref(), &_room_id)); async {} }); let drop_guard_state = self.room.client().event_handler_drop_guard(handle_state); @@ -224,8 +226,11 @@ impl MatrixDriver { // 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, _: Option| { - let _ = tx.send(raw); + move |raw: Raw, encryption_info: Option| { + // Only sent encrypted to-device events + if encryption_info.is_some() { + let _ = tx.send(raw); + } async {} }, ); @@ -248,9 +253,36 @@ impl MatrixDriver { let client = self.room.client(); let request = if encrypted { - return Err(Error::UnknownError( - "Sending encrypted to-device events is not supported by the widget driver.".into(), - )); + // We first want to get all missing session before we start any to device + // sending! + client.claim_one_time_keys(messages.keys().map(|u| u.as_ref())).await?; + let encrypted_content: BTreeMap< + OwnedUserId, + BTreeMap>, + > = join_all(messages.into_iter().map(|(user_id, device_content_map)| { + let event_type = event_type.clone(); + async move { + ( + user_id.clone(), + to_device_crypto::encrypted_device_content_map( + &self.room.client(), + &user_id, + &event_type, + device_content_map, + ) + .await, + ) + } + })) + .await + .into_iter() + .collect(); + + RumaToDeviceRequest::new_raw( + ToDeviceEventType::RoomEncrypted, + TransactionId::new(), + encrypted_content, + ) } else { RumaToDeviceRequest::new_raw(event_type, TransactionId::new(), messages) }; @@ -280,13 +312,153 @@ fn attach_room_id(raw_ev: &Raw, room_id: &RoomId) -> Raw, + devices: Vec, + event_type: &ToDeviceEventType, + ) -> Result)>> { + let content: Value = unencrypted_content.deserialize_as().map_err(Into::::into)?; + let event_type = event_type.clone(); + let device_content_tasks = devices.into_iter().map(|device| spawn({ + let event_type = event_type.clone(); + let content = content.clone(); + + async move { + // This is not yet used. It is incompatible with the spa guest mode (the spa will not verify its crypto identity) + // if !device.is_cross_signed_by_owner() { + // info!("Device {} is not verified, skipping encryption", device.device_id()); + // return None; + // } + match device + .inner + .encrypt_event_raw(&event_type.to_string(), &content) + .await { + Ok(encrypted) => Some((device.device_id().to_owned().into(), encrypted.cast())), + Err(e) =>{ info!("Failed to encrypt to_device event from widget for device: {} because, {}", device.device_id(), e); None}, + } + } + })); + let device_encrypted_content_map = + join_all(device_content_tasks).await.into_iter().flatten().flatten(); + Ok(device_encrypted_content_map) + } + + /// Convert the device content map for one user into the same content + /// map with encrypted content This needs to flatten the vectors + /// we get from `encrypted_content_for_devices` + /// since one `DeviceIdOrAllDevices` id can be multiple devices. + pub(super) async fn encrypted_device_content_map( + client: &Client, + user_id: &UserId, + event_type: &ToDeviceEventType, + device_content_map: BTreeMap>, + ) -> BTreeMap> { + let device_map_futures = + device_content_map.into_iter().map(|(device_or_all_id, content)| spawn({ + let client = client.clone(); + let user_id = user_id.to_owned(); + let event_type = event_type.clone(); + async move { + let Ok(user_devices) = client.encryption().get_user_devices(&user_id).await else { + warn!("Failed to get user devices for user: {}", user_id); + return None; + }; + // This is not yet used. It is incompatible with the spa guest mode (the spa will not verify its crypto identity) + // let Ok(user_identity) = client.encryption().get_user_identity(&user_id).await else{ + // warn!("Failed to get user identity for user: {}", user_id); + // return None; + // }; + // if user_identity.map(|i|i.has_verification_violation()).unwrap_or(false) { + // info!("User {} has a verification violation, skipping encryption", user_id); + // return None; + // } + let devices: Vec = match device_or_all_id { + DeviceIdOrAllDevices::DeviceId(device_id) => { + vec![user_devices.get(&device_id)].into_iter().flatten().collect() + } + DeviceIdOrAllDevices::AllDevices => user_devices.devices().collect(), + }; + encrypted_content_for_devices( + &content, + devices, + &event_type, + ) + .await + .map_err(|e| info!("WidgetDriver: could not encrypt content for to device widget event content: {}. because, {}", content.json(), e)) + .ok() + }})); + let content_map_iterator = join_all(device_map_futures).await.into_iter(); + + // The first flatten takes the iterator over Result)>>, JoinError>> + // and flattens the Result (drops Err() items) + // The second takes the iterator over: Option)>> + // and flattens the Option (drops None items) + // The third takes the iterator over iterators: impl Iterator)> + // and flattens it to just an iterator over (DeviceIdOrAllDevices, + // Raw) + content_map_iterator.flatten().flatten().flatten().collect() + } +} + #[cfg(test)] mod tests { use insta; - use ruma::{events::AnyTimelineEvent, room_id, serde::Raw}; + use ruma::{ + events::{AnySyncTimelineEvent, AnyTimelineEvent}, + room_id, + serde::Raw, + }; use serde_json::{json, Value}; use super::attach_room_id; + #[test] + fn test_app_props_to_raw() { + let raw = Raw::new(&json!({ + "encrypted": true, + "type": "m.room.message", + "content": { + "body": "Hello world" + } + })) + .unwrap() + .cast::(); + let room_id = room_id!("!my_id:example.org"); + let new = attach_room_id(&raw, room_id); + assert_eq!( + serde_json::to_value(new).unwrap(), + json!({ + "encrypted": true, + "room_id": "!my_id:example.org", + "type": "m.room.message", + "content": { + "body": "Hello world" + } + }) + ); + } #[test] fn test_add_room_id_to_raw() { From 84403e92bf797d9b8d762f82981bf36299e7229c Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 26 May 2025 16:43:44 +0200 Subject: [PATCH 2/2] temp: sync discussion fixme --- crates/matrix-sdk/src/widget/matrix.rs | 152 ++---------------- crates/matrix-sdk/tests/integration/widget.rs | 3 +- 2 files changed, 19 insertions(+), 136 deletions(-) diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 93e43863f25..ddc419eb82e 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -24,7 +24,7 @@ use ruma::{ account::request_openid_token::v3::{Request as OpenIdRequest, Response as OpenIdResponse}, delayed_events::{self, update_delayed_event::unstable::UpdateAction}, filter::RoomEventFilter, - to_device::send_event_to_device::{self, v3::Request as RumaToDeviceRequest}, + to_device::send_event_to_device::{self}, }, assign, events::{ @@ -228,6 +228,8 @@ impl MatrixDriver { // not (as per MSC3819). move |raw: Raw, encryption_info: Option| { // Only sent encrypted to-device events + // TODO only if the room is enxrypted + // self.room. if encryption_info.is_some() { let _ = tx.send(raw); } @@ -252,44 +254,35 @@ impl MatrixDriver { ) -> Result { let client = self.room.client(); + // TODO: always encrypt based on the room encryption event + // Temporarly just log that the encryption flag is deprecated. + // TODO make encrypted a Option let request = if encrypted { // We first want to get all missing session before we start any to device // sending! - client.claim_one_time_keys(messages.keys().map(|u| u.as_ref())).await?; - let encrypted_content: BTreeMap< + // TODO transform this into an array of devicesLists per content. + + let responses: BTreeMap< OwnedUserId, BTreeMap>, > = join_all(messages.into_iter().map(|(user_id, device_content_map)| { + // TODO maybe group by content. let event_type = event_type.clone(); async move { - ( - user_id.clone(), - to_device_crypto::encrypted_device_content_map( - &self.room.client(), - &user_id, - &event_type, - device_content_map, - ) - .await, - ) + client.encryption().send_raw_to_device(devices, event_type, content); } })) .await .into_iter() .collect(); - - RumaToDeviceRequest::new_raw( - ToDeviceEventType::RoomEncrypted, - TransactionId::new(), - encrypted_content, - ) + //TODO do sth with the responsesn + response.map_err(Into::into) } else { - RumaToDeviceRequest::new_raw(event_type, TransactionId::new(), messages) - }; + RumaToDeviceRequest::new_raw(event_type, TransactionId::new(), messages); + let response = client.send(request).await; - let response = client.send(request).await; - - response.map_err(Into::into) + response.map_err(Into::into) + }; } } @@ -312,117 +305,6 @@ fn attach_room_id(raw_ev: &Raw, room_id: &RoomId) -> Raw, - devices: Vec, - event_type: &ToDeviceEventType, - ) -> Result)>> { - let content: Value = unencrypted_content.deserialize_as().map_err(Into::::into)?; - let event_type = event_type.clone(); - let device_content_tasks = devices.into_iter().map(|device| spawn({ - let event_type = event_type.clone(); - let content = content.clone(); - - async move { - // This is not yet used. It is incompatible with the spa guest mode (the spa will not verify its crypto identity) - // if !device.is_cross_signed_by_owner() { - // info!("Device {} is not verified, skipping encryption", device.device_id()); - // return None; - // } - match device - .inner - .encrypt_event_raw(&event_type.to_string(), &content) - .await { - Ok(encrypted) => Some((device.device_id().to_owned().into(), encrypted.cast())), - Err(e) =>{ info!("Failed to encrypt to_device event from widget for device: {} because, {}", device.device_id(), e); None}, - } - } - })); - let device_encrypted_content_map = - join_all(device_content_tasks).await.into_iter().flatten().flatten(); - Ok(device_encrypted_content_map) - } - - /// Convert the device content map for one user into the same content - /// map with encrypted content This needs to flatten the vectors - /// we get from `encrypted_content_for_devices` - /// since one `DeviceIdOrAllDevices` id can be multiple devices. - pub(super) async fn encrypted_device_content_map( - client: &Client, - user_id: &UserId, - event_type: &ToDeviceEventType, - device_content_map: BTreeMap>, - ) -> BTreeMap> { - let device_map_futures = - device_content_map.into_iter().map(|(device_or_all_id, content)| spawn({ - let client = client.clone(); - let user_id = user_id.to_owned(); - let event_type = event_type.clone(); - async move { - let Ok(user_devices) = client.encryption().get_user_devices(&user_id).await else { - warn!("Failed to get user devices for user: {}", user_id); - return None; - }; - // This is not yet used. It is incompatible with the spa guest mode (the spa will not verify its crypto identity) - // let Ok(user_identity) = client.encryption().get_user_identity(&user_id).await else{ - // warn!("Failed to get user identity for user: {}", user_id); - // return None; - // }; - // if user_identity.map(|i|i.has_verification_violation()).unwrap_or(false) { - // info!("User {} has a verification violation, skipping encryption", user_id); - // return None; - // } - let devices: Vec = match device_or_all_id { - DeviceIdOrAllDevices::DeviceId(device_id) => { - vec![user_devices.get(&device_id)].into_iter().flatten().collect() - } - DeviceIdOrAllDevices::AllDevices => user_devices.devices().collect(), - }; - encrypted_content_for_devices( - &content, - devices, - &event_type, - ) - .await - .map_err(|e| info!("WidgetDriver: could not encrypt content for to device widget event content: {}. because, {}", content.json(), e)) - .ok() - }})); - let content_map_iterator = join_all(device_map_futures).await.into_iter(); - - // The first flatten takes the iterator over Result)>>, JoinError>> - // and flattens the Result (drops Err() items) - // The second takes the iterator over: Option)>> - // and flattens the Option (drops None items) - // The third takes the iterator over iterators: impl Iterator)> - // and flattens it to just an iterator over (DeviceIdOrAllDevices, - // Raw) - content_map_iterator.flatten().flatten().flatten().collect() - } -} - #[cfg(test)] mod tests { use insta; diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index b9d2d6e1ab4..0a0665d3d57 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -27,6 +27,7 @@ use matrix_sdk::{ use matrix_sdk_common::{executor::spawn, timeout::timeout}; use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE, BOB}; use once_cell::sync::Lazy; +use rand::rngs::mock; use ruma::{ event_id, events::{room::member::MembershipState, MessageLikeEventType, StateEventType}, @@ -967,7 +968,7 @@ async fn test_send_encrypted_to_device_event() { }, } }), - json! {{"error":{"message":"Sending encrypted to-device events is not supported by the widget driver."}}}, + json! {{}}, 0, ) .await;