diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 6ef33dbfd20..f19d523eeae 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -628,6 +628,59 @@ pub struct UnableToDecryptInfo { /// `m.megolm.v1.aes-sha2` algorithm. #[serde(skip_serializing_if = "Option::is_none")] pub session_id: Option, + + /// Reason code for the decryption failure + #[serde(default = "unknown_utd_reason")] + pub reason: UnableToDecryptReason, +} + +fn unknown_utd_reason() -> UnableToDecryptReason { + UnableToDecryptReason::Unknown +} + +/// Reason code for a decryption failure +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum UnableToDecryptReason { + /// The reason for the decryption failure is unknown. This is only intended + /// for use when deserializing old UnableToDecryptInfo instances. + #[doc(hidden)] + Unknown, + + /// The `m.room.encrypted` event that should have been decrypted is + /// malformed in some way (e.g. unsupported algorithm, missing fields, + /// unknown megolm message type). + MalformedEncryptedEvent, + + /// Decryption failed because we're missing the megolm session that was used + /// to encrypt the event. + /// + /// TODO: support withheld codes? + MissingMegolmSession, + + /// Decryption failed because, while we have the megolm session that was + /// used to encrypt the message, it is ratcheted too far forward. + UnknownMegolmMessageIndex, + + /// We found the Megolm session, but were unable to decrypt the event using + /// that session for some reason (e.g. incorrect MAC). + /// + /// This represents all `vodozemac::megolm::DecryptionError`s, except + /// `UnknownMessageIndex`, which is represented as + /// `UnknownMegolmMessageIndex`. + MegolmDecryptionFailure, + + /// The event could not be deserialized after decryption. + PayloadDeserializationFailure, + + /// Decryption failed because of a mismatch between the identity keys of the + /// device we received the room key from and the identity keys recorded in + /// the plaintext of the room key to-device message. + MismatchedIdentityKeys, + + /// An encrypted message wasn't decrypted, because the sender's + /// cross-signing identity did not satisfy the requested + /// `TrustRequirement`. + SenderIdentityNotTrusted(VerificationLevel), } /// Deserialization helper for [`SyncTimelineEvent`], for the modern format. @@ -705,6 +758,8 @@ impl From for SyncTimelineEvent { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use assert_matches::assert_matches; use ruma::{ event_id, @@ -717,7 +772,8 @@ mod tests { use super::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, - TimelineEventKind, VerificationState, + TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, + UnsignedEventLocation, VerificationState, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -808,7 +864,13 @@ mod tests { }, verification_state: VerificationState::Verified, }, - unsigned_encryption_info: None, + unsigned_encryption_info: Some(BTreeMap::from([( + UnsignedEventLocation::RelationsReplace, + UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { + session_id: Some("xyz".to_owned()), + reason: UnableToDecryptReason::MalformedEncryptedEvent, + }), + )])), }), push_actions: Default::default(), }; @@ -840,6 +902,12 @@ mod tests { }, "verification_state": "Verified", }, + "unsigned_encryption_info": { + "RelationsReplace": {"UnableToDecrypt": { + "session_id": "xyz", + "reason": "MalformedEncryptedEvent", + }} + } } } }) @@ -881,5 +949,49 @@ mod tests { event.encryption_info().unwrap().algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { .. } ); + + // Test that the previous format, with an undecryptable unsigned event, can also + // be deserialized. + let serialized = json!({ + "event": { + "content": {"body": "secret", "msgtype": "m.text"}, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message", + }, + "encryption_info": { + "sender": "@sender:example.com", + "sender_device": null, + "algorithm_info": { + "MegolmV1AesSha2": { + "curve25519_key": "xxx", + "sender_claimed_keys": {} + } + }, + "verification_state": "Verified", + }, + "unsigned_encryption_info": { + "RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}} + } + }); + let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); + assert_matches!( + event.encryption_info().unwrap().algorithm_info, + AlgorithmInfo::MegolmV1AesSha2 { .. } + ); + assert_matches!(event.kind, TimelineEventKind::Decrypted(decrypted) => { + assert_matches!(decrypted.unsigned_encryption_info, Some(map) => { + assert_eq!(map.len(), 1); + let (location, result) = map.into_iter().next().unwrap(); + assert_eq!(location, UnsignedEventLocation::RelationsReplace); + assert_matches!(result, UnsignedDecryptionResult::UnableToDecrypt(utd_info) => { + assert_eq!(utd_info.session_id, Some("xyz".to_owned())); + assert_eq!(utd_info.reason, UnableToDecryptReason::Unknown); + }) + }); + }); } } diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index bbcf222dbdb..dd3ac96a215 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -2,6 +2,12 @@ Changes: +- Add new method `OlmMachine::try_decrypt_room_event`. + ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) + +- Add reason code to `matrix_sdk_common::deserialized_responses::UnableToDecryptInfo`. + ([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116)) + - The `UserIdentity` struct has been renamed to `OtherUserIdentity` ([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036])) diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index cfad6268a6d..b6c50526876 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -89,6 +89,7 @@ pub use identities::{ OwnUserIdentityData, UserDevices, UserIdentity, UserIdentityData, }; pub use machine::{CrossSigningBootstrapRequests, EncryptionSyncChanges, OlmMachine}; +use matrix_sdk_common::deserialized_responses::{DecryptedRoomEvent, UnableToDecryptInfo}; #[cfg(feature = "qrcode")] pub use matrix_sdk_qrcode; pub use olm::{Account, CrossSigningStatus, EncryptionSettings, Session}; @@ -142,3 +143,14 @@ pub struct DecryptionSettings { /// [`MegolmError::SenderIdentityNotTrusted`] will be returned. pub sender_device_trust_requirement: TrustRequirement, } + +/// The result of an attempt to decrypt a room event: either a successful +/// decryption, or information on a failure. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum RoomEventDecryptionResult { + /// A successfully-decrypted encrypted event. + Decrypted(DecryptedRoomEvent), + + /// We were unable to decrypt the event + UnableToDecrypt(UnableToDecryptInfo), +} diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index 80b2c1d44d5..3624f4aa2cb 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -22,7 +22,8 @@ use itertools::Itertools; use matrix_sdk_common::{ deserialized_responses::{ AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, UnableToDecryptInfo, - UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState, + UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, + VerificationState, }, BoxFuture, }; @@ -94,7 +95,7 @@ use crate::{ utilities::timestamp_to_iso8601, verification::{Verification, VerificationMachine, VerificationRequest}, CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, KeysQueryRequest, - LocalTrust, SignatureError, ToDeviceRequest, TrustRequirement, + LocalTrust, RoomEventDecryptionResult, SignatureError, ToDeviceRequest, TrustRequirement, }; /// State machine implementation of the Olm/Megolm encryption protocol used for @@ -1734,6 +1735,34 @@ impl OlmMachine { } } + /// Attempt to decrypt an event from a room timeline, returning information + /// on the failure if it fails. + /// + /// # Arguments + /// + /// * `event` - The event that should be decrypted. + /// + /// * `room_id` - The ID of the room where the event was sent to. + /// + /// # Returns + /// + /// The decrypted event, if it was successfully decrypted. Otherwise, + /// information on the failure, unless the failure was due to an + /// internal error, in which case, an `Err` result. + pub async fn try_decrypt_room_event( + &self, + raw_event: &Raw, + room_id: &RoomId, + decryption_settings: &DecryptionSettings, + ) -> Result { + match self.decrypt_room_event_inner(raw_event, room_id, true, decryption_settings).await { + Ok(decrypted) => Ok(RoomEventDecryptionResult::Decrypted(decrypted)), + Err(err) => Ok(RoomEventDecryptionResult::UnableToDecrypt(megolm_error_to_utd_info( + raw_event, err, + )?)), + } + } + /// Decrypt an event from a room timeline. /// /// # Arguments @@ -1902,18 +1931,13 @@ impl OlmMachine { *event = serde_json::to_value(decrypted_event.event).ok()?; Some(UnsignedDecryptionResult::Decrypted(decrypted_event.encryption_info)) } - Err(_) => { - let session_id = - raw_event.deserialize().ok().and_then(|ev| match ev.content.scheme { - RoomEventEncryptionScheme::MegolmV1AesSha2(s) => Some(s.session_id), - #[cfg(feature = "experimental-algorithms")] - RoomEventEncryptionScheme::MegolmV2AesSha2(s) => Some(s.session_id), - RoomEventEncryptionScheme::Unknown(_) => None, - }); - - Some(UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { - session_id, - })) + Err(err) => { + // For now, we throw away crypto store errors and just treat the unsigned event + // as unencrypted. Crypto store errors represent problems with the application + // rather than normal UTD errors, so they should probably be propagated + // rather than swallowed. + let utd_info = megolm_error_to_utd_info(&raw_event, err).ok()?; + Some(UnsignedDecryptionResult::UnableToDecrypt(utd_info)) } } }) @@ -2540,6 +2564,45 @@ pub struct EncryptionSyncChanges<'a> { pub next_batch_token: Option, } +/// Convert a [`MegolmError`] into an [`UnableToDecryptInfo`] or a +/// [`CryptoStoreError`]. +/// +/// Most `MegolmError` codes are converted into a suitable +/// `UnableToDecryptInfo`. The exception is [`MegolmError::Store`], which +/// represents a problem with our datastore rather than with the message itself, +/// and is therefore returned as a `CryptoStoreError`. +fn megolm_error_to_utd_info( + raw_event: &Raw, + error: MegolmError, +) -> Result { + use MegolmError::*; + let reason = match error { + EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent, + Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent, + MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession, + Decryption(DecryptionError::UnknownMessageIndex(_, _)) => { + UnableToDecryptReason::UnknownMegolmMessageIndex + } + Decryption(_) => UnableToDecryptReason::MegolmDecryptionFailure, + JsonError(_) => UnableToDecryptReason::PayloadDeserializationFailure, + MismatchedIdentityKeys(_) => UnableToDecryptReason::MismatchedIdentityKeys, + SenderIdentityNotTrusted(level) => UnableToDecryptReason::SenderIdentityNotTrusted(level), + + // Pass through crypto store errors, which indicate a problem with our + // application, rather than a UTD. + Store(error) => Err(error)?, + }; + + let session_id = raw_event.deserialize().ok().and_then(|ev| match ev.content.scheme { + RoomEventEncryptionScheme::MegolmV1AesSha2(s) => Some(s.session_id), + #[cfg(feature = "experimental-algorithms")] + RoomEventEncryptionScheme::MegolmV2AesSha2(s) => Some(s.session_id), + RoomEventEncryptionScheme::Unknown(_) => None, + }); + + Ok(UnableToDecryptInfo { session_id, reason }) +} + #[cfg(test)] pub(crate) mod test_helpers; diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 4a0dd8e60f8..21dabf2023a 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -14,11 +14,11 @@ use std::{collections::BTreeMap, iter, ops::Not, sync::Arc, time::Duration}; -use assert_matches2::assert_matches; +use assert_matches2::{assert_let, assert_matches}; use futures_util::{pin_mut, FutureExt, StreamExt}; use itertools::Itertools; use matrix_sdk_common::deserialized_responses::{ - UnableToDecryptInfo, UnsignedDecryptionResult, UnsignedEventLocation, + UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, }; use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json}; use ruma::{ @@ -71,7 +71,7 @@ use crate::{ utilities::json_convert, verification::tests::bob_id, Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError, - OutgoingRequests, ToDeviceRequest, TrustRequirement, + OutgoingRequests, RoomEventDecryptionResult, ToDeviceRequest, TrustRequirement, }; mod decryption_verification_state; @@ -555,13 +555,11 @@ async fn test_megolm_encryption() { let decryption_settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted }; - let decrypted_event = bob - .decrypt_room_event(&event, room_id, &decryption_settings) - .await - .unwrap() - .event - .deserialize() - .unwrap(); + + let decryption_result = + bob.try_decrypt_room_event(&event, room_id, &decryption_settings).await.unwrap(); + assert_let!(RoomEventDecryptionResult::Decrypted(decrypted_event) = decryption_result); + let decrypted_event = decrypted_event.event.deserialize().unwrap(); if let AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original( OriginalMessageLikeEvent { sender, content, .. }, @@ -678,6 +676,13 @@ async fn test_withheld_unverified() { let err = decrypt_result.err().unwrap(); assert_matches!(err, MegolmError::MissingRoomKey(Some(WithheldCode::Unverified))); + + // Also check `try_decrypt_room_event`. + let decrypt_result = + bob.try_decrypt_room_event(&room_event, room_id, &decryption_settings).await.unwrap(); + assert_let!(RoomEventDecryptionResult::UnableToDecrypt(utd_info) = decrypt_result); + assert!(utd_info.session_id.is_some()); + assert_eq!(utd_info.reason, UnableToDecryptReason::MissingMegolmSession); } /// Test what happens when we feed an unencrypted event into the decryption @@ -1355,7 +1360,8 @@ async fn test_unsigned_decryption() { assert_matches!( replace_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { - session_id: Some(second_room_key_session_id) + session_id: Some(second_room_key_session_id), + reason: UnableToDecryptReason::MissingMegolmSession, }) ); @@ -1460,7 +1466,8 @@ async fn test_unsigned_decryption() { assert_matches!( thread_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { - session_id: Some(third_room_key_session_id) + session_id: Some(third_room_key_session_id), + reason: UnableToDecryptReason::MissingMegolmSession, }) );