From a0c86d964528d4cccd35d3add1761397a60c7db1 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Nov 2024 18:02:27 +0100 Subject: [PATCH] feat(utd_hook): Report historical expected UTD with new reason This PR introduces a new variant to `UtdCause` specifically for device-historical messages (`HistoricalMessage`). These messages cannot be decrypted if key storage is inaccessible. Applications can leverage this new variant to provide more informative error messages to users. --- .../matrix-sdk-crypto/src/types/events/mod.rs | 2 +- .../src/types/events/utd_cause.rs | 292 ++++++++++++++++-- .../src/timeline/controller/state.rs | 2 +- .../src/timeline/event_handler.rs | 43 ++- .../matrix-sdk-ui/src/timeline/tests/mod.rs | 17 +- crates/matrix-sdk-ui/src/timeline/traits.rs | 10 +- crates/matrix-sdk/src/room/mod.rs | 29 +- 7 files changed, 346 insertions(+), 49 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/events/mod.rs b/crates/matrix-sdk-crypto/src/types/events/mod.rs index 1ebc7239887..8c766554e8f 100644 --- a/crates/matrix-sdk-crypto/src/types/events/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/events/mod.rs @@ -31,7 +31,7 @@ mod utd_cause; use ruma::serde::Raw; pub use to_device::{ToDeviceCustomEvent, ToDeviceEvent, ToDeviceEvents}; -pub use utd_cause::UtdCause; +pub use utd_cause::{CryptoContextInfo, 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 index 5a260ad50d9..685dc24d03b 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -15,7 +15,7 @@ use matrix_sdk_common::deserialized_responses::{ UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, }; -use ruma::{events::AnySyncTimelineEvent, serde::Raw}; +use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch}; use serde::Deserialize; /// Our best guess at the reason why an event can't be decrypted. @@ -47,6 +47,16 @@ pub enum UtdCause { /// data was obtained from an insecure source (imported from a file, /// obtained from a legacy (asymmetric) backup, unsafe key forward, etc.) UnknownDevice = 4, + + /// We are missing the keys for this event, but it is a "device-historical" + /// message and no backup is accessible or usable. + /// + /// Device-historical means that the message was sent before the current + /// device existed (but the current user was probably a member of the room + /// at the time the message was sent). Not to + /// be confused with pre-join or pre-invite messages (see + /// [`UtdCause::SentBeforeWeJoined`] for that). + HistoricalMessage = 5, } /// MSC4115 membership info in the unsigned area. @@ -65,10 +75,25 @@ enum Membership { Join, } +/// Contextual crypto information used by [`UtdCause::determine`] to properly +/// identify an Unable-To-Decrypt cause in addition to the +/// [`UnableToDecryptInfo`] and raw event info. +#[derive(Debug, Clone, Copy)] +pub struct CryptoContextInfo { + /// The current device creation timestamp, used as a heuristic to determine + /// if an event is device-historical or not (sent before the current device + /// existed). + pub device_creation_ts: MilliSecondsSinceUnixEpoch, + /// True if key storage is correctly set up and can be used by the current + /// client to download and decrypt message keys. + pub is_backup_configured: bool, +} + impl UtdCause { /// Decide the cause of this UTD, based on the evidence we have. pub fn determine( - raw_event: Option<&Raw>, + raw_event: &Raw, + crypto_context_info: CryptoContextInfo, unable_to_decrypt_info: &UnableToDecryptInfo, ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. @@ -76,16 +101,27 @@ impl UtdCause { UnableToDecryptReason::MissingMegolmSession | UnableToDecryptReason::UnknownMegolmMessageIndex => { // 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 Some(unsigned) = + raw_event.get_field::("unsigned").ok().flatten() + { + if let Membership::Leave = unsigned.membership { + // We were not a member - this is the cause of the UTD + return UtdCause::SentBeforeWeJoined; + } + } + + if let Ok(timeline_event) = raw_event.deserialize() { + if crypto_context_info.is_backup_configured + && timeline_event.origin_server_ts() + < crypto_context_info.device_creation_ts { - if let Membership::Leave = unsigned.membership { - // We were not a member - this is the cause of the UTD - return UtdCause::SentBeforeWeJoined; - } + // It's a device-historical message and there is no accessible + // backup. The key is missing and it + // is expected. + return UtdCause::HistoricalMessage; } } + UtdCause::Unknown } @@ -111,10 +147,10 @@ mod tests { use matrix_sdk_common::deserialized_responses::{ DeviceLinkProblem, UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, }; - use ruma::{events::AnySyncTimelineEvent, serde::Raw}; + use ruma::{events::AnySyncTimelineEvent, serde::Raw, uint, MilliSecondsSinceUnixEpoch}; use serde_json::{json, value::to_raw_value}; - use crate::types::events::UtdCause; + use crate::types::events::{utd_cause::CryptoContextInfo, UtdCause}; #[test] fn test_a_missing_raw_event_means_we_guess_unknown() { @@ -122,7 +158,8 @@ mod tests { // is unknown. assert_eq!( UtdCause::determine( - None, + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession, @@ -137,7 +174,8 @@ mod tests { // If our JSON contains no membership info, then we guess the UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -153,7 +191,8 @@ mod tests { // we guess the UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": 3 } }))), + &raw_event(json!({ "unsigned": { "membership": 3 } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -169,7 +208,8 @@ mod tests { // UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "invite" } }),)), + &raw_event(json!({ "unsigned": { "membership": "invite" } }),), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -185,7 +225,8 @@ mod tests { // UTD is unknown. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "join" } }))), + &raw_event(json!({ "unsigned": { "membership": "join" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -201,7 +242,8 @@ mod tests { // until we have MSC3061. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + &raw_event(json!({ "unsigned": { "membership": "leave" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -218,7 +260,8 @@ mod tests { // even if membership=leave. assert_eq!( UtdCause::determine( - Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + &raw_event(json!({ "unsigned": { "membership": "leave" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MalformedEncryptedEvent @@ -233,9 +276,8 @@ mod tests { // Before MSC4115 is merged, we support the unstable prefix too. assert_eq!( UtdCause::determine( - Some(&raw_event( - json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) - )), + &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -249,7 +291,8 @@ mod tests { fn test_verification_violation_is_passed_through() { assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -265,7 +308,8 @@ mod tests { fn test_unsigned_device_is_passed_through() { assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -281,7 +325,8 @@ mod tests { fn test_unknown_device_is_passed_through() { assert_eq!( UtdCause::determine( - Some(&raw_event(json!({}))), + &raw_event(json!({})), + some_crypto_context_info(), &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -293,7 +338,206 @@ mod tests { ); } + #[test] + fn test_historical_expected_reason_depending_on_origin_ts_for_missing_session() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let older_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts - 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &raw_event(json!({})), + older_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); + + let newer_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::HistoricalMessage + ); + } + + #[test] + fn test_historical_expected_reason_depending_on_origin_ts_for_ratcheted_session() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let older_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts - 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &raw_event(json!({})), + older_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::Unknown + ); + + let newer_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::HistoricalMessage + ); + } + + #[test] + fn test_historical_expected_reason_depending_on_origin_only_for_correct_reason() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let newer_than_event_device = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::HistoricalMessage + ); + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MalformedEncryptedEvent + } + ), + UtdCause::Unknown + ); + + assert_eq!( + UtdCause::determine( + &utd_event, + newer_than_event_device, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MegolmDecryptionFailure + } + ), + UtdCause::Unknown + ); + } + + #[test] + fn test_historical_expected_only_if_backup_configured() { + let message_creation_ts = 10000; + let utd_event = a_utd_event_with_origin_ts(message_creation_ts); + + let crypto_context_info = CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: false, + }; + + assert_eq!( + UtdCause::determine( + &utd_event, + crypto_context_info, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); + + assert_eq!( + UtdCause::determine( + &utd_event, + crypto_context_info, + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::Unknown + ); + } + + fn a_utd_event_with_origin_ts(origin_server_ts: i32) -> Raw { + raw_event(json!({ + "type": "m.room.encrypted", + "event_id": "$0", + // the values don't matter much but the expected fields should be there. + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "FOO", + "sender_key": "SENDERKEYSENDERKEY", + "device_id": "ABCDEFGH", + "session_id": "A0", + }, + "sender": "@bob:localhost", + "origin_server_ts": origin_server_ts, + "unsigned": { "membership": "join" } + })) + } + fn raw_event(value: serde_json::Value) -> Raw { Raw::from_json(to_raw_value(&value).unwrap()) } + + fn some_crypto_context_info() -> CryptoContextInfo { + CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch(uint!(42)), + is_backup_configured: false, + } + } } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 8ead599f6cc..13adcbd2ab5 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -493,7 +493,7 @@ impl TimelineStateTransaction<'_> { event.sender().to_owned(), event.origin_server_ts(), event.transaction_id().map(ToOwned::to_owned), - TimelineEventKind::from_event(event, &room_version, utd_info), + TimelineEventKind::from_event(event, &raw, room_data_provider, utd_info).await, should_add, ) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index aad559c0469..cc01448714b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -47,7 +47,6 @@ use ruma::{ }, serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, - RoomVersionId, }; use tracing::{debug, error, field::debug, info, instrument, trace, warn}; @@ -71,6 +70,7 @@ use crate::{ controller::PendingEdit, event_item::{ReactionInfo, ReactionStatus}, reactions::PendingReaction, + traits::RoomDataProvider, RepliedToEvent, }, }; @@ -118,6 +118,7 @@ impl Flow { pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, + /// The event's `origin_server_ts` field (or creation time for local echo). pub(super) timestamp: MilliSecondsSinceUnixEpoch, pub(super) is_own_event: bool, pub(super) read_receipts: IndexMap, @@ -141,10 +142,7 @@ pub(super) enum TimelineEventKind { }, /// An encrypted event that could not be decrypted - UnableToDecrypt { - content: RoomEncryptedEventContent, - unable_to_decrypt_info: UnableToDecryptInfo, - }, + UnableToDecrypt { content: RoomEncryptedEventContent, utd_cause: UtdCause }, /// Some remote event that was redacted a priori, i.e. we never had the /// original content, so we'll just display a dummy redacted timeline @@ -180,15 +178,27 @@ pub(super) enum TimelineEventKind { } impl TimelineEventKind { - /// Creates a new `TimelineEventKind` with the given event and room version. - pub fn from_event( + /// Creates a new `TimelineEventKind`. + /// + /// # Arguments + /// + /// * `event` - The event for which we should create a `TimelineEventKind`. + /// * `raw_event` - The [`Raw`] JSON for `event`. (Required so that we can + /// access `unsigned` data.) + /// * `room_data_provider` - An object which will provide information about + /// the room containing the event. + /// * `unable_to_decrypt_info` - If `event` represents a failure to decrypt, + /// information about that failure. Otherwise, `None`. + pub async fn from_event( event: AnySyncTimelineEvent, - room_version: &RoomVersionId, + raw_event: &Raw, + room_data_provider: &P, unable_to_decrypt_info: Option, ) -> Self { + let room_version = room_data_provider.room_version(); match event { AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(ev)) => { - if let Some(redacts) = ev.redacts(room_version).map(ToOwned::to_owned) { + if let Some(redacts) = ev.redacts(&room_version).map(ToOwned::to_owned) { Self::Redaction { redacts } } else { Self::RedactedMessage { event_type: ev.event_type() } @@ -198,7 +208,12 @@ impl TimelineEventKind { Some(AnyMessageLikeEventContent::RoomEncrypted(content)) => { // An event which is still encrypted. if let Some(unable_to_decrypt_info) = unable_to_decrypt_info { - Self::UnableToDecrypt { content, unable_to_decrypt_info } + let utd_cause = UtdCause::determine( + raw_event, + room_data_provider.crypto_context_info().await, + &unable_to_decrypt_info, + ); + Self::UnableToDecrypt { content, utd_cause } } else { // If we get here, it means that some part of the code has created a // `SyncTimelineEvent` containing an `m.room.encrypted` event @@ -426,17 +441,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { } }, - TimelineEventKind::UnableToDecrypt { content, unable_to_decrypt_info } => { + TimelineEventKind::UnableToDecrypt { content, utd_cause } => { // TODO: Handle replacements if the replaced event is also UTD - let raw_event = self.ctx.flow.raw_event(); - let cause = UtdCause::determine(raw_event, &unable_to_decrypt_info); - self.add_item(TimelineItemContent::unable_to_decrypt(content, cause), None); + self.add_item(TimelineItemContent::unable_to_decrypt(content, utd_cause), None); // 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 Some(event_id) = &self.ctx.flow.event_id() { - hook.on_utd(event_id, cause).await; + hook.on_utd(event_id, utd_cause).await; } } } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 943b641b099..b1d1cc29573 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -17,7 +17,9 @@ use std::{ collections::{BTreeMap, HashMap}, future::ready, + ops::Sub, sync::Arc, + time::{Duration, SystemTime}, }; use eyeball::{SharedObservable, Subscriber}; @@ -33,7 +35,9 @@ use matrix_sdk::{ send_queue::RoomSendQueueUpdate, BoxFuture, }; -use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo, RoomState}; +use matrix_sdk_base::{ + crypto::types::events::CryptoContextInfo, latest_event::LatestEvent, RoomInfo, RoomState, +}; use matrix_sdk_test::{ event_factory::EventFactory, EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID, }; @@ -376,6 +380,17 @@ impl RoomDataProvider for TestRoomDataProvider { RoomVersionId::V10 } + fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo> { + ready(CryptoContextInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch::from_system_time( + SystemTime::now().sub(Duration::from_secs(60 * 3)), + ) + .unwrap_or(MilliSecondsSinceUnixEpoch::now()), + is_backup_configured: false, + }) + .boxed() + } + fn profile_from_user_id<'a>(&'a self, _user_id: &'a UserId) -> BoxFuture<'a, Option> { ready(None).boxed() } diff --git a/crates/matrix-sdk-ui/src/timeline/traits.rs b/crates/matrix-sdk-ui/src/timeline/traits.rs index 24015c46774..d4ad9bdf288 100644 --- a/crates/matrix-sdk-ui/src/timeline/traits.rs +++ b/crates/matrix-sdk-ui/src/timeline/traits.rs @@ -20,8 +20,8 @@ use indexmap::IndexMap; #[cfg(test)] use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement}; use matrix_sdk::{ - deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture, - Result, Room, + crypto::types::events::CryptoContextInfo, deserialized_responses::TimelineEvent, + event_cache::paginator::PaginableRoom, BoxFuture, Result, Room, }; use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo}; use ruma::{ @@ -76,6 +76,8 @@ pub(super) trait RoomDataProvider: fn own_user_id(&self) -> &UserId; fn room_version(&self) -> RoomVersionId; + fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo>; + fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option>; fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option; @@ -121,6 +123,10 @@ impl RoomDataProvider for Room { (**self).clone_info().room_version_or_default() } + fn crypto_context_info(&self) -> BoxFuture<'_, CryptoContextInfo> { + async move { self.crypto_context_info().await }.boxed() + } + fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, Option> { async move { match self.get_member_no_sync(user_id).await { diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 285411de6e5..fcafbca63b9 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -48,11 +48,6 @@ use matrix_sdk_base::{ }; use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, timeout::timeout}; use mime::Mime; -#[cfg(feature = "e2e-encryption")] -use ruma::events::{ - room::encrypted::OriginalSyncRoomEncryptedEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, - SyncMessageLikeEvent, -}; use ruma::{ api::client::{ config::{set_global_account_data, set_room_account_data}, @@ -113,6 +108,14 @@ use ruma::{ EventId, Int, MatrixToUri, MatrixUri, MxcUri, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId, }; +#[cfg(feature = "e2e-encryption")] +use ruma::{ + events::{ + room::encrypted::OriginalSyncRoomEncryptedEvent, AnySyncMessageLikeEvent, + AnySyncTimelineEvent, SyncMessageLikeEvent, + }, + MilliSecondsSinceUnixEpoch, +}; use serde::de::DeserializeOwned; use thiserror::Error; use tokio::sync::broadcast; @@ -139,6 +142,8 @@ use crate::{ utils::{IntoRawMessageLikeEventContent, IntoRawStateEventContent}, BaseRoom, Client, Error, HttpResult, Result, RoomState, TransmissionProgress, }; +#[cfg(feature = "e2e-encryption")] +use crate::{crypto::types::events::CryptoContextInfo, encryption::backups::BackupState}; pub mod edit; pub mod futures; @@ -610,6 +615,20 @@ impl Room { Ok(self.inner.is_encrypted()) } + /// Gets additional context info about the client crypto. + #[cfg(feature = "e2e-encryption")] + pub async fn crypto_context_info(&self) -> CryptoContextInfo { + let encryption = self.client.encryption(); + CryptoContextInfo { + device_creation_ts: match encryption.get_own_device().await { + Ok(Some(device)) => device.first_time_seen_ts(), + // Should not happen, there will always be an own device + _ => MilliSecondsSinceUnixEpoch::now(), + }, + is_backup_configured: encryption.backups().state() == BackupState::Enabled, + } + } + fn are_events_visible(&self) -> bool { if let RoomState::Invited = self.inner.state() { return matches!(