matrix_sdk_crypto/types/events/
utd_cause.rs1use matrix_sdk_common::deserialized_responses::{
16 UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, WithheldCode,
17};
18use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch};
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,
72
73 WithheldForUnverifiedOrInsecureDevice = 6,
78
79 WithheldBySender = 7,
85
86 HistoricalMessageAndDeviceIsUnverified = 8,
98}
99
100#[derive(Deserialize)]
102struct UnsignedWithMembership {
103 #[serde(alias = "io.element.msc4115.membership")]
104 membership: Membership,
105}
106
107#[derive(Deserialize)]
109#[serde(rename_all = "lowercase")]
110enum Membership {
111 Leave,
112 Invite,
113 Join,
114}
115
116#[derive(Debug, Clone, Copy)]
120pub struct CryptoContextInfo {
121 pub device_creation_ts: MilliSecondsSinceUnixEpoch,
125
126 pub this_device_is_verified: bool,
128
129 pub backup_exists_on_server: bool,
132
133 pub is_backup_configured: bool,
136}
137
138impl UtdCause {
139 pub fn determine(
141 raw_event: &Raw<AnySyncTimelineEvent>,
142 crypto_context_info: CryptoContextInfo,
143 unable_to_decrypt_info: &UnableToDecryptInfo,
144 ) -> Self {
145 match &unable_to_decrypt_info.reason {
147 UnableToDecryptReason::MissingMegolmSession { withheld_code: Some(reason) } => {
148 match reason {
149 WithheldCode::Unverified => UtdCause::WithheldForUnverifiedOrInsecureDevice,
150 WithheldCode::Blacklisted
151 | WithheldCode::Unauthorised
152 | WithheldCode::Unavailable
153 | WithheldCode::NoOlm
154 | WithheldCode::_Custom(_) => UtdCause::WithheldBySender,
155 }
156 }
157 UnableToDecryptReason::MissingMegolmSession { withheld_code: None }
158 | UnableToDecryptReason::UnknownMegolmMessageIndex => {
159 if let Some(unsigned) =
161 raw_event.get_field::<UnsignedWithMembership>("unsigned").ok().flatten()
162 {
163 if let Membership::Leave = unsigned.membership {
164 return UtdCause::SentBeforeWeJoined;
166 }
167 }
168
169 if let Ok(timeline_event) = raw_event.deserialize() {
170 if timeline_event.origin_server_ts() < crypto_context_info.device_creation_ts {
171 return UtdCause::determine_historical(crypto_context_info);
173 }
174 }
175
176 UtdCause::Unknown
177 }
178
179 UnableToDecryptReason::SenderIdentityNotTrusted(
180 VerificationLevel::VerificationViolation,
181 ) => UtdCause::VerificationViolation,
182
183 UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::UnsignedDevice) => {
184 UtdCause::UnsignedDevice
185 }
186
187 UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::None(_)) => {
188 UtdCause::UnknownDevice
189 }
190
191 _ => UtdCause::Unknown,
192 }
193 }
194
195 fn determine_historical(crypto_context_info: CryptoContextInfo) -> UtdCause {
218 let backup_disabled = !crypto_context_info.backup_exists_on_server;
219 let backup_failing = !crypto_context_info.is_backup_configured;
220 let unverified = !crypto_context_info.this_device_is_verified;
221
222 if backup_disabled {
223 UtdCause::HistoricalMessageAndBackupIsDisabled
224 } else if backup_failing && unverified {
225 UtdCause::HistoricalMessageAndDeviceIsUnverified
226 } else {
227 UtdCause::Unknown
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use matrix_sdk_common::deserialized_responses::{
243 DeviceLinkProblem, UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel,
244 };
245 use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch};
246 use serde_json::{json, value::to_raw_value};
247
248 use crate::types::events::{utd_cause::CryptoContextInfo, UtdCause};
249
250 const EVENT_TIME: usize = 5555;
251 const BEFORE_EVENT_TIME: usize = 1111;
252 const AFTER_EVENT_TIME: usize = 9999;
253
254 #[test]
255 fn test_if_there_is_no_membership_info_we_guess_unknown() {
256 assert_eq!(
258 UtdCause::determine(&raw_event(json!({})), device_old(), &missing_megolm_session()),
259 UtdCause::Unknown
260 );
261 }
262
263 #[test]
264 fn test_if_membership_info_cant_be_parsed_we_guess_unknown() {
265 assert_eq!(
268 UtdCause::determine(
269 &raw_event(json!({ "unsigned": { "membership": 3 } })),
270 device_old(),
271 &missing_megolm_session()
272 ),
273 UtdCause::Unknown
274 );
275 }
276
277 #[test]
278 fn test_if_membership_is_invite_we_guess_unknown() {
279 assert_eq!(
282 UtdCause::determine(
283 &raw_event(json!({ "unsigned": { "membership": "invite" } }),),
284 device_old(),
285 &missing_megolm_session()
286 ),
287 UtdCause::Unknown
288 );
289 }
290
291 #[test]
292 fn test_if_membership_is_join_we_guess_unknown() {
293 assert_eq!(
296 UtdCause::determine(
297 &raw_event(json!({ "unsigned": { "membership": "join" } })),
298 device_old(),
299 &missing_megolm_session()
300 ),
301 UtdCause::Unknown
302 );
303 }
304
305 #[test]
306 fn test_if_membership_is_leave_we_guess_membership() {
307 assert_eq!(
310 UtdCause::determine(
311 &raw_event(json!({ "unsigned": { "membership": "leave" } })),
312 device_old(),
313 &missing_megolm_session()
314 ),
315 UtdCause::SentBeforeWeJoined
316 );
317 }
318
319 #[test]
320 fn test_if_reason_is_not_missing_key_we_guess_unknown_even_if_membership_is_leave() {
321 assert_eq!(
325 UtdCause::determine(
326 &raw_event(json!({ "unsigned": { "membership": "leave" } })),
327 device_old(),
328 &malformed_encrypted_event()
329 ),
330 UtdCause::Unknown
331 );
332 }
333
334 #[test]
335 fn test_if_unstable_prefix_membership_is_leave_we_guess_membership() {
336 assert_eq!(
338 UtdCause::determine(
339 &raw_event(json!({ "unsigned": { "io.element.msc4115.membership": "leave" } })),
340 device_old(),
341 &missing_megolm_session()
342 ),
343 UtdCause::SentBeforeWeJoined
344 );
345 }
346
347 #[test]
348 fn test_verification_violation_is_passed_through() {
349 assert_eq!(
350 UtdCause::determine(&raw_event(json!({})), device_old(), &verification_violation()),
351 UtdCause::VerificationViolation
352 );
353 }
354
355 #[test]
356 fn test_unsigned_device_is_passed_through() {
357 assert_eq!(
358 UtdCause::determine(&raw_event(json!({})), device_old(), &unsigned_device()),
359 UtdCause::UnsignedDevice
360 );
361 }
362
363 #[test]
364 fn test_unknown_device_is_passed_through() {
365 assert_eq!(
366 UtdCause::determine(&raw_event(json!({})), device_old(), &missing_device()),
367 UtdCause::UnknownDevice
368 );
369 }
370
371 #[test]
372 fn test_old_devices_dont_cause_historical_utds() {
373 let info = missing_megolm_session();
375
376 let context = device_old();
378
379 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
381
382 let info = unknown_megolm_message_index();
384 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
385 }
386
387 #[test]
388 fn test_if_backup_is_disabled_historical_utd_is_expected() {
389 let info = missing_megolm_session();
391
392 let mut context = device_new();
394
395 context.backup_exists_on_server = false;
397
398 assert_eq!(
401 UtdCause::determine(&utd_event(), context, &info),
402 UtdCause::HistoricalMessageAndBackupIsDisabled
403 );
404
405 let info = unknown_megolm_message_index();
407 assert_eq!(
408 UtdCause::determine(&utd_event(), context, &info),
409 UtdCause::HistoricalMessageAndBackupIsDisabled
410 );
411 }
412
413 #[test]
414 fn test_malformed_events_are_never_expected_utds() {
415 let info = malformed_encrypted_event();
417
418 let mut context = device_new();
420
421 context.backup_exists_on_server = false;
423
424 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
427
428 let info = megolm_decryption_failure();
430 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
431 }
432
433 #[test]
434 fn test_new_devices_with_nonworking_backups_because_unverified_cause_expected_utds() {
435 let info = missing_megolm_session();
437
438 let mut context = device_new();
440
441 context.backup_exists_on_server = true;
443
444 context.is_backup_configured = false;
446
447 context.this_device_is_verified = false;
450
451 assert_eq!(
453 UtdCause::determine(&utd_event(), context, &info),
454 UtdCause::HistoricalMessageAndDeviceIsUnverified
455 );
456
457 let info = unknown_megolm_message_index();
459 assert_eq!(
460 UtdCause::determine(&utd_event(), context, &info),
461 UtdCause::HistoricalMessageAndDeviceIsUnverified
462 );
463 }
464
465 #[test]
466 fn test_if_backup_is_working_then_historical_utd_is_unexpected() {
467 let info = missing_megolm_session();
469
470 let mut context = device_new();
472
473 context.backup_exists_on_server = true;
475
476 context.is_backup_configured = true;
478
479 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
482
483 let info = unknown_megolm_message_index();
485 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
486 }
487
488 #[test]
489 fn test_if_backup_is_not_working_even_though_verified_then_historical_utd_is_unexpected() {
490 let info = missing_megolm_session();
492
493 let mut context = device_new();
495
496 context.backup_exists_on_server = true;
498
499 context.is_backup_configured = false;
501
502 context.this_device_is_verified = true;
505
506 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
513
514 let info = unknown_megolm_message_index();
516 assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
517 }
518
519 fn utd_event() -> Raw<AnySyncTimelineEvent> {
520 raw_event(json!({
521 "type": "m.room.encrypted",
522 "event_id": "$0",
523 "content": {
525 "algorithm": "m.megolm.v1.aes-sha2",
526 "ciphertext": "FOO",
527 "sender_key": "SENDERKEYSENDERKEY",
528 "device_id": "ABCDEFGH",
529 "session_id": "A0",
530 },
531 "sender": "@bob:localhost",
532 "origin_server_ts": EVENT_TIME,
533 "unsigned": { "membership": "join" }
534 }))
535 }
536
537 fn raw_event(value: serde_json::Value) -> Raw<AnySyncTimelineEvent> {
538 Raw::from_json(to_raw_value(&value).unwrap())
539 }
540
541 fn device_old() -> CryptoContextInfo {
542 CryptoContextInfo {
543 device_creation_ts: MilliSecondsSinceUnixEpoch((BEFORE_EVENT_TIME).try_into().unwrap()),
544 this_device_is_verified: false,
545 is_backup_configured: false,
546 backup_exists_on_server: false,
547 }
548 }
549
550 fn device_new() -> CryptoContextInfo {
551 CryptoContextInfo {
552 device_creation_ts: MilliSecondsSinceUnixEpoch((AFTER_EVENT_TIME).try_into().unwrap()),
553 this_device_is_verified: false,
554 is_backup_configured: false,
555 backup_exists_on_server: false,
556 }
557 }
558
559 fn missing_megolm_session() -> UnableToDecryptInfo {
560 UnableToDecryptInfo {
561 session_id: None,
562 reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
563 }
564 }
565
566 fn malformed_encrypted_event() -> UnableToDecryptInfo {
567 UnableToDecryptInfo {
568 session_id: None,
569 reason: UnableToDecryptReason::MalformedEncryptedEvent,
570 }
571 }
572
573 fn unknown_megolm_message_index() -> UnableToDecryptInfo {
574 UnableToDecryptInfo {
575 session_id: None,
576 reason: UnableToDecryptReason::UnknownMegolmMessageIndex,
577 }
578 }
579
580 fn megolm_decryption_failure() -> UnableToDecryptInfo {
581 UnableToDecryptInfo {
582 session_id: None,
583 reason: UnableToDecryptReason::MegolmDecryptionFailure,
584 }
585 }
586
587 fn verification_violation() -> UnableToDecryptInfo {
588 UnableToDecryptInfo {
589 session_id: None,
590 reason: UnableToDecryptReason::SenderIdentityNotTrusted(
591 VerificationLevel::VerificationViolation,
592 ),
593 }
594 }
595
596 fn unsigned_device() -> UnableToDecryptInfo {
597 UnableToDecryptInfo {
598 session_id: None,
599 reason: UnableToDecryptReason::SenderIdentityNotTrusted(
600 VerificationLevel::UnsignedDevice,
601 ),
602 }
603 }
604
605 fn missing_device() -> UnableToDecryptInfo {
606 UnableToDecryptInfo {
607 session_id: None,
608 reason: UnableToDecryptReason::SenderIdentityNotTrusted(VerificationLevel::None(
609 DeviceLinkProblem::MissingDevice,
610 )),
611 }
612 }
613}