Skip to content

Commit

Permalink
feat(utd_hook): Report historical expected UTD with new reason
Browse files Browse the repository at this point in the history
  • Loading branch information
BillCarsonFr committed Nov 20, 2024
1 parent 8a6ced0 commit 23fa445
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 7 deletions.
2 changes: 1 addition & 1 deletion crates/matrix-sdk-crypto/src/types/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
241 changes: 238 additions & 3 deletions crates/matrix-sdk-crypto/src/types/events/utd_cause.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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<AnySyncTimelineEvent>>,
client_info: Option<ClientInfo>,
unable_to_decrypt_info: &UnableToDecryptInfo,
) -> Self {
// TODO: in future, use more information to give a richer answer. E.g.
Expand All @@ -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
}

Expand All @@ -111,17 +143,18 @@ 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() {
// When we don't provide any JSON to check for membership, then we guess the UTD
// is unknown.
assert_eq!(
UtdCause::determine(
None,
None,
&UnableToDecryptInfo {
session_id: None,
Expand All @@ -138,6 +171,7 @@ mod tests {
assert_eq!(
UtdCause::determine(
Some(&raw_event(json!({}))),
None,
&UnableToDecryptInfo {
session_id: None,
reason: UnableToDecryptReason::MissingMegolmSession
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -236,6 +275,7 @@ mod tests {
Some(&raw_event(
json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })
)),
None,
&UnableToDecryptInfo {
session_id: None,
reason: UnableToDecryptReason::MissingMegolmSession
Expand All @@ -250,6 +290,7 @@ mod tests {
assert_eq!(
UtdCause::determine(
Some(&raw_event(json!({}))),
None,
&UnableToDecryptInfo {
session_id: None,
reason: UnableToDecryptReason::SenderIdentityNotTrusted(
Expand All @@ -266,6 +307,7 @@ mod tests {
assert_eq!(
UtdCause::determine(
Some(&raw_event(json!({}))),
None,
&UnableToDecryptInfo {
session_id: None,
reason: UnableToDecryptReason::SenderIdentityNotTrusted(
Expand All @@ -282,6 +324,7 @@ mod tests {
assert_eq!(
UtdCause::determine(
Some(&raw_event(json!({}))),
None,
&UnableToDecryptInfo {
session_id: None,
reason: UnableToDecryptReason::SenderIdentityNotTrusted(
Expand All @@ -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<AnySyncTimelineEvent> {
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<AnySyncTimelineEvent> {
Raw::from_json(to_raw_value(&value).unwrap())
}
Expand Down
7 changes: 6 additions & 1 deletion crates/matrix-sdk-ui/src/timeline/event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ impl Flow {
pub(super) struct TimelineEventContext {
pub(super) sender: OwnedUserId,
pub(super) sender_profile: Option<Profile>,
/// 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<OwnedUserId, Receipt>,
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 23fa445

Please sign in to comment.