diff --git a/bindings/matrix-sdk-ffi/src/authentication_service.rs b/bindings/matrix-sdk-ffi/src/authentication_service.rs index bd057733a..a9f1de9e9 100644 --- a/bindings/matrix-sdk-ffi/src/authentication_service.rs +++ b/bindings/matrix-sdk-ffi/src/authentication_service.rs @@ -4,6 +4,7 @@ use std::{ }; use matrix_sdk::{ + encryption::BackupDownloadStrategy, oidc::{ registrations::{ClientId, OidcRegistrations, OidcRegistrationsError}, types::{ @@ -621,12 +622,9 @@ impl AuthenticationService { .passphrase(self.passphrase.clone()) .homeserver_url(homeserver_url) .sliding_sync_proxy(sliding_sync_proxy) - .with_encryption_settings(matrix_sdk::encryption::EncryptionSettings { - auto_enable_cross_signing: true, - backup_download_strategy: - matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure, - auto_enable_backups: true, - }) + .auto_enable_cross_signing(true) + .backup_download_strategy(BackupDownloadStrategy::AfterDecryptionFailure) + .auto_enable_backups(true) .username(user_id.to_string()); if let Some(proxy) = &self.proxy { diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 937d7851e..09865b68b 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -76,6 +76,7 @@ pub struct ClientBuilder { cross_process_refresh_lock_id: Option, session_delegate: Option>, additional_root_certificates: Vec>, + encryption_settings: EncryptionSettings, } #[uniffi::export(async_runtime = "tokio")] @@ -93,14 +94,16 @@ impl ClientBuilder { proxy: None, disable_ssl_verification: false, disable_automatic_token_refresh: false, - inner: MatrixClient::builder().with_encryption_settings(EncryptionSettings { - auto_enable_cross_signing: false, - backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure, - auto_enable_backups: false, - }), + inner: MatrixClient::builder(), cross_process_refresh_lock_id: None, session_delegate: None, additional_root_certificates: Default::default(), + encryption_settings: EncryptionSettings { + auto_enable_cross_signing: false, + backup_download_strategy: + matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure, + auto_enable_backups: false, + }, }) } @@ -203,21 +206,41 @@ impl ClientBuilder { Arc::new(builder) } + pub fn auto_enable_cross_signing( + self: Arc, + auto_enable_cross_signing: bool, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing; + Arc::new(builder) + } + + /// Select a strategy to download room keys from the backup. By default + /// we download after a decryption failure. + /// + /// Take a look at the [`BackupDownloadStrategy`] enum for more options. + pub fn backup_download_strategy( + self: Arc, + backup_download_strategy: BackupDownloadStrategy, + ) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.encryption_settings.backup_download_strategy = backup_download_strategy; + Arc::new(builder) + } + + /// Automatically create a backup version if no backup exists. + pub fn auto_enable_backups(self: Arc, auto_enable_backups: bool) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.encryption_settings.auto_enable_backups = auto_enable_backups; + Arc::new(builder) + } + pub async fn build(self: Arc) -> Result, ClientBuildError> { Ok(Arc::new(self.build_inner().await?)) } } impl ClientBuilder { - pub(crate) fn with_encryption_settings( - self: Arc, - settings: EncryptionSettings, - ) -> Arc { - let mut builder = unwrap_or_clone_arc(self); - builder.inner = builder.inner.with_encryption_settings(settings); - Arc::new(builder) - } - pub(crate) fn enable_cross_process_refresh_lock_inner( self: Arc, process_id: String, @@ -316,6 +339,8 @@ impl ClientBuilder { ); } + inner_builder = inner_builder.with_encryption_settings(builder.encryption_settings); + let sdk_client = inner_builder.build().await?; // At this point, `sdk_client` might contain a `sliding_sync_proxy` that has diff --git a/bindings/matrix-sdk-ffi/src/sync_service.rs b/bindings/matrix-sdk-ffi/src/sync_service.rs index 4b0c05489..0afff7cbf 100644 --- a/bindings/matrix-sdk-ffi/src/sync_service.rs +++ b/bindings/matrix-sdk-ffi/src/sync_service.rs @@ -15,7 +15,7 @@ use std::{fmt::Debug, sync::Arc, time::Duration}; use futures_util::pin_mut; -use matrix_sdk::Client; +use matrix_sdk::{crypto::types::events::UtdCause, Client}; use matrix_sdk_ui::{ sync_service::{ State as MatrixSyncServiceState, SyncService as MatrixSyncService, @@ -187,6 +187,10 @@ pub struct UnableToDecryptInfo { /// /// If set, this is in milliseconds. pub time_to_decrypt_ms: Option, + + /// What we know about what caused this UTD. E.g. was this event sent when + /// we were not a member of this room? + pub cause: UtdCause, } impl From for UnableToDecryptInfo { @@ -194,6 +198,7 @@ impl From for UnableToDecryptInfo { Self { event_id: value.event_id.to_string(), time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64), + cause: value.cause, } } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index de9302b7e..3c41525ae 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -14,7 +14,7 @@ use std::{collections::HashMap, sync::Arc}; -use matrix_sdk::room::power_levels::power_level_user_changes; +use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes}; use matrix_sdk_ui::timeline::{PollResult, TimelineDetails}; use tracing::warn; @@ -214,6 +214,10 @@ pub enum EncryptedMessage { MegolmV1AesSha2 { /// The ID of the session used to encrypt the message. session_id: String, + + /// What we know about what caused this UTD. E.g. was this event sent + /// when we were not a member of this room? + cause: UtdCause, }, Unknown, } @@ -227,9 +231,9 @@ impl EncryptedMessage { let sender_key = sender_key.clone(); Self::OlmV1Curve25519AesSha2 { sender_key } } - Message::MegolmV1AesSha2 { session_id, .. } => { + Message::MegolmV1AesSha2 { session_id, cause, .. } => { let session_id = session_id.clone(); - Self::MegolmV1AesSha2 { session_id } + Self::MegolmV1AesSha2 { session_id, cause: *cause } } Message::Unknown => Self::Unknown, } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 62372acc1..74006625f 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -186,7 +186,7 @@ impl Timeline { /// Paginate forwards, when in focused mode. /// /// Returns whether we hit the end of the timeline or not. - pub async fn paginate_forwards(&self, num_events: u16) -> Result { + pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result { Ok(self.inner.focused_paginate_forwards(num_events).await?) } diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 471d1ceb0..a662e964a 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -23,7 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"] automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"] message-ids = ["matrix-sdk-crypto?/message-ids"] experimental-sliding-sync = ["ruma/unstable-msc3575"] -uniffi = ["dep:uniffi"] +uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi"] # helpers for testing features build upon this testing = [ diff --git a/crates/matrix-sdk-crypto/src/types/events/mod.rs b/crates/matrix-sdk-crypto/src/types/events/mod.rs index 7c8f52d64..1ebc72398 100644 --- a/crates/matrix-sdk-crypto/src/types/events/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/events/mod.rs @@ -27,9 +27,11 @@ pub mod room_key_request; pub mod room_key_withheld; pub mod secret_send; mod to_device; +mod utd_cause; use ruma::serde::Raw; pub use to_device::{ToDeviceCustomEvent, ToDeviceEvent, ToDeviceEvents}; +pub use utd_cause::UtdCause; /// A trait for event contents to define their event type. pub trait EventType { diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs new file mode 100644 index 000000000..1bd99fde2 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -0,0 +1,153 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use ruma::{events::AnySyncTimelineEvent, serde::Raw}; +use serde::Deserialize; + +/// Our best guess at the reason why an event can't be decrypted. +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +pub enum UtdCause { + /// We don't have an explanation for why this UTD happened - it is probably + /// a bug, or a network split between the two homeservers. + #[default] + Unknown = 0, + + /// This event was sent when we were not a member of the room (or invited), + /// so it is impossible to decrypt (without MSC3061). + Membership = 1, + // + // TODO: Other causes for UTDs. For example, this message is device-historical, information + // extracted from the WithheldCode in the MissingRoomKey object, or various types of Olm + // session problems. + // + // Note: This needs to be a simple enum so we can export it via FFI, so if more information + // needs to be provided, it should be through a separate type. +} + +/// MSC4115 membership info in the unsigned area. +#[derive(Deserialize)] +struct UnsignedWithMembership { + #[serde(alias = "io.element.msc4115.membership")] + membership: Membership, +} + +/// MSC4115 contents of the membership property +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +enum Membership { + Leave, + Invite, + Join, +} + +impl UtdCause { + /// Decide the cause of this UTD, based on the evidence we have. + pub fn determine(raw_event: Option<&Raw>) -> Self { + // TODO: in future, use more information to give a richer answer. E.g. + // is this event device-historical? Was the Olm communication disrupted? + // Did the sender refuse to send the key because we're not verified? + + // Look in the unsigned area for a `membership` field. + if let Some(raw_event) = raw_event { + if let Ok(Some(unsigned)) = raw_event.get_field::("unsigned") { + if let Membership::Leave = unsigned.membership { + // We were not a member - this is the cause of the UTD + return UtdCause::Membership; + } + } + } + + // We can't find an explanation for this UTD + UtdCause::Unknown + } +} + +#[cfg(test)] +mod tests { + use ruma::{events::AnySyncTimelineEvent, serde::Raw}; + use serde_json::{json, value::to_raw_value}; + + use crate::types::events::UtdCause; + + #[test] + fn a_missing_raw_event_means_we_guess_unknown() { + // When we don't provide any JSON to check for membership, then we guess the UTD + // is unknown. + assert_eq!(UtdCause::determine(None), UtdCause::Unknown); + } + + #[test] + fn if_there_is_no_membership_info_we_guess_unknown() { + // If our JSON contains no membership info, then we guess the UTD is unknown. + assert_eq!(UtdCause::determine(Some(&raw_event(json!({})))), UtdCause::Unknown); + } + + #[test] + fn if_membership_info_cant_be_parsed_we_guess_unknown() { + // If our JSON contains a membership property but not the JSON we expected, then + // we guess the UTD is unknown. + assert_eq!( + UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": 3 } })))), + UtdCause::Unknown + ); + } + + #[test] + fn if_membership_is_invite_we_guess_unknown() { + // If membership=invite then we expected to be sent the keys so the cause of the + // UTD is unknown. + assert_eq!( + UtdCause::determine(Some(&raw_event( + json!({ "unsigned": { "membership": "invite" } }), + ))), + UtdCause::Unknown + ); + } + + #[test] + fn if_membership_is_join_we_guess_unknown() { + // If membership=join then we expected to be sent the keys so the cause of the + // UTD is unknown. + assert_eq!( + UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "join" } })))), + UtdCause::Unknown + ); + } + + #[test] + fn if_membership_is_leave_we_guess_membership() { + // If membership=leave then we have an explanation for why we can't decrypt, + // until we have MSC3061. + assert_eq!( + UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "leave" } })))), + UtdCause::Membership + ); + } + + #[test] + fn if_unstable_prefix_membership_is_leave_we_guess_membership() { + // Before MSC4115 is merged, we support the unstable prefix too. + assert_eq!( + UtdCause::determine(Some(&raw_event( + json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) + ))), + UtdCause::Membership + ); + } + + fn raw_event(value: serde_json::Value) -> Raw { + Raw::from_json(to_raw_value(&value).unwrap()) + } +} diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index 6cbe12d96..2c6b461ac 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -18,7 +18,7 @@ experimental-room-list-with-unified-invites = [] native-tls = ["matrix-sdk/native-tls"] rustls-tls = ["matrix-sdk/rustls-tls"] -uniffi = ["dep:uniffi"] +uniffi = ["dep:uniffi", "matrix-sdk/uniffi", "matrix-sdk-base/uniffi"] [dependencies] as_variant = { workspace = true } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 4f06b437e..dc2c45b1c 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use as_variant::as_variant; use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry}; use indexmap::{map::Entry, IndexMap}; -use matrix_sdk::deserialized_responses::EncryptionInfo; +use matrix_sdk::{crypto::types::events::UtdCause, deserialized_responses::EncryptionInfo}; use ruma::{ events::{ poll::{ @@ -276,11 +276,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { /// Handle an event. /// /// Returns the number of timeline updates that were made. + /// + /// `raw_event` is only needed to determine the cause of any UTDs, + /// so if we know this is not a UTD it can be None. #[instrument(skip_all, fields(txn_id, event_id, position))] pub(super) fn handle_event( mut self, day_divider_adjuster: &mut DayDividerAdjuster, event_kind: TimelineEventKind, + raw_event: Option<&Raw>, ) -> HandleEventResult { let span = tracing::Span::current(); @@ -348,13 +352,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } AnyMessageLikeEventContent::RoomEncrypted(c) => { // TODO: Handle replacements if the replaced event is also UTD - self.add(should_add, TimelineItemContent::unable_to_decrypt(c)); + let cause = UtdCause::determine(raw_event); + self.add(true, TimelineItemContent::unable_to_decrypt(c, cause)); // Let the hook know that we ran into an unable-to-decrypt that is added to the // timeline. if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() { if let Flow::Remote { event_id, .. } = &self.ctx.flow { - hook.on_utd(event_id); + hook.on_utd(event_id, cause); } } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 06e655209..6b147f3fb 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -16,6 +16,7 @@ use std::sync::Arc; use as_variant::as_variant; use imbl::Vector; +use matrix_sdk::crypto::types::events::UtdCause; use matrix_sdk_base::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; use ruma::{ events::{ @@ -248,8 +249,8 @@ impl TimelineItemContent { } } - pub(crate) fn unable_to_decrypt(content: RoomEncryptedEventContent) -> Self { - Self::UnableToDecrypt(content.into()) + pub(crate) fn unable_to_decrypt(content: RoomEncryptedEventContent, cause: UtdCause) -> Self { + Self::UnableToDecrypt(EncryptedMessage::from_content(content, cause)) } pub(crate) fn room_member( @@ -356,21 +357,26 @@ pub enum EncryptedMessage { /// The ID of the session used to encrypt the message. session_id: String, + + /// What we know about what caused this UTD. E.g. was this event sent + /// when we were not a member of this room? + cause: UtdCause, }, /// No metadata because the event uses an unknown algorithm. Unknown, } -impl From for EncryptedMessage { - fn from(c: RoomEncryptedEventContent) -> Self { - match c.scheme { +impl EncryptedMessage { + fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self { + match content.scheme { EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => { Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key } } #[allow(deprecated)] EncryptedEventScheme::MegolmV1AesSha2(s) => { let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s; - Self::MegolmV1AesSha2 { sender_key, device_id, session_id } + + Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause } } _ => Self::Unknown, } diff --git a/crates/matrix-sdk-ui/src/timeline/inner/mod.rs b/crates/matrix-sdk-ui/src/timeline/inner/mod.rs index 4f4be2b81..a61ada30c 100644 --- a/crates/matrix-sdk-ui/src/timeline/inner/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/inner/mod.rs @@ -875,6 +875,8 @@ impl TimelineInner

{ decryptor: impl Decryptor, session_ids: Option>, ) { + use matrix_sdk::crypto::types::events::UtdCause; + use super::EncryptedMessage; let mut state = self.state.clone().write_owned().await; @@ -953,9 +955,11 @@ impl TimelineInner

{ "Successfully decrypted event that previously failed to decrypt" ); + let cause = UtdCause::determine(Some(original_json)); + // Notify observers that we managed to eventually decrypt an event. if let Some(hook) = unable_to_decrypt_hook { - hook.on_late_decrypt(&remote_event.event_id); + hook.on_late_decrypt(&remote_event.event_id, cause); } Some(event) diff --git a/crates/matrix-sdk-ui/src/timeline/inner/state.rs b/crates/matrix-sdk-ui/src/timeline/inner/state.rs index fb95d0452..1cdb91d49 100644 --- a/crates/matrix-sdk-ui/src/timeline/inner/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/inner/state.rs @@ -191,7 +191,13 @@ impl TimelineInnerState { let mut day_divider_adjuster = DayDividerAdjuster::default(); - TimelineEventHandler::new(&mut txn, ctx).handle_event(&mut day_divider_adjuster, content); + TimelineEventHandler::new(&mut txn, ctx).handle_event( + &mut day_divider_adjuster, + content, + // Local events are never UTD, so no need to pass in a raw_event - this is only used to + // determine the type of UTD if there is one. + None, + ); txn.adjust_day_dividers(day_divider_adjuster); @@ -564,14 +570,18 @@ impl TimelineInnerStateTransaction<'_> { is_highlighted: event.push_actions.iter().any(Action::is_highlight), flow: Flow::Remote { event_id: event_id.clone(), - raw_event: raw, + raw_event: raw.clone(), txn_id, position, should_add, }, }; - TimelineEventHandler::new(self, ctx).handle_event(day_divider_adjuster, event_kind) + TimelineEventHandler::new(self, ctx).handle_event( + day_divider_adjuster, + event_kind, + Some(&raw), + ) } fn clear(&mut self) { diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 21f2653be..baa34a323 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -23,16 +23,22 @@ use std::{ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; -use matrix_sdk::crypto::{decrypt_room_key_export, OlmMachine}; +use matrix_sdk::crypto::{decrypt_room_key_export, types::events::UtdCause, OlmMachine}; use matrix_sdk_test::{async_test, BOB}; use ruma::{ assign, - events::room::encrypted::{ - EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, - RoomEncryptedEventContent, + events::{ + room::encrypted::{ + EncryptedEventScheme, MegolmV1AesSha2ContentInit, Relation, Replacement, + RoomEncryptedEventContent, + }, + AnySyncTimelineEvent, }, - room_id, user_id, + room_id, + serde::Raw, + user_id, }; +use serde_json::{json, value::to_raw_value}; use stream_assert::assert_next_matches; use super::TestTimeline; @@ -455,3 +461,104 @@ async fn test_retry_message_decryption_highlighted() { assert_eq!(message.body(), "A secret to everybody but Alice"); assert!(event.is_highlighted()); } + +#[async_test] +async fn test_utd_cause_for_nonmember_event_is_found() { + // Given a timline + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe().await; + + // When we add an event with "membership: leave" + timeline.handle_live_event(raw_event_with_unsigned(json!({ "membership": "leave" }))).await; + + // Then its UTD cause is membership + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let event = item.as_event().unwrap(); + assert_let!( + TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { cause, .. }) = + event.content() + ); + assert_eq!(*cause, UtdCause::Membership); +} + +#[async_test] +async fn test_utd_cause_for_nonmember_event_is_found_unstable_prefix() { + // Given a timline + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe().await; + + // When we add an event with "io.element.msc4115.membership: leave" + timeline + .handle_live_event(raw_event_with_unsigned( + json!({ "io.element.msc4115.membership": "leave" }), + )) + .await; + + // Then its UTD cause is membership + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let event = item.as_event().unwrap(); + assert_let!( + TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { cause, .. }) = + event.content() + ); + assert_eq!(*cause, UtdCause::Membership); +} + +#[async_test] +async fn test_utd_cause_for_member_event_is_unknown() { + // Given a timline + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe().await; + + // When we add an event with "membership: join" + timeline.handle_live_event(raw_event_with_unsigned(json!({ "membership": "join" }))).await; + + // Then its UTD cause is membership + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let event = item.as_event().unwrap(); + assert_let!( + TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { cause, .. }) = + event.content() + ); + assert_eq!(*cause, UtdCause::Unknown); +} + +#[async_test] +async fn test_utd_cause_for_missing_membership_is_unknown() { + // Given a timline + let timeline = TestTimeline::new(); + let mut stream = timeline.subscribe().await; + + // When we add an event with no membership in unsigned + timeline.handle_live_event(raw_event_with_unsigned(json!({}))).await; + + // Then its UTD cause is membership + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let event = item.as_event().unwrap(); + assert_let!( + TimelineItemContent::UnableToDecrypt(EncryptedMessage::MegolmV1AesSha2 { cause, .. }) = + event.content() + ); + assert_eq!(*cause, UtdCause::Unknown); +} + +fn raw_event_with_unsigned(unsigned: serde_json::Value) -> Raw { + Raw::from_json( + to_raw_value(&json!({ + "event_id": "$myevent", + "sender": "@u:s", + "origin_server_ts": 3, + "type": "m.room.encrypted", + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "NOT_REAL_CIPHERTEXT", + "sender_key": "SENDER_KEY", + "device_id": "DEVICE_ID", + "session_id": "SESSION_ID", + }, + "unsigned": unsigned + + })) + .unwrap(), + ) +} diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index 98adbf89a..e7758339c 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -24,6 +24,7 @@ use std::{ time::{Duration, Instant}, }; +use matrix_sdk::crypto::types::events::UtdCause; use ruma::{EventId, OwnedEventId}; use tokio::{spawn, task::JoinHandle, time::sleep}; @@ -50,6 +51,10 @@ pub struct UnableToDecryptInfo { /// time it took to decrypt the event. If it is not set, this is /// considered a definite UTD. pub time_to_decrypt: Option, + + /// What we know about what caused this UTD. E.g. was this event sent when + /// we were not a member of this room? + pub cause: UtdCause, } type PendingUtdReports = Vec<(OwnedEventId, JoinHandle<()>)>; @@ -111,7 +116,7 @@ impl UtdHookManager { /// The function to call whenever a UTD is seen for the first time. /// /// Pipe in any information that needs to be included in the final report. - pub(crate) fn on_utd(&self, event_id: &EventId) { + pub(crate) fn on_utd(&self, event_id: &EventId, cause: UtdCause) { // Only let the parent hook know if the event wasn't already handled. { let mut known_utds = self.known_utds.lock().unwrap(); @@ -123,7 +128,8 @@ impl UtdHookManager { known_utds.insert(event_id.to_owned(), Instant::now()); } - let info = UnableToDecryptInfo { event_id: event_id.to_owned(), time_to_decrypt: None }; + let info = + UnableToDecryptInfo { event_id: event_id.to_owned(), time_to_decrypt: None, cause }; let Some(max_delay) = self.max_delay else { // No delay: immediately report the event to the parent hook. @@ -163,7 +169,7 @@ impl UtdHookManager { /// /// Note: if this is called for an event that was never marked as a UTD /// before, it has no effect. - pub(crate) fn on_late_decrypt(&self, event_id: &EventId) { + pub(crate) fn on_late_decrypt(&self, event_id: &EventId, cause: UtdCause) { // Only let the parent hook know if the event was known to be a UTDs. let Some(marked_utd_at) = self.known_utds.lock().unwrap().remove(event_id) else { return; @@ -172,6 +178,7 @@ impl UtdHookManager { let info = UnableToDecryptInfo { event_id: event_id.to_owned(), time_to_decrypt: Some(marked_utd_at.elapsed()), + cause, }; // Cancel and remove the task from the outstanding set immediately. @@ -226,12 +233,12 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone()); // And I call the `on_utd` method multiple times, sometimes on the same event, - wrapper.on_utd(event_id!("$1")); - wrapper.on_utd(event_id!("$1")); - wrapper.on_utd(event_id!("$2")); - wrapper.on_utd(event_id!("$1")); - wrapper.on_utd(event_id!("$2")); - wrapper.on_utd(event_id!("$3")); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown); + wrapper.on_utd(event_id!("$2"), UtdCause::Unknown); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown); + wrapper.on_utd(event_id!("$2"), UtdCause::Unknown); + wrapper.on_utd(event_id!("$3"), UtdCause::Unknown); // Then the event ids have been deduplicated, { @@ -258,7 +265,7 @@ mod tests { // And I call the `on_late_decrypt` method before the event had been marked as // utd, - wrapper.on_late_decrypt(event_id!("$1")); + wrapper.on_late_decrypt(event_id!("$1"), UtdCause::Unknown); // Then nothing is registered in the parent hook. assert!(hook.utds.lock().unwrap().is_empty()); @@ -273,7 +280,7 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone()); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1")); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown); // Then the UTD has been notified, but not as late-decrypted event. { @@ -284,7 +291,7 @@ mod tests { } // And when I call the `on_late_decrypt` method, - wrapper.on_late_decrypt(event_id!("$1")); + wrapper.on_late_decrypt(event_id!("$1"), UtdCause::Unknown); // Then the event is now reported as a late-decryption too. { @@ -312,7 +319,7 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone()).with_max_delay(Duration::from_secs(2)); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1")); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown); // Then the UTD is not being reported immediately. assert!(hook.utds.lock().unwrap().is_empty()); @@ -348,7 +355,7 @@ mod tests { let wrapper = UtdHookManager::new(hook.clone()).with_max_delay(Duration::from_secs(2)); // And I call the `on_utd` method for an event, - wrapper.on_utd(event_id!("$1")); + wrapper.on_utd(event_id!("$1"), UtdCause::Unknown); // Then the UTD has not been notified quite yet. assert!(hook.utds.lock().unwrap().is_empty()); @@ -357,7 +364,7 @@ mod tests { // If I wait for 1 second, and mark the event as late-decrypted, sleep(Duration::from_secs(1)).await; - wrapper.on_late_decrypt(event_id!("$1")); + wrapper.on_late_decrypt(event_id!("$1"), UtdCause::Unknown); // Then it's being immediately reported as a late-decryption UTD. { diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 47b63cfb8..6d6e328c7 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -161,6 +161,7 @@ pub struct EncryptionSettings { /// Settings for end-to-end encryption features. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum BackupDownloadStrategy { /// Automatically download all room keys from the backup when the backup /// recovery key has been received. The backup recovery key can be received