From 40090f3ca092423bd4547817717da63459e23307 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 18 Nov 2024 10:21:08 +0100 Subject: [PATCH] feat(utd_hook): Report historical expected UTD with new reason --- .../matrix-sdk-crypto/src/types/events/mod.rs | 2 +- .../src/types/events/utd_cause.rs | 241 +++++++++++++++++- .../src/timeline/event_handler.rs | 7 +- .../src/unable_to_decrypt_hook.rs | 18 +- 4 files changed, 261 insertions(+), 7 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..aa20f8c07d9 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::{ClientInfo, 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..495ebf21a3a 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,10 @@ 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 an historical message + /// and no backup is accessible or usable. + HistoricalMessage = 5, } /// MSC4115 membership info in the unsigned area. @@ -65,10 +69,24 @@ enum Membership { Join, } +/// Contextual information used by the `UTDHookManager` to properly identify the +/// cause of an UTD. +#[derive(Debug, Clone, Copy)] +pub struct ClientInfo { + /// 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. + 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>, + client_info: Option, unable_to_decrypt_info: &UnableToDecryptInfo, ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. @@ -85,7 +103,21 @@ impl UtdCause { return UtdCause::SentBeforeWeJoined; } } + if let Some(client_info) = client_info { + if let Ok(timeline_event) = raw_event.deserialize() { + if client_info.is_backup_configured + && timeline_event.origin_server_ts() + < client_info.device_creation_ts + { + // 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 +143,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, MilliSecondsSinceUnixEpoch}; use serde_json::{json, value::to_raw_value}; - use crate::types::events::UtdCause; + use crate::types::events::{ClientInfo, UtdCause}; #[test] fn test_a_missing_raw_event_means_we_guess_unknown() { @@ -122,6 +154,7 @@ mod tests { // is unknown. assert_eq!( UtdCause::determine( + None, None, &UnableToDecryptInfo { session_id: None, @@ -138,6 +171,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -154,6 +188,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({ "unsigned": { "membership": 3 } }))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -170,6 +205,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({ "unsigned": { "membership": "invite" } }),)), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -186,6 +222,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({ "unsigned": { "membership": "join" } }))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -202,6 +239,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -219,6 +257,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({ "unsigned": { "membership": "leave" } }))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MalformedEncryptedEvent @@ -236,6 +275,7 @@ mod tests { Some(&raw_event( json!({ "unsigned": { "io.element.msc4115.membership": "leave" } }) )), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::MissingMegolmSession @@ -250,6 +290,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -266,6 +307,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -282,6 +324,7 @@ mod tests { assert_eq!( UtdCause::determine( Some(&raw_event(json!({}))), + None, &UnableToDecryptInfo { session_id: None, reason: UnableToDecryptReason::SenderIdentityNotTrusted( @@ -293,6 +336,198 @@ 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 = ClientInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts - 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({}))), + Some(older_than_event_device), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); + + let newer_than_event_device = ClientInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(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 = ClientInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts - 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + Some(&raw_event(json!({}))), + Some(older_than_event_device), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::Unknown + ); + + let newer_than_event_device = ClientInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(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 = ClientInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: true, + }; + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(newer_than_event_device), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::UnknownMegolmMessageIndex + } + ), + UtdCause::HistoricalMessage + ); + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(newer_than_event_device), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MalformedEncryptedEvent + } + ), + UtdCause::Unknown + ); + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(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 client_info = ClientInfo { + device_creation_ts: MilliSecondsSinceUnixEpoch( + (message_creation_ts + 1000).try_into().unwrap(), + ), + is_backup_configured: false, + }; + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(client_info), + &UnableToDecryptInfo { + session_id: None, + reason: UnableToDecryptReason::MissingMegolmSession + } + ), + UtdCause::Unknown + ); + + assert_eq!( + UtdCause::determine( + Some(&utd_event), + Some(client_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()) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 941823b967a..2ed11e9ee9b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -118,6 +118,7 @@ impl Flow { pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, + /// The event `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, @@ -420,7 +421,11 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { TimelineEventKind::UnableToDecrypt { content, unable_to_decrypt_info } => { // 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); + let client_info = match self.meta.unable_to_decrypt_hook.as_ref() { + Some(hook) => Some(hook.client_info().await), + None => None, + }; + let cause = UtdCause::determine(raw_event, client_info, &unable_to_decrypt_info); self.add_item(TimelineItemContent::unable_to_decrypt(content, cause), None); // Let the hook know that we ran into an unable-to-decrypt that is added to the 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 14f72aee824..70f7864f808 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -25,9 +25,12 @@ use std::{ }; use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; -use matrix_sdk::{crypto::types::events::UtdCause, Client}; +use matrix_sdk::{ + crypto::types::events::{ClientInfo, UtdCause}, + Client, +}; use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError}; -use ruma::{EventId, OwnedEventId}; +use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; use tokio::{ spawn, sync::{Mutex as AsyncMutex, MutexGuard}, @@ -176,6 +179,17 @@ impl UtdHookManager { self } + pub async fn client_info(&self) -> ClientInfo { + ClientInfo { + device_creation_ts: match self.client.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: self.client.encryption().backups().are_enabled().await, + } + } + /// Load the persistent data for the UTD hook from the store. /// /// If the client previously used a UtdHookManager, and UTDs were