diff --git a/Cargo.lock b/Cargo.lock index be7d083fb1a..cdd479221ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3369,6 +3369,7 @@ dependencies = [ "glob", "itertools 0.13.0", "matrix-sdk-base", + "matrix-sdk-common", "matrix-sdk-crypto", "matrix-sdk-store-encryption", "matrix-sdk-test", diff --git a/bindings/matrix-sdk-crypto-ffi/src/error.rs b/bindings/matrix-sdk-crypto-ffi/src/error.rs index f2a0a2af47e..116244b68cf 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/error.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/error.rs @@ -112,7 +112,7 @@ mod tests { #[test] fn test_withheld_error_mapping() { - use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode; + use matrix_sdk_common::deserialized_responses::WithheldCode; let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified)); diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 19957e1aa08..79f0276f8c0 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -2656,7 +2656,7 @@ mod tests { .unwrap(), UnableToDecryptInfo { session_id: Some("".to_owned()), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }, ) } diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 2eed248ac36..a1b07c0267e 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -17,7 +17,10 @@ use std::{collections::BTreeMap, fmt}; use ruma::{ events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent}, push::Action, - serde::{JsonObject, Raw}, + serde::{ + AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw, + SerializeAsRefStr, + }, DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId, }; use serde::{Deserialize, Serialize}; @@ -666,7 +669,7 @@ pub struct UnableToDecryptInfo { pub session_id: Option, /// Reason code for the decryption failure - #[serde(default = "unknown_utd_reason")] + #[serde(default = "unknown_utd_reason", deserialize_with = "deserialize_utd_reason")] pub reason: UnableToDecryptReason, } @@ -674,6 +677,24 @@ fn unknown_utd_reason() -> UnableToDecryptReason { UnableToDecryptReason::Unknown } +/// Provides basic backward compatibility for deserializing older serialized +/// `UnableToDecryptReason` values. +pub fn deserialize_utd_reason<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + // Start by deserializing as to an untyped JSON value. + let v: serde_json::Value = Deserialize::deserialize(d)?; + // Backwards compatibility: `MissingMegolmSession` used to be stored without the + // withheld code. + if v.as_str().is_some_and(|s| s == "MissingMegolmSession") { + return Ok(UnableToDecryptReason::MissingMegolmSession { withheld_code: None }); + } + // Otherwise, use the derived deserialize impl to turn the JSON into a + // UnableToDecryptReason + serde_json::from_value::(v).map_err(serde::de::Error::custom) +} + /// Reason code for a decryption failure #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum UnableToDecryptReason { @@ -689,9 +710,11 @@ pub enum UnableToDecryptReason { /// Decryption failed because we're missing the megolm session that was used /// to encrypt the event. - /// - /// TODO: support withheld codes? - MissingMegolmSession, + MissingMegolmSession { + /// If the key was withheld on purpose, the associated code. `None` + /// means no withheld code was received. + withheld_code: Option, + }, /// Decryption failed because, while we have the megolm session that was /// used to encrypt the message, it is ratcheted too far forward. @@ -723,7 +746,86 @@ impl UnableToDecryptReason { /// Returns true if this UTD is due to a missing room key (and hence might /// resolve itself if we wait a bit.) pub fn is_missing_room_key(&self) -> bool { - matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex) + // In case of MissingMegolmSession with a withheld code we return false here + // given that this API is used to decide if waiting a bit will help. + matches!( + self, + Self::MissingMegolmSession { withheld_code: None } | Self::UnknownMegolmMessageIndex + ) + } +} + +/// A machine-readable code for why a Megolm key was not sent. +/// +/// Normally sent as the payload of an [`m.room_key.withheld`](https://spec.matrix.org/v1.12/client-server-api/#mroom_keywithheld) to-device message. +#[derive( + Clone, + PartialEq, + Eq, + Hash, + AsStrAsRefStr, + AsRefStr, + FromString, + DebugAsRefStr, + SerializeAsRefStr, + DeserializeFromCowStr, +)] +pub enum WithheldCode { + /// the user/device was blacklisted. + #[ruma_enum(rename = "m.blacklisted")] + Blacklisted, + + /// the user/devices is unverified. + #[ruma_enum(rename = "m.unverified")] + Unverified, + + /// The user/device is not allowed have the key. For example, this would + /// usually be sent in response to a key request if the user was not in + /// the room when the message was sent. + #[ruma_enum(rename = "m.unauthorised")] + Unauthorised, + + /// Sent in reply to a key request if the device that the key is requested + /// from does not have the requested key. + #[ruma_enum(rename = "m.unavailable")] + Unavailable, + + /// An olm session could not be established. + /// This may happen, for example, if the sender was unable to obtain a + /// one-time key from the recipient. + #[ruma_enum(rename = "m.no_olm")] + NoOlm, + + #[doc(hidden)] + _Custom(PrivOwnedStr), +} + +impl fmt::Display for WithheldCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let string = match self { + WithheldCode::Blacklisted => "The sender has blocked you.", + WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.", + WithheldCode::Unauthorised => "You are not authorised to read the message.", + WithheldCode::Unavailable => "The requested key was not found.", + WithheldCode::NoOlm => "Unable to establish a secure channel.", + _ => self.as_str(), + }; + + f.write_str(string) + } +} + +// The Ruma macro expects the type to have this name. +// The payload is counter intuitively made public in order to avoid having +// multiple copies of this struct. +#[doc(hidden)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PrivOwnedStr(pub Box); + +#[cfg(not(tarpaulin_include))] +impl fmt::Debug for PrivOwnedStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) } } @@ -817,7 +919,7 @@ mod tests { use super::{ AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent, TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, - UnsignedEventLocation, VerificationState, + UnsignedEventLocation, VerificationState, WithheldCode, }; use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel}; @@ -1038,4 +1140,111 @@ mod tests { }); }); } + + #[test] + fn sync_timeline_event_deserialisation_migration_for_withheld() { + // Old serialized version was + // "utd_info": { + // "reason": "MissingMegolmSession", + // "session_id": "session000" + // } + + // The new version would be + // "utd_info": { + // "reason": { + // "MissingMegolmSession": { + // "withheld_code": null + // } + // }, + // "session_id": "session000" + // } + + let serialized = json!({ + "kind": { + "UnableToDecrypt": { + "event": { + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "AwgAEoABzL1JYhqhjW9jXrlT3M6H8mJ4qffYtOQOnPuAPNxsuG20oiD/Fnpv6jnQGhU6YbV9pNM+1mRnTvxW3CbWOPjLKqCWTJTc7Q0vDEVtYePg38ncXNcwMmfhgnNAoW9S7vNs8C003x3yUl6NeZ8bH+ci870BZL+kWM/lMl10tn6U7snNmSjnE3ckvRdO+11/R4//5VzFQpZdf4j036lNSls/WIiI67Fk9iFpinz9xdRVWJFVdrAiPFwb8L5xRZ8aX+e2JDMlc1eW8gk", + "device_id": "SKCGPNUWAU", + "sender_key": "Gim/c7uQdSXyrrUbmUOrBT6sMC0gO7QSLmOK6B7NOm0", + "session_id": "hgLyeSqXfb8vc5AjQLsg6TSHVu0HJ7HZ4B6jgMvxkrs" + }, + "event_id": "$xxxxx:example.org", + "origin_server_ts": 2189, + "room_id": "!someroom:example.com", + "sender": "@carl:example.com", + "type": "m.room.message" + }, + "utd_info": { + "reason": "MissingMegolmSession", + "session_id": "session000" + } + } + } + }); + + let result = serde_json::from_value(serialized); + assert!(result.is_ok()); + + // should have migrated to the new format + let event: SyncTimelineEvent = result.unwrap(); + assert_matches!( + event.kind, + TimelineEventKind::UnableToDecrypt { utd_info, .. }=> { + assert_matches!( + utd_info.reason, + UnableToDecryptReason::MissingMegolmSession { withheld_code: None } + ); + } + ) + } + + #[test] + fn unable_to_decrypt_info_migration_for_withheld() { + let old_format = json!({ + "reason": "MissingMegolmSession", + "session_id": "session000" + }); + + let deserialized = serde_json::from_value::(old_format).unwrap(); + let session_id = Some("session000".to_owned()); + + assert_eq!(deserialized.session_id, session_id); + assert_eq!( + deserialized.reason, + UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, + ); + + let new_format = json!({ + "session_id": "session000", + "reason": { + "MissingMegolmSession": { + "withheld_code": null + } + } + }); + + let deserialized = serde_json::from_value::(new_format).unwrap(); + + assert_eq!( + deserialized.reason, + UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, + ); + assert_eq!(deserialized.session_id, session_id); + } + + #[test] + fn unable_to_decrypt_reason_is_missing_room_key() { + let reason = UnableToDecryptReason::MissingMegolmSession { withheld_code: None }; + assert!(reason.is_missing_room_key()); + + let reason = UnableToDecryptReason::MissingMegolmSession { + withheld_code: Some(WithheldCode::Blacklisted), + }; + assert!(!reason.is_missing_room_key()); + + let reason = UnableToDecryptReason::UnknownMegolmMessageIndex; + assert!(reason.is_missing_room_key()); + } } diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index e069c95dd4e..92e18098859 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +- Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`. + These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors + when the sender either did not wish to share or was unable to share the room_key. + ([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305)) + ## [0.8.0] - 2024-11-19 ### Features diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 2e3348dce11..7a8ba63a8ed 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; -use matrix_sdk_common::deserialized_responses::VerificationLevel; +use matrix_sdk_common::deserialized_responses::{VerificationLevel, WithheldCode}; use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId}; use serde::{ser::SerializeMap, Serializer}; use serde_json::Error as SerdeError; @@ -22,10 +22,7 @@ use thiserror::Error; use vodozemac::{Curve25519PublicKey, Ed25519PublicKey}; use super::store::CryptoStoreError; -use crate::{ - olm::SessionExportError, - types::{events::room_key_withheld::WithheldCode, SignedKey}, -}; +use crate::{olm::SessionExportError, types::SignedKey}; #[cfg(doc)] use crate::{CollectStrategy, Device, LocalTrust, OtherUserIdentity}; diff --git a/crates/matrix-sdk-crypto/src/identities/device.rs b/crates/matrix-sdk-crypto/src/identities/device.rs index d518c12ac7a..da84d068a41 100644 --- a/crates/matrix-sdk-crypto/src/identities/device.rs +++ b/crates/matrix-sdk-crypto/src/identities/device.rs @@ -21,6 +21,7 @@ use std::{ }, }; +use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{ api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest, events::{key::verification::VerificationMethod, AnyToDeviceEventContent}, @@ -48,8 +49,7 @@ use crate::{ types::{ events::{ forwarded_room_key::ForwardedRoomKeyContent, - room::encrypted::ToDeviceEncryptedEventContent, room_key_withheld::WithheldCode, - EventType, + room::encrypted::ToDeviceEncryptedEventContent, EventType, }, requests::{OutgoingVerificationRequest, ToDeviceRequest}, DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures, SignedKey, diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index d5c80809b38..aa5bbc9a00e 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -2582,7 +2582,9 @@ fn megolm_error_to_utd_info( let reason = match error { EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent, Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent, - MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession, + MissingRoomKey(maybe_withheld) => { + UnableToDecryptReason::MissingMegolmSession { withheld_code: maybe_withheld } + } Decryption(DecryptionError::UnknownMessageIndex(_, _)) => { UnableToDecryptReason::UnknownMegolmMessageIndex } diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index f1c52747c6b..86c2526e4b9 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -19,6 +19,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt}; use itertools::Itertools; use matrix_sdk_common::deserialized_responses::{ UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, + WithheldCode, }; use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json}; use ruma::{ @@ -61,9 +62,7 @@ use crate::{ types::{ events::{ room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent}, - room_key_withheld::{ - MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, WithheldCode, - }, + room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent}, ToDeviceEvent, }, requests::{AnyOutgoingRequest, ToDeviceRequest}, @@ -683,7 +682,12 @@ async fn test_withheld_unverified() { 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); + assert_eq!( + utd_info.reason, + UnableToDecryptReason::MissingMegolmSession { + withheld_code: Some(WithheldCode::Unverified) + } + ); } /// Test what happens when we feed an unencrypted event into the decryption @@ -1362,7 +1366,7 @@ async fn test_unsigned_decryption() { replace_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { session_id: Some(second_room_key_session_id), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }) ); @@ -1468,7 +1472,7 @@ async fn test_unsigned_decryption() { thread_encryption_result, UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo { session_id: Some(third_room_key_session_id), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }) ); diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index e7a51644704..1c5539d7203 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -23,6 +23,7 @@ use std::{ time::Duration, }; +use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{ events::{ room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility}, @@ -54,7 +55,7 @@ use crate::{ MegolmV1AesSha2Content, RoomEncryptedEventContent, RoomEventEncryptionScheme, }, room_key::{MegolmV1AesSha2Content as MegolmV1AesSha2RoomKeyContent, RoomKeyContent}, - room_key_withheld::{RoomKeyWithheldContent, WithheldCode}, + room_key_withheld::RoomKeyWithheldContent, }, requests::ToDeviceRequest, EventEncryptionAlgorithm, diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index 72c44ca7365..b6ddeaaa290 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -22,7 +22,7 @@ use std::{ use futures_util::future::join_all; use itertools::Itertools; -use matrix_sdk_common::executor::spawn; +use matrix_sdk_common::{deserialized_responses::WithheldCode, executor::spawn}; use ruma::{ events::{AnyMessageLikeEventContent, ToDeviceEventType}, serde::Raw, @@ -41,10 +41,7 @@ use crate::{ ShareInfo, ShareState, }, store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store}, - types::{ - events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode}, - requests::ToDeviceRequest, - }, + types::{events::room::encrypted::RoomEncryptedEventContent, requests::ToDeviceRequest}, Device, DeviceData, EncryptionSettings, OlmError, }; @@ -782,6 +779,7 @@ mod tests { }; use assert_matches2::assert_let; + use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_test::{async_test, ruma_response_from_json}; use ruma::{ api::client::{ @@ -804,10 +802,7 @@ mod tests { types::{ events::{ room::encrypted::EncryptedToDeviceEvent, - room_key_withheld::{ - RoomKeyWithheldContent::{self, MegolmV1AesSha2}, - WithheldCode, - }, + room_key_withheld::RoomKeyWithheldContent::{self, MegolmV1AesSha2}, }, requests::ToDeviceRequest, DeviceKeys, EventEncryptionAlgorithm, diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index 7b3df39f41a..f3e5b074b00 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -19,6 +19,7 @@ use std::{ }; use itertools::{Either, Itertools}; +use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; use serde::{Deserialize, Serialize}; use tracing::{debug, instrument, trace}; @@ -27,7 +28,6 @@ use super::OutboundGroupSession; use crate::{ error::{OlmResult, SessionRecipientCollectionError}, store::Store, - types::events::room_key_withheld::WithheldCode, DeviceData, EncryptionSettings, LocalTrust, OlmError, OwnUserIdentityData, UserIdentityData, }; #[cfg(doc)] @@ -517,6 +517,7 @@ mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; + use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_test::{ async_test, test_json, test_json::keys_query_sets::{ @@ -536,7 +537,6 @@ mod tests { group_sessions::share_strategy::collect_session_recipients, CollectStrategy, }, testing::simulate_key_query_response_for_verification, - types::events::room_key_withheld::WithheldCode, CrossSigningKeyExport, EncryptionSettings, LocalTrust, OlmError, OlmMachine, }; diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index f454a3e9567..a84442f4525 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -44,6 +44,7 @@ macro_rules! cryptostore_integration_tests { }; use serde_json::value::to_raw_value; use serde_json::json; + use matrix_sdk_common::deserialized_responses::WithheldCode; use $crate::{ olm::{ Account, Curve25519PublicKey, InboundGroupSession, OlmMessageHash, @@ -61,7 +62,7 @@ macro_rules! cryptostore_integration_tests { room_key_request::MegolmV1AesSha2Content, room_key_withheld::{ CommonWithheldCodeContent, MegolmV1AesSha2WithheldContent, - RoomKeyWithheldContent, WithheldCode, + RoomKeyWithheldContent, }, secret_send::SecretSendContent, ToDeviceEvent, @@ -70,10 +71,8 @@ macro_rules! cryptostore_integration_tests { DeviceKeys, EventEncryptionAlgorithm, }, - GossippedSecret, LocalTrust, DeviceData, SecretInfo, TrackedUser, - vodozemac::{ - megolm::{GroupSession, SessionConfig}, - }, + vodozemac::megolm::{GroupSession, SessionConfig}, DeviceData, GossippedSecret, LocalTrust, SecretInfo, + TrackedUser, }; use super::get_store; diff --git a/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs b/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs index 8b9a8367763..61b1bd91d5c 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room_key_withheld.rs @@ -16,19 +16,14 @@ use std::collections::BTreeMap; -use ruma::{ - exports::ruma_macros::AsStrAsRefStr, - serde::{AsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, SerializeAsRefStr}, - OwnedDeviceId, OwnedRoomId, -}; +use matrix_sdk_common::deserialized_responses::WithheldCode; +use ruma::{OwnedDeviceId, OwnedRoomId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use vodozemac::Curve25519PublicKey; use super::{EventType, ToDeviceEvent}; -use crate::types::{ - deserialize_curve_key, serialize_curve_key, EventEncryptionAlgorithm, PrivOwnedStr, -}; +use crate::types::{deserialize_curve_key, serialize_curve_key, EventEncryptionAlgorithm}; /// The `m.room_key_request` to-device event. pub type RoomKeyWithheldEvent = ToDeviceEvent; @@ -160,65 +155,6 @@ impl EventType for RoomKeyWithheldContent { const EVENT_TYPE: &'static str = "m.room_key.withheld"; } -/// A machine-readable code for why the megolm key was not sent. -#[derive( - Clone, - PartialEq, - Eq, - Hash, - AsStrAsRefStr, - AsRefStr, - FromString, - DebugAsRefStr, - SerializeAsRefStr, - DeserializeFromCowStr, -)] -#[non_exhaustive] -pub enum WithheldCode { - /// the user/device was blacklisted. - #[ruma_enum(rename = "m.blacklisted")] - Blacklisted, - - /// the user/devices is unverified. - #[ruma_enum(rename = "m.unverified")] - Unverified, - - /// The user/device is not allowed have the key. For example, this would - /// usually be sent in response to a key request if the user was not in - /// the room when the message was sent. - #[ruma_enum(rename = "m.unauthorised")] - Unauthorised, - - /// Sent in reply to a key request if the device that the key is requested - /// from does not have the requested key. - #[ruma_enum(rename = "m.unavailable")] - Unavailable, - - /// An olm session could not be established. - /// This may happen, for example, if the sender was unable to obtain a - /// one-time key from the recipient. - #[ruma_enum(rename = "m.no_olm")] - NoOlm, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} - -impl std::fmt::Display for WithheldCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - let string = match self { - WithheldCode::Blacklisted => "The sender has blocked you.", - WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.", - WithheldCode::Unauthorised => "You are not authorised to read the message.", - WithheldCode::Unavailable => "The requested key was not found.", - WithheldCode::NoOlm => "Unable to establish a secure channel.", - _ => self.as_str(), - }; - - f.write_str(string) - } -} - #[derive(Debug, Deserialize, Serialize)] struct WithheldHelper { pub algorithm: EventEncryptionAlgorithm, @@ -490,15 +426,14 @@ pub(super) mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; + use matrix_sdk_common::deserialized_responses::WithheldCode; use ruma::{device_id, room_id, serde::Raw, to_device::DeviceIdOrAllDevices, user_id}; use serde_json::{json, Value}; use vodozemac::Curve25519PublicKey; use super::RoomKeyWithheldEvent; use crate::types::{ - events::room_key_withheld::{ - MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, WithheldCode, - }, + events::room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent}, EventEncryptionAlgorithm, }; diff --git a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs index c7d2a0810fc..bef568ea10c 100644 --- a/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs +++ b/crates/matrix-sdk-crypto/src/types/events/utd_cause.rs @@ -13,7 +13,7 @@ // limitations under the License. use matrix_sdk_common::deserialized_responses::{ - UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, + UnableToDecryptInfo, UnableToDecryptReason, VerificationLevel, WithheldCode, }; use ruma::{events::AnySyncTimelineEvent, serde::Raw, MilliSecondsSinceUnixEpoch}; use serde::Deserialize; @@ -57,6 +57,19 @@ pub enum UtdCause { /// be confused with pre-join or pre-invite messages (see /// [`UtdCause::SentBeforeWeJoined`] for that). HistoricalMessage = 5, + + /// The keys for this event are intentionally withheld. + /// + /// The sender has refused to share the key because our device does not meet + /// the sender's security requirements. + WithheldForUnverifiedOrInsecureDevice = 6, + + /// The keys for this event are missing, likely because the sender was + /// unable to share them (e.g., failure to establish an Olm 1:1 + /// channel). Alternatively, the sender may have deliberately excluded + /// this device by cherry-picking and blocking it, in which case, no action + /// can be taken on our side. + WithheldBySender = 7, } /// MSC4115 membership info in the unsigned area. @@ -97,8 +110,18 @@ impl UtdCause { unable_to_decrypt_info: &UnableToDecryptInfo, ) -> Self { // TODO: in future, use more information to give a richer answer. E.g. - match unable_to_decrypt_info.reason { - UnableToDecryptReason::MissingMegolmSession + match &unable_to_decrypt_info.reason { + UnableToDecryptReason::MissingMegolmSession { withheld_code: Some(reason) } => { + match reason { + WithheldCode::Unverified => UtdCause::WithheldForUnverifiedOrInsecureDevice, + WithheldCode::Blacklisted + | WithheldCode::Unauthorised + | WithheldCode::Unavailable + | WithheldCode::NoOlm + | WithheldCode::_Custom(_) => UtdCause::WithheldBySender, + } + } + UnableToDecryptReason::MissingMegolmSession { withheld_code: None } | UnableToDecryptReason::UnknownMegolmMessageIndex => { // Look in the unsigned area for a `membership` field. if let Some(unsigned) = @@ -424,7 +447,7 @@ mod tests { fn missing_megolm_session() -> UnableToDecryptInfo { UnableToDecryptInfo { session_id: None, - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, } } diff --git a/crates/matrix-sdk-crypto/src/types/mod.rs b/crates/matrix-sdk-crypto/src/types/mod.rs index bd22d3a78ec..8bb3f324583 100644 --- a/crates/matrix-sdk-crypto/src/types/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/mod.rs @@ -34,6 +34,7 @@ use std::{ }; use as_variant::as_variant; +use matrix_sdk_common::deserialized_responses::PrivOwnedStr; use ruma::{ serde::StringEnum, DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, OwnedUserId, UserId, }; @@ -425,20 +426,6 @@ impl Algorithm for DeviceKeyAlgorithm { } } -// Wrapper around `Box` that cannot be used in a meaningful way outside of -// this crate. Used for string enums because their `_Custom` variant can't be -// truly private (only `#[doc(hidden)]`). -#[doc(hidden)] -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PrivOwnedStr(Box); - -#[cfg(not(tarpaulin_include))] -impl std::fmt::Debug for PrivOwnedStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - /// An encryption algorithm to be used to encrypt messages sent to a room. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, StringEnum)] #[non_exhaustive] diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index f23d45a9433..c94b0f764f6 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -37,6 +37,7 @@ vodozemac = { workspace = true } assert_matches = { workspace = true } glob = "0.3.1" matrix-sdk-base = { workspace = true, features = ["testing"] } +matrix-sdk-common = { workspace = true } matrix-sdk-crypto = { workspace = true, features = ["testing"] } matrix-sdk-test = { workspace = true } once_cell = { workspace = true } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index 131cffae580..1daebc20160 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -774,7 +774,7 @@ fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { raw, matrix_sdk::deserialized_responses::UnableToDecryptInfo { session_id: Some("SESSION_ID".into()), - reason: UnableToDecryptReason::MissingMegolmSession, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, }, ) } diff --git a/crates/matrix-sdk/src/authentication/qrcode/messages.rs b/crates/matrix-sdk/src/authentication/qrcode/messages.rs index 6ea48198823..2abbef5a979 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/messages.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/messages.rs @@ -13,6 +13,7 @@ // limitations under the License. use matrix_sdk_base::crypto::types::SecretsBundle; +use matrix_sdk_common::deserialized_responses::PrivOwnedStr; use openidconnect::{ core::CoreDeviceAuthorizationResponse, EndUserVerificationUrl, VerificationUriComplete, }; @@ -183,15 +184,6 @@ where s.serialize_str(&key.to_base64()) } -// Wrapper around `Box` that cannot be used in a meaningful way outside of -// this crate. Used for string enums because their `_Custom` variant can't be -// truly private (only `#[doc(hidden)]`). -// TODO: It probably makes sense to move the above messages into Ruma, if for -// nothing else, to get rid of this `PrivOwnedStr`. -#[doc(hidden)] -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PrivOwnedStr(Box); - #[cfg(test)] mod test { use assert_matches2::assert_let; diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 16787599cc6..1055215a029 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -211,7 +211,10 @@ impl EventBuilder { SyncTimelineEvent::new_utd_event( self.into(), - UnableToDecryptInfo { session_id, reason: UnableToDecryptReason::MissingMegolmSession }, + UnableToDecryptInfo { + session_id, + reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None }, + }, ) } }