diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 3c58139ce9e..10b32cd1f34 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -18,7 +18,8 @@ use matrix_sdk_crypto::{ olm::ExportedRoomKey, store::types::{BackupDecryptionKey, Changes}, types::requests::ToDeviceRequest, - DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity, + CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, + UserIdentity as SdkUserIdentity, }; use ruma::{ api::{ @@ -832,6 +833,7 @@ impl OlmMachine { device_id: String, event_type: String, content: String, + share_strategy: CollectStrategy, ) -> Result, CryptoStoreError> { let user_id = parse_user_id(&user_id)?; let device_id = device_id.as_str().into(); @@ -840,8 +842,11 @@ impl OlmMachine { let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?; if let Some(device) = device { - let encrypted_content = - self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?; + let encrypted_content = self.runtime.block_on(device.encrypt_event_raw( + &event_type, + &content, + share_strategy, + ))?; let request = ToDeviceRequest::new( user_id.as_ref(), diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index ff03de305e1..513de3a5d47 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,13 +6,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate -## [0.13.0] - 2025-07-10 - ### Features -- [**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 @@ -25,6 +20,17 @@ All notable changes to this project will be documented in this file. 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) +- [**breaking**] The `Device::encrypt_event_raw` and (experimental) + `OlmMachine::encrypt_content_for_devices` have new `share_strategy` parameters + to ensure that the recipients are sufficiently trusted. + ([#5457](https://github.com/matrix-org/matrix-rust-sdk/pull/5457/)) + +## [0.13.0] - 2025-07-10 + +### Features + +- [**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)) ### Refactor diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 10e8ddf03ee..09915b44d5f 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -77,6 +77,10 @@ pub enum OlmError { #[error("encryption failed due to an error collecting the recipient devices: {0}")] SessionRecipientCollectionError(SessionRecipientCollectionError), + /// Encrypted content is withheld from this device + #[error("encryption content is withheld from this: {0}")] + Withheld(WithheldCode), + /// Refused to decrypt because the sender was not verified or did not meet /// the required VerificationLevel. #[error( diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index 5f2471f6b27..cb54e9f28a5 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -43,6 +43,7 @@ use crate::{ olm::{ InboundGroupSession, OutboundGroupSession, Session, ShareInfo, SignedJsonObject, VerifyJson, }, + session_manager::{withheld_code_for_device_for_share_strategy, CollectStrategy}, store::{ caches::SequenceNumber, types::{Changes, DeviceChanges}, @@ -462,6 +463,8 @@ impl Device { /// * `event_type` - The type of the event to be sent. /// * `content` - The content of the event to be sent. This should be a type /// that implements the `Serialize` trait. + /// * `share_strategy` - The share strategy to use to determine whether we + /// should encrypt to the device. /// /// # Returns /// @@ -472,7 +475,19 @@ impl Device { &self, event_type: &str, content: &Value, + share_strategy: CollectStrategy, ) -> OlmResult> { + if let Some(withheld_code) = withheld_code_for_device_for_share_strategy( + &self.inner, + share_strategy, + &self.own_identity, + &self.device_owner_identity, + ) + .await? + { + return Err(OlmError::Withheld(withheld_code)); + } + let (used_session, raw_encrypted) = self.encrypt(event_type, content).await?; // Persist the used session diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index c18c26b9e05..fbe0c664165 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -63,6 +63,8 @@ use vodozemac::{ Curve25519PublicKey, Ed25519Signature, }; +#[cfg(feature = "experimental-send-custom-to-device")] +use crate::session_manager::split_devices_for_share_strategy; use crate::{ backups::{BackupMachine, MegolmV1BackupKey}, dehydrated_devices::{DehydratedDevices, DehydrationError}, @@ -1159,15 +1161,17 @@ impl OlmMachine { devices: Vec, event_type: &str, content: &Value, + share_strategy: CollectStrategy, ) -> OlmResult<(Vec, Vec<(DeviceData, WithheldCode)>)> { - // TODO: Use a `CollectStrategy` arguments to filter our devices depending on - // safety settings (like not sending to insecure devices). let mut changes = Changes::default(); + let (allowed_devices, mut blocked_devices) = + split_devices_for_share_strategy(&self.inner.store, devices, share_strategy).await?; + let result = self .inner .group_session_manager - .encrypt_content_for_devices(devices, event_type, content.clone(), &mut changes) + .encrypt_content_for_devices(allowed_devices, event_type, content.clone(), &mut changes) .await; // Persist any changes we might have collected. @@ -1182,7 +1186,10 @@ impl OlmMachine { ); } - result + result.map(|(to_device_requests, mut withheld)| { + withheld.append(&mut blocked_devices); + (to_device_requests, withheld) + }) } /// Collect the devices belonging to the given user, and send the details of /// a room key bundle to those devices. diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index 4c5f542a2c3..97c3ecf40f2 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -39,6 +39,7 @@ use tokio::sync::Mutex; use crate::{ machine::tests, olm::PrivateCrossSigningIdentity, + session_manager::CollectStrategy, store::{types::Changes, CryptoStoreWrapper, MemoryStore}, types::{ events::ToDeviceEvent, @@ -194,7 +195,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, CollectStrategy::AllDevices) .await .expect("Should have encrypted the content"); 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 1a2d1ab7b3c..5e789fc5759 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 @@ -34,6 +34,7 @@ use crate::{ tests::{self, decryption_verification_state::mark_alice_identity_as_verified_test_helper}, }, olm::SenderData, + session_manager::CollectStrategy, types::{ events::{ room::encrypted::ToDeviceEncryptedEventContent, EventType as _, ToDeviceCustomEvent, @@ -443,7 +444,7 @@ async fn test_processed_to_device_variants() { 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) + .encrypt_event_raw(custom_event_type, &custom_content, CollectStrategy::AllDevices) .await .expect("Should have encryted the content"); @@ -600,7 +601,7 @@ async fn test_send_encrypted_to_device_no_session() { .await .unwrap() .unwrap() - .encrypt_event_raw(custom_event_type, &custom_content) + .encrypt_event_raw(custom_event_type, &custom_content, CollectStrategy::AllDevices) .await; assert_matches!(encryption_result, Err(OlmError::MissingSession)); @@ -700,3 +701,50 @@ async fn make_alice_unverified(alice: &OlmMachine, bob: &OlmMachine) { alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap(); } + +#[async_test] +/// Test that when we get an error when we try to encrypt to a device that +/// doesn't satisfy the share strategy. +async fn test_share_strategy_prevents_encryption() { + use matrix_sdk_common::deserialized_responses::WithheldCode; + use matrix_sdk_test::test_json::keys_query_sets::KeyDistributionTestData as DataSet; + use ruma::TransactionId; + + use crate::CrossSigningKeyExport; + + // Create the local user (`@me`), and import the public identity keys + let machine = OlmMachine::new(DataSet::me_id(), DataSet::me_device_id()).await; + let keys_query = DataSet::me_keys_query_response(); + machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap(); + + // Also import the private cross signing keys + machine + .import_cross_signing_keys(CrossSigningKeyExport { + master_key: DataSet::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(), + self_signing_key: DataSet::SELF_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(), + user_signing_key: DataSet::USER_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(), + }) + .await + .unwrap(); + + let keys_query = DataSet::dan_keys_query_response(); + let txn_id = TransactionId::new(); + machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap(); + + let custom_event_type = "m.new_device"; + + let custom_content = json!({ + "device_id": "XYZABCDE", + "rooms": ["!726s6s6q:example.com"] + }); + + let encryption_result = machine + .get_device(DataSet::dan_id(), DataSet::dan_unsigned_device_id(), None) + .await + .unwrap() + .unwrap() + .encrypt_event_raw(custom_event_type, &custom_content, CollectStrategy::OnlyTrustedDevices) + .await; + + assert_matches!(encryption_result, Err(OlmError::Withheld(WithheldCode::Unverified))); +} 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 484b8594052..4ba8d8fc000 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 @@ -35,8 +35,12 @@ use ruma::{ UserId, }; use serde::Serialize; -pub(crate) use share_strategy::CollectRecipientsResult; +#[cfg(feature = "experimental-send-custom-to-device")] +pub(crate) use share_strategy::split_devices_for_share_strategy; pub use share_strategy::CollectStrategy; +pub(crate) use share_strategy::{ + withheld_code_for_device_for_share_strategy, CollectRecipientsResult, +}; use tracing::{debug, error, info, instrument, trace, warn, Instrument}; use crate::{ diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 9e63678f239..f9c7e41f1b7 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -455,6 +455,266 @@ fn is_session_overshared_for_user( should_rotate } +#[cfg(feature = "experimental-send-custom-to-device")] +/// Partition the devices based on the given collect strategy +pub(crate) async fn split_devices_for_share_strategy( + store: &Store, + devices: Vec, + share_strategy: CollectStrategy, +) -> OlmResult<(Vec, Vec<(DeviceData, WithheldCode)>)> { + let own_identity = store.get_user_identity(store.user_id()).await?.and_then(|i| i.into_own()); + + let mut verified_users_with_new_identities: BTreeSet = Default::default(); + + let mut allowed_devices: Vec = Default::default(); + let mut blocked_devices: Vec<(DeviceData, WithheldCode)> = Default::default(); + + let mut user_identities_cache: BTreeMap> = + Default::default(); + let mut get_user_identity = async move |user_id| -> OlmResult<_> { + match user_identities_cache.get(user_id) { + Some(user_identity) => Ok(user_identity.clone()), + None => { + let user_identity = store.get_user_identity(user_id).await?; + user_identities_cache.insert(user_id.to_owned(), user_identity.clone()); + Ok(user_identity) + } + } + }; + + match share_strategy { + CollectStrategy::AllDevices => { + for device in devices.iter() { + let user_id = device.user_id(); + let device_owner_identity = get_user_identity(user_id).await?; + + if let Some(withheld_code) = withheld_code_for_device_for_all_devices_strategy( + device, + &own_identity, + &device_owner_identity, + ) { + blocked_devices.push((device.clone(), withheld_code)); + } else { + allowed_devices.push(device.clone()); + } + } + } + + CollectStrategy::ErrorOnVerifiedUserProblem => { + // We throw an error if any user has a verification violation. So + // we loop through all the devices given, and check if the + // associated user has a verification violation. If so, we add the + // device to `unsigned_devices_of_verified_users`, which will be + // returned with the error. + let mut unsigned_devices_of_verified_users: BTreeMap> = + Default::default(); + let mut add_device_to_unsigned_devices_map = |user_id: &UserId, device: &DeviceData| { + let device_id = device.device_id().to_owned(); + if let Some(devices) = unsigned_devices_of_verified_users.get_mut(user_id) { + devices.push(device_id); + } else { + unsigned_devices_of_verified_users.insert(user_id.to_owned(), vec![device_id]); + } + }; + + for device in devices.iter() { + let user_id = device.user_id(); + let device_owner_identity = get_user_identity(user_id).await?; + + if has_identity_verification_violation( + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + verified_users_with_new_identities.insert(user_id.to_owned()); + } else { + match handle_device_for_user_for_error_on_verified_user_problem_strategy( + device, + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + ErrorOnVerifiedUserProblemDeviceDecision::Ok => { + allowed_devices.push(device.clone()) + } + ErrorOnVerifiedUserProblemDeviceDecision::Withhold(code) => { + blocked_devices.push((device.clone(), code)) + } + ErrorOnVerifiedUserProblemDeviceDecision::UnsignedOfVerified => { + add_device_to_unsigned_devices_map(user_id, device); + } + } + } + } + + if !unsigned_devices_of_verified_users.is_empty() { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice( + unsigned_devices_of_verified_users, + ), + )); + } + } + + CollectStrategy::IdentityBasedStrategy => { + // We require our own cross-signing to be properly set up for the + // identity-based strategy, so return an error if it isn't. + match &own_identity { + None => { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::CrossSigningNotSetup, + )); + } + Some(identity) if !identity.is_verified() => { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::SendingFromUnverifiedDevice, + )); + } + Some(_) => (), + } + + for device in devices.iter() { + let user_id = device.user_id(); + let device_owner_identity = get_user_identity(user_id).await?; + + if has_identity_verification_violation( + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + verified_users_with_new_identities.insert(user_id.to_owned()); + } else if let Some(device_owner_identity) = device_owner_identity { + if let Some(withheld_code) = + withheld_code_for_device_with_owner_for_identity_based_strategy( + device, + &device_owner_identity, + ) + { + blocked_devices.push((device.clone(), withheld_code)); + } else { + allowed_devices.push(device.clone()); + } + } else { + panic!("Should have verification violation if device_owner_identity is None") + } + } + } + + CollectStrategy::OnlyTrustedDevices => { + for device in devices.iter() { + let user_id = device.user_id(); + let device_owner_identity = get_user_identity(user_id).await?; + + if let Some(withheld_code) = + withheld_code_for_device_for_only_trusted_devices_strategy( + device, + &own_identity, + &device_owner_identity, + ) + { + blocked_devices.push((device.clone(), withheld_code)); + } else { + allowed_devices.push(device.clone()); + } + } + } + } + + if !verified_users_with_new_identities.is_empty() { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity( + verified_users_with_new_identities.into_iter().collect(), + ), + )); + } + + Ok((allowed_devices, blocked_devices)) +} + +pub(crate) async fn withheld_code_for_device_for_share_strategy( + device: &DeviceData, + share_strategy: CollectStrategy, + own_identity: &Option, + device_owner_identity: &Option, +) -> OlmResult> { + match share_strategy { + CollectStrategy::AllDevices => Ok(withheld_code_for_device_for_all_devices_strategy( + device, + own_identity, + device_owner_identity, + )), + CollectStrategy::ErrorOnVerifiedUserProblem => { + if has_identity_verification_violation( + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(vec![device + .user_id() + .to_owned()]), + )); + } + match handle_device_for_user_for_error_on_verified_user_problem_strategy( + device, + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + ErrorOnVerifiedUserProblemDeviceDecision::Ok => Ok(None), + ErrorOnVerifiedUserProblemDeviceDecision::Withhold(code) => Ok(Some(code)), + ErrorOnVerifiedUserProblemDeviceDecision::UnsignedOfVerified => { + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice( + BTreeMap::from([( + device.user_id().to_owned(), + vec![device.device_id().to_owned()], + )]), + ), + )) + } + } + } + CollectStrategy::IdentityBasedStrategy => { + // We require our own cross-signing to be properly set up for the + // identity-based strategy, so return false if it isn't. + match &own_identity { + None => { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::CrossSigningNotSetup, + )); + } + Some(identity) if !identity.is_verified() => { + return Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::SendingFromUnverifiedDevice, + )); + } + Some(_) => (), + } + + if has_identity_verification_violation( + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(vec![device + .user_id() + .to_owned()]), + )) + } else if let Some(device_owner_identity) = device_owner_identity { + Ok(withheld_code_for_device_with_owner_for_identity_based_strategy( + device, + device_owner_identity, + )) + } else { + panic!("Should have verification violation if device_owner_identity is None") + } + } + CollectStrategy::OnlyTrustedDevices => { + Ok(withheld_code_for_device_for_only_trusted_devices_strategy( + device, + own_identity, + device_owner_identity, + )) + } + } +} + /// Result type for [`split_devices_for_user_for_all_devices_strategy`], /// [`split_devices_for_user_for_error_on_verified_user_problem_strategy`], /// [`split_devices_for_user_for_identity_based_strategy`], @@ -489,16 +749,12 @@ fn split_devices_for_user_for_all_devices_strategy( device_owner_identity: &Option, ) -> RecipientDevicesForUser { let (left, right) = user_devices.into_values().partition_map(|d| { - if d.is_blacklisted() { - Either::Right((d, WithheldCode::Blacklisted)) - } else if d.is_dehydrated() - && should_withhold_to_dehydrated_device( - &d, - own_identity.as_ref(), - device_owner_identity.as_ref(), - ) - { - Either::Right((d, WithheldCode::Unverified)) + if let Some(withheld_code) = withheld_code_for_device_for_all_devices_strategy( + &d, + own_identity, + device_owner_identity, + ) { + Either::Right((d, withheld_code)) } else { Either::Left(d) } @@ -507,6 +763,29 @@ fn split_devices_for_user_for_all_devices_strategy( RecipientDevicesForUser { allowed_devices: left, denied_devices_with_code: right } } +/// Determine whether we should withhold encrypted messages from the given +/// device, for [`CollectStrategy::AllDevices`], and if so, what withheld code +/// to send. +fn withheld_code_for_device_for_all_devices_strategy( + device_data: &DeviceData, + own_identity: &Option, + device_owner_identity: &Option, +) -> Option { + if device_data.is_blacklisted() { + Some(WithheldCode::Blacklisted) + } else if device_data.is_dehydrated() + && should_withhold_to_dehydrated_device( + device_data, + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) + { + Some(WithheldCode::Unverified) + } else { + None + } +} + /// Helper for [`split_devices_for_user_for_all_devices_strategy`]. /// /// Given a dehydrated device `device`, decide if we should withhold the room @@ -634,10 +913,15 @@ fn split_devices_for_user_for_identity_based_strategy( Vec, Vec<(DeviceData, WithheldCode)>, ) = user_devices.into_values().partition_map(|d| { - if d.is_cross_signed_by_owner(device_owner_identity) { - Either::Left(d) + if let Some(withheld_code) = + withheld_code_for_device_with_owner_for_identity_based_strategy( + &d, + device_owner_identity, + ) + { + Either::Right((d, withheld_code)) } else { - Either::Right((d, WithheldCode::Unverified)) + Either::Left(d) } }); RecipientDevicesForUser { @@ -648,6 +932,20 @@ fn split_devices_for_user_for_identity_based_strategy( } } +/// Determine whether we should withhold encrypted messages from the given +/// device, for [`CollectStrategy::IdentityBased`], and if so, what withheld +/// code to send. +fn withheld_code_for_device_with_owner_for_identity_based_strategy( + device_data: &DeviceData, + device_owner_identity: &UserIdentityData, +) -> Option { + if device_data.is_cross_signed_by_owner(device_owner_identity) { + None + } else { + Some(WithheldCode::Unverified) + } +} + /// Partition the list of a user's devices according to whether they should /// receive the key, for [`CollectStrategy::OnlyTrustedDevices`]. fn split_devices_for_user_for_only_trusted_devices( @@ -656,19 +954,38 @@ fn split_devices_for_user_for_only_trusted_devices( device_owner_identity: &Option, ) -> RecipientDevicesForUser { let (left, right) = user_devices.into_values().partition_map(|d| { - match ( - d.local_trust_state(), - d.is_cross_signing_trusted(own_identity, device_owner_identity), + if let Some(withheld_code) = withheld_code_for_device_for_only_trusted_devices_strategy( + &d, + own_identity, + device_owner_identity, ) { - (LocalTrust::BlackListed, _) => Either::Right((d, WithheldCode::Blacklisted)), - (LocalTrust::Ignored | LocalTrust::Verified, _) => Either::Left(d), - (LocalTrust::Unset, false) => Either::Right((d, WithheldCode::Unverified)), - (LocalTrust::Unset, true) => Either::Left(d), + Either::Right((d, withheld_code)) + } else { + Either::Left(d) } }); RecipientDevicesForUser { allowed_devices: left, denied_devices_with_code: right } } +/// Determine whether we should withhold encrypted messages from the given +/// device, for [`CollectStrategy::OnlyTrustedDevices`], and if so, what +/// withheld code to send. +fn withheld_code_for_device_for_only_trusted_devices_strategy( + device_data: &DeviceData, + own_identity: &Option, + device_owner_identity: &Option, +) -> Option { + match ( + device_data.local_trust_state(), + device_data.is_cross_signing_trusted(own_identity, device_owner_identity), + ) { + (LocalTrust::BlackListed, _) => Some(WithheldCode::Blacklisted), + (LocalTrust::Ignored | LocalTrust::Verified, _) => None, + (LocalTrust::Unset, false) => Some(WithheldCode::Unverified), + (LocalTrust::Unset, true) => None, + } +} + fn is_unsigned_device_of_verified_user( own_identity: Option<&OwnUserIdentityData>, device_owner_identity: Option<&UserIdentityData>, @@ -710,7 +1027,7 @@ fn is_user_verified( #[cfg(test)] mod tests { - use std::{collections::BTreeMap, iter, sync::Arc}; + use std::{collections::BTreeMap, iter, ops::Deref, sync::Arc}; use assert_matches::assert_matches; use assert_matches2::assert_let; @@ -726,20 +1043,25 @@ mod tests { use ruma::{ device_id, events::{dummy::ToDeviceDummyEventContent, room::history_visibility::HistoryVisibility}, - room_id, TransactionId, + room_id, DeviceId, TransactionId, UserId, }; use serde_json::json; + #[cfg(feature = "experimental-send-custom-to-device")] + use super::split_devices_for_share_strategy; use crate::{ error::SessionRecipientCollectionError, olm::{OutboundGroupSession, ShareInfo}, session_manager::{ - group_sessions::share_strategy::collect_session_recipients, CollectStrategy, + group_sessions::share_strategy::{ + collect_session_recipients, withheld_code_for_device_for_share_strategy, + }, + CollectStrategy, }, store::caches::SequenceNumber, testing::simulate_key_query_response_for_verification, types::requests::ToDeviceRequest, - CrossSigningKeyExport, EncryptionSettings, LocalTrust, OlmError, OlmMachine, + CrossSigningKeyExport, DeviceData, EncryptionSettings, LocalTrust, OlmError, OlmMachine, }; /// Returns an `OlmMachine` set up for the test user in @@ -766,6 +1088,38 @@ mod tests { machine } + /// Get the `DeviceData` struct for the given user's device. + async fn get_device_data( + machine: &OlmMachine, + user_id: &UserId, + device_id: &DeviceId, + ) -> DeviceData { + machine.get_device(user_id, device_id, None).await.unwrap().unwrap().deref().clone() + } + + async fn get_own_identity_data( + machine: &OlmMachine, + user_id: &UserId, + ) -> Option { + machine + .get_identity(user_id, None) + .await + .unwrap() + .and_then(|i| i.own()) + .map(|i| i.deref().clone()) + } + + async fn get_user_identity_data( + machine: &OlmMachine, + user_id: &UserId, + ) -> Option { + use crate::{identities::user::UserIdentityData, UserIdentity}; + machine.get_identity(user_id, None).await.unwrap().map(|i| match i { + UserIdentity::Own(i) => UserIdentityData::Own(i.deref().clone()), + UserIdentity::Other(i) => UserIdentityData::Other(i.deref().clone()), + }) + } + /// Import device data for `@dan`, `@dave`, and `@good`, as referenced in /// [`KeyDistributionTestData`], into the given OlmMachine async fn import_known_users_to_test_machine(machine: &OlmMachine) { @@ -874,6 +1228,49 @@ mod tests { assert_eq!(dan_devices_shared.len(), 2); assert_eq!(dave_devices_shared.len(), 1); assert_eq!(good_devices_shared.len(), 2); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + // construct the list of all devices from the result of + // collect_session_recipients, because that gives us the devices as + // `DeviceData` + let mut all_devices = dan_devices_shared.clone(); + all_devices.append(&mut dave_devices_shared.clone()); + all_devices.append(&mut good_devices_shared.clone()); + + let (shared_devices, withheld_devices) = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::AllDevices, + ) + .await + .unwrap(); + + assert_eq!(shared_devices.len(), 5); + assert_eq!(withheld_devices.len(), 0); + } + + let own_identity_data = + get_own_identity_data(&machine, KeyDistributionTestData::me_id()).await; + let dan_identity_data = + get_user_identity_data(&machine, KeyDistributionTestData::dan_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data( + &machine, + KeyDistributionTestData::dan_id(), + KeyDistributionTestData::dan_signed_device_id() + ) + .await, + CollectStrategy::AllDevices, + &own_identity_data, + &dan_identity_data, + ) + .await + .unwrap(), + None, + ); } #[async_test] @@ -938,6 +1335,124 @@ mod tests { .expect("This daves's device should receive a withheld code"); assert_eq!(code, &WithheldCode::Unverified); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices: Vec = vec![ + get_device_data( + &machine, + KeyDistributionTestData::dan_id(), + KeyDistributionTestData::dan_unsigned_device_id(), + ) + .await, + get_device_data( + &machine, + KeyDistributionTestData::dan_id(), + KeyDistributionTestData::dan_signed_device_id(), + ) + .await, + get_device_data( + &machine, + KeyDistributionTestData::dave_id(), + KeyDistributionTestData::dave_device_id(), + ) + .await, + get_device_data( + &machine, + KeyDistributionTestData::good_id(), + KeyDistributionTestData::good_device_1_id(), + ) + .await, + get_device_data( + &machine, + KeyDistributionTestData::good_id(), + KeyDistributionTestData::good_device_2_id(), + ) + .await, + ]; + + let (shared_devices, withheld_devices) = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::OnlyTrustedDevices, + ) + .await + .unwrap(); + + assert_eq!(shared_devices.len(), 1); + assert_eq!( + shared_devices[0].device_id().as_str(), + KeyDistributionTestData::dan_signed_device_id() + ); + + assert_eq!(withheld_devices.len(), 4); + assert_eq!( + withheld_devices[0].0.device_id().as_str(), + KeyDistributionTestData::dan_unsigned_device_id() + ); + assert_eq!(withheld_devices[0].1, WithheldCode::Unverified); + assert_eq!( + withheld_devices[1].0.device_id().as_str(), + KeyDistributionTestData::dave_device_id() + ); + assert_eq!(withheld_devices[1].1, WithheldCode::Unverified); + } + + let own_identity_data = + get_own_identity_data(&machine, KeyDistributionTestData::me_id()).await; + let dan_identity_data = + get_user_identity_data(&machine, KeyDistributionTestData::dan_id()).await; + let dave_identity_data = + get_user_identity_data(&machine, KeyDistributionTestData::dave_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data( + &machine, + KeyDistributionTestData::dan_id(), + KeyDistributionTestData::dan_signed_device_id() + ) + .await, + CollectStrategy::OnlyTrustedDevices, + &own_identity_data, + &dan_identity_data, + ) + .await + .unwrap(), + None, + ); + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data( + &machine, + KeyDistributionTestData::dan_id(), + KeyDistributionTestData::dan_unsigned_device_id() + ) + .await, + CollectStrategy::OnlyTrustedDevices, + &own_identity_data, + &dan_identity_data, + ) + .await + .unwrap(), + Some(WithheldCode::Unverified), + ); + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data( + &machine, + KeyDistributionTestData::dave_id(), + KeyDistributionTestData::dave_device_id() + ) + .await, + CollectStrategy::OnlyTrustedDevices, + &own_identity_data, + &dave_identity_data, + ) + .await + .unwrap(), + Some(WithheldCode::Unverified), + ); } /// Test that [`collect_session_recipients`] returns an error if there are @@ -994,6 +1509,77 @@ mod tests { ), ]) ); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + get_device_data(&machine, DataSet::carol_id(), DataSet::carol_signed_device_id()) + .await, + get_device_data(&machine, DataSet::carol_id(), DataSet::carol_unsigned_device_id()) + .await, + ]; + + let split_result = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await; + + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice( + unverified_devices + ) + )) = split_result + ); + + // Check the list of devices in the error. + assert_eq!( + unverified_devices, + BTreeMap::from([ + (DataSet::bob_id().to_owned(), vec![DataSet::bob_device_2_id().to_owned()]), + ( + DataSet::carol_id().to_owned(), + vec![DataSet::carol_unsigned_device_id().to_owned()] + ), + ]) + ); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let carol_identity_data = get_user_identity_data(&machine, DataSet::carol_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::carol_id(), DataSet::carol_signed_device_id()) + .await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &carol_identity_data, + ) + .await + .unwrap(), + None, + ); + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice(_) + )) = withheld_code_for_device_for_share_strategy( + &get_device_data( + &machine, + DataSet::carol_id(), + DataSet::carol_unsigned_device_id() + ) + .await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &carol_identity_data, + ) + .await + ); } /// Test that we can resolve errors from @@ -1030,6 +1616,40 @@ mod tests { assert_eq!(2, share_result.devices.get(DataSet::bob_id()).unwrap().len()); assert_eq!(0, share_result.withheld_devices.len()); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + ]; + + let (shared_devices, withheld_devices) = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await + .unwrap(); + + assert_eq!(shared_devices.len(), 2); + assert_eq!(withheld_devices.len(), 0); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let bob_identity_data = get_user_identity_data(&machine, DataSet::bob_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &bob_identity_data, + ) + .await + .unwrap(), + None, + ); } /// Test that we can resolve errors from @@ -1074,6 +1694,41 @@ mod tests { withheld_list, vec![(DataSet::bob_device_2_id().to_owned(), WithheldCode::Blacklisted)] ); + + let bob_device_2 = + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await; + #[cfg(feature = "experimental-send-custom-to-device")] + { + let bob_device_1 = + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await; + let all_devices = vec![bob_device_1.clone(), bob_device_2.clone()]; + + let (shared_devices, withheld_devices) = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await + .unwrap(); + + assert_eq!(shared_devices, vec![bob_device_1.clone()]); + assert_eq!(withheld_devices, vec![(bob_device_2.clone(), WithheldCode::Blacklisted)]); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let bob_identity_data = get_user_identity_data(&machine, DataSet::bob_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &bob_device_2, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &bob_identity_data, + ) + .await + .unwrap(), + Some(WithheldCode::Blacklisted), + ); } /// Test that [`collect_session_recipients`] returns an error when @@ -1120,6 +1775,55 @@ mod tests { vec![DataSet::own_unsigned_device_id()] ),]) ); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::own_id(), &DataSet::own_signed_device_id()) + .await, + get_device_data(&machine, DataSet::own_id(), &DataSet::own_unsigned_device_id()) + .await, + ]; + + let split_result = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await; + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice( + unverified_devices + ) + )) = split_result + ); + + // Check the list of devices in the error. + assert_eq!( + unverified_devices, + BTreeMap::from([( + DataSet::own_id().to_owned(), + vec![DataSet::own_unsigned_device_id()] + ),]) + ); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let own_user_identity_data = get_user_identity_data(&machine, DataSet::own_id()).await; + + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice(_) + )) = withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::own_id(), &DataSet::own_unsigned_device_id()) + .await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &own_user_identity_data, + ) + .await + ); } /// Test that an unsigned device of an unverified user doesn't cause an @@ -1170,6 +1874,40 @@ mod tests { ) .await .unwrap(); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + ]; + + let (shared_devices, withheld_devices) = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await + .unwrap(); + + assert_eq!(shared_devices.len(), 2); + assert_eq!(withheld_devices.len(), 0); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let bob_identity_data = get_user_identity_data(&machine, DataSet::bob_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &bob_identity_data, + ) + .await + .unwrap(), + None, + ); } /// Test that an unsigned device of a signed user doesn't cause an @@ -1217,6 +1955,40 @@ mod tests { ) .await .unwrap(); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + ]; + + let (shared_devices, withheld_devices) = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await + .unwrap(); + + assert_eq!(shared_devices.len(), 2); + assert_eq!(withheld_devices.len(), 0); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let bob_identity_data = get_user_identity_data(&machine, DataSet::bob_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &bob_identity_data, + ) + .await + .unwrap(), + None, + ); } /// Test that a verified user changing their identity causes an error in @@ -1256,6 +2028,42 @@ mod tests { ); assert_eq!(violating_users, vec![DataSet::bob_id()]); + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + ]; + + let split_result = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await; + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(violating_users) + )) = split_result + ); + assert_eq!(violating_users, vec![DataSet::bob_id()]); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let bob_identity_data = get_user_identity_data(&machine, DataSet::bob_id()).await; + + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(_) + )) = withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &bob_identity_data, + ) + .await + ); + // Resolve by calling withdraw_verification bob_identity.withdraw_verification().await.unwrap(); @@ -1267,6 +2075,37 @@ mod tests { ) .await .unwrap(); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices = vec![ + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_2_id()).await, + ]; + + split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await + .unwrap(); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let bob_identity_data = get_user_identity_data(&machine, DataSet::bob_id()).await; + + assert_eq!( + withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::bob_id(), DataSet::bob_device_1_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &bob_identity_data, + ) + .await + .unwrap(), + None, + ); } /// Test that our own identity being changed causes an error in @@ -1306,6 +2145,41 @@ mod tests { ); assert_eq!(violating_users, vec![DataSet::own_id()]); + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices: Vec = + vec![get_device_data(&machine, DataSet::own_id(), machine.device_id()).await]; + + let split_result = split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await; + + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(violating_users) + )) = split_result + ); + assert_eq!(violating_users, vec![DataSet::own_id()]); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let own_user_identity_data = get_user_identity_data(&machine, DataSet::own_id()).await; + + assert_let!( + Err(OlmError::SessionRecipientCollectionError( + SessionRecipientCollectionError::VerifiedUserChangedIdentity(_) + )) = withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::own_id(), machine.device_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &own_user_identity_data, + ) + .await + ); + // Resolve by calling withdraw_verification own_identity.withdraw_verification().await.unwrap(); @@ -1317,6 +2191,32 @@ mod tests { ) .await .unwrap(); + + #[cfg(feature = "experimental-send-custom-to-device")] + { + let all_devices: Vec = + vec![get_device_data(&machine, DataSet::own_id(), machine.device_id()).await]; + + split_devices_for_share_strategy( + machine.store(), + all_devices, + CollectStrategy::ErrorOnVerifiedUserProblem, + ) + .await + .unwrap(); + } + + let own_identity_data = get_own_identity_data(&machine, DataSet::own_id()).await; + let own_user_identity_data = get_user_identity_data(&machine, DataSet::own_id()).await; + + withheld_code_for_device_for_share_strategy( + &get_device_data(&machine, DataSet::own_id(), machine.device_id()).await, + CollectStrategy::ErrorOnVerifiedUserProblem, + &own_identity_data, + &own_user_identity_data, + ) + .await + .unwrap(); } /// A set of tests for the behaviour of [`collect_session_recipients`] with diff --git a/crates/matrix-sdk-crypto/src/session_manager/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/mod.rs index 45480c9e284..c7f15b19b4e 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/mod.rs @@ -15,6 +15,10 @@ mod group_sessions; mod sessions; +#[cfg(feature = "experimental-send-custom-to-device")] +pub(crate) use group_sessions::split_devices_for_share_strategy; pub use group_sessions::CollectStrategy; -pub(crate) use group_sessions::{GroupSessionCache, GroupSessionManager}; +pub(crate) use group_sessions::{ + withheld_code_for_device_for_share_strategy, GroupSessionCache, GroupSessionManager, +}; pub(crate) use sessions::SessionManager; diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index dcebd8d42b1..dd45267b579 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -95,6 +95,13 @@ All notable changes to this project will be documented in this file. - [**breaking**] `RoomEventCacheGenericUpdate` gains a new `Clear` variant, and sees its `TimelineUpdated` variant being renamed to `UpdateTimeline`. ([#5363](https://github.com/matrix-org/matrix-rust-sdk/pull/5363/)) +- [**breaking**]: The element call widget URL configuration struct uses the new `header` url parameter + instead of the now deprecated `hideHeader` parameter. This is only compatible + with EC v0.13.0 or newer. +- [**breaking**]: The experimental `Encryption::encrypt_and_send_raw_to_device` + function now takes a `share_strategy` parameter, and will not send to devices + that do not satisfy the given share strategy. + ([#5457](https://github.com/matrix-org/matrix-rust-sdk/pull/5457/)) ### Refactor diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 2723ef6981f..9d575f2855f 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -32,6 +32,8 @@ use futures_util::{ future::try_join, stream::{self, StreamExt}, }; +#[cfg(feature = "experimental-send-custom-to-device")] +use matrix_sdk_base::crypto::CollectStrategy; use matrix_sdk_base::crypto::{ store::types::{RoomKeyBundleInfo, RoomKeyInfo}, types::requests::{ @@ -1808,6 +1810,7 @@ impl Encryption { recipient_devices: Vec<&Device>, event_type: &str, content: Raw, + share_strategy: CollectStrategy, ) -> Result> { let users = recipient_devices.iter().map(|device| device.user_id()); @@ -1826,6 +1829,7 @@ impl Encryption { &content .deserialize_as::() .expect("Deserialize as Value will always work"), + share_strategy, ) .await?; diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 36aa67c4cc0..2de55311a75 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -18,7 +18,10 @@ use std::collections::{BTreeMap, BTreeSet}; use as_variant::as_variant; -use matrix_sdk_base::deserialized_responses::{EncryptionInfo, RawAnySyncOrStrippedState}; +use matrix_sdk_base::{ + crypto::CollectStrategy, + deserialized_responses::{EncryptionInfo, RawAnySyncOrStrippedState}, +}; use ruma::{ api::client::{ account::request_openid_token::v3::{Request as OpenIdRequest, Response as OpenIdResponse}, @@ -494,6 +497,7 @@ impl MatrixDriver { recipient_devices.iter().collect(), &event_type.to_string(), Raw::from_json_string(content.to_owned())?, + CollectStrategy::AllDevices, ) .await?; diff --git a/crates/matrix-sdk/tests/integration/encryption/to_device.rs b/crates/matrix-sdk/tests/integration/encryption/to_device.rs index 46394901b11..907c4b35c6d 100644 --- a/crates/matrix-sdk/tests/integration/encryption/to_device.rs +++ b/crates/matrix-sdk/tests/integration/encryption/to_device.rs @@ -5,6 +5,7 @@ use std::{future, sync::Arc}; use assert_matches::assert_matches; use assert_matches2::assert_let; use matrix_sdk::test_utils::mocks::MatrixMockServer; +use matrix_sdk_base::crypto::CollectStrategy; use matrix_sdk_common::{ deserialized_responses::{AlgorithmInfo, EncryptionInfo}, locks::Mutex, @@ -63,7 +64,12 @@ async fn test_encrypt_and_send_to_device() { alice .encryption() - .encrypt_and_send_raw_to_device(vec![&alice_bob_device], "call.keys", content_raw) + .encrypt_and_send_raw_to_device( + vec![&alice_bob_device], + "call.keys", + content_raw, + CollectStrategy::AllDevices, + ) .await .unwrap(); } @@ -115,7 +121,12 @@ async fn test_encrypt_and_send_to_device_report_failures_server() { let result = alice .encryption() - .encrypt_and_send_raw_to_device(vec![&alice_bob_device], "call.keys", content_raw) + .encrypt_and_send_raw_to_device( + vec![&alice_bob_device], + "call.keys", + content_raw, + CollectStrategy::AllDevices, + ) .await .unwrap(); @@ -165,7 +176,12 @@ async fn test_to_device_event_handler_olm_encryption_info() { alice .encryption() - .encrypt_and_send_raw_to_device(vec![&alice_bob_device], "call.keys", content_raw) + .encrypt_and_send_raw_to_device( + vec![&alice_bob_device], + "call.keys", + content_raw, + CollectStrategy::AllDevices, + ) .await .unwrap(); @@ -247,7 +263,12 @@ async fn test_encrypt_and_send_to_device_report_failures_encryption_error() { let result = alice .encryption() - .encrypt_and_send_raw_to_device(vec![&alice_bob_device], "call.keys", content_raw) + .encrypt_and_send_raw_to_device( + vec![&alice_bob_device], + "call.keys", + content_raw, + CollectStrategy::AllDevices, + ) .await .unwrap(); diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index f5924f0e60d..36d9baa5aac 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -26,6 +26,7 @@ use matrix_sdk::{ }, Client, }; +use matrix_sdk_base::crypto::CollectStrategy; use matrix_sdk_common::{ deserialized_responses::EncryptionInfo, executor::spawn, locks::Mutex, timeout::timeout, }; @@ -625,7 +626,12 @@ async fn test_accept_encrypted_to_device_in_e2ee_room() { mock_server.mock_capture_put_to_device_then_sync_back(bob.user_id().unwrap(), &alice).await; bob.encryption() - .encrypt_and_send_raw_to_device(vec![&bob_alice_device], "my.custom.to.device", content_raw) + .encrypt_and_send_raw_to_device( + vec![&bob_alice_device], + "my.custom.to.device", + content_raw, + CollectStrategy::AllDevices, + ) .await .unwrap(); diff --git a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs index 75ce3cf219a..dd97fc47a2f 100644 --- a/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs +++ b/testing/matrix-sdk-test/src/test_json/keys_query_sets.rs @@ -605,6 +605,14 @@ impl KeyDistributionTestData { pub fn good_id() -> &'static UserId { user_id!("@good:localhost") } + + pub fn good_device_1_id() -> &'static DeviceId { + device_id!("JAXGBVZYLA") + } + + pub fn good_device_2_id() -> &'static DeviceId { + device_id!("ZGLCFWEPCY") + } } /// A set of keys query to test identity changes,