Skip to content

Refuse to decrypt to-device messages from unverified devices (when in exclude insecure mode) #5319

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

Merged
merged 6 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use matrix_sdk_crypto::{
RehydratedDevice as InnerRehydratedDevice,
},
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
DecryptionSettings,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use serde_json::json;
Expand Down Expand Up @@ -154,9 +155,13 @@ impl Drop for RehydratedDevice {

#[matrix_sdk_ffi_macros::export]
impl RehydratedDevice {
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
pub fn receive_events(
&self,
events: String,
decryption_settings: &DecryptionSettings,
) -> Result<(), crate::CryptoStoreError> {
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
self.runtime.block_on(self.inner.receive_events(events))?;
self.runtime.block_on(self.inner.receive_events(events, decryption_settings))?;

Ok(())
}
Expand Down
21 changes: 12 additions & 9 deletions bindings/matrix-sdk-crypto-ffi/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ impl OlmMachine {
key_counts: HashMap<String, i32>,
unused_fallback_keys: Option<Vec<String>>,
next_batch_token: String,
decryption_settings: &DecryptionSettings,
) -> Result<SyncChangesResult, CryptoStoreError> {
let to_device: ToDevice = serde_json::from_str(&events)?;
let device_changes: RumaDeviceLists = device_changes.into();
Expand All @@ -544,15 +545,17 @@ impl OlmMachine {
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());

let (to_device_events, room_key_infos) = self.runtime.block_on(
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
to_device_events: to_device.events,
changed_devices: &device_changes,
one_time_keys_counts: &key_counts,
unused_fallback_keys: unused_fallback_keys.as_deref(),
next_batch_token: Some(next_batch_token),
}),
)?;
let (to_device_events, room_key_infos) =
self.runtime.block_on(self.inner.receive_sync_changes(
matrix_sdk_crypto::EncryptionSyncChanges {
to_device_events: to_device.events,
changed_devices: &device_changes,
one_time_keys_counts: &key_counts,
unused_fallback_keys: unused_fallback_keys.as_deref(),
next_batch_token: Some(next_batch_token),
},
decryption_settings,
))?;

let to_device_events = to_device_events
.into_iter()
Expand Down
24 changes: 20 additions & 4 deletions crates/matrix-sdk-base/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,12 @@ impl BaseClient {
let processors::e2ee::to_device::Output {
processed_to_device_events: to_device,
room_key_updates,
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
} = processors::e2ee::to_device::from_sync_v2(
&response,
olm_machine.as_ref(),
&self.decryption_settings,
)
.await?;

processors::latest_event::decrypt_from_rooms(
&mut context,
Expand All @@ -619,14 +624,25 @@ impl BaseClient {
.events
.into_iter()
.map(|raw| {
use matrix_sdk_common::deserialized_responses::{
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
ToDeviceUnableToDecryptReason,
};

if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
if event_type == "m.room.encrypted" {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::UnableToDecrypt(raw)
ProcessedToDeviceEvent::UnableToDecrypt {
encrypted_event: raw,
utd_info: ToDeviceUnableToDecryptInfo {
reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
},
}
} else {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::PlainText(raw)
ProcessedToDeviceEvent::PlainText(raw)
}
} else {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::Invalid(raw) // Exclude events with no type
// Exclude events with no type
ProcessedToDeviceEvent::Invalid(raw)
}
})
.collect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@

use std::collections::BTreeMap;

use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo};
use matrix_sdk_common::deserialized_responses::{
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason,
};
use matrix_sdk_crypto::{
DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo,
};
use ruma::{
OneTimeKeyAlgorithm, UInt,
api::client::sync::sync_events::{DeviceLists, v3, v5},
Expand All @@ -34,6 +38,7 @@ pub async fn from_msc4186(
to_device: Option<&v5::response::ToDevice>,
e2ee: &v5::response::E2EE,
olm_machine: Option<&OlmMachine>,
decryption_settings: &DecryptionSettings,
) -> Result<Output> {
process(
olm_machine,
Expand All @@ -42,6 +47,7 @@ pub async fn from_msc4186(
&e2ee.device_one_time_keys_count,
e2ee.device_unused_fallback_key_types.as_deref(),
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
decryption_settings,
)
.await
}
Expand All @@ -54,6 +60,7 @@ pub async fn from_msc4186(
pub async fn from_sync_v2(
response: &v3::Response,
olm_machine: Option<&OlmMachine>,
decryption_settings: &DecryptionSettings,
) -> Result<Output> {
process(
olm_machine,
Expand All @@ -62,6 +69,7 @@ pub async fn from_sync_v2(
&response.device_one_time_keys_count,
response.device_unused_fallback_key_types.as_deref(),
Some(response.next_batch.clone()),
decryption_settings,
)
.await
}
Expand All @@ -77,6 +85,7 @@ async fn process(
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
next_batch_token: Option<String>,
decryption_settings: &DecryptionSettings,
) -> Result<Output> {
let encryption_sync_changes = EncryptionSyncChanges {
to_device_events,
Expand All @@ -92,7 +101,7 @@ async fn process(
// This makes sure that we have the decryption keys for the room
// events at hand.
let (events, room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?;

Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
} else {
Expand All @@ -107,7 +116,12 @@ async fn process(
.map(|raw| {
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
if event_type == "m.room.encrypted" {
ProcessedToDeviceEvent::UnableToDecrypt(raw)
ProcessedToDeviceEvent::UnableToDecrypt {
encrypted_event: raw,
utd_info: ToDeviceUnableToDecryptInfo {
reason: ToDeviceUnableToDecryptReason::NoOlmMachine,
},
}
} else {
ProcessedToDeviceEvent::PlainText(raw)
}
Expand Down
9 changes: 7 additions & 2 deletions crates/matrix-sdk-base/src/sliding_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ impl BaseClient {
let mut context = processors::Context::default();

let processors::e2ee::to_device::Output { processed_to_device_events, room_key_updates } =
processors::e2ee::to_device::from_msc4186(to_device, e2ee, olm_machine.as_ref())
.await?;
processors::e2ee::to_device::from_msc4186(
to_device,
e2ee,
olm_machine.as_ref(),
&self.decryption_settings,
)
.await?;

processors::latest_event::decrypt_from_rooms(
&mut context,
Expand Down
38 changes: 35 additions & 3 deletions crates/matrix-sdk-common/src/deserialized_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,33 @@ impl From<SyncTimelineEventDeserializationHelperV0> for TimelineEvent {
}
}

/// Reason code for a to-device decryption failure
#[derive(Debug, Clone, PartialEq)]
pub enum ToDeviceUnableToDecryptReason {
/// An error occurred while encrypting the event. This covers all
/// `OlmError` types.
DecryptionFailure,

/// We refused to decrypt the message because the sender's device is not
/// verified, or more generally, the sender's identity did not match the
/// trust requirement we were asked to provide.
UnverifiedSenderDevice,

/// We have no `OlmMachine`. This should not happen unless we forget to set
/// things up by calling `OlmMachine::activate()`.
NoOlmMachine,

/// The Matrix SDK was compiled without encryption support.
EncryptionIsDisabled,
}

/// Metadata about a to-device event that could not be decrypted.
#[derive(Clone, Debug)]
pub struct ToDeviceUnableToDecryptInfo {
/// Reason code for the decryption failure
pub reason: ToDeviceUnableToDecryptReason,
}

/// Represents a to-device event after it has been processed by the Olm machine.
#[derive(Clone, Debug)]
pub enum ProcessedToDeviceEvent {
Expand All @@ -1192,7 +1219,10 @@ pub enum ProcessedToDeviceEvent {
},

/// An encrypted event which could not be decrypted.
UnableToDecrypt(Raw<AnyToDeviceEvent>),
UnableToDecrypt {
encrypted_event: Raw<AnyToDeviceEvent>,
utd_info: ToDeviceUnableToDecryptInfo,
},

/// An unencrypted event.
PlainText(Raw<AnyToDeviceEvent>),
Expand All @@ -1209,7 +1239,9 @@ impl ProcessedToDeviceEvent {
pub fn to_raw(&self) -> Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw.clone(),
ProcessedToDeviceEvent::UnableToDecrypt(event) => event.clone(),
ProcessedToDeviceEvent::UnableToDecrypt { encrypted_event, .. } => {
encrypted_event.clone()
}
ProcessedToDeviceEvent::PlainText(event) => event.clone(),
ProcessedToDeviceEvent::Invalid(event) => event.clone(),
}
Expand All @@ -1219,7 +1251,7 @@ impl ProcessedToDeviceEvent {
pub fn as_raw(&self) -> &Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw,
ProcessedToDeviceEvent::UnableToDecrypt(event) => event,
ProcessedToDeviceEvent::UnableToDecrypt { encrypted_event, .. } => encrypted_event,
ProcessedToDeviceEvent::PlainText(event) => event,
ProcessedToDeviceEvent::Invalid(event) => event,
}
Expand Down
13 changes: 13 additions & 0 deletions crates/matrix-sdk-crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ All notable changes to this project will be documented in this file.
- [**breaking**] Add a new `VerificationLevel::MismatchedSender` to indicate that the sender of an event appears to have been tampered with.
([#5219](https://github.com/matrix-org/matrix-rust-sdk/pull/5219))

- [**breaking**]: When in "exclude insecure devices" mode, refuse to decrypt
incoming to-device messages from unverified devices, except for some
exceptions for certain event types. To support this, a new variant has been
added to `ProcessedToDeviceEvent`: `UnverifiedSender`, which is returned from
`OlmMachine::receive_sync_changes` when we are excluding insecure devices and
the sender's device is not verified. Also, several methods now take a
`DecryptionSettings` argument to allow controlling the processing of to-device
events based on those settings. To recreate the previous behaviour pass in:
`DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }`.
Affected methods are `OlmMachine::receive_sync_changes`,
`RehydratedDevice::receive_events`, and several internal methods.
([#5319](https://github.com/matrix-org/matrix-rust-sdk/pull/5319)

### Refactor

- [**breaking**] The `PendingChanges`, `Changes`, `StoredRoomKeyBundleData`,
Expand Down
31 changes: 19 additions & 12 deletions crates/matrix-sdk-crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ The state machine works in a push/pull manner:
```rust,no_run
use std::collections::BTreeMap;

use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine, OlmError};
use ruma::{
api::client::sync::sync_events::{v3::ToDevice, DeviceLists},
device_id, user_id,
use matrix_sdk_crypto::{
DecryptionSettings, EncryptionSyncChanges, OlmError, OlmMachine, TrustRequirement,
};
use ruma::{api::client::sync::sync_events::DeviceLists, device_id, user_id};

#[tokio::main]
async fn main() -> Result<(), OlmError> {
Expand All @@ -33,19 +32,27 @@ async fn main() -> Result<(), OlmError> {
let unused_fallback_keys = Some(Vec::new());
let next_batch_token = "T0K3N".to_owned();

let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

// Push changes that the server sent to us in a sync response.
let decrypted_to_device = machine.receive_sync_changes(EncryptionSyncChanges {
to_device_events: vec![],
changed_devices: &changed_devices,
one_time_keys_counts: &one_time_key_counts,
unused_fallback_keys: unused_fallback_keys.as_deref(),
next_batch_token: Some(next_batch_token),
}).await?;
let decrypted_to_device = machine
.receive_sync_changes(
EncryptionSyncChanges {
to_device_events: vec![],
changed_devices: &changed_devices,
one_time_keys_counts: &one_time_key_counts,
unused_fallback_keys: unused_fallback_keys.as_deref(),
next_batch_token: Some(next_batch_token),
},
&decryption_settings,
)
.await?;

// Pull requests that we need to send out.
let outgoing_requests = machine.outgoing_requests().await?;

// Send the requests here out and call machine.mark_request_as_sent().
// Send the requests out here and call machine.mark_request_as_sent().

Ok(())
}
Expand Down
Loading
Loading