Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(utd_hook): Report historical expected UTD with new reason #4275

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
243 changes: 240 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,200 @@ 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
Loading