matrix_sdk_crypto/types/events/
utd_cause.rs1use matrix_sdk_common::deserialized_responses::{
16 UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, WithheldCode,
17};
18use ruma::{MilliSecondsSinceUnixEpoch, events::AnySyncTimelineEvent, serde::Raw};
19use serde::Deserialize;
20
21#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
24pub enum UtdCause {
25 #[default]
37 Unknown = 0,
38
39 SentBeforeWeJoined = 1,
42
43 VerificationViolation = 2,
46
47 UnsignedDevice = 3,
50
51 UnknownDevice = 4,
59
60 HistoricalMessageAndBackupIsDisabled = 5,
71
72 WithheldForUnverifiedOrInsecureDevice = 6,
77
78 WithheldBySender = 7,
84
85 HistoricalMessageAndDeviceIsUnverified = 8,
96}
97
98#[derive(Deserialize)]
100struct UnsignedWithMembership {
101 #[serde(alias = "io.element.msc4115.membership")]
102 membership: Membership,
103}
104
105#[derive(Deserialize)]
107#[serde(rename_all = "lowercase")]
108enum Membership {
109 Leave,
110 Invite,
111 Join,
112}
113
114#[derive(Debug, Clone, Copy)]
118pub struct CryptoContextInfo {
119 pub device_creation_ts: MilliSecondsSinceUnixEpoch,
123
124 pub this_device_is_verified: bool,
126
127 pub backup_exists_on_server: bool,
130
131 pub is_backup_configured: bool,
134}
135
136impl UtdCause {
137 pub fn determine(
139 raw_event: &Raw<AnySyncTimelineEvent>,
140 crypto_context_info: CryptoContextInfo,
141 unable_to_decrypt_info: &UnableToDecryptInfo,
142 ) -> Self {
143 use UnableToDecryptReason::*;
144 match &unable_to_decrypt_info.reason {
145 MissingMegolmSession { withheld_code: Some(WithheldCode::Unverified) } => {
146 UtdCause::WithheldForUnverifiedOrInsecureDevice
147 }
148
149 MissingMegolmSession { withheld_code: Some(WithheldCode::Blacklisted) }
150 | MissingMegolmSession { withheld_code: Some(WithheldCode::Unauthorised) }
151 | MissingMegolmSession { withheld_code: Some(WithheldCode::Unavailable) }
152 | MissingMegolmSession { withheld_code: Some(WithheldCode::NoOlm) }
153 | MissingMegolmSession { withheld_code: Some(WithheldCode::_Custom(_)) } => {
154 UtdCause::WithheldBySender
155 }
156
157 MissingMegolmSession { withheld_code: None }
158 | MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
159 | UnknownMegolmMessageIndex => {
160 if let Some(unsigned) =
162 raw_event.get_field::<UnsignedWithMembership>("unsigned").ok().flatten()
163 && let Membership::Leave = unsigned.membership
164 {
165 return UtdCause::SentBeforeWeJoined;
167 }
168
169 if let Ok(timeline_event) = raw_event.deserialize()
170 && timeline_event.origin_server_ts() < crypto_context_info.device_creation_ts
171 {
172 return UtdCause::determine_historical(crypto_context_info);
175 }
176
177 UtdCause::Unknown
178 }
179
180 SenderIdentityNotTrusted(VerificationLevel::VerificationViolation) => {
181 UtdCause::VerificationViolation
182 }
183
184 SenderIdentityNotTrusted(VerificationLevel::UnsignedDevice) => UtdCause::UnsignedDevice,
185
186 SenderIdentityNotTrusted(VerificationLevel::None(_)) => UtdCause::UnknownDevice,
187
188 _ => UtdCause::Unknown,
189 }
190 }
191
192 fn determine_historical(crypto_context_info: CryptoContextInfo) -> UtdCause {
215 let backup_disabled = !crypto_context_info.backup_exists_on_server;
216 let backup_failing = !crypto_context_info.is_backup_configured;
217 let unverified = !crypto_context_info.this_device_is_verified;
218
219 if backup_disabled {
220 UtdCause::HistoricalMessageAndBackupIsDisabled
221 } else if backup_failing && unverified {
222 UtdCause::HistoricalMessageAndDeviceIsUnverified
223 } else {
224 UtdCause::Unknown
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use matrix_sdk_common::deserialized_responses::{
240 DeviceLinkProblem, UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel,
241 WithheldCode,
242 };
243 use ruma::{MilliSecondsSinceUnixEpoch, events::AnySyncTimelineEvent, serde::Raw};
244 use serde_json::{json, value::to_raw_value};
245
246 use crate::types::events::{UtdCause, utd_cause::CryptoContextInfo};
247
248 const EVENT_TIME: usize = 5555;
249 const BEFORE_EVENT_TIME: usize = 1111;
250 const AFTER_EVENT_TIME: usize = 9999;
251
252 #[test]
253 fn test_if_there_is_no_membership_info_we_guess_unknown() {
254 assert_eq!(
257 UtdCause::determine(&raw_event(json!({})), device_old(), &missing_megolm_session()),
258 UtdCause::Unknown
259 );
260 }
261
262 #[test]
263 fn test_if_membership_info_cant_be_parsed_we_guess_unknown() {
264 assert_eq!(
267 UtdCause::determine(
268 &raw_event(json!({ "unsigned": { "membership": 3 } })),
269 device_old(),
270 &missing_megolm_session()
271 ),
272 UtdCause::Unknown
273 );
274 }
275
276 #[test]
277 fn test_if_membership_is_invite_we_guess_unknown() {
278 assert_eq!(
281 UtdCause::determine(
282 &raw_event(json!({ "unsigned": { "membership": "invite" } }),),
283 device_old(),
284 &missing_megolm_session()
285 ),
286 UtdCause::Unknown
287 );
288 }
289
290 #[test]
291 fn test_if_membership_is_join_we_guess_unknown() {
292 assert_eq!(
295 UtdCause::determine(
296 &raw_event(json!({ "unsigned": { "membership": "join" } })),
297 device_old(),
298 &missing_megolm_session()
299 ),
300 UtdCause::Unknown
301 );
302 }
303
304 #[test]
305 fn test_if_membership_is_leave_we_guess_membership() {
306 assert_eq!(
309 UtdCause::determine(
310 &raw_event(json!({ "unsigned": { "membership": "leave" } })),
311 device_old(),
312 &missing_megolm_session()
313 ),
314 UtdCause::SentBeforeWeJoined
315 );
316 }
317
318 #[test]
319 fn test_if_membership_is_leave_and_session_not_shared_we_guess_membership() {
320 assert_eq!(
323 UtdCause::determine(
324 &raw_event(json!({ "unsigned": { "membership": "leave" } })),
325 device_old(),
326 &UnableToDecryptInfo {
327 session_id: None,
328 reason: UnableToDecryptReason::MissingMegolmSession {
329 withheld_code: Some(WithheldCode::HistoryNotShared)
330 },
331 }
332 ),
333 UtdCause::SentBeforeWeJoined
334 );
335 }
336
337 #[test]
338 fn test_if_reason_is_not_missing_key_we_guess_unknown_even_if_membership_is_leave() {
339 assert_eq!(
343 UtdCause::determine(
344 &raw_event(json!({ "unsigned": { "membership": "leave" } })),
345 device_old(),
346 &malformed_encrypted_event()
347 ),
348 UtdCause::Unknown
349 );
350 }
351
352 #[test]
353 fn test_if_unstable_prefix_membership_is_leave_we_guess_membership() {
354 assert_eq!(
356 UtdCause::determine(
357 &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })),
358 device_old(),
359 &missing_megolm_session()
360 ),
361 UtdCause::SentBeforeWeJoined
362 );
363 }
364
365 #[test]
366 fn test_verification_violation_is_passed_through() {
367 assert_eq!(
368 UtdCause::determine(&raw_event(json!({})), device_old(), &verification_violation()),
369 UtdCause::VerificationViolation
370 );
371 }
372
373 #[test]
374 fn test_unsigned_device_is_passed_through() {
375 assert_eq!(
376 UtdCause::determine(&raw_event(json!({})), device_old(), &unsigned_device()),
377 UtdCause::UnsignedDevice
378 );
379 }
380
381 #[test]
382 fn test_unknown_device_is_passed_through() {
383 assert_eq!(
384 UtdCause::determine(&raw_event(json!({})), device_old(), &missing_device()),
385 UtdCause::UnknownDevice
386 );
387 }
388
389 #[test]
390 fn test_old_devices_dont_cause_historical_utds() {
391 let info = missing_megolm_session();
393
394 let context = device_old();
396
397 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
399
400 let info = unknown_megolm_message_index();
402 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
403 }
404
405 #[test]
406 fn test_if_backup_is_disabled_historical_utd_is_expected() {
407 let info = missing_megolm_session();
409
410 let mut context = device_new();
412
413 context.backup_exists_on_server = false;
415
416 assert_eq!(
419 UtdCause::determine(&utd_event(), context, &info),
420 UtdCause::HistoricalMessageAndBackupIsDisabled
421 );
422
423 let info = unknown_megolm_message_index();
425 assert_eq!(
426 UtdCause::determine(&utd_event(), context, &info),
427 UtdCause::HistoricalMessageAndBackupIsDisabled
428 );
429 }
430
431 #[test]
432 fn test_malformed_events_are_never_expected_utds() {
433 let info = malformed_encrypted_event();
435
436 let mut context = device_new();
438
439 context.backup_exists_on_server = false;
441
442 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
446
447 let info = megolm_decryption_failure();
449 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
450 }
451
452 #[test]
453 fn test_new_devices_with_nonworking_backups_because_unverified_cause_expected_utds() {
454 let info = missing_megolm_session();
456
457 let mut context = device_new();
459
460 context.backup_exists_on_server = true;
462
463 context.is_backup_configured = false;
465
466 context.this_device_is_verified = false;
468
469 assert_eq!(
471 UtdCause::determine(&utd_event(), context, &info),
472 UtdCause::HistoricalMessageAndDeviceIsUnverified
473 );
474
475 let info = unknown_megolm_message_index();
477 assert_eq!(
478 UtdCause::determine(&utd_event(), context, &info),
479 UtdCause::HistoricalMessageAndDeviceIsUnverified
480 );
481 }
482
483 #[test]
484 fn test_if_backup_is_working_then_historical_utd_is_unexpected() {
485 let info = missing_megolm_session();
487
488 let mut context = device_new();
490
491 context.backup_exists_on_server = true;
493
494 context.is_backup_configured = true;
496
497 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
500
501 let info = unknown_megolm_message_index();
503 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
504 }
505
506 #[test]
507 fn test_if_backup_is_not_working_even_though_verified_then_historical_utd_is_unexpected() {
508 let info = missing_megolm_session();
510
511 let mut context = device_new();
513
514 context.backup_exists_on_server = true;
516
517 context.is_backup_configured = false;
519
520 context.this_device_is_verified = true;
522
523 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
530
531 let info = unknown_megolm_message_index();
533 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
534 }
535
536 fn utd_event() -> Raw<AnySyncTimelineEvent> {
537 raw_event(json!({
538 "type": "m.room.encrypted",
539 "event_id": "$0",
540 "content": {
542 "algorithm": "m.megolm.v1.aes-sha2",
543 "ciphertext": "FOO",
544 "sender_key": "SENDERKEYSENDERKEY",
545 "device_id": "ABCDEFGH",
546 "session_id": "A0",
547 },
548 "sender": "@bob:localhost",
549 "origin_server_ts": EVENT_TIME,
550 "unsigned": { "membership": "join" }
551 }))
552 }
553
554 fn raw_event(value: serde_json::Value) -> Raw<AnySyncTimelineEvent> {
555 Raw::from_json(to_raw_value(&value).unwrap())
556 }
557
558 fn device_old() -> CryptoContextInfo {
559 CryptoContextInfo {
560 device_creation_ts: MilliSecondsSinceUnixEpoch((BEFORE_EVENT_TIME).try_into().unwrap()),
561 this_device_is_verified: false,
562 is_backup_configured: false,
563 backup_exists_on_server: false,
564 }
565 }
566
567 fn device_new() -> CryptoContextInfo {
568 CryptoContextInfo {
569 device_creation_ts: MilliSecondsSinceUnixEpoch((AFTER_EVENT_TIME).try_into().unwrap()),
570 this_device_is_verified: false,
571 is_backup_configured: false,
572 backup_exists_on_server: false,
573 }
574 }
575
576 fn missing_megolm_session() -> UnableToDecryptInfo {
577 UnableToDecryptInfo {
578 session_id: None,
579 reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
580 }
581 }
582
583 fn malformed_encrypted_event() -> UnableToDecryptInfo {
584 UnableToDecryptInfo {
585 session_id: None,
586 reason: UnableToDecryptReason::MalformedEncryptedEvent,
587 }
588 }
589
590 fn unknown_megolm_message_index() -> UnableToDecryptInfo {
591 UnableToDecryptInfo {
592 session_id: None,
593 reason: UnableToDecryptReason::UnknownMegolmMessageIndex,
594 }
595 }
596
597 fn megolm_decryption_failure() -> UnableToDecryptInfo {
598 UnableToDecryptInfo {
599 session_id: None,
600 reason: UnableToDecryptReason::MegolmDecryptionFailure,
601 }
602 }
603
604 fn verification_violation() -> UnableToDecryptInfo {
605 UnableToDecryptInfo {
606 session_id: None,
607 reason: UnableToDecryptReason::SenderIdentityNotTrusted(
608 VerificationLevel::VerificationViolation,
609 ),
610 }
611 }
612
613 fn unsigned_device() -> UnableToDecryptInfo {
614 UnableToDecryptInfo {
615 session_id: None,
616 reason: UnableToDecryptReason::SenderIdentityNotTrusted(
617 VerificationLevel::UnsignedDevice,
618 ),
619 }
620 }
621
622 fn missing_device() -> UnableToDecryptInfo {
623 UnableToDecryptInfo {
624 session_id: None,
625 reason: UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::None(
626 DeviceLinkProblem::MissingDevice,
627 )),
628 }
629 }
630}