From e6484a6275fd8ed25783b58d00544f62c26c7568 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 10 Mar 2025 16:40:41 +0100 Subject: [PATCH 1/7] feat: Introduce `EncryptionState`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch introduces the new `EncryptionState` to represent the 3 possible states: `Encrypted`, `NotEncrypted` or `Unknown`. All the `is_encrypted` methods have been replaced by `encryption_state`. The most noticable change is in `matrix_sdk::Room` where `async fn is_encrypted(&self) -> Result` has been replaced by `fn fn encryption_state(&self) -> EncryptionState`. However, a new `async fn latest_encryption_state(&self) -> Result` method “restores” the previous behaviour by calling `request_encryption_state` if necessary. The idea is that the caller is now responsible to call `request_encryption_state` if desired, or use `latest_encryption_state` to automate the call if necessary. `encryption_state` is now non-async and infallible everywhere. `matrix-sdk-ffi` has been updated but no methods have been added for the moment. --- bindings/matrix-sdk-ffi/src/room.rs | 2 +- bindings/matrix-sdk-ffi/src/room_list.rs | 6 +- crates/matrix-sdk-base/src/client.rs | 6 +- crates/matrix-sdk-base/src/lib.rs | 6 +- crates/matrix-sdk-base/src/rooms/mod.rs | 4 +- crates/matrix-sdk-base/src/rooms/normal.rs | 70 ++++++++++++------- crates/matrix-sdk-base/src/sliding_sync.rs | 4 +- .../matrix-sdk-ui/src/notification_client.rs | 6 +- crates/matrix-sdk-ui/src/timeline/builder.rs | 7 +- .../src/timeline/controller/mod.rs | 4 +- crates/matrix-sdk/src/encryption/mod.rs | 6 +- crates/matrix-sdk/src/room/futures.rs | 2 +- crates/matrix-sdk/src/room/mod.rs | 47 +++++++++---- crates/matrix-sdk/src/send_queue/mod.rs | 2 +- crates/matrix-sdk/tests/integration/client.rs | 21 +++--- .../tests/integration/encryption/backups.rs | 4 +- .../tests/integration/room/joined.rs | 4 +- .../src/tests/nse.rs | 6 +- .../src/tests/sliding_sync/room.rs | 10 +-- .../src/tests/timeline.rs | 19 ++--- 20 files changed, 151 insertions(+), 85 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index bfe3d896d61..08af2ca45eb 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -243,7 +243,7 @@ impl Room { } pub fn is_encrypted(&self) -> Result { - Ok(RUNTIME.block_on(self.inner.is_encrypted())?) + Ok(RUNTIME.block_on(self.inner.latest_encryption_state())?.is_encrypted()) } pub async fn members(&self) -> Result, ClientError> { diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 08edd781418..7ababe61d3c 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -679,7 +679,11 @@ impl RoomListItem { /// **Note**: this info may not be reliable if you don't set up /// `m.room.encryption` as required state. async fn is_encrypted(&self) -> bool { - self.inner.is_encrypted().await.unwrap_or(false) + self.inner + .latest_encryption_state() + .await + .map(|state| state.is_encrypted()) + .unwrap_or(false) } async fn latest_event(&self) -> Option { diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 574105e8c3f..16b46202e79 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1081,9 +1081,9 @@ impl BaseClient { let mut room_info = changes.room_infos.get(&room_id).unwrap().clone(); #[cfg(feature = "e2e-encryption")] - if room_info.is_encrypted() { + if room_info.encryption_state().is_encrypted() { if let Some(o) = self.olm_machine().await.as_ref() { - if !room.is_encrypted() { + if !room.encryption_state().is_encrypted() { // The room turned on encryption in this sync, we need // to also get all the existing users and mark them for // tracking. @@ -1406,7 +1406,7 @@ impl BaseClient { } #[cfg(feature = "e2e-encryption")] - if room.is_encrypted() { + if room.encryption_state().is_encrypted() { if let Some(o) = self.olm_machine().await.as_ref() { o.update_tracked_users(user_ids.iter().map(Deref::deref)).await? } diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index cc4e5128403..f8863c53908 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -55,9 +55,9 @@ pub use http; pub use matrix_sdk_crypto as crypto; pub use once_cell; pub use rooms::{ - apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, - RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, - RoomMemberships, RoomState, RoomStateFilter, + apply_redaction, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, + RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, + RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, }; pub use store::{ ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index a128a10f17e..c476c21ae96 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -12,8 +12,8 @@ use std::{ use bitflags::bitflags; pub use members::RoomMember; pub use normal::{ - apply_redaction, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, - RoomMembersUpdate, RoomState, RoomStateFilter, + apply_redaction, EncryptionState, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, + RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter, }; use regex::Regex; use ruma::{ diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index fc4164df55d..864afcab1d6 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -426,16 +426,6 @@ impl Room { self.inner.read().sync_info != SyncInfo::NoState } - /// Check if the room has its encryption event synced. - /// - /// The encryption event can be missing when the room hasn't appeared in - /// sync yet. - /// - /// Returns true if the encryption state is synced, false otherwise. - pub fn is_encryption_state_synced(&self) -> bool { - self.inner.read().encryption_state_synced - } - /// Get the `prev_batch` token that was received from the last sync. May be /// `None` if the last sync contained the full room history. pub fn last_prev_batch(&self) -> Option { @@ -531,9 +521,9 @@ impl Room { self.inner.read().base_info.dm_targets.len() } - /// Is the room encrypted. - pub fn is_encrypted(&self) -> bool { - self.inner.read().is_encrypted() + /// Get the encryption state of this room. + pub fn encryption_state(&self) -> EncryptionState { + self.inner.read().encryption_state() } /// Get the `m.room.encryption` content that enabled end to end encryption @@ -1576,9 +1566,15 @@ impl RoomInfo { self.room_state } - /// Returns whether this is an encrypted room. - pub fn is_encrypted(&self) -> bool { - self.base_info.encryption.is_some() + /// Returns the encryption state of this room. + pub fn encryption_state(&self) -> EncryptionState { + if !self.encryption_state_synced { + EncryptionState::Unknown + } else if self.base_info.encryption.is_some() { + EncryptionState::Encrypted + } else { + EncryptionState::NotEncrypted + } } /// Set the encryption event content in this room. @@ -1596,9 +1592,7 @@ impl RoomInfo { // then we can be certain that we have synced the encryption state event, so // mark it here as synced. if let AnySyncStateEvent::RoomEncryption(_) = event { - if self.is_encrypted() { - self.mark_encryption_state_synced(); - } + self.mark_encryption_state_synced(); } ret @@ -2151,6 +2145,32 @@ fn compute_display_name_from_heroes( } } +/// Represents the state of a room encryption. +#[derive(Debug)] +pub enum EncryptionState { + /// The room is encrypted. + Encrypted, + + /// The room is not encrypted. + NotEncrypted, + + /// The state of the room encryption is unknown, probably because the + /// `/sync` did not provide all data needed to decide. + Unknown, +} + +impl EncryptionState { + /// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted]. + pub fn is_encrypted(&self) -> bool { + matches!(self, Self::Encrypted) + } + + /// Check whether `EncryptionState` is [`Unknown`][Self::Unknown]. + pub fn is_unknown(&self) -> bool { + matches!(self, Self::Unknown) + } +} + #[cfg(test)] mod tests { use std::{ @@ -2161,6 +2181,7 @@ mod tests { time::Duration, }; + use assert_matches::assert_matches; use assign::assign; use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_test::{ @@ -2197,7 +2218,10 @@ mod tests { use similar_asserts::assert_eq; use stream_assert::{assert_pending, assert_ready}; - use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo}; + use super::{ + compute_display_name_from_heroes, EncryptionState, Room, RoomHero, RoomInfo, RoomState, + SyncInfo, + }; use crate::{ latest_event::LatestEvent, rooms::RoomNotableTags, @@ -3570,8 +3594,7 @@ mod tests { fn test_encryption_is_set_when_encryption_event_is_received() { let (_store, room) = make_room_test_helper(RoomState::Joined); - assert!(room.is_encryption_state_synced().not()); - assert!(room.is_encrypted().not()); + assert_matches!(room.encryption_state(), EncryptionState::Unknown); let encryption_content = RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2); @@ -3589,8 +3612,7 @@ mod tests { )); receive_state_events(&room, vec![&encryption_event]); - assert!(room.is_encryption_state_synced()); - assert!(room.is_encrypted()); + assert_matches!(room.encryption_state(), EncryptionState::Encrypted); } #[async_test] diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 094ac845a0c..ed38ee4d56b 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -457,9 +457,9 @@ impl BaseClient { .await; #[cfg(feature = "e2e-encryption")] - if room_info.is_encrypted() { + if room_info.encryption_state().is_encrypted() { if let Some(o) = self.olm_machine().await.as_ref() { - if !room.is_encrypted() { + if !room.encryption_state().is_encrypted() { // The room turned on encryption in this sync, we need // to also get all the existing users and mark them for // tracking. diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index b7b9f46cffd..6d27f55a72d 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -745,7 +745,11 @@ impl NotificationItem { room_avatar_url: room.avatar_url().map(|s| s.to_string()), room_canonical_alias: room.canonical_alias().map(|c| c.to_string()), is_direct_message_room: room.is_direct().await?, - is_room_encrypted: room.is_encrypted().await.ok(), + is_room_encrypted: room + .latest_encryption_state() + .await + .map(|state| state.is_encrypted()) + .ok(), joined_members_count: room.joined_members_count(), is_noisy, has_mention, diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 4626e1ec351..9c424e64918 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -164,7 +164,12 @@ impl TimelineBuilder { let is_live = matches!(focus, TimelineFocus::Live); let is_pinned_events = matches!(focus, TimelineFocus::PinnedEvents { .. }); - let is_room_encrypted = room.is_encrypted().await.ok().unwrap_or_default(); + let is_room_encrypted = room + .latest_encryption_state() + .await + .map(|state| state.is_encrypted()) + .ok() + .unwrap_or_default(); let controller = TimelineController::new( room, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 184b22816ed..1abf2a94865 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -394,7 +394,7 @@ impl TimelineController { state.mark_all_events_as_encrypted(); }; - if room_info.get().is_encrypted() { + if room_info.get().encryption_state().is_encrypted() { // If the room was already encrypted, it won't toggle to unencrypted, so we can // shut down this task early. mark_encrypted().await; @@ -402,7 +402,7 @@ impl TimelineController { } while let Some(info) = room_info.next().await { - if info.is_encrypted() { + if info.encryption_state().is_encrypted() { mark_encrypted().await; // Once the room is encrypted, it cannot switch back to unencrypted, so our work // here is done. diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index b5604c6c929..005d0059ad6 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -1822,7 +1822,11 @@ mod tests { client.base_client().receive_sync_response(response).await.unwrap(); let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Room should exist"); - assert!(room.is_encrypted().await.expect("Getting encryption state")); + assert!(room + .latest_encryption_state() + .await + .expect("Getting encryption state") + .is_encrypted()); let event_id = event_id!("$1:example.org"); let reaction = ReactionEventContent::new(Annotation::new(event_id.into(), "🐈".to_owned())); diff --git a/crates/matrix-sdk/src/room/futures.rs b/crates/matrix-sdk/src/room/futures.rs index 962a9e90970..77a030fbd93 100644 --- a/crates/matrix-sdk/src/room/futures.rs +++ b/crates/matrix-sdk/src/room/futures.rs @@ -178,7 +178,7 @@ impl<'a> IntoFuture for SendRawMessageLikeEvent<'a> { trace!("Sending plaintext event to room because we don't have encryption support."); #[cfg(feature = "e2e-encryption")] - if room.is_encrypted().await? { + if room.latest_encryption_state().await?.is_encrypted() { Span::current().record("is_room_encrypted", true); // Reactions are currently famously not encrypted, skip encrypting // them until they are. diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index f3164ba2fbb..73fb55a50e8 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -46,8 +46,8 @@ use matrix_sdk_base::{ event_cache::store::media::IgnoreMediaRetentionPolicy, media::MediaThumbnailSettings, store::StateStoreExt, - ComposerDraft, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges, StateStoreDataKey, - StateStoreDataValue, + ComposerDraft, EncryptionState, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges, + StateStoreDataKey, StateStoreDataValue, }; #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] use matrix_sdk_common::BoxFuture; @@ -588,7 +588,15 @@ impl Room { .await } - async fn request_encryption_state(&self) -> Result<()> { + /// Request to update the encryption state for this room. + /// + /// It does nothing if the encryption state is already + /// [`EncryptionState::Encrypted`] or [`EncryptionState::NotEncrypted`]. + pub async fn request_encryption_state(&self) -> Result<()> { + if !self.inner.encryption_state().is_unknown() { + return Ok(()); + } + self.client .locks() .encryption_state_deduplicated_handler @@ -625,16 +633,23 @@ impl Room { .await } - /// Check whether this room is encrypted. If the room encryption state is - /// not synced yet, it will send a request to fetch it. + /// Check the encryption state of this room. /// - /// Returns true if the room is encrypted, otherwise false. - pub async fn is_encrypted(&self) -> Result { - if !self.is_encryption_state_synced() { - self.request_encryption_state().await?; - } + /// If the result is [`EncryptionState::Unknown`], one might want to call + /// [`Room::request_encryption_state`]. + pub fn encryption_state(&self) -> EncryptionState { + self.inner.encryption_state() + } + + /// Force to update the encryption state by calling + /// [`Room::request_encryption_state`], and then calling + /// [`Room::encryption_state`]. + /// + /// This method is useful to ensure the encryption state is up-to-date. + pub async fn latest_encryption_state(&self) -> Result { + self.request_encryption_state().await?; - Ok(self.inner.is_encrypted()) + Ok(self.encryption_state()) } /// Gets additional context info about the client crypto. @@ -1635,7 +1650,7 @@ impl Room { }; const SYNC_WAIT_TIME: Duration = Duration::from_secs(3); - if !self.is_encrypted().await? { + if !self.latest_encryption_state().await?.is_encrypted() { let content = RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2); self.send_state_event(content).await?; @@ -1650,7 +1665,7 @@ impl Room { // the SDK to re-request it later for confirmation, instead of // assuming it's sync'd and correct (and not encrypted). let _sync_lock = self.client.base_client().sync_lock().lock().await; - if !self.inner.is_encrypted() { + if !self.inner.encryption_state().is_encrypted() { debug!("still not marked as encrypted, marking encryption state as missing"); let mut room_info = self.clone_info(); @@ -2024,7 +2039,7 @@ impl Room { }; #[cfg(feature = "e2e-encryption")] - let (media_source, thumbnail) = if self.is_encrypted().await? { + let (media_source, thumbnail) = if self.latest_encryption_state().await?.is_encrypted() { self.client .upload_encrypted_media_and_thumbnail(content_type, &data, thumbnail, send_progress) .await? @@ -2966,7 +2981,9 @@ impl Room { if notification_mode.is_some() { notification_mode - } else if let Ok(is_encrypted) = self.is_encrypted().await { + } else if let Ok(is_encrypted) = + self.latest_encryption_state().await.map(|state| state.is_encrypted()) + { // Otherwise, if encrypted status is available, get the default mode for this // type of room. // From the point of view of notification settings, a `one-to-one` room is one diff --git a/crates/matrix-sdk/src/send_queue/mod.rs b/crates/matrix-sdk/src/send_queue/mod.rs index be839ec39a7..1caa9190ed9 100644 --- a/crates/matrix-sdk/src/send_queue/mod.rs +++ b/crates/matrix-sdk/src/send_queue/mod.rs @@ -719,7 +719,7 @@ impl RoomSendQueue { ))?; #[cfg(feature = "e2e-encryption")] - let media_source = if room.is_encrypted().await? { + let media_source = if room.latest_encryption_state().await?.is_encrypted() { trace!("upload will be encrypted (encrypted room)"); let mut cursor = std::io::Cursor::new(data); let encrypted_file = room diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index bd8f1e479d1..1faa6d74e9d 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -400,9 +400,9 @@ async fn test_subscribe_all_room_updates() { } } -// Check that the `Room::is_encrypted()` is properly deduplicated, meaning we -// only make a single request to the server, and that multiple calls do return -// the same result. +// Check that the `Room::latest_encryption_state().await?.is_encrypted()` is +// properly deduplicated, meaning we only make a single request to the server, +// and that multiple calls do return the same result. #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] #[async_test] async fn test_request_encryption_event_before_sending() { @@ -427,8 +427,8 @@ async fn test_request_encryption_event_before_sending() { "rotation_period_ms": 604800000, "rotation_period_msgs": 100 })) - // Introduce a delay so the first `is_encrypted()` doesn't finish before we make - // the second call. + // Introduce a delay so the first `latest_encryption_state()` doesn't finish before + // we make the second call. .set_delay(Duration::from_millis(50)), ) .mount(&server) @@ -436,10 +436,12 @@ async fn test_request_encryption_event_before_sending() { let first_handle = tokio::spawn({ let room = room.to_owned(); - async move { room.to_owned().is_encrypted().await } + async move { room.to_owned().latest_encryption_state().await.map(|state| state.is_encrypted()) } }); - let second_handle = tokio::spawn(async move { room.is_encrypted().await }); + let second_handle = tokio::spawn(async move { + room.latest_encryption_state().await.map(|state| state.is_encrypted()) + }); let first_encrypted = first_handle.await.unwrap().expect("We should be able to test if the room is encrypted."); @@ -690,7 +692,10 @@ async fn test_encrypt_room_event() { .await; assert!( - room.is_encrypted().await.expect("We should be able to check if the room is encrypted"), + room.latest_encryption_state() + .await + .expect("We should be able to check if the room is encrypted") + .is_encrypted(), "The room should be encrypted" ); diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index cfe2b6e9ae5..baef047c71e 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -669,7 +669,7 @@ async fn test_incremental_upload_of_keys() -> Result<()> { alice_room.enable_encryption().await?; - assert!(alice_room.is_encrypted().await?, "room should be encrypted"); + assert!(alice_room.latest_encryption_state().await?.is_encrypted(), "room should be encrypted"); // Send a message to create an outbound session that should be uploaded to // backup @@ -749,7 +749,7 @@ async fn test_incremental_upload_of_keys_sliding_sync() -> Result<()> { alice_room.enable_encryption().await?; - assert!(alice_room.is_encrypted().await?, "room should be encrypted"); + assert!(alice_room.latest_encryption_state().await?.is_encrypted(), "room should be encrypted"); // Send a message to create an outbound session that should be uploaded to // backup diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 3909ae44b7a..ddf1dac8cbc 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -820,14 +820,14 @@ async fn test_enable_encryption_doesnt_stay_unencrypted() { let room_id = room_id!("!a:b.c"); let room = mock.sync_joined_room(&client, room_id).await; - assert!(!room.is_encrypted().await.unwrap()); + assert!(!room.latest_encryption_state().await.unwrap().is_encrypted()); room.enable_encryption().await.expect("enabling encryption should work"); mock.verify_and_reset().await; mock.mock_room_state_encryption().encrypted().mount().await; - assert!(room.is_encrypted().await.unwrap()); + assert!(room.latest_encryption_state().await.unwrap().is_encrypted()); } #[async_test] diff --git a/testing/matrix-sdk-integration-testing/src/tests/nse.rs b/testing/matrix-sdk-integration-testing/src/tests/nse.rs index 6aa24ff08a5..55b3aa966c1 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/nse.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/nse.rs @@ -217,7 +217,8 @@ impl ClientWrapper { /// encrypted. async fn enable_encryption(&self, room: &Room, rotation_period_msgs: usize) { // Adapted from crates/matrix-sdk/src/room/mod.rs enable_encryption - if !room.is_encrypted().await.expect("Failed to check encrypted") { + if !room.latest_encryption_state().await.expect("Failed to check encrypted").is_encrypted() + { let content: RoomEncryptionEventContent = serde_json::from_value(json!({ "algorithm": EventEncryptionAlgorithm::MegolmV1AesSha2, "rotation_period_msgs": rotation_period_msgs, @@ -256,9 +257,10 @@ impl ClientWrapper { async fn room_is_encrypted(&self, room_id: &RoomId) -> bool { self.wait_until_room_exists(room_id) .await - .is_encrypted() + .latest_encryption_state() .await .expect("Failed to check encrypted") + .is_encrypted() } /// Wait (syncing if needed) until the room with supplied ID exists, or time diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 9df02816d36..0033edbddd9 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -770,10 +770,10 @@ async fn test_delayed_decryption_latest_event() -> Result<()> { bob_room.join().await.unwrap(); assert_eq!(alice_room.state(), RoomState::Joined); - assert!(alice_room.is_encrypted().await.unwrap()); + assert!(alice_room.latest_encryption_state().await.unwrap().is_encrypted()); assert_eq!(bob_room.state(), RoomState::Joined); - assert!(bob_room.is_encrypted().await.unwrap()); + assert!(bob_room.latest_encryption_state().await.unwrap().is_encrypted()); // Get the room list of Alice. let alice_all_rooms = alice_sync_service.room_list_service().all_rooms().await.unwrap(); @@ -899,9 +899,9 @@ async fn test_delayed_invite_response_and_sent_message_decryption() { bob_room.join().await.unwrap(); assert_eq!(alice_room.state(), RoomState::Joined); - assert!(alice_room.is_encrypted().await.unwrap()); + assert!(alice_room.latest_encryption_state().await.unwrap().is_encrypted()); assert_eq!(bob_room.state(), RoomState::Joined); - assert!(bob_room.is_encrypted().await.unwrap()); + assert!(bob_room.latest_encryption_state().await.unwrap().is_encrypted()); // Get previous events, including the sent messages. bob_timeline.paginate_backwards(3).await.unwrap(); @@ -987,7 +987,7 @@ async fn test_room_info_notable_update_deduplication() -> Result<()> { let alice_room = wait_for_room(&alice, alice_room.room_id()).await; assert_eq!(alice_room.state(), RoomState::Joined); - assert!(alice_room.is_encrypted().await.unwrap()); + assert!(alice_room.latest_encryption_state().await.unwrap().is_encrypted()); // Bob sees and joins the room. let bob_room = wait_for_room(&bob, alice_room.room_id()).await; diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 556d779b095..380a6e972c8 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -414,9 +414,10 @@ async fn test_enabling_backups_retries_decryption() { .unwrap(); assert!(room - .is_encrypted() + .latest_encryption_state() .await - .expect("We should be able to check that the room is encrypted")); + .expect("We should be able to check that the room is encrypted") + .is_encrypted()); let event_id = room .send(RoomMessageEventContent::text_plain("It's a secret to everybody!")) @@ -549,9 +550,10 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { .unwrap(); assert!(alice_room - .is_encrypted() + .latest_encryption_state() .await - .expect("We should be able to check that the room is encrypted")); + .expect("We should be able to check that the room is encrypted") + .is_encrypted()); // Create stream listening for devices. let devices_stream = alice @@ -601,7 +603,7 @@ async fn test_room_keys_received_on_notification_client_trigger_redecryption() { debug!("Bob joined the room"); assert_eq!(bob_room.state(), RoomState::Joined); - assert!(bob_room.is_encrypted().await.unwrap()); + assert!(bob_room.latest_encryption_state().await.unwrap().is_encrypted()); // Now we need to wait for Bob's device to turn up. let wait_for_bob_device = async { @@ -766,7 +768,7 @@ async fn test_new_users_first_messages_dont_warn_about_insecure_device_if_it_is_ room.join().await.expect("should be able to join the room"); assert_eq!(room.state(), RoomState::Joined); - assert!(room.is_encrypted().await.unwrap()); + assert!(room.latest_encryption_state().await.unwrap().is_encrypted()); sync_service.stop().await; @@ -796,9 +798,10 @@ async fn test_new_users_first_messages_dont_warn_about_insecure_device_if_it_is_ .expect("should not fail to create room"); assert!(room - .is_encrypted() + .latest_encryption_state() .await - .expect("should be able to check that the room is encrypted")); + .expect("should be able to check that the room is encrypted") + .is_encrypted()); room } From 77d638b996d9738019917905377e1b92b5450020 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 10 Mar 2025 16:52:30 +0100 Subject: [PATCH 2/7] test(base): Test `EncryptionState::NotEncrypted`. --- crates/matrix-sdk-base/src/rooms/normal.rs | 16 +++++++++++++++- crates/matrix-sdk/src/test_utils/mocks/mod.rs | 6 +++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 864afcab1d6..7240a29aba5 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -3591,7 +3591,7 @@ mod tests { } #[test] - fn test_encryption_is_set_when_encryption_event_is_received() { + fn test_encryption_is_set_when_encryption_event_is_received_encrypted() { let (_store, room) = make_room_test_helper(RoomState::Joined); assert_matches!(room.encryption_state(), EncryptionState::Unknown); @@ -3615,6 +3615,20 @@ mod tests { assert_matches!(room.encryption_state(), EncryptionState::Encrypted); } + #[test] + fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() { + let (_store, room) = make_room_test_helper(RoomState::Joined); + + assert_matches!(room.encryption_state(), EncryptionState::Unknown); + room.inner.update_if(|info| { + info.mark_encryption_state_synced(); + + false + }); + + assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted); + } + #[async_test] async fn test_room_info_migration_v1() { let store = MemoryStore::new().into_state_store(); diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 9d0683b9cd3..1f3ef480fed 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -408,7 +408,7 @@ impl MatrixMockServer { /// .await; /// /// assert!( - /// room.is_encrypted().await?, + /// room.latest_encryption_state().await?.is_encrypted(), /// "The room should be marked as encrypted." /// ); /// # anyhow::Ok(()) }); @@ -1898,7 +1898,7 @@ impl<'a> MockEndpoint<'a, EncryptionStateEndpoint> { /// .await; /// /// assert!( - /// room.is_encrypted().await?, + /// room.latest_encryption_state().await?.is_encrypted(), /// "The room should be marked as encrypted." /// ); /// # anyhow::Ok(()) }); @@ -1927,7 +1927,7 @@ impl<'a> MockEndpoint<'a, EncryptionStateEndpoint> { /// .await; /// /// assert!( - /// !room.is_encrypted().await?, + /// !room.latest_encryption_state().await?.is_encrypted(), /// "The room should not be marked as encrypted." /// ); /// # anyhow::Ok(()) }); From b3b9c8118df74bc3b2c6aea34466f3d7d206d126 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 10 Mar 2025 16:54:38 +0100 Subject: [PATCH 3/7] test(base): Test `EncryptionState` helpers. --- crates/matrix-sdk-base/src/rooms/normal.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 7240a29aba5..a78bc2c01d1 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -3629,6 +3629,17 @@ mod tests { assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted); } + #[test] + fn test_encryption_state() { + assert!(EncryptionState::Unknown.is_unknown()); + assert!(EncryptionState::Encrypted.is_unknown().not()); + assert!(EncryptionState::NotEncrypted.is_unknown().not()); + + assert!(EncryptionState::Unknown.is_encrypted().not()); + assert!(EncryptionState::Encrypted.is_encrypted()); + assert!(EncryptionState::NotEncrypted.is_encrypted().not()); + } + #[async_test] async fn test_room_info_migration_v1() { let store = MemoryStore::new().into_state_store(); From d185534750e09b073d8db7d96b50b2813b683782 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 10 Mar 2025 17:01:12 +0100 Subject: [PATCH 4/7] test(sdk): Test `encryption_state()` vs `latest_encryption_state()`. --- .../tests/integration/room/joined.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index ddf1dac8cbc..6a31001a1a3 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -4,6 +4,7 @@ use std::{ time::Duration, }; +use assert_matches::assert_matches; use assert_matches2::assert_let; use futures_util::{future::join_all, pin_mut}; use matrix_sdk::{ @@ -12,7 +13,7 @@ use matrix_sdk::{ room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, test_utils::mocks::MatrixMockServer, }; -use matrix_sdk_base::{RoomMembersUpdate, RoomState}; +use matrix_sdk_base::{EncryptionState, RoomMembersUpdate, RoomState}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, @@ -810,24 +811,29 @@ async fn test_make_reply_event_doesnt_require_event_cache() { } #[async_test] -async fn test_enable_encryption_doesnt_stay_unencrypted() { +async fn test_enable_encryption_doesnt_stay_unknown() { let mock = MatrixMockServer::new().await; let client = mock.client_builder().build().await; - mock.mock_room_state_encryption().plain().mount().await; - mock.mock_set_room_state_encryption().ok(event_id!("$1")).mount().await; - let room_id = room_id!("!a:b.c"); let room = mock.sync_joined_room(&client, room_id).await; - assert!(!room.latest_encryption_state().await.unwrap().is_encrypted()); + assert_matches!(room.encryption_state(), EncryptionState::Unknown); + + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_set_room_state_encryption().ok(event_id!("$1")).mount().await; + + assert_matches!(room.latest_encryption_state().await.unwrap(), EncryptionState::NotEncrypted); room.enable_encryption().await.expect("enabling encryption should work"); mock.verify_and_reset().await; mock.mock_room_state_encryption().encrypted().mount().await; + assert_matches!(room.encryption_state(), EncryptionState::Unknown); + assert!(room.latest_encryption_state().await.unwrap().is_encrypted()); + assert_matches!(room.encryption_state(), EncryptionState::Encrypted); } #[async_test] From f8edc0a30b2be2cad132275a76e44f3ddc156bf6 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 10 Mar 2025 17:21:26 +0100 Subject: [PATCH 5/7] doc: Update the `CHANGELOG.md`s. --- crates/matrix-sdk-base/CHANGELOG.md | 5 +++++ crates/matrix-sdk/CHANGELOG.md | 25 +++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index ab80617133c..6a6e35bc1e0 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -18,6 +18,11 @@ All notable changes to this project will be documented in this file. - `BaseClient` now has a `handle_verification_events` field which is `true` by default and can be negated so the `NotificationClient` won't handle received verification events too, causing errors in the `VerificationMachine`. +- [**breaking**] `Room::is_encryption_state_synced` has been removed + ([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)) +- [**breaking**] `Room::is_encrypted` is replaced by `Room::encryption_state` + which returns a value of the new `EncryptionState` enum + ([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)) ## [0.10.0] - 2025-02-04 diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index f78d195bf96..8cdc24f49ec 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -21,7 +21,6 @@ simpler methods: this URI is not desirable, the `Oidc::fetch_account_management_url` method can be used. ([#4663](https://github.com/matrix-org/matrix-rust-sdk/pull/4663)) - - The `MediaRetentionPolicy` can now trigger regular cleanups with its new `cleanup_frequency` setting. ([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603)) @@ -29,9 +28,27 @@ simpler methods: [BCP 195](https://datatracker.ietf.org/doc/bcp195/). ([#4647](https://github.com/matrix-org/matrix-rust-sdk/pull/4647)) - Add `Room::report_room` api. ([#4713](https://github.com/matrix-org/matrix-rust-sdk/pull/4713)) -- `Client::notification_client` will create a copy of the existing `Client`, but now it'll make sure - it doesn't handle any verification events to avoid an issue with these events being received and - processed twice if `NotificationProcessSetup` was `SingleSetup`. +- `Client::notification_client` will create a copy of the existing `Client`, + but now it'll make sure it doesn't handle any verification events to + avoid an issue with these events being received and processed twice if + `NotificationProcessSetup` was `SingleSetup`. +- [**breaking**] `Room::is_encrypted` is replaced by + `Room::latest_encryption_state` which returns a value of the new + `EncryptionState` enum; another `Room::encryption_state` non-async and + infallible method is added to get the `EncryptionState` without calling + `Room::request_encryption_state`. This latter method is also now public. + ([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)). One can + safely replace: + + ```rust + room.is_encrypted().await? + ``` + + by + + ```rust + room.latest_encryption_state().await?.is_encrypted() + ``` ### Bug Fixes From 90ff3137c467f59ca95ba462a10e51396527f9bd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 11 Mar 2025 10:59:47 +0100 Subject: [PATCH 6/7] feat(ffi): Replace `Room::is_encrypted` by `encryption_state` and `latest_encryption_state`. --- bindings/matrix-sdk-ffi/src/room.rs | 14 +++++++++++--- crates/matrix-sdk-base/src/rooms/normal.rs | 1 + crates/matrix-sdk/src/lib.rs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 08af2ca45eb..4bb2a54c9c5 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -7,7 +7,7 @@ use matrix_sdk::{ room::{ edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole, }, - ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, + ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState, RoomHero as SdkRoomHero, RoomMemberships, RoomState, }; use matrix_sdk_ui::timeline::{default_event_filter, RoomExt}; @@ -242,8 +242,16 @@ impl Room { self.inner.room_id().to_string() } - pub fn is_encrypted(&self) -> Result { - Ok(RUNTIME.block_on(self.inner.latest_encryption_state())?.is_encrypted()) + pub fn encryption_state(&self) -> EncryptionState { + self.inner.encryption_state() + } + + pub async fn latest_encryption_state(&self) -> Result { + Ok(self.inner.latest_encryption_state().await?) + } + + pub async fn is_encrypted(&self) -> Result { + Ok(self.latest_encryption_state().await?.is_encrypted()) } pub async fn members(&self) -> Result, ClientError> { diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index a78bc2c01d1..7542b6413eb 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -2147,6 +2147,7 @@ fn compute_display_name_from_heroes( /// Represents the state of a room encryption. #[derive(Debug)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum EncryptionState { /// The room is encrypted. Encrypted, diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index ca15aa30206..791fd91d5b7 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -25,7 +25,7 @@ pub use matrix_sdk_base::crypto; pub use matrix_sdk_base::{ deserialized_responses, store::{self, DynStateStore, MemoryStore, StateStoreExt}, - ComposerDraft, ComposerDraftType, QueueWedgeError, Room as BaseRoom, + ComposerDraft, ComposerDraftType, EncryptionState, QueueWedgeError, Room as BaseRoom, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomMember as BaseRoomMember, RoomMemberships, RoomState, SessionMeta, StateChanges, StateStore, StoreError, From 3c6ae5aa94646f526f025d64e3d478bf4bc54c2b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 11 Mar 2025 11:05:30 +0100 Subject: [PATCH 7/7] doc(ffi): Update the `CHANGELOG.md`. --- bindings/matrix-sdk-ffi/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 4f2d2f570df..8531a73a84b 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -29,9 +29,28 @@ Breaking changes: - There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback ( or fails to present). - The rest of `AuthenticationError` is now found in the OidcError type. + - `OidcAuthenticationData` is now called `OidcAuthorizationData`. + - The `get_element_call_required_permissions` function now requires the device_id. +- `Room::is_encrypted` is replaced by `Room::latest_encryption_state` + which returns a value of the new `EncryptionState` enum; another + `Room::encryption_state` non-async and infallible method is added to get the + `EncryptionState` without running a network request. + ([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)). One can + safely replace: + + ```rust + room.is_encrypted().await? + ``` + + by + + ```rust + room.latest_encryption_state().await?.is_encrypted() + ``` + Additions: - Add `Encryption::get_user_identity` which returns `UserIdentity`