feat: Introduce EncryptionState.

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<bool>` has been replaced by `fn fn
encryption_state(&self) -> EncryptionState`. However, a new `async
fn latest_encryption_state(&self) -> Result<EncryptionState>` 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.
This commit is contained in:
Ivan Enderlin
2025-03-10 16:40:41 +01:00
parent ea8664c487
commit d03ed3063c
20 changed files with 151 additions and 85 deletions
+1 -1
View File
@@ -243,7 +243,7 @@ impl Room {
}
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
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<Arc<RoomMembersIterator>, ClientError> {
+5 -1
View File
@@ -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<EventTimelineItem> {
+3 -3
View File
@@ -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?
}
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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::{
+46 -24
View File
@@ -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<String> {
@@ -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]
+2 -2
View File
@@ -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.
@@ -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,
+6 -1
View File
@@ -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,
@@ -394,7 +394,7 @@ impl<P: RoomDataProvider, D: Decryptor> TimelineController<P, D> {
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<P: RoomDataProvider, D: Decryptor> TimelineController<P, D> {
}
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.
+5 -1
View File
@@ -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()));
+1 -1
View File
@@ -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.
+32 -15
View File
@@ -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<bool> {
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()
}
Ok(self.inner.is_encrypted())
/// 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<EncryptionState> {
self.request_encryption_state().await?;
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
+1 -1
View File
@@ -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
+13 -8
View File
@@ -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"
);
@@ -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
@@ -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]
@@ -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
@@ -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;
@@ -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
}