matrix_sdk_crypto/types/events/
utd_cause.rs

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
15use matrix_sdk_common::deserialized_responses::{
16    UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, WithheldCode,
17};
18use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch};
19use serde::Deserialize;
20
21/// Our best guess at the reason why an event can't be decrypted.
22#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
24pub enum UtdCause {
25    /// We don't have an explanation for why this UTD happened - it is probably
26    /// a bug, or a network split between the two homeservers.
27    ///
28    /// For example:
29    ///
30    /// - the keys for this event are missing, but a key storage backup exists
31    ///   and is working, so we should be able to find the keys in the backup.
32    ///
33    /// - the keys for this event are missing, and a key storage backup exists
34    ///   on the server, but that backup is not working on this client even
35    ///   though this device is verified.
36    #[default]
37    Unknown = 0,
38
39    /// We are missing the keys for this event, and the event was sent when we
40    /// were not a member of the room (or invited).
41    SentBeforeWeJoined = 1,
42
43    /// The message was sent by a user identity we have not verified, but the
44    /// user was previously verified.
45    VerificationViolation = 2,
46
47    /// The [`crate::TrustRequirement`] requires that the sending device be
48    /// signed by its owner, and it was not.
49    UnsignedDevice = 3,
50
51    /// The [`crate::TrustRequirement`] requires that the sending device be
52    /// signed by its owner, and we were unable to securely find the device.
53    ///
54    /// This could be because the device has since been deleted, because we
55    /// haven't yet downloaded it from the server, or because the session
56    /// data was obtained from an insecure source (imported from a file,
57    /// obtained from a legacy (asymmetric) backup, unsafe key forward, etc.)
58    UnknownDevice = 4,
59
60    /// We are missing the keys for this event, but it is a "device-historical"
61    /// message and there is no key storage backup on the server, presumably
62    /// because the user has turned it off.
63    ///
64    /// Device-historical means that the message was sent before the current
65    /// device existed (but the current user was probably a member of the room
66    /// at the time the message was sent). Not to
67    /// be confused with pre-join or pre-invite messages (see
68    /// [`UtdCause::SentBeforeWeJoined`] for that).
69    ///
70    /// Expected message to user: "History is not available on this device".
71    HistoricalMessageAndBackupIsDisabled = 5,
72
73    /// The keys for this event are intentionally withheld.
74    ///
75    /// The sender has refused to share the key because our device does not meet
76    /// the sender's security requirements.
77    WithheldForUnverifiedOrInsecureDevice = 6,
78
79    /// The keys for this event are missing, likely because the sender was
80    /// unable to share them (e.g., failure to establish an Olm 1:1
81    /// channel). Alternatively, the sender may have deliberately excluded
82    /// this device by cherry-picking and blocking it, in which case, no action
83    /// can be taken on our side.
84    WithheldBySender = 7,
85
86    /// We are missing the keys for this event, but it is a "device-historical"
87    /// message, and even though a key storage backup does exist, we can't use
88    /// it because our device is unverified.
89    ///
90    /// Device-historical means that the message was sent before the current
91    /// device existed (but the current user was probably a member of the room
92    /// at the time the message was sent). Not to
93    /// be confused with pre-join or pre-invite messages (see
94    /// [`UtdCause::SentBeforeWeJoined`] for that).
95    ///
96    /// Expected message to user: "You need to verify this device".
97    HistoricalMessageAndDeviceIsUnverified = 8,
98}
99
100/// MSC4115 membership info in the unsigned area.
101#[derive(Deserialize)]
102struct UnsignedWithMembership {
103    #[serde(alias = "io.element.msc4115.membership")]
104    membership: Membership,
105}
106
107/// MSC4115 contents of the membership property
108#[derive(Deserialize)]
109#[serde(rename_all = "lowercase")]
110enum Membership {
111    Leave,
112    Invite,
113    Join,
114}
115
116/// Contextual crypto information used by [`UtdCause::determine`] to properly
117/// identify an Unable-To-Decrypt cause in addition to the
118/// [`UnableToDecryptInfo`] and raw event info.
119#[derive(Debug, Clone, Copy)]
120pub struct CryptoContextInfo {
121    /// The current device creation timestamp, used as a heuristic to determine
122    /// if an event is device-historical or not (sent before the current device
123    /// existed).
124    pub device_creation_ts: MilliSecondsSinceUnixEpoch,
125
126    /// True if this device is secure because it has been verified by us
127    pub this_device_is_verified: bool,
128
129    /// True if key storage exists on the server, even if we are unable to use
130    /// it
131    pub backup_exists_on_server: bool,
132
133    /// True if key storage is correctly set up and can be used by the current
134    /// client to download and decrypt message keys.
135    pub is_backup_configured: bool,
136}
137
138impl UtdCause {
139    /// Decide the cause of this UTD, based on the evidence we have.
140    pub fn determine(
141        raw_event: &Raw<AnySyncTimelineEvent>,
142        crypto_context_info: CryptoContextInfo,
143        unable_to_decrypt_info: &UnableToDecryptInfo,
144    ) -> Self {
145        // TODO: in future, use more information to give a richer answer. E.g.
146        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                // Look in the unsigned area for a `membership` field.
160                if let Some(unsigned) =
161                    raw_event.get_field::<UnsignedWithMembership>("unsigned").ok().flatten()
162                {
163                    if let Membership::Leave = unsigned.membership {
164                        // We were not a member - this is the cause of the UTD
165                        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                        // This event was sent before this device existed, so it is "historical"
172                        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    /**
196     * Below is the flow chart we follow for deciding whether historical
197     * UTDs are expected. This function starts at position `B`.
198     *
199     * ```text
200     * A: Is the message newer than the device?
201     *   No -> B
202     *   Yes - Normal UTD error
203     *
204     * B: Is there a backup on the server?
205     *   No -> History is not available on this device
206     *   Yes -> C
207     *
208     * C: Is backup working on this device?
209     *   No -> D
210     *   Yes -> Normal UTD error
211     *
212     * D: Is this device verified?
213     *   No -> You need to verify this device
214     *   Yes -> Normal UTD error
215     * ```
216     */
217    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            // We didn't get the key from key storage backup, but we think we should have,
228            // because either:
229            //
230            // * backup is working (so why didn't we get it?), or
231            // * backup is not working for an unknown reason (because the device is
232            //   verified, and that is the only reason we check).
233            //
234            // In either case, we shrug and give an `Unknown` cause.
235            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        // If our JSON contains no membership info, then we guess the UTD is unknown.
257        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        // If our JSON contains a membership property but not the JSON we expected, then
266        // we guess the UTD is unknown.
267        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        // If membership=invite then we expected to be sent the keys so the cause of the
280        // UTD is unknown.
281        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        // If membership=join then we expected to be sent the keys so the cause of the
294        // UTD is unknown.
295        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        // If membership=leave then we have an explanation for why we can't decrypt,
308        // until we have MSC3061.
309        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        // If the UnableToDecryptReason is other than MissingMegolmSession or
322        // UnknownMegolmMessageIndex, we do not know the reason for the failure
323        // even if membership=leave.
324        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        // Before MSC4115 is merged, we support the unstable prefix too.
337        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        // Message key is missing.
374        let info = missing_megolm_session();
375
376        // The device is old.
377        let context = device_old();
378
379        // So we have no explanation for this UTD.
380        assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
381
382        // Same for unknown megolm message index
383        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        // Message key is missing.
390        let info = missing_megolm_session();
391
392        // The device is new.
393        let mut context = device_new();
394
395        // There is no key storage backup on the server.
396        context.backup_exists_on_server = false;
397
398        // So this UTD is expected, and the solution (for future messages!) is to turn
399        // on key storage backups.
400        assert_eq!(
401            UtdCause::determine(&utd_event(), context, &info),
402            UtdCause::HistoricalMessageAndBackupIsDisabled
403        );
404
405        // Same for unknown megolm message index
406        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        // The event was malformed.
416        let info = malformed_encrypted_event();
417
418        // The device is new.
419        let mut context = device_new();
420
421        // There is no key storage backup on the server.
422        context.backup_exists_on_server = false;
423
424        // So this could be expected historical like the previous test, but because the
425        // encrypted event is malformed, that takes precedence, and it's unexpected.
426        assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
427
428        // Same for decryption failures
429        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        // Message key is missing.
436        let info = missing_megolm_session();
437
438        // The device is new.
439        let mut context = device_new();
440
441        // There is a key storage backup on the server.
442        context.backup_exists_on_server = true;
443
444        // The key storage backup is not working,
445        context.is_backup_configured = false;
446
447        // probably because...
448        // Our device is not verified.
449        context.this_device_is_verified = false;
450
451        // So this UTD is expected, and the solution is (hopefully) to verify.
452        assert_eq!(
453            UtdCause::determine(&utd_event(), context, &info),
454            UtdCause::HistoricalMessageAndDeviceIsUnverified
455        );
456
457        // Same for unknown megolm message index
458        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        // Message key is missing.
468        let info = missing_megolm_session();
469
470        // The device is new.
471        let mut context = device_new();
472
473        // There is a key storage backup on the server.
474        context.backup_exists_on_server = true;
475
476        // The key storage backup is working.
477        context.is_backup_configured = true;
478
479        // So this UTD is unexpected since we should be able to fetch the key from
480        // storage.
481        assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
482
483        // Same for unknown megolm message index
484        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        // Message key is missing.
491        let info = missing_megolm_session();
492
493        // The device is new.
494        let mut context = device_new();
495
496        // There is a key storage backup on the server.
497        context.backup_exists_on_server = true;
498
499        // The key storage backup is working.
500        context.is_backup_configured = false;
501
502        // even though...
503        // Our device is verified.
504        context.this_device_is_verified = true;
505
506        // So this UTD is unexpected since we can't explain why our backup is not
507        // working.
508        //
509        // TODO: it might be nice to tell the user that our backup is not working!
510        // Currently we don't distinguish between Unknown cases, since we want
511        // to make sure they are all reported as unexpected UTDs.
512        assert_eq!(UtdCause::determine(&utd_event(), context, &info), UtdCause::Unknown);
513
514        // Same for unknown megolm message index
515        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            // the values don't matter much but the expected fields should be there.
524            "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}