From 97ca9eec24c2fb97406632709d50325b16ad3ca1 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 24 Jun 2025 13:37:11 +0100 Subject: [PATCH 1/7] refactor(crypto): Pass DecryptionSettings in to OlmMachine::decrypt_to_device_event This will be used in the next commit, but it was very noisy, so I separated it out into this commit to make the next one easier to read. --- .../src/dehydrated_devices.rs | 9 +- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 21 ++- crates/matrix-sdk-base/src/client.rs | 7 +- .../src/response_processors/e2ee/to_device.rs | 11 +- crates/matrix-sdk-base/src/sliding_sync.rs | 9 +- crates/matrix-sdk-crypto/README.md | 31 ++-- .../src/dehydrated_devices.rs | 27 ++- .../src/gossiping/machine.rs | 21 ++- crates/matrix-sdk-crypto/src/lib.rs | 20 ++- crates/matrix-sdk-crypto/src/machine/mod.rs | 33 +++- .../src/machine/test_helpers.rs | 22 ++- .../tests/decryption_verification_state.rs | 25 ++- .../src/machine/tests/megolm_sender_data.rs | 42 +++-- .../src/machine/tests/mod.rs | 167 +++++++++++++----- .../src/machine/tests/olm_encryption.rs | 31 +++- .../machine/tests/send_encrypted_to_device.rs | 24 ++- .../src/session_manager/group_sessions/mod.rs | 23 ++- 17 files changed, 395 insertions(+), 128 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs index b3477ee1739..7597d0ffb21 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/dehydrated_devices.rs @@ -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; @@ -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> = 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(()) } diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index cd5784d80e9..3c58139ce9e 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -526,6 +526,7 @@ impl OlmMachine { key_counts: HashMap, unused_fallback_keys: Option>, next_batch_token: String, + decryption_settings: &DecryptionSettings, ) -> Result { let to_device: ToDevice = serde_json::from_str(&events)?; let device_changes: RumaDeviceLists = device_changes.into(); @@ -544,15 +545,17 @@ impl OlmMachine { let unused_fallback_keys: Option> = 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() diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e0861b7d485..16f69462cd8 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -500,7 +500,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, diff --git a/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs b/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs index 74557ea2436..2a62baa5d7d 100644 --- a/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs +++ b/crates/matrix-sdk-base/src/response_processors/e2ee/to_device.rs @@ -15,7 +15,9 @@ use std::collections::BTreeMap; use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent; -use matrix_sdk_crypto::{store::types::RoomKeyInfo, EncryptionSyncChanges, OlmMachine}; +use matrix_sdk_crypto::{ + store::types::RoomKeyInfo, DecryptionSettings, EncryptionSyncChanges, OlmMachine, +}; use ruma::{ api::client::sync::sync_events::{v3, v5, DeviceLists}, events::AnyToDeviceEvent, @@ -34,6 +36,7 @@ pub async fn from_msc4186( to_device: Option<&v5::response::ToDevice>, e2ee: &v5::response::E2EE, olm_machine: Option<&OlmMachine>, + decryption_settings: &DecryptionSettings, ) -> Result { process( olm_machine, @@ -42,6 +45,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 } @@ -54,6 +58,7 @@ pub async fn from_msc4186( pub async fn from_sync_v2( response: &v3::Response, olm_machine: Option<&OlmMachine>, + decryption_settings: &DecryptionSettings, ) -> Result { process( olm_machine, @@ -62,6 +67,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 } @@ -77,6 +83,7 @@ async fn process( one_time_keys_counts: &BTreeMap, unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>, next_batch_token: Option, + decryption_settings: &DecryptionSettings, ) -> Result { let encryption_sync_changes = EncryptionSyncChanges { to_device_events, @@ -92,7 +99,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 { diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 4837aac7d42..51af75e2c7c 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -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, diff --git a/crates/matrix-sdk-crypto/README.md b/crates/matrix-sdk-crypto/README.md index 154c9f0fed7..a0e1c83c52e 100644 --- a/crates/matrix-sdk-crypto/README.md +++ b/crates/matrix-sdk-crypto/README.md @@ -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> { @@ -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(()) } diff --git a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs index 903fdde3f83..90fee8ae9ec 100644 --- a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs +++ b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs @@ -60,7 +60,8 @@ use crate::{ CryptoStoreWrapper, MemoryStore, Store, }, verification::VerificationMachine, - Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError, + Account, CryptoStoreError, DecryptionSettings, EncryptionSyncChanges, OlmError, OlmMachine, + SignatureError, }; /// Error type for device dehydration issues. @@ -215,7 +216,9 @@ impl RehydratedDevice { /// /// ```no_run /// # use anyhow::Result; - /// # use matrix_sdk_crypto::{ OlmMachine, store::types::DehydratedDeviceKey }; + /// # use matrix_sdk_crypto::{ + /// DecryptionSettings, OlmMachine, TrustRequirement, store::types::DehydratedDeviceKey + /// }; /// # use ruma::{api::client::dehydrated_device, DeviceId}; /// # async fn example() -> Result<()> { /// # let machine: OlmMachine = unimplemented!(); @@ -245,6 +248,9 @@ impl RehydratedDevice { /// /// let mut since_token = None; /// let mut imported_room_keys = 0; + /// let decryption_settings = DecryptionSettings { + /// sender_device_trust_requirement: TrustRequirement::Untrusted + /// }; /// /// loop { /// let response = @@ -255,7 +261,7 @@ impl RehydratedDevice { /// } /// /// since_token = response.next_batch.as_deref(); - /// imported_room_keys += rehydrated.receive_events(response.events).await?.len(); + /// imported_room_keys += rehydrated.receive_events(response.events, &decryption_settings).await?.len(); /// } /// /// println!("Successfully imported {imported_room_keys} from the dehydrated device."); @@ -273,6 +279,7 @@ impl RehydratedDevice { pub async fn receive_events( &self, events: Vec>, + decryption_settings: &DecryptionSettings, ) -> Result, OlmError> { trace!("Receiving events for a rehydrated Device"); @@ -290,7 +297,7 @@ impl RehydratedDevice { let (_, changes) = self .rehydrated - .preprocess_sync_changes(&mut rehydrated_transaction, sync_changes) + .preprocess_sync_changes(&mut rehydrated_transaction, sync_changes, decryption_settings) .await?; // Now take the room keys and persist them in our original `OlmMachine`. @@ -423,7 +430,7 @@ mod tests { store::types::DehydratedDeviceKey, types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType}, utilities::json_convert, - EncryptionSettings, OlmMachine, + DecryptionSettings, EncryptionSettings, OlmMachine, TrustRequirement, }; fn pickle_key() -> DehydratedDeviceKey { @@ -568,9 +575,12 @@ mod tests { assert_eq!(rehydrated.rehydrated.device_id(), request.device_id); assert_eq!(rehydrated.original.device_id(), alice.device_id()); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Push the to-device event containing the room key into the rehydrated device. let ret = rehydrated - .receive_events(vec![event]) + .receive_events(vec![event], &decryption_settings) .await .expect("We should be able to push to-device events into the rehydrated device"); @@ -680,9 +690,12 @@ mod tests { assert_eq!(rehydrated.rehydrated.device_id(), &device_id); assert_eq!(rehydrated.original.device_id(), alice.device_id()); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Push the to-device event containing the room key into the rehydrated device. let ret = rehydrated - .receive_events(vec![event]) + .receive_events(vec![event], &decryption_settings) .await .expect("We should be able to push to-device events into the rehydrated device"); diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 912887d93c2..a2c3014dd22 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -1142,6 +1142,7 @@ mod tests { EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent, }, verification::VerificationMachine, + DecryptionSettings, TrustRequirement, }; fn alice_id() -> &'static UserId { @@ -2051,14 +2052,20 @@ mod tests { let stream = bob_machine.store().secrets_stream(); pin_mut!(stream); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + bob_machine - .receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }) + .receive_sync_changes( + EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }, + &decryption_settings, + ) .await .unwrap(); diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index 6103438f0b5..632e1f576cb 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -590,7 +590,9 @@ pub enum RoomEventDecryptionResult { /// /// ```no_run /// # use anyhow::Result; -/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine}; +/// # use matrix_sdk_crypto::{ +/// DecryptionSettings, EncryptionSyncChanges, OlmMachine, TrustRequirement +/// }; /// # use ruma::api::client::sync::sync_events::v3::Response; /// # #[tokio::main] /// # async fn main() -> Result<()> { @@ -617,11 +619,15 @@ pub enum RoomEventDecryptionResult { /// next_batch_token: Some(response.next_batch), /// }; /// +/// let decryption_settings = DecryptionSettings { +/// sender_device_trust_requirement: TrustRequirement::Untrusted +/// }; +/// /// // Push the sync changes into the OlmMachine, make sure that this is /// // happening before the `next_batch` token of the sync is persisted. /// let to_device_events = client /// .olm_machine -/// .receive_sync_changes(sync_changes) +/// .receive_sync_changes(sync_changes, &decryption_settings) /// .await?; /// /// // Send the outgoing requests out that the sync changes produced. @@ -768,7 +774,9 @@ pub enum RoomEventDecryptionResult { /// ```no_run /// # use anyhow::Result; /// # use std::ops::Deref; -/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine}; +/// # use matrix_sdk_crypto::{ +/// # DecryptionSettings, EncryptionSyncChanges, OlmMachine, TrustRequirement +/// # }; /// # use ruma::api::client::sync::sync_events::v3::{Response, JoinedRoom}; /// # use ruma::{OwnedUserId, serde::Raw, events::AnySyncStateEvent}; /// # #[tokio::main] @@ -805,11 +813,15 @@ pub enum RoomEventDecryptionResult { /// next_batch_token: Some(response.next_batch), /// }; /// +/// let decryption_settings = DecryptionSettings { +/// sender_device_trust_requirement: TrustRequirement::Untrusted +/// }; +/// /// // Push the sync changes into the OlmMachine, make sure that this is /// // happening before the `next_batch` token of the sync is persisted. /// let to_device_events = client /// .olm_machine -/// .receive_sync_changes(sync_changes) +/// .receive_sync_changes(sync_changes, &decryption_settings) /// .await?; /// /// // Send the outgoing requests out that the sync changes produced. diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 406cb6b4773..965881726a1 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -862,6 +862,7 @@ impl OlmMachine { transaction: &mut StoreTransaction, event: &EncryptedToDeviceEvent, changes: &mut Changes, + _decryption_settings: &DecryptionSettings, ) -> Result { // Decrypt the event let mut decrypted = @@ -1401,6 +1402,7 @@ impl OlmMachine { transaction: &mut StoreTransaction, changes: &mut Changes, raw_event: Raw, + decryption_settings: &DecryptionSettings, ) -> Option { Self::record_message_id(&raw_event); @@ -1417,7 +1419,14 @@ impl OlmMachine { match event { ToDeviceEvents::RoomEncrypted(e) => { - self.receive_encrypted_to_device_event(transaction, changes, raw_event, e).await + self.receive_encrypted_to_device_event( + transaction, + changes, + raw_event, + e, + decryption_settings, + ) + .await } e => { self.handle_to_device_event(changes, &e).await; @@ -1439,8 +1448,12 @@ impl OlmMachine { changes: &mut Changes, mut raw_event: Raw, e: ToDeviceEvent, + decryption_settings: &DecryptionSettings, ) -> Option { - let decrypted = match self.decrypt_to_device_event(transaction, &e, changes).await { + let decrypted = match self + .decrypt_to_device_event(transaction, &e, changes, decryption_settings) + .await + { Ok(decrypted) => decrypted, Err(DecryptToDeviceError::OlmError(err)) => { if let OlmError::SessionWedged(sender, curve_key) = err { @@ -1549,11 +1562,13 @@ impl OlmMachine { pub async fn receive_sync_changes( &self, sync_changes: EncryptionSyncChanges<'_>, + decryption_settings: &DecryptionSettings, ) -> OlmResult<(Vec, Vec)> { let mut store_transaction = self.inner.store.transaction().await; - let (events, changes) = - self.preprocess_sync_changes(&mut store_transaction, sync_changes).await?; + let (events, changes) = self + .preprocess_sync_changes(&mut store_transaction, sync_changes, decryption_settings) + .await?; // Technically save_changes also does the same work, so if it's slow we could // refactor this to do it only once. @@ -1578,6 +1593,7 @@ impl OlmMachine { &self, transaction: &mut StoreTransaction, sync_changes: EncryptionSyncChanges<'_>, + decryption_settings: &DecryptionSettings, ) -> OlmResult<(Vec, Changes)> { // Remove verification objects that have expired or are done. let mut events: Vec = self @@ -1615,8 +1631,13 @@ impl OlmMachine { } for raw_event in sync_changes.to_device_events { - let processed_event = - Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event)).await; + let processed_event = Box::pin(self.receive_to_device_event( + transaction, + &mut changes, + raw_event, + decryption_settings, + )) + .await; if let Some(processed_event) = processed_event { events.push(processed_event); diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 148240b4b01..0fdf1317b90 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -47,8 +47,8 @@ use crate::{ }, utilities::json_convert, verification::VerificationMachine, - Account, CrossSigningBootstrapRequests, Device, DeviceData, EncryptionSyncChanges, OlmMachine, - OtherUserIdentityData, + Account, CrossSigningBootstrapRequests, DecryptionSettings, Device, DeviceData, + EncryptionSyncChanges, OlmMachine, OtherUserIdentityData, TrustRequirement, }; /// These keys need to be periodically uploaded to the server. @@ -218,7 +218,11 @@ pub async fn send_and_receive_encrypted_to_device_test_helper( next_batch_token: None, }; - let (decrypted, _) = recipient.receive_sync_changes(sync_changes).await.unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (decrypted, _) = + recipient.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); decrypted[0].clone() } @@ -265,10 +269,20 @@ pub async fn get_machine_pair_with_setup_sessions_test_helper( let event = ToDeviceEvent::new(alice.user_id().to_owned(), content.deserialize_as().unwrap()); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decrypted = bob .store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await diff --git a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs index 9581034b8cd..c2f49af52b0 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs @@ -79,10 +79,20 @@ async fn test_decryption_verification_state() { tests::to_device_requests_to_content(to_device_requests), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let group_session = bob .store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await @@ -633,11 +643,20 @@ async fn encrypt_message( tests::to_device_requests_to_content(to_device_requests), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let group_session = recipient .store() .with_transaction(|mut tr| async { - let res = - recipient.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = recipient + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await diff --git a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs index 48ad9230912..9427af3e6a9 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/megolm_sender_data.rs @@ -35,7 +35,8 @@ use crate::{ olm::{InboundGroupSession, SenderData}, store::types::RoomKeyInfo, types::events::{room::encrypted::ToDeviceEncryptedEventContent, EventType, ToDeviceEvent}, - DeviceData, EncryptionSettings, EncryptionSyncChanges, OlmMachine, Session, + DecryptionSettings, DeviceData, EncryptionSettings, EncryptionSyncChanges, OlmMachine, Session, + TrustRequirement, }; /// Test the behaviour when a megolm session is received from an unknown device, @@ -55,8 +56,11 @@ async fn test_receive_megolm_session_from_unknown_device() { let room_id = room_id!("!test:example.org"); let event = create_and_share_session_without_sender_data(&alice, &bob, room_id).await; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Bob receives the to-device message - receive_to_device_event(&bob, &event).await; + receive_to_device_event(&bob, &event, &decryption_settings).await; // Then Bob should know about the session, and it should have // `SenderData::UnknownDevice`. @@ -92,8 +96,11 @@ async fn test_receive_megolm_session_from_known_device() { ), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Bob receives the to-device message - receive_to_device_event(&bob, &event).await; + receive_to_device_event(&bob, &event, &decryption_settings).await; // Then Bob should know about the session, and it should have // `SenderData::DeviceInfo` @@ -126,8 +133,11 @@ async fn test_update_unknown_device_senderdata_on_keys_query() { let room_id = room_id!("!test:example.org"); let event = create_and_share_session_without_sender_data(&alice, &bob, room_id).await; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Bob receives the to-device message - receive_to_device_event(&bob, &event).await; + receive_to_device_event(&bob, &event, &decryption_settings).await; // and now Bob should know about the session. let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); @@ -183,8 +193,12 @@ async fn test_update_device_info_senderdata_on_keys_query() { alice.user_id().to_owned(), to_device_requests_to_content(to_device_requests), ); + + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Bob receives the to-device message - receive_to_device_event(&bob, &event).await; + receive_to_device_event(&bob, &event, &decryption_settings).await; // and now Bob should know about the session. let room_key_info = get_room_key_received_update(&mut bob_room_keys_received_stream); @@ -287,6 +301,7 @@ async fn create_and_share_session_without_sender_data( pub async fn receive_to_device_event( machine: &OlmMachine, event: &ToDeviceEvent, + decryption_settings: &DecryptionSettings, ) -> (Vec, Vec) where C: EventType + Serialize + Debug, @@ -294,13 +309,16 @@ where let event_json = serde_json::to_string(event).expect("Unable to serialize to-device message"); machine - .receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![serde_json::from_str(&event_json).unwrap()], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }) + .receive_sync_changes( + EncryptionSyncChanges { + to_device_events: vec![serde_json::from_str(&event_json).unwrap()], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }, + decryption_settings, + ) .await .expect("Error receiving to-device event") } diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 4499aa8d2ba..9458f8c21c8 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -284,15 +284,22 @@ fn test_one_time_key_signing() { async fn test_keys_for_upload() { let machine = OlmMachine::new(user_id(), alice_device_id()).await; + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let key_counts = BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, 49u8.into())]); + machine - .receive_sync_changes(EncryptionSyncChanges { - to_device_events: Vec::new(), - changed_devices: &Default::default(), - one_time_keys_counts: &key_counts, - unused_fallback_keys: None, - next_batch_token: None, - }) + .receive_sync_changes( + EncryptionSyncChanges { + to_device_events: Vec::new(), + changed_devices: &Default::default(), + one_time_keys_counts: &key_counts, + unused_fallback_keys: None, + next_batch_token: None, + }, + &decryption_settings, + ) .await .expect("We should be able to update our one-time key counts"); @@ -506,14 +513,20 @@ async fn send_room_key_to_device( ); let event = json_convert(&event).unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + receiver - .receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }) + .receive_sync_changes( + EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }, + &decryption_settings, + ) .await } @@ -631,10 +644,20 @@ async fn test_megolm_encryption() { let mut room_keys_received_stream = Box::pin(bob.store().room_keys_received_stream()); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let group_session = bob .store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await @@ -742,13 +765,19 @@ async fn test_withheld_unverified() { let event = json_convert(&event).unwrap(); - bob.receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }) + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + bob.receive_sync_changes( + EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }, + &decryption_settings, + ) .await .unwrap(); @@ -1002,10 +1031,20 @@ async fn test_query_ratcheted_key() { to_device_requests_to_content(to_device_requests), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let group_session = bob .store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await @@ -1054,14 +1093,20 @@ async fn test_room_key_over_megolm() { let changed_devices = DeviceLists::new(); let key_counts: BTreeMap<_, _> = Default::default(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let _ = bob - .receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &changed_devices, - one_time_keys_counts: &key_counts, - unused_fallback_keys: None, - next_batch_token: None, - }) + .receive_sync_changes( + EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &changed_devices, + one_time_keys_counts: &key_counts, + unused_fallback_keys: None, + next_batch_token: None, + }, + &decryption_settings, + ) .await .unwrap(); @@ -1086,10 +1131,20 @@ async fn test_room_key_over_megolm() { let event: EncryptedToDeviceEvent = serde_json::from_value(event).unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let decrypt_result = bob .store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await; @@ -1098,13 +1153,19 @@ async fn test_room_key_over_megolm() { let event: Raw = json_convert(&event).unwrap(); - bob.receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &changed_devices, - one_time_keys_counts: &key_counts, - unused_fallback_keys: None, - next_batch_token: None, - }) + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + bob.receive_sync_changes( + EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &changed_devices, + one_time_keys_counts: &key_counts, + unused_fallback_keys: None, + next_batch_token: None, + }, + &decryption_settings, + ) .await .unwrap(); @@ -1385,12 +1446,20 @@ async fn test_unsigned_decryption() { to_device_requests_to_content(to_device_requests), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Save the first room key. let group_session = bob .store() .with_transaction(|mut tr| async { let res = bob - .decrypt_to_device_event(&mut tr, &first_room_key_event, &mut Changes::default()) + .decrypt_to_device_event( + &mut tr, + &first_room_key_event, + &mut Changes::default(), + &decryption_settings, + ) .await?; Ok((tr, res)) }) @@ -1490,12 +1559,20 @@ async fn test_unsigned_decryption() { }) ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Give Bob the second room key. let group_session = bob .store() .with_transaction(|mut tr| async { let res = bob - .decrypt_to_device_event(&mut tr, &second_room_key_event, &mut Changes::default()) + .decrypt_to_device_event( + &mut tr, + &second_room_key_event, + &mut Changes::default(), + &decryption_settings, + ) .await?; Ok((tr, res)) }) @@ -1596,12 +1673,20 @@ async fn test_unsigned_decryption() { }) ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Give Bob the third room key. let group_session = bob .store() .with_transaction(|mut tr| async { let res = bob - .decrypt_to_device_event(&mut tr, &third_room_key_event, &mut Changes::default()) + .decrypt_to_device_event( + &mut tr, + &third_room_key_event, + &mut Changes::default(), + &decryption_settings, + ) .await?; Ok((tr, res)) }) diff --git a/crates/matrix-sdk-crypto/src/machine/tests/olm_encryption.rs b/crates/matrix-sdk-crypto/src/machine/tests/olm_encryption.rs index 78197886da5..9eb03d96cfd 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/olm_encryption.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/olm_encryption.rs @@ -34,13 +34,12 @@ use crate::{ create_session, get_machine_pair, get_machine_pair_with_session, get_machine_pair_with_setup_sessions_test_helper, }, - tests, - tests::megolm_sender_data::receive_to_device_event, + tests::{self, megolm_sender_data::receive_to_device_event}, }, olm::utility::SignJson, store::types::Changes, types::{events::ToDeviceEvent, DeviceKeys}, - DeviceData, OlmMachine, + DecryptionSettings, DeviceData, OlmMachine, TrustRequirement, }; #[async_test] @@ -210,11 +209,21 @@ async fn olm_encryption_test_helper(use_fallback_key: bool) { content.deserialize_as().expect("We should be able to deserialize the encrypted content"), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Decrypting the first time should succeed. let decrypted = bob .store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await @@ -230,7 +239,14 @@ async fn olm_encryption_test_helper(use_fallback_key: bool) { // Replaying the event should now result in a decryption failure. bob.store() .with_transaction(|mut tr| async { - let res = bob.decrypt_to_device_event(&mut tr, &event, &mut Changes::default()).await?; + let res = bob + .decrypt_to_device_event( + &mut tr, + &event, + &mut Changes::default(), + &decryption_settings, + ) + .await?; Ok((tr, res)) }) .await @@ -290,8 +306,11 @@ async fn test_decrypt_to_device_message_with_unsigned_sender_keys() { alice_session.build_encrypted_event(ciphertext, None).await.unwrap(), ); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + // Bob receives the to-device message - let (to_device_events, _) = receive_to_device_event(&bob, &event).await; + let (to_device_events, _) = receive_to_device_event(&bob, &event, &decryption_settings).await; let event = to_device_events.first().expect("Bob did not get a to-device event").clone(); diff --git a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs index 74c925e6837..962e4aba782 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs @@ -27,8 +27,7 @@ use crate::{ build_session_for_pair, get_machine_pair, get_machine_pair_with_session, get_prepared_machine_test_helper, send_and_receive_encrypted_to_device_test_helper, }, - tests, - tests::decryption_verification_state::mark_alice_identity_as_verified_test_helper, + tests::{self, decryption_verification_state::mark_alice_identity_as_verified_test_helper}, }, types::{ events::{ToDeviceCustomEvent, ToDeviceEvent}, @@ -36,7 +35,8 @@ use crate::{ }, utilities::json_convert, verification::tests::bob_id, - DeviceData, EncryptionSyncChanges, LocalTrust, OlmError, OlmMachine, + DecryptionSettings, DeviceData, EncryptionSyncChanges, LocalTrust, OlmError, OlmMachine, + TrustRequirement, }; #[async_test] @@ -90,7 +90,11 @@ async fn test_send_encrypted_to_device() { next_batch_token: None, }; - let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (decrypted, _) = + bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); let processed_event = &decrypted[0]; @@ -188,7 +192,11 @@ async fn test_receive_custom_encrypted_to_device_fails_if_device_unknown() { next_batch_token: None, }; - let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (decrypted, _) = + bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); let processed_event = &decrypted[0]; @@ -453,7 +461,11 @@ async fn test_processed_to_device_variants() { next_batch_token: None, }; - let (processed, _) = bob.receive_sync_changes(sync_changes).await.unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (processed, _) = + bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(4, processed.len()); diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index 57507dcc88e..b1c9d863baf 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -1068,7 +1068,7 @@ mod tests { requests::ToDeviceRequest, DeviceKeys, EventEncryptionAlgorithm, }, - EncryptionSettings, LocalTrust, OlmMachine, + DecryptionSettings, EncryptionSettings, LocalTrust, OlmMachine, TrustRequirement, }; fn alice_id() -> &'static UserId { @@ -1649,7 +1649,12 @@ mod tests { unused_fallback_keys: None, next_batch_token: None, }; - let (decrypted, _) = machine.receive_sync_changes(sync_changes).await.unwrap(); + + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (decrypted, _) = + machine.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); } @@ -1714,7 +1719,12 @@ mod tests { unused_fallback_keys: None, next_batch_token: None, }; - let (decrypted, _) = machine.receive_sync_changes(sync_changes).await.unwrap(); + + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (decrypted, _) = + machine.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); } @@ -1822,7 +1832,12 @@ mod tests { unused_fallback_keys: None, next_batch_token: None, }; - let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap(); + + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + + let (decrypted, _) = + bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); use crate::types::events::EventType; assert_let!( From 94b28a1d414b3871e7ac1cd554b39fa94bf57d9c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 25 Jun 2025 14:08:14 +0100 Subject: [PATCH 2/7] refactor(crypto): Extract a method to check for to-device events from dehydrated devices --- crates/matrix-sdk-crypto/src/machine/mod.rs | 45 ++++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 965881726a1..68615e56a90 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -868,27 +868,13 @@ impl OlmMachine { let mut decrypted = transaction.account().await?.decrypt_to_device_event(&self.inner.store, event).await?; - let from_dehydrated_device = - self.to_device_event_is_from_dehydrated_device(&decrypted, &event.sender).await?; - - // Check whether this event is from a dehydrated device - if so, return Ok(None) - // to skip it because we don't expect ever to receive an event from a - // dehydrated device. - if from_dehydrated_device { - // Device is dehydrated: ignore this event - warn!( - sender = ?event.sender, - session = ?decrypted.session, - "Received a to-device event from a dehydrated device. This is unexpected: ignoring event" - ); - Err(DecryptToDeviceError::FromDehydratedDevice) - } else { - // Device is not dehydrated: handle it as normal e.g. create a Megolm session - self.handle_decrypted_to_device_event(transaction.cache(), &mut decrypted, changes) - .await?; + // Return early if the sending device is decrypted + self.check_to_device_event_is_not_from_dehydrated_device(&decrypted, &event.sender).await?; - Ok(decrypted) - } + // Device is not dehydrated: handle it as normal e.g. create a Megolm session + self.handle_decrypted_to_device_event(transaction.cache(), &mut decrypted, changes).await?; + + Ok(decrypted) } #[instrument( @@ -1507,6 +1493,25 @@ impl OlmMachine { }) } + /// Return an error if the supplied to-device event was sent from a + /// dehydrated device. + async fn check_to_device_event_is_not_from_dehydrated_device( + &self, + decrypted: &OlmDecryptionInfo, + sender_user_id: &UserId, + ) -> Result<(), DecryptToDeviceError> { + if self.to_device_event_is_from_dehydrated_device(decrypted, sender_user_id).await? { + warn!( + sender = ?sender_user_id, + session = ?decrypted.session, + "Received a to-device event from a dehydrated device. This is unexpected: ignoring event" + ); + Err(DecryptToDeviceError::FromDehydratedDevice) + } else { + Ok(()) + } + } + /// Decide whether a decrypted to-device event was sent from a dehydrated /// device. /// From c3c6025621d02037e776cb5d5e834d405984c9ef Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 1 Jul 2025 11:26:25 +0100 Subject: [PATCH 3/7] refactor(tests): Take a reference to content in send_and_receive_encrypted_to_device_test_helper This will allow us to re-use it in more tests. --- crates/matrix-sdk-crypto/src/machine/test_helpers.rs | 2 +- .../src/machine/tests/send_encrypted_to_device.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 0fdf1317b90..8ed8b803f63 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -187,7 +187,7 @@ pub async fn send_and_receive_encrypted_to_device_test_helper( sender: &OlmMachine, recipient: &OlmMachine, event_type: &str, - content: Value, + content: &Value, ) -> ProcessedToDeviceEvent { let device = sender.get_device(recipient.user_id(), recipient.device_id(), None).await.unwrap().unwrap(); diff --git a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs index 962e4aba782..0e7d6262013 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs @@ -226,7 +226,7 @@ async fn test_send_olm_encryption_info_unverified_identity() { &alice, &bob, custom_event_type, - custom_content, + &custom_content, ) .await; @@ -266,7 +266,7 @@ async fn test_send_olm_encryption_info_verified_identity() { &alice, &bob, custom_event_type, - custom_content, + &custom_content, ) .await; @@ -302,7 +302,7 @@ async fn test_send_olm_encryption_info_verified_locally() { &alice, &bob, custom_event_type, - custom_content, + &custom_content, ) .await; @@ -344,7 +344,7 @@ async fn test_send_olm_encryption_info_verification_violation() { &alice, &bob, custom_event_type, - custom_content, + &custom_content, ) .await; From e6386777b1066eeeb91a61ef08869d4ae5c2d9be Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 1 Jul 2025 11:34:40 +0100 Subject: [PATCH 4/7] refactor(tests): Re-use send_and_receive_encrypted_to_device_test_helper in 2 more tests --- .../machine/tests/send_encrypted_to_device.rs | 99 +++---------------- 1 file changed, 15 insertions(+), 84 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs index 0e7d6262013..f9e7427ef3e 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs @@ -51,53 +51,13 @@ async fn test_send_encrypted_to_device() { "rooms": ["!726s6s6q:example.com"] }); - let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap(); - let raw_encrypted = device - .encrypt_event_raw(custom_event_type, &custom_content) - .await - .expect("Should have encrypted the content"); - - let request = ToDeviceRequest::new( - bob.user_id(), - DeviceIdOrAllDevices::DeviceId(tests::bob_device_id().to_owned()), - "m.room.encrypted", - raw_encrypted.cast(), - ); - - assert_eq!("m.room.encrypted", request.event_type.to_string()); - - let messages = &request.messages; - assert_eq!(1, messages.len()); - assert!(messages.get(bob.user_id()).is_some()); - let target_devices = messages.get(bob.user_id()).unwrap(); - assert_eq!(1, target_devices.len()); - assert!(target_devices - .get(&DeviceIdOrAllDevices::DeviceId(tests::bob_device_id().to_owned())) - .is_some()); - - let event = ToDeviceEvent::new( - alice.user_id().to_owned(), - tests::to_device_requests_to_content(vec![request.clone().into()]), - ); - - let event = json_convert(&event).unwrap(); - - let sync_changes = EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }; - - let decryption_settings = - DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; - - let (decrypted, _) = - bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); - - assert_eq!(1, decrypted.len()); - let processed_event = &decrypted[0]; + let processed_event = send_and_receive_encrypted_to_device_test_helper( + &alice, + &bob, + custom_event_type, + &custom_content, + ) + .await; assert_let!(ProcessedToDeviceEvent::Decrypted { raw, encryption_info } = processed_event); @@ -105,7 +65,7 @@ async fn test_send_encrypted_to_device() { assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned()); - let decrypted_value = to_raw_value(&decrypted[0].to_raw()).unwrap(); + let decrypted_value = to_raw_value(&raw).unwrap(); let decrypted_value = serde_json::to_value(decrypted_value).unwrap(); assert_eq!( @@ -164,42 +124,13 @@ async fn test_receive_custom_encrypted_to_device_fails_if_device_unknown() { "rooms": ["!726s6s6q:example.com"] }); - let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap(); - let raw_encrypted = device - .encrypt_event_raw(custom_event_type, &custom_content) - .await - .expect("Should have encrypted the content"); - - let request = ToDeviceRequest::new( - bob.user_id(), - DeviceIdOrAllDevices::DeviceId(tests::bob_device_id().to_owned()), - "m.room.encrypted", - raw_encrypted.cast(), - ); - - let event = ToDeviceEvent::new( - alice.user_id().to_owned(), - tests::to_device_requests_to_content(vec![request.clone().into()]), - ); - - let event = json_convert(&event).unwrap(); - - let sync_changes = EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }; - - let decryption_settings = - DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; - - let (decrypted, _) = - bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); - - assert_eq!(1, decrypted.len()); - let processed_event = &decrypted[0]; + let processed_event = send_and_receive_encrypted_to_device_test_helper( + &alice, + &bob, + custom_event_type, + &custom_content, + ) + .await; assert_let!(ProcessedToDeviceEvent::UnableToDecrypt(_) = processed_event); } From 92d064e996bcb1f44ef0f1f468e8dfb779706198 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 1 Jul 2025 11:38:26 +0100 Subject: [PATCH 5/7] refactor(tests): Pass decryption_settings in to send_and_receive_encrypted_to_device_test_helper To allow passing in different values in future tests. --- .../src/machine/test_helpers.rs | 5 ++-- .../machine/tests/send_encrypted_to_device.rs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 8ed8b803f63..9354563e145 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -188,6 +188,7 @@ pub async fn send_and_receive_encrypted_to_device_test_helper( recipient: &OlmMachine, event_type: &str, content: &Value, + decryption_settings: &DecryptionSettings, ) -> ProcessedToDeviceEvent { let device = sender.get_device(recipient.user_id(), recipient.device_id(), None).await.unwrap().unwrap(); @@ -218,11 +219,9 @@ pub async fn send_and_receive_encrypted_to_device_test_helper( next_batch_token: None, }; - let decryption_settings = - DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; - let (decrypted, _) = recipient.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); + assert_eq!(1, decrypted.len()); decrypted[0].clone() } diff --git a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs index f9e7427ef3e..53c036ab105 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs @@ -51,11 +51,15 @@ async fn test_send_encrypted_to_device() { "rooms": ["!726s6s6q:example.com"] }); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let processed_event = send_and_receive_encrypted_to_device_test_helper( &alice, &bob, custom_event_type, &custom_content, + &decryption_settings, ) .await; @@ -124,11 +128,15 @@ async fn test_receive_custom_encrypted_to_device_fails_if_device_unknown() { "rooms": ["!726s6s6q:example.com"] }); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let processed_event = send_and_receive_encrypted_to_device_test_helper( &alice, &bob, custom_event_type, &custom_content, + &decryption_settings, ) .await; @@ -153,11 +161,15 @@ async fn test_send_olm_encryption_info_unverified_identity() { "rooms": ["!726s6s6q:example.com"] }); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let processed_event = send_and_receive_encrypted_to_device_test_helper( &alice, &bob, custom_event_type, &custom_content, + &decryption_settings, ) .await; @@ -193,11 +205,15 @@ async fn test_send_olm_encryption_info_verified_identity() { "rooms": ["!726s6s6q:example.com"] }); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let processed_event = send_and_receive_encrypted_to_device_test_helper( &alice, &bob, custom_event_type, &custom_content, + &decryption_settings, ) .await; @@ -229,11 +245,15 @@ async fn test_send_olm_encryption_info_verified_locally() { .await .unwrap(); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let processed_event = send_and_receive_encrypted_to_device_test_helper( &alice, &bob, custom_event_type, &custom_content, + &decryption_settings, ) .await; @@ -271,11 +291,15 @@ async fn test_send_olm_encryption_info_verification_violation() { "rooms": ["!726s6s6q:example.com"] }); + let decryption_settings = + DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; + let processed_event = send_and_receive_encrypted_to_device_test_helper( &alice, &bob, custom_event_type, &custom_content, + &decryption_settings, ) .await; From b899348a3320de3bf13d9aa4850ad18610a0a9e5 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 1 Jul 2025 13:04:47 +0100 Subject: [PATCH 6/7] feat(crypto): Refuse to decrypt to-device messages from unverified devices (when in exclude insecure mode) --- .../src/deserialized_responses.rs | 11 + crates/matrix-sdk-crypto/CHANGELOG.md | 15 ++ crates/matrix-sdk-crypto/src/machine/mod.rs | 153 +++++++++++- .../src/machine/test_helpers.rs | 4 +- .../machine/tests/send_encrypted_to_device.rs | 222 +++++++++++++++++- 5 files changed, 389 insertions(+), 16 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 1dd9fb0fa85..984d42db55b 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -1196,6 +1196,15 @@ pub enum ProcessedToDeviceEvent { /// required information to be processed (like no event `type` for /// example) Invalid(Raw), + + /// A to device event that was ignored because the sender device was not + /// verified. + UnverifiedSender { + /// The raw decrypted event + raw: Raw, + /// The Olm encryption info + encryption_info: EncryptionInfo, + }, } impl ProcessedToDeviceEvent { @@ -1207,6 +1216,7 @@ impl ProcessedToDeviceEvent { ProcessedToDeviceEvent::UnableToDecrypt(event) => event.clone(), ProcessedToDeviceEvent::PlainText(event) => event.clone(), ProcessedToDeviceEvent::Invalid(event) => event.clone(), + ProcessedToDeviceEvent::UnverifiedSender { raw, .. } => raw.clone(), } } @@ -1217,6 +1227,7 @@ impl ProcessedToDeviceEvent { ProcessedToDeviceEvent::UnableToDecrypt(event) => event, ProcessedToDeviceEvent::PlainText(event) => event, ProcessedToDeviceEvent::Invalid(event) => event, + ProcessedToDeviceEvent::UnverifiedSender { raw, .. } => raw, } } } diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 798871e95c9..c7e30863c49 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -11,6 +11,21 @@ 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 (see the documentation on + `OlmMachine::decrypt_to_device_event` for details). To support this, a new + variant has been added to `ProcessedToDeviceEvent`: `UnverifiedSender`, which + is returned from `OlmMachine::receive_encrypted_to_device_event` 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`, diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 68615e56a90..36e60b383e5 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -845,9 +845,17 @@ impl OlmMachine { /// Decrypt and handle a to-device event. /// - /// If decryption (or checking the sender device) fails, returns + /// If decryption (or checking the sender device) fails, returns an /// `Err(DecryptToDeviceError::OlmError)`. /// + /// If we are in strict "exclude insecure devices" mode and the sender + /// device is not verified, and the decrypted event type is not on the + /// allow list, returns `Err(DecryptToDeviceError::UnverifiedSender)` + /// + /// (The allow list of types that are processed even if the sender is + /// unverified is: `m.room_key`, `m.room_key.withheld`, + /// `m.room_key_request`, `m.secret.request` and `m.key.verification.*`.) + /// /// If the sender device is dehydrated, does no handling and immediately /// returns `Err(DecryptToDeviceError::FromDehydratedDevice)`. /// @@ -862,12 +870,17 @@ impl OlmMachine { transaction: &mut StoreTransaction, event: &EncryptedToDeviceEvent, changes: &mut Changes, - _decryption_settings: &DecryptionSettings, + decryption_settings: &DecryptionSettings, ) -> Result { // Decrypt the event - let mut decrypted = + let decrypted = transaction.account().await?.decrypt_to_device_event(&self.inner.store, event).await?; + let mut decrypted = self.check_to_device_is_from_verified_device_or_allowed_type( + decryption_settings, + decrypted, + )?; + // Return early if the sending device is decrypted self.check_to_device_event_is_not_from_dehydrated_device(&decrypted, &event.sender).await?; @@ -1324,7 +1337,10 @@ impl OlmMachine { match event { // These are handled here because we accept them either plaintext or - // encrypted + // encrypted. + // + // Note: this list should match the allowed types in + // check_to_device_is_from_verified_device_or_allowed_type RoomKeyRequest(e) => self.inner.key_request_machine.receive_incoming_key_request(e), SecretRequest(e) => self.inner.key_request_machine.receive_incoming_secret_request(e), RoomKeyWithheld(e) => self.add_withheld_info(changes, e), @@ -1426,8 +1442,14 @@ impl OlmMachine { /// /// Return the same event, decrypted if possible. /// - /// If we can identify that this to-device event came from a dehydrated - /// device, this method does not process it, and returns `None`. + /// If we are in strict "exclude insecure devices" mode and the sender + /// device is not verified, and the decrypted event type is not on the + /// allow list, or if this event comes from a dehydrated device, this method + /// does not process it, and returns `None`. + /// + /// (The allow list of types that are processed even if the sender is + /// unverified is: `m.room_key`, `m.room_key.withheld`, + /// `m.room_key_request`, `m.secret.request` and `m.key.verification.*`.) async fn receive_encrypted_to_device_event( &self, transaction: &mut StoreTransaction, @@ -1456,6 +1478,12 @@ impl OlmMachine { return Some(ProcessedToDeviceEvent::UnableToDecrypt(raw_event)); } Err(DecryptToDeviceError::FromDehydratedDevice) => return None, + Err(DecryptToDeviceError::UnverifiedSender(olm_decryption_info)) => { + return Some(ProcessedToDeviceEvent::UnverifiedSender { + raw: olm_decryption_info.result.raw_event, + encryption_info: olm_decryption_info.result.encryption_info, + }) + } }; // New sessions modify the account so we need to save that @@ -1493,6 +1521,66 @@ impl OlmMachine { }) } + fn check_to_device_is_from_verified_device_or_allowed_type( + &self, + decryption_settings: &DecryptionSettings, + decrypted: OlmDecryptionInfo, + ) -> Result { + let event_type = decrypted.result.event.event_type(); + + // If we're in "exclude insecure devices" mode, we prevent most + // to-device events with unverified senders from being allowed + // through here, but there are some exceptions: + // + // * m.room_key - we hold on to these until later, so if the sender becomes + // verified later we can still use the key. + // + // * m.room_key_request, m.room_key.withheld, m.key.verification.*, + // m.secret.request - these are allowed as plaintext events, so we also allow + // them encrypted from insecure devices. Note: the list of allowed types here + // should match with what is allowed in handle_to_device_event. + match event_type { + "m.room_key" + | "m.room_key.withheld" + | "m.room_key_request" + | "m.secret.request" + | "m.key.verification.key" + | "m.key.verification.mac" + | "m.key.verification.done" + | "m.key.verification.ready" + | "m.key.verification.start" + | "m.key.verification.accept" + | "m.key.verification.cancel" + | "m.key.verification.request" => { + // This is one of the exception types - we allow it even if the sender device is + // not verified. + Ok(decrypted) + } + _ => { + // This is not an exception type - check for "exclude insecure devices" mode, + // and whether the sender is verified. + + let exclude_unverified_devices = match decryption_settings + .sender_device_trust_requirement + { + TrustRequirement::Untrusted => false, + TrustRequirement::CrossSignedOrLegacy | TrustRequirement::CrossSigned => true, + }; + + if exclude_unverified_devices + && sending_device_is_unverified(&decrypted.result.encryption_info) + { + // Refuse to decrypt - the sender is unverified + Err(DecryptToDeviceError::UnverifiedSender(Box::new(decrypted))) + } else { + // Either we're not excluding insecure devices, or the sender is verified, so + // allow decryption to continue. + Ok(decrypted) + } + } + } + } + /// Return an error if the supplied to-device event was sent from a /// dehydrated device. async fn check_to_device_event_is_not_from_dehydrated_device( @@ -1594,6 +1682,15 @@ impl OlmMachine { /// If any of the to-device events in the supplied changes were sent from /// dehydrated devices, these are not processed, and are omitted from /// the returned list, as per MSC3814. + /// + /// If we are in strict "exclude insecure devices" mode and the sender + /// device of any event is not verified, and the decrypted event type is not + /// on the allow list, these events are not processed and are omitted from + /// the returned list. + /// + /// (The allow list of types that are processed even if the sender is + /// unverified is: `m.room_key`, `m.room_key.withheld`, + /// `m.room_key_request`, `m.secret.request` and `m.key.verification.*`.) pub(crate) async fn preprocess_sync_changes( &self, transaction: &mut StoreTransaction, @@ -2842,6 +2939,28 @@ impl OlmMachine { } } +fn sending_device_is_unverified(encryption_info: &EncryptionInfo) -> bool { + if let VerificationState::Unverified(level) = &encryption_info.verification_state { + // Sender is not fully verified + match level { + VerificationLevel::UnverifiedIdentity => { + // The user's identity is only pinned: fine + false + } + VerificationLevel::VerificationViolation + | VerificationLevel::UnsignedDevice + | VerificationLevel::None(_) + | VerificationLevel::MismatchedSender => { + // The device is not verified or the user is in verification violation: not fine + true + } + } + } else { + // Verified sender: fine + false + } +} + fn sender_data_to_verification_state( sender_data: SenderData, session_has_been_imported: bool, @@ -2960,14 +3079,27 @@ fn megolm_error_to_utd_info( Ok(UnableToDecryptInfo { session_id, reason }) } -/// An error that can occur during [`OlmMachine::decrypt_to_device_event`] - -/// either because decryption failed, or because the sender device was a -/// dehydrated device, which should never send any to-device messages. +/// An error that can occur during [`OlmMachine::decrypt_to_device_event`]: +/// +/// * because decryption failed, or +/// +/// * because the sender device was not verified when we are in strict "exclude +/// insecure devices" mode, or +/// +/// * because the sender device was a dehydrated device, which should never send +/// any to-device messages. #[derive(Debug, thiserror::Error)] pub(crate) enum DecryptToDeviceError { #[error("An Olm error occurred meaning we failed to decrypt the event")] OlmError(#[from] OlmError), + #[error( + "We were able to decrypt the event, but the sender device was not \ + verified, and we have 'exclude insecure devices' enabled, so we \ + choose not to use this event." + )] + UnverifiedSender(Box), + #[error("The event was sent from a dehydrated device")] FromDehydratedDevice, } @@ -2988,6 +3120,9 @@ impl From for OlmError { DecryptToDeviceError::FromDehydratedDevice => { panic!("Expected an OlmError but found FromDehydratedDevice") } + DecryptToDeviceError::UnverifiedSender(_) => { + panic!("Expected and OlmError but found UnverifiedSender") + } } } } diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 9354563e145..a2b95eb4f06 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -194,7 +194,7 @@ pub async fn send_and_receive_encrypted_to_device_test_helper( sender.get_device(recipient.user_id(), recipient.device_id(), None).await.unwrap().unwrap(); let raw_encrypted = device - .encrypt_event_raw(event_type, &content) + .encrypt_event_raw(event_type, content) .await .expect("Should have encrypted the content"); @@ -220,7 +220,7 @@ pub async fn send_and_receive_encrypted_to_device_test_helper( }; let (decrypted, _) = - recipient.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap(); + recipient.receive_sync_changes(sync_changes, decryption_settings).await.unwrap(); assert_eq!(1, decrypted.len()); decrypted[0].clone() diff --git a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs index 53c036ab105..077b73f285f 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/send_encrypted_to_device.rs @@ -17,8 +17,11 @@ use insta::assert_json_snapshot; use matrix_sdk_common::deserialized_responses::{ AlgorithmInfo, ProcessedToDeviceEvent, VerificationLevel, VerificationState, }; -use matrix_sdk_test::async_test; -use ruma::{events::AnyToDeviceEvent, serde::Raw, to_device::DeviceIdOrAllDevices}; +use matrix_sdk_test::{async_test, ruma_response_from_json}; +use ruma::{ + events::AnyToDeviceEvent, room_id, serde::Raw, to_device::DeviceIdOrAllDevices, RoomId, + TransactionId, +}; use serde_json::{json, value::to_raw_value, Value}; use crate::{ @@ -29,14 +32,18 @@ use crate::{ }, tests::{self, decryption_verification_state::mark_alice_identity_as_verified_test_helper}, }, + olm::SenderData, types::{ - events::{ToDeviceCustomEvent, ToDeviceEvent}, + events::{ + room::encrypted::ToDeviceEncryptedEventContent, EventType as _, ToDeviceCustomEvent, + ToDeviceEvent, + }, requests::ToDeviceRequest, }, utilities::json_convert, verification::tests::bob_id, - DecryptionSettings, DeviceData, EncryptionSyncChanges, LocalTrust, OlmError, OlmMachine, - TrustRequirement, + CrossSigningBootstrapRequests, DecryptionSettings, DeviceData, EncryptionSettings, + EncryptionSyncChanges, LocalTrust, OlmError, OlmMachine, Session, TrustRequirement, }; #[async_test] @@ -143,6 +150,116 @@ async fn test_receive_custom_encrypted_to_device_fails_if_device_unknown() { assert_let!(ProcessedToDeviceEvent::UnableToDecrypt(_) = processed_event); } +#[async_test] +async fn test_excluding_insecure_means_custom_to_device_events_from_unverified_devices_are_ignored() +{ + // Given we are in "exclude insecure devices" mode + let decryption_settings = DecryptionSettings { + sender_device_trust_requirement: TrustRequirement::CrossSignedOrLegacy, + }; + + // Bob is the receiver + let (bob, otk) = get_prepared_machine_test_helper(bob_id(), false).await; + + // Alice is the sender + let alice = OlmMachine::new(tests::alice_id(), tests::alice_device_id()).await; + + let bob_device = DeviceData::from_machine_test_helper(&bob).await.unwrap(); + alice.store().save_device_data(&[bob_device]).await.unwrap(); + + let (alice, bob) = build_session_for_pair(alice, bob, otk).await; + + // And the receiving device does not consider the sending device verified + make_alice_unverified(&alice, &bob).await; + + assert!(!bob + .get_device(alice.user_id(), alice.device_id(), None) + .await + .unwrap() + .unwrap() + .is_verified()); + + // When we send a custom event + let custom_event_type = "m.new_device"; + + let custom_content = json!({ + "device_id": "XYZABCDE", + "rooms": ["!726s6s6q:example.com"] + }); + + let processed_event = send_and_receive_encrypted_to_device_test_helper( + &alice, + &bob, + custom_event_type, + &custom_content, + &decryption_settings, + ) + .await; + + // Then it was not processed because the sending device was not verified + assert_let!( + ProcessedToDeviceEvent::UnverifiedSender { raw, encryption_info } = processed_event + ); + + // And the info provided in UnverifiedSender matches what we supplied + let received: Value = serde_json::from_str(raw.json().get()).unwrap(); + assert_eq!(received.as_object().unwrap().get("content").unwrap(), &custom_content); + assert_eq!(encryption_info.sender, tests::alice_id()); + assert_eq!(encryption_info.sender_device.as_ref().unwrap(), tests::alice_device_id()); + assert_eq!( + encryption_info.verification_state, + VerificationState::Unverified(VerificationLevel::UnsignedDevice) + ); +} + +#[async_test] +async fn test_excluding_insecure_does_not_prevent_key_events_being_processed() { + // Given we are in "exclude insecure devices" mode + let decryption_settings = DecryptionSettings { + sender_device_trust_requirement: TrustRequirement::CrossSignedOrLegacy, + }; + + // Bob is the receiver + let (bob, otk) = get_prepared_machine_test_helper(bob_id(), false).await; + + // Alice is the sender + let alice = OlmMachine::new(tests::alice_id(), tests::alice_device_id()).await; + + let bob_device = DeviceData::from_machine_test_helper(&bob).await.unwrap(); + alice.store().save_device_data(&[bob_device]).await.unwrap(); + + let (alice, bob) = build_session_for_pair(alice, bob, otk).await; + + // And the receiving device does not consider the sending device verified + make_alice_unverified(&alice, &bob).await; + + assert!(!bob + .get_device(alice.user_id(), alice.device_id(), None) + .await + .unwrap() + .unwrap() + .is_verified()); + + // When we send a room key event + let key_event = + create_and_share_session_without_sender_data(&alice, &bob, room_id!("!23:s.co")).await; + + let key_event_content = serde_json::to_value(&key_event.content).unwrap(); + + let processed_event = send_and_receive_encrypted_to_device_test_helper( + &alice, + &bob, + "m.room_key", + &key_event_content, + &decryption_settings, + ) + .await; + + // Then it was processed because even though the sending device was not + // verified, room key events are allowed through. + assert_matches!(processed_event, ProcessedToDeviceEvent::Decrypted { .. }); +} + #[async_test] async fn test_send_olm_encryption_info_unverified_identity() { let (alice, bob) = @@ -495,3 +612,98 @@ async fn test_send_encrypted_to_device_no_session() { assert_matches!(encryption_result, Err(OlmError::MissingSession)); } + +/// Create a new [`OutboundGroupSession`], and build a to-device event to share +/// it with another [`OlmMachine`], *without* sending the MSC4147 sender data. +/// +/// # Arguments +/// +/// * `alice` - sending device. +/// * `bob` - receiving device. +/// * `room_id` - room to create a session for. +async fn create_and_share_session_without_sender_data( + alice: &OlmMachine, + bob: &OlmMachine, + room_id: &RoomId, +) -> ToDeviceEvent { + let (outbound_session, _) = alice + .inner + .group_session_manager + .get_or_create_outbound_session( + room_id, + EncryptionSettings::default(), + SenderData::unknown(), + ) + .await + .unwrap(); + + // In future, we might want to save the session to the store, to better match + // the behaviour of the real implementation. See + // `GroupSessionManager::share_room_key` for inspiration on how to do that. + + let olm_sessions = alice + .store() + .get_sessions(&bob.identity_keys().curve25519.to_base64()) + .await + .unwrap() + .unwrap(); + let mut olm_session: Session = olm_sessions.lock().await[0].clone(); + + let room_key_content = outbound_session.as_content().await; + let plaintext = serde_json::to_string(&json!({ + "sender": alice.user_id(), + "sender_device": alice.device_id(), + "keys": { "ed25519": alice.identity_keys().ed25519.to_base64() }, + // We deliberately do *not* include: + // "org.matrix.msc4147.device_keys": alice_device_keys, + "recipient": bob.user_id(), + "recipient_keys": { "ed25519": bob.identity_keys().ed25519.to_base64() }, + "type": room_key_content.event_type(), + "content": room_key_content, + })) + .unwrap(); + + let ciphertext = olm_session.encrypt_helper(&plaintext).await; + ToDeviceEvent::new( + alice.user_id().to_owned(), + olm_session.build_encrypted_event(ciphertext, None).await.unwrap(), + ) +} + +/// Simulate uploading keys for alice that mean bob thinks alice's device +/// exists, but is unverified. +async fn make_alice_unverified(alice: &OlmMachine, bob: &OlmMachine) { + let CrossSigningBootstrapRequests { upload_signing_keys_req: upload_signing, .. } = + alice.bootstrap_cross_signing(false).await.expect("Expect Alice x-signing key request"); + + let device_keys = alice + .get_device(alice.user_id(), alice.device_id(), None) + .await + .unwrap() + .unwrap() + .as_device_keys() + .to_owned(); + + let updated_keys_with_x_signing = json!({ device_keys.device_id.to_string(): device_keys }); + + let json = json!({ + "device_keys": { + alice.user_id() : updated_keys_with_x_signing + }, + "failures": {}, + "master_keys": { + alice.user_id() : upload_signing.master_key.unwrap(), + }, + "user_signing_keys": { + alice.user_id() : upload_signing.user_signing_key.unwrap(), + }, + "self_signing_keys": { + alice.user_id() : upload_signing.self_signing_key.unwrap(), + }, + } + ); + + let kq_response = ruma_response_from_json(&json); + alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); + bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); +} From 4c1ef53abbb3108f57b30c6d6cc46b456d212875 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 2 Jul 2025 12:40:22 +0100 Subject: [PATCH 7/7] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Damir Jelić Signed-off-by: Andy Balaam --- crates/matrix-sdk-common/src/deserialized_responses.rs | 2 +- crates/matrix-sdk-crypto/src/machine/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 984d42db55b..8889cc22b1f 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -1197,7 +1197,7 @@ pub enum ProcessedToDeviceEvent { /// example) Invalid(Raw), - /// A to device event that was ignored because the sender device was not + /// A to-device event that was ignored because the sender device was not /// verified. UnverifiedSender { /// The raw decrypted event diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 36e60b383e5..733cad0294f 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -881,7 +881,7 @@ impl OlmMachine { decrypted, )?; - // Return early if the sending device is decrypted + // Return early if the sending device is a dehydrated device self.check_to_device_event_is_not_from_dehydrated_device(&decrypted, &event.sender).await?; // Device is not dehydrated: handle it as normal e.g. create a Megolm session