Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f14ecc9
feat(crypto): Add `EncryptionInfo` to `Decrypted` to-device variant
BillCarsonFr May 22, 2025
0c4a895
fixup: coverage, test deserialize new format
BillCarsonFr May 22, 2025
c4cbcf9
feat(crypto): Support EncryptionInfo for olm encrypted via event handler
BillCarsonFr May 26, 2025
fc4a97e
Merge branch 'main' into valere/crypto/receive_encrypted_to_device
BillCarsonFr Jun 5, 2025
125aec6
refactor: Move ProcessedToDeviceEvent to common crate
BillCarsonFr Jun 5, 2025
d9137e9
fixup: when not(e2e-encryption) ignore encrypted to-device processing
BillCarsonFr Jun 5, 2025
53aeeb7
fixup: Test wrongly expecting encrypted to-device
BillCarsonFr Jun 5, 2025
290f963
fixup: flag dependent imports
BillCarsonFr Jun 5, 2025
10a9688
Merge branch 'main' into valere/crypto/receive_encrypted_to_device
BillCarsonFr Jun 5, 2025
7dd01ee
fixup: merge error in crypto changelog added a double entry
BillCarsonFr Jun 5, 2025
566d935
Merge branch 'main' into valere/crypto/receive_encrypted_to_device
BillCarsonFr Jun 6, 2025
041e50d
review: Improve test, better var names
BillCarsonFr Jun 6, 2025
27bc725
update changelog
BillCarsonFr Jun 6, 2025
d705261
Merge branch 'main' into valere/crypto/receive_encrypted_to_device
BillCarsonFr Jun 6, 2025
88d1fa5
feat(widget): Receive to-device custom messages in widgets in e2ee rooms
BillCarsonFr May 27, 2025
d6819d0
test(refactor): add arg to setup the room encryption or not
BillCarsonFr May 28, 2025
41e0af9
test: basic exclude clear to-device traffic for widgets in e2ee room
BillCarsonFr May 28, 2025
2cbbceb
test: Add integration test for e2e to-device and widgets
BillCarsonFr Jun 10, 2025
228a142
fixup: unused imports
BillCarsonFr Jun 10, 2025
7a938ae
Merge branch 'main' into valere/crypto/widget_receive_encrypted_to_de…
BillCarsonFr Jun 13, 2025
2faa3b2
fix typo
BillCarsonFr Jun 13, 2025
481ba3f
post merge cleanup
BillCarsonFr Jun 13, 2025
d9ab76e
update test that should only work in clear room
BillCarsonFr Jun 13, 2025
618bf31
review: Move TODO comment for to-device decryption trust requirement
BillCarsonFr Jun 16, 2025
71c02ab
review: cleanup message filtering + better trace
BillCarsonFr Jun 16, 2025
a9aa3f4
review: quick typos
BillCarsonFr Jun 17, 2025
51523e1
review: added a combined utility to capture and sync back a to-device
BillCarsonFr Jun 17, 2025
c714793
review: add example in doc for capture to-device
BillCarsonFr Jun 17, 2025
9e802a8
fix empty line after doc comment
BillCarsonFr Jun 17, 2025
7cc8f01
minor change in trace! format
BillCarsonFr Jun 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/matrix-sdk-crypto/src/machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,11 @@ impl OlmMachine {
Ok(event) => {
self.handle_to_device_event(changes, &event).await;

// TODO: we should have access to some decryption settings here
// (TrustRequirement) and use it (at least for
// custom to-devices) to manually reject the decryption.
// Similar to check_sender_trust_requirement for room events

raw_event = event
.serialize_zeroized()
.expect("Zeroizing and reserializing our events should always work")
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ sso-login = ["local-server"]

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

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

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

Expand Down
78 changes: 73 additions & 5 deletions crates/matrix-sdk/src/test_utils/mocks/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
/// encrypted to-device message.
pub async fn mock_capture_put_to_device(
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

  1. The functions i kind off tested as a doctest
  2. Other people will more easily discover how to use it

&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
Expand Down
85 changes: 75 additions & 10 deletions crates/matrix-sdk/src/widget/matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
// 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.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not attaching any data to this log line, it will be hard to know what got filtered out. Consider moving this into the should_filter_message_...() method so we can at least log the event type.

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);
return;
};

let room_encrypted = room.latest_encryption_state().await
.map(|s| s.is_encrypted())
// Default consider encrypted
.unwrap_or(true);
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
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;
}

// 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(
Expand Down
58 changes: 8 additions & 50 deletions crates/matrix-sdk/tests/integration/encryption/to_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ use matrix_sdk_common::{
locks::Mutex,
};
use matrix_sdk_test::{async_test, test_json};
use ruma::{
api::client::to_device::send_event_to_device::v3::Messages, events::AnyToDeviceEvent,
serde::Raw, MilliSecondsSinceUnixEpoch, OwnedUserId,
};
use serde_json::{json, Value};
use ruma::{events::AnyToDeviceEvent, serde::Raw};
use serde_json::json;
use wiremock::{
matchers::{method, path_regex},
Mock, Request, ResponseTemplate,
Mock, ResponseTemplate,
};

#[async_test]
Expand Down Expand Up @@ -128,37 +125,6 @@ async fn test_encrypt_and_send_to_device_report_failures_server() {
assert_eq!(bob_device_id.to_owned(), failure.1);
}

// A simple mock to capture an encrypted to device message via `sendToDevice`.
// Expect the request payload to be for an encrypted event and to only have one
// message.
fn mock_send_encrypted_to_device_responder(
sender: OwnedUserId,
to_device: Arc<Mutex<Option<Value>>>,
) -> impl Fn(&Request) -> ResponseTemplate {
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,
});

*to_device.lock() = Some(event);

ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY)
}
}

#[async_test]
async fn test_to_device_event_handler_olm_encryption_info() {
// ===========
Expand Down Expand Up @@ -194,18 +160,9 @@ async fn test_to_device_event_handler_olm_encryption_info() {
.cast();

// Capture the event sent by Alice to feed it back to Bob's client later.
let event_as_sent_by_alice: Arc<Mutex<Option<Value>>> = Default::default();
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/.*/sendToDevice/m.room.encrypted/.*"))
.respond_with(mock_send_encrypted_to_device_responder(
alice.user_id().unwrap().to_owned(),
event_as_sent_by_alice.clone(),
))
// Should be called once
.expect(1)
.named("send_to_device")
.mount(&server.server())
.await;

let (guard, event_as_sent_by_alice) =
server.mock_capture_put_to_device(alice.user_id().unwrap()).await;

alice
.encryption()
Expand All @@ -225,7 +182,8 @@ async fn test_to_device_event_handler_olm_encryption_info() {
});

// feed back the event to Bob's client
let event_as_sent_by_alice = event_as_sent_by_alice.lock().clone().unwrap();
let event_as_sent_by_alice = event_as_sent_by_alice.await;
drop(guard);
server
.mock_sync()
.ok_and_run(&bob, |builder| {
Expand Down
Loading
Loading