Skip to content

Commit 4156170

Browse files
authored
Merge pull request #3337 from matrix-org/andybalaam/utd-type-info
crypto: UtdCause enum in reporting hooks and encryption event
2 parents 2c7afc2 + 89abb75 commit 4156170

File tree

12 files changed

+342
-39
lines changed

12 files changed

+342
-39
lines changed

bindings/matrix-sdk-ffi/src/sync_service.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use std::{fmt::Debug, sync::Arc, time::Duration};
1616

1717
use futures_util::pin_mut;
18-
use matrix_sdk::Client;
18+
use matrix_sdk::{crypto::types::events::UtdCause, Client};
1919
use matrix_sdk_ui::{
2020
sync_service::{
2121
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
@@ -187,13 +187,18 @@ pub struct UnableToDecryptInfo {
187187
///
188188
/// If set, this is in milliseconds.
189189
pub time_to_decrypt_ms: Option<u64>,
190+
191+
/// What we know about what caused this UTD. E.g. was this event sent when
192+
/// we were not a member of this room?
193+
pub cause: UtdCause,
190194
}
191195

192196
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
193197
fn from(value: SdkUnableToDecryptInfo) -> Self {
194198
Self {
195199
event_id: value.event_id.to_string(),
196200
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
201+
cause: value.cause,
197202
}
198203
}
199204
}

bindings/matrix-sdk-ffi/src/timeline/content.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
use std::{collections::HashMap, sync::Arc};
1616

17-
use matrix_sdk::room::power_levels::power_level_user_changes;
17+
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
1818
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
1919
use tracing::warn;
2020

@@ -214,6 +214,10 @@ pub enum EncryptedMessage {
214214
MegolmV1AesSha2 {
215215
/// The ID of the session used to encrypt the message.
216216
session_id: String,
217+
218+
/// What we know about what caused this UTD. E.g. was this event sent
219+
/// when we were not a member of this room?
220+
cause: UtdCause,
217221
},
218222
Unknown,
219223
}
@@ -227,9 +231,9 @@ impl EncryptedMessage {
227231
let sender_key = sender_key.clone();
228232
Self::OlmV1Curve25519AesSha2 { sender_key }
229233
}
230-
Message::MegolmV1AesSha2 { session_id, .. } => {
234+
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
231235
let session_id = session_id.clone();
232-
Self::MegolmV1AesSha2 { session_id }
236+
Self::MegolmV1AesSha2 { session_id, cause: *cause }
233237
}
234238
Message::Unknown => Self::Unknown,
235239
}

crates/matrix-sdk-base/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"]
2323
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
2424
message-ids = ["matrix-sdk-crypto?/message-ids"]
2525
experimental-sliding-sync = ["ruma/unstable-msc3575"]
26-
uniffi = ["dep:uniffi"]
26+
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi"]
2727

2828
# helpers for testing features build upon this
2929
testing = [

crates/matrix-sdk-crypto/src/types/events/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ pub mod room_key_request;
2727
pub mod room_key_withheld;
2828
pub mod secret_send;
2929
mod to_device;
30+
mod utd_cause;
3031

3132
use ruma::serde::Raw;
3233
pub use to_device::{ToDeviceCustomEvent, ToDeviceEvent, ToDeviceEvents};
34+
pub use utd_cause::UtdCause;
3335

3436
/// A trait for event contents to define their event type.
3537
pub trait EventType {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
16+
use serde::Deserialize;
17+
18+
/// Our best guess at the reason why an event can't be decrypted.
19+
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
20+
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
21+
pub enum UtdCause {
22+
/// We don't have an explanation for why this UTD happened - it is probably
23+
/// a bug, or a network split between the two homeservers.
24+
#[default]
25+
Unknown = 0,
26+
27+
/// This event was sent when we were not a member of the room (or invited),
28+
/// so it is impossible to decrypt (without MSC3061).
29+
Membership = 1,
30+
//
31+
// TODO: Other causes for UTDs. For example, this message is device-historical, information
32+
// extracted from the WithheldCode in the MissingRoomKey object, or various types of Olm
33+
// session problems.
34+
//
35+
// Note: This needs to be a simple enum so we can export it via FFI, so if more information
36+
// needs to be provided, it should be through a separate type.
37+
}
38+
39+
/// MSC4115 membership info in the unsigned area.
40+
#[derive(Deserialize)]
41+
struct UnsignedWithMembership {
42+
#[serde(alias = "io.element.msc4115.membership")]
43+
membership: Membership,
44+
}
45+
46+
/// MSC4115 contents of the membership property
47+
#[derive(Deserialize)]
48+
#[serde(rename_all = "lowercase")]
49+
enum Membership {
50+
Leave,
51+
Invite,
52+
Join,
53+
}
54+
55+
impl UtdCause {
56+
/// Decide the cause of this UTD, based on the evidence we have.
57+
pub fn determine(raw_event: Option<&Raw<AnySyncTimelineEvent>>) -> Self {
58+
// TODO: in future, use more information to give a richer answer. E.g.
59+
// is this event device-historical? Was the Olm communication disrupted?
60+
// Did the sender refuse to send the key because we're not verified?
61+
62+
// Look in the unsigned area for a `membership` field.
63+
if let Some(raw_event) = raw_event {
64+
if let Ok(Some(unsigned)) = raw_event.get_field::<UnsignedWithMembership>("unsigned") {
65+
if let Membership::Leave = unsigned.membership {
66+
// We were not a member - this is the cause of the UTD
67+
return UtdCause::Membership;
68+
}
69+
}
70+
}
71+
72+
// We can't find an explanation for this UTD
73+
UtdCause::Unknown
74+
}
75+
}
76+
77+
#[cfg(test)]
78+
mod tests {
79+
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
80+
use serde_json::{json, value::to_raw_value};
81+
82+
use crate::types::events::UtdCause;
83+
84+
#[test]
85+
fn a_missing_raw_event_means_we_guess_unknown() {
86+
// When we don't provide any JSON to check for membership, then we guess the UTD
87+
// is unknown.
88+
assert_eq!(UtdCause::determine(None), UtdCause::Unknown);
89+
}
90+
91+
#[test]
92+
fn if_there_is_no_membership_info_we_guess_unknown() {
93+
// If our JSON contains no membership info, then we guess the UTD is unknown.
94+
assert_eq!(UtdCause::determine(Some(&raw_event(json!({})))), UtdCause::Unknown);
95+
}
96+
97+
#[test]
98+
fn if_membership_info_cant_be_parsed_we_guess_unknown() {
99+
// If our JSON contains a membership property but not the JSON we expected, then
100+
// we guess the UTD is unknown.
101+
assert_eq!(
102+
UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": 3 } })))),
103+
UtdCause::Unknown
104+
);
105+
}
106+
107+
#[test]
108+
fn if_membership_is_invite_we_guess_unknown() {
109+
// If membership=invite then we expected to be sent the keys so the cause of the
110+
// UTD is unknown.
111+
assert_eq!(
112+
UtdCause::determine(Some(&raw_event(
113+
json!({ "unsigned": { "membership": "invite" } }),
114+
))),
115+
UtdCause::Unknown
116+
);
117+
}
118+
119+
#[test]
120+
fn if_membership_is_join_we_guess_unknown() {
121+
// If membership=join then we expected to be sent the keys so the cause of the
122+
// UTD is unknown.
123+
assert_eq!(
124+
UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "join" } })))),
125+
UtdCause::Unknown
126+
);
127+
}
128+
129+
#[test]
130+
fn if_membership_is_leave_we_guess_membership() {
131+
// If membership=leave then we have an explanation for why we can't decrypt,
132+
// until we have MSC3061.
133+
assert_eq!(
134+
UtdCause::determine(Some(&raw_event(json!({ "unsigned": { "membership": "leave" } })))),
135+
UtdCause::Membership
136+
);
137+
}
138+
139+
#[test]
140+
fn if_unstable_prefix_membership_is_leave_we_guess_membership() {
141+
// Before MSC4115 is merged, we support the unstable prefix too.
142+
assert_eq!(
143+
UtdCause::determine(Some(&raw_event(
144+
json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })
145+
))),
146+
UtdCause::Membership
147+
);
148+
}
149+
150+
fn raw_event(value: serde_json::Value) -> Raw<AnySyncTimelineEvent> {
151+
Raw::from_json(to_raw_value(&value).unwrap())
152+
}
153+
}

crates/matrix-sdk-ui/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ experimental-room-list-with-unified-invites = []
1818
native-tls = ["matrix-sdk/native-tls"]
1919
rustls-tls = ["matrix-sdk/rustls-tls"]
2020

21-
uniffi = ["dep:uniffi"]
21+
uniffi = ["dep:uniffi", "matrix-sdk/uniffi", "matrix-sdk-base/uniffi"]
2222

2323
[dependencies]
2424
as_variant = { workspace = true }

crates/matrix-sdk-ui/src/timeline/event_handler.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::sync::Arc;
1717
use as_variant::as_variant;
1818
use eyeball_im::{ObservableVectorTransaction, ObservableVectorTransactionEntry};
1919
use indexmap::{map::Entry, IndexMap};
20-
use matrix_sdk::deserialized_responses::EncryptionInfo;
20+
use matrix_sdk::{crypto::types::events::UtdCause, deserialized_responses::EncryptionInfo};
2121
use ruma::{
2222
events::{
2323
poll::{
@@ -269,11 +269,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
269269
/// Handle an event.
270270
///
271271
/// Returns the number of timeline updates that were made.
272+
///
273+
/// `raw_event` is only needed to determine the cause of any UTDs,
274+
/// so if we know this is not a UTD it can be None.
272275
#[instrument(skip_all, fields(txn_id, event_id, position))]
273276
pub(super) fn handle_event(
274277
mut self,
275278
day_divider_adjuster: &mut DayDividerAdjuster,
276279
event_kind: TimelineEventKind,
280+
raw_event: Option<&Raw<AnySyncTimelineEvent>>,
277281
) -> HandleEventResult {
278282
let span = tracing::Span::current();
279283

@@ -315,13 +319,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
315319
}
316320
AnyMessageLikeEventContent::RoomEncrypted(c) => {
317321
// TODO: Handle replacements if the replaced event is also UTD
318-
self.add(true, TimelineItemContent::unable_to_decrypt(c));
322+
let cause = UtdCause::determine(raw_event);
323+
self.add(true, TimelineItemContent::unable_to_decrypt(c, cause));
319324

320325
// Let the hook know that we ran into an unable-to-decrypt that is added to the
321326
// timeline.
322327
if let Some(hook) = self.meta.unable_to_decrypt_hook.as_ref() {
323328
if let Flow::Remote { event_id, .. } = &self.ctx.flow {
324-
hook.on_utd(event_id);
329+
hook.on_utd(event_id, cause);
325330
}
326331
}
327332
}

crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::sync::Arc;
1616

1717
use as_variant::as_variant;
1818
use imbl::Vector;
19+
use matrix_sdk::crypto::types::events::UtdCause;
1920
use matrix_sdk_base::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent};
2021
use ruma::{
2122
events::{
@@ -248,8 +249,8 @@ impl TimelineItemContent {
248249
}
249250
}
250251

251-
pub(crate) fn unable_to_decrypt(content: RoomEncryptedEventContent) -> Self {
252-
Self::UnableToDecrypt(content.into())
252+
pub(crate) fn unable_to_decrypt(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
253+
Self::UnableToDecrypt(EncryptedMessage::from_content(content, cause))
253254
}
254255

255256
pub(crate) fn room_member(
@@ -356,21 +357,26 @@ pub enum EncryptedMessage {
356357

357358
/// The ID of the session used to encrypt the message.
358359
session_id: String,
360+
361+
/// What we know about what caused this UTD. E.g. was this event sent
362+
/// when we were not a member of this room?
363+
cause: UtdCause,
359364
},
360365
/// No metadata because the event uses an unknown algorithm.
361366
Unknown,
362367
}
363368

364-
impl From<RoomEncryptedEventContent> for EncryptedMessage {
365-
fn from(c: RoomEncryptedEventContent) -> Self {
366-
match c.scheme {
369+
impl EncryptedMessage {
370+
fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
371+
match content.scheme {
367372
EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
368373
Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
369374
}
370375
#[allow(deprecated)]
371376
EncryptedEventScheme::MegolmV1AesSha2(s) => {
372377
let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
373-
Self::MegolmV1AesSha2 { sender_key, device_id, session_id }
378+
379+
Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
374380
}
375381
_ => Self::Unknown,
376382
}

crates/matrix-sdk-ui/src/timeline/inner/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ impl<P: RoomDataProvider> TimelineInner<P> {
727727
decryptor: impl Decryptor,
728728
session_ids: Option<BTreeSet<String>>,
729729
) {
730+
use matrix_sdk::crypto::types::events::UtdCause;
731+
730732
use super::EncryptedMessage;
731733

732734
let mut state = self.state.clone().write_owned().await;
@@ -805,9 +807,11 @@ impl<P: RoomDataProvider> TimelineInner<P> {
805807
"Successfully decrypted event that previously failed to decrypt"
806808
);
807809

810+
let cause = UtdCause::determine(Some(original_json));
811+
808812
// Notify observers that we managed to eventually decrypt an event.
809813
if let Some(hook) = unable_to_decrypt_hook {
810-
hook.on_late_decrypt(&remote_event.event_id);
814+
hook.on_late_decrypt(&remote_event.event_id, cause);
811815
}
812816

813817
Some(event)

crates/matrix-sdk-ui/src/timeline/inner/state.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,13 @@ impl TimelineInnerState {
186186

187187
let mut day_divider_adjuster = DayDividerAdjuster::default();
188188

189-
TimelineEventHandler::new(&mut txn, ctx).handle_event(&mut day_divider_adjuster, content);
189+
TimelineEventHandler::new(&mut txn, ctx).handle_event(
190+
&mut day_divider_adjuster,
191+
content,
192+
// Local events are never UTD, so no need to pass in a raw_event - this is only used to
193+
// determine the type of UTD if there is one.
194+
None,
195+
);
190196

191197
txn.adjust_day_dividers(day_divider_adjuster);
192198

@@ -551,14 +557,18 @@ impl TimelineInnerStateTransaction<'_> {
551557
is_highlighted: event.push_actions.iter().any(Action::is_highlight),
552558
flow: Flow::Remote {
553559
event_id: event_id.clone(),
554-
raw_event: raw,
560+
raw_event: raw.clone(),
555561
txn_id,
556562
position,
557563
should_add,
558564
},
559565
};
560566

561-
TimelineEventHandler::new(self, ctx).handle_event(day_divider_adjuster, event_kind)
567+
TimelineEventHandler::new(self, ctx).handle_event(
568+
day_divider_adjuster,
569+
event_kind,
570+
Some(&raw),
571+
)
562572
}
563573

564574
fn clear(&mut self) {

0 commit comments

Comments
 (0)