Skip to content

Commit f333da8

Browse files
committed
feat(widgetDriver): add to-device encryption support
1 parent e53eaf4 commit f333da8

File tree

2 files changed

+181
-8
lines changed

2 files changed

+181
-8
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ impl FromMatrixDriverResponse for SendEventResponse {
273273

274274
/// Ask the client to send a to-device message that corresponds to the given
275275
/// description.
276+
/// see [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140)) as a response.
276277
#[derive(Clone, Debug, Deserialize)]
277278
pub(crate) struct SendToDeviceRequest {
278279
/// The type of the to-device message.

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

Lines changed: 180 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
1818
use std::collections::BTreeMap;
1919

20+
use futures_util::future::join_all;
2021
use matrix_sdk_base::deserialized_responses::{EncryptionInfo, RawAnySyncOrStrippedState};
2122
use ruma::{
2223
api::client::{
@@ -197,10 +198,11 @@ impl MatrixDriver {
197198
async {}
198199
});
199200
let drop_guard_msg_like = self.room.client().event_handler_drop_guard(handle_msg_like);
200-
201+
let _room_id = room_id;
202+
let _tx = tx;
201203
// Get only all state events from the state section of the sync.
202204
let handle_state = self.room.add_event_handler(move |raw: Raw<AnySyncStateEvent>| {
203-
let _ = tx.send(attach_room_id(raw.cast_ref(), &room_id));
205+
let _ = _tx.send(attach_room_id(raw.cast_ref(), &_room_id));
204206
async {}
205207
});
206208
let drop_guard_state = self.room.client().event_handler_drop_guard(handle_state);
@@ -224,8 +226,11 @@ impl MatrixDriver {
224226
// EncryptionInfo. The widgetAPI expects a boolean `encrypted` to be added
225227
// (!) to the raw content to know if the to-device message was encrypted or
226228
// not (as per MSC3819).
227-
move |raw: Raw<AnyToDeviceEvent>, _: Option<EncryptionInfo>| {
228-
let _ = tx.send(raw);
229+
move |raw: Raw<AnyToDeviceEvent>, encryption_info: Option<EncryptionInfo>| {
230+
// Only sent encrypted to-device events
231+
if encryption_info.is_some() {
232+
let _ = tx.send(raw);
233+
}
229234
async {}
230235
},
231236
);
@@ -248,9 +253,36 @@ impl MatrixDriver {
248253
let client = self.room.client();
249254

250255
let request = if encrypted {
251-
return Err(Error::UnknownError(
252-
"Sending encrypted to-device events is not supported by the widget driver.".into(),
253-
));
256+
// We first want to get all missing session before we start any to device
257+
// sending!
258+
client.claim_one_time_keys(messages.keys().map(|u| u.as_ref())).await?;
259+
let encrypted_content: BTreeMap<
260+
OwnedUserId,
261+
BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>,
262+
> = join_all(messages.into_iter().map(|(user_id, device_content_map)| {
263+
let event_type = event_type.clone();
264+
async move {
265+
(
266+
user_id.clone(),
267+
to_device_crypto::encrypted_device_content_map(
268+
&self.room.client(),
269+
&user_id,
270+
&event_type,
271+
device_content_map,
272+
)
273+
.await,
274+
)
275+
}
276+
}))
277+
.await
278+
.into_iter()
279+
.collect();
280+
281+
RumaToDeviceRequest::new_raw(
282+
ToDeviceEventType::RoomEncrypted,
283+
TransactionId::new(),
284+
encrypted_content,
285+
)
254286
} else {
255287
RumaToDeviceRequest::new_raw(event_type, TransactionId::new(), messages)
256288
};
@@ -280,13 +312,153 @@ fn attach_room_id(raw_ev: &Raw<AnySyncTimelineEvent>, room_id: &RoomId) -> Raw<A
280312
Raw::new(&ev_obj).unwrap().cast()
281313
}
282314

315+
/// Move this into the `matrix_crypto` crate!
316+
/// This module contains helper functions to encrypt to device events.
317+
mod to_device_crypto {
318+
use std::collections::BTreeMap;
319+
320+
use futures_util::future::join_all;
321+
use ruma::{
322+
events::{AnyToDeviceEventContent, ToDeviceEventType},
323+
serde::Raw,
324+
to_device::DeviceIdOrAllDevices,
325+
UserId,
326+
};
327+
use serde_json::Value;
328+
use tracing::{info, warn};
329+
330+
use crate::{encryption::identities::Device, executor::spawn, Client, Error, Result};
331+
332+
/// This encrypts to device content for a collection of devices.
333+
/// It will ignore all devices where errors occurred or where the device
334+
/// is not verified or where th user has a has_verification_violation.
335+
async fn encrypted_content_for_devices(
336+
unencrypted_content: &Raw<AnyToDeviceEventContent>,
337+
devices: Vec<Device>,
338+
event_type: &ToDeviceEventType,
339+
) -> Result<impl Iterator<Item = (DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>)>> {
340+
let content: Value = unencrypted_content.deserialize_as().map_err(Into::<Error>::into)?;
341+
let event_type = event_type.clone();
342+
let device_content_tasks = devices.into_iter().map(|device| spawn({
343+
let event_type = event_type.clone();
344+
let content = content.clone();
345+
346+
async move {
347+
// This is not yet used. It is incompatible with the spa guest mode (the spa will not verify its crypto identity)
348+
// if !device.is_cross_signed_by_owner() {
349+
// info!("Device {} is not verified, skipping encryption", device.device_id());
350+
// return None;
351+
// }
352+
match device
353+
.inner
354+
.encrypt_event_raw(&event_type.to_string(), &content)
355+
.await {
356+
Ok(encrypted) => Some((device.device_id().to_owned().into(), encrypted.cast())),
357+
Err(e) =>{ info!("Failed to encrypt to_device event from widget for device: {} because, {}", device.device_id(), e); None},
358+
}
359+
}
360+
}));
361+
let device_encrypted_content_map =
362+
join_all(device_content_tasks).await.into_iter().flatten().flatten();
363+
Ok(device_encrypted_content_map)
364+
}
365+
366+
/// Convert the device content map for one user into the same content
367+
/// map with encrypted content This needs to flatten the vectors
368+
/// we get from `encrypted_content_for_devices`
369+
/// since one `DeviceIdOrAllDevices` id can be multiple devices.
370+
pub(super) async fn encrypted_device_content_map(
371+
client: &Client,
372+
user_id: &UserId,
373+
event_type: &ToDeviceEventType,
374+
device_content_map: BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>,
375+
) -> BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>> {
376+
let device_map_futures =
377+
device_content_map.into_iter().map(|(device_or_all_id, content)| spawn({
378+
let client = client.clone();
379+
let user_id = user_id.to_owned();
380+
let event_type = event_type.clone();
381+
async move {
382+
let Ok(user_devices) = client.encryption().get_user_devices(&user_id).await else {
383+
warn!("Failed to get user devices for user: {}", user_id);
384+
return None;
385+
};
386+
// This is not yet used. It is incompatible with the spa guest mode (the spa will not verify its crypto identity)
387+
// let Ok(user_identity) = client.encryption().get_user_identity(&user_id).await else{
388+
// warn!("Failed to get user identity for user: {}", user_id);
389+
// return None;
390+
// };
391+
// if user_identity.map(|i|i.has_verification_violation()).unwrap_or(false) {
392+
// info!("User {} has a verification violation, skipping encryption", user_id);
393+
// return None;
394+
// }
395+
let devices: Vec<Device> = match device_or_all_id {
396+
DeviceIdOrAllDevices::DeviceId(device_id) => {
397+
vec![user_devices.get(&device_id)].into_iter().flatten().collect()
398+
}
399+
DeviceIdOrAllDevices::AllDevices => user_devices.devices().collect(),
400+
};
401+
encrypted_content_for_devices(
402+
&content,
403+
devices,
404+
&event_type,
405+
)
406+
.await
407+
.map_err(|e| info!("WidgetDriver: could not encrypt content for to device widget event content: {}. because, {}", content.json(), e))
408+
.ok()
409+
}}));
410+
let content_map_iterator = join_all(device_map_futures).await.into_iter();
411+
412+
// The first flatten takes the iterator over Result<Option<impl Iterator<Item =
413+
// (DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>)>>, JoinError>>
414+
// and flattens the Result (drops Err() items)
415+
// The second takes the iterator over: Option<impl Iterator<Item =
416+
// (DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>)>>
417+
// and flattens the Option (drops None items)
418+
// The third takes the iterator over iterators: impl Iterator<Item =
419+
// (DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>)>
420+
// and flattens it to just an iterator over (DeviceIdOrAllDevices,
421+
// Raw<AnyToDeviceEventContent>)
422+
content_map_iterator.flatten().flatten().flatten().collect()
423+
}
424+
}
425+
283426
#[cfg(test)]
284427
mod tests {
285428
use insta;
286-
use ruma::{events::AnyTimelineEvent, room_id, serde::Raw};
429+
use ruma::{
430+
events::{AnySyncTimelineEvent, AnyTimelineEvent},
431+
room_id,
432+
serde::Raw,
433+
};
287434
use serde_json::{json, Value};
288435

289436
use super::attach_room_id;
437+
#[test]
438+
fn test_app_props_to_raw() {
439+
let raw = Raw::new(&json!({
440+
"encrypted": true,
441+
"type": "m.room.message",
442+
"content": {
443+
"body": "Hello world"
444+
}
445+
}))
446+
.unwrap()
447+
.cast::<AnySyncTimelineEvent>();
448+
let room_id = room_id!("!my_id:example.org");
449+
let new = attach_room_id(&raw, room_id);
450+
assert_eq!(
451+
serde_json::to_value(new).unwrap(),
452+
json!({
453+
"encrypted": true,
454+
"room_id": "!my_id:example.org",
455+
"type": "m.room.message",
456+
"content": {
457+
"body": "Hello world"
458+
}
459+
})
460+
);
461+
}
290462

291463
#[test]
292464
fn test_add_room_id_to_raw() {

0 commit comments

Comments
 (0)