Skip to content

Commit 1009ea8

Browse files
authored
Merge pull request #4305 from matrix-org/valere/support_for_withheld_reason
feat(crypto): Add optional withheld reason to `UnableToDecryptReason`
2 parents 7d8e7af + 6801811 commit 1009ea8

File tree

20 files changed

+293
-139
lines changed

20 files changed

+293
-139
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/matrix-sdk-crypto-ffi/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ mod tests {
112112

113113
#[test]
114114
fn test_withheld_error_mapping() {
115-
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
115+
use matrix_sdk_common::deserialized_responses::WithheldCode;
116116

117117
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
118118

crates/matrix-sdk-base/src/sliding_sync/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2656,7 +2656,7 @@ mod tests {
26562656
.unwrap(),
26572657
UnableToDecryptInfo {
26582658
session_id: Some("".to_owned()),
2659-
reason: UnableToDecryptReason::MissingMegolmSession,
2659+
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
26602660
},
26612661
)
26622662
}

crates/matrix-sdk-common/src/deserialized_responses.rs

Lines changed: 216 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ use std::{collections::BTreeMap, fmt};
1717
use ruma::{
1818
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent},
1919
push::Action,
20-
serde::{JsonObject, Raw},
20+
serde::{
21+
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
22+
SerializeAsRefStr,
23+
},
2124
DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId,
2225
};
2326
use serde::{Deserialize, Serialize};
@@ -666,14 +669,32 @@ pub struct UnableToDecryptInfo {
666669
pub session_id: Option<String>,
667670

668671
/// Reason code for the decryption failure
669-
#[serde(default = "unknown_utd_reason")]
672+
#[serde(default = "unknown_utd_reason", deserialize_with = "deserialize_utd_reason")]
670673
pub reason: UnableToDecryptReason,
671674
}
672675

673676
fn unknown_utd_reason() -> UnableToDecryptReason {
674677
UnableToDecryptReason::Unknown
675678
}
676679

680+
/// Provides basic backward compatibility for deserializing older serialized
681+
/// `UnableToDecryptReason` values.
682+
pub fn deserialize_utd_reason<'de, D>(d: D) -> Result<UnableToDecryptReason, D::Error>
683+
where
684+
D: serde::Deserializer<'de>,
685+
{
686+
// Start by deserializing as to an untyped JSON value.
687+
let v: serde_json::Value = Deserialize::deserialize(d)?;
688+
// Backwards compatibility: `MissingMegolmSession` used to be stored without the
689+
// withheld code.
690+
if v.as_str().is_some_and(|s| s == "MissingMegolmSession") {
691+
return Ok(UnableToDecryptReason::MissingMegolmSession { withheld_code: None });
692+
}
693+
// Otherwise, use the derived deserialize impl to turn the JSON into a
694+
// UnableToDecryptReason
695+
serde_json::from_value::<UnableToDecryptReason>(v).map_err(serde::de::Error::custom)
696+
}
697+
677698
/// Reason code for a decryption failure
678699
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
679700
pub enum UnableToDecryptReason {
@@ -689,9 +710,11 @@ pub enum UnableToDecryptReason {
689710

690711
/// Decryption failed because we're missing the megolm session that was used
691712
/// to encrypt the event.
692-
///
693-
/// TODO: support withheld codes?
694-
MissingMegolmSession,
713+
MissingMegolmSession {
714+
/// If the key was withheld on purpose, the associated code. `None`
715+
/// means no withheld code was received.
716+
withheld_code: Option<WithheldCode>,
717+
},
695718

696719
/// Decryption failed because, while we have the megolm session that was
697720
/// used to encrypt the message, it is ratcheted too far forward.
@@ -723,7 +746,86 @@ impl UnableToDecryptReason {
723746
/// Returns true if this UTD is due to a missing room key (and hence might
724747
/// resolve itself if we wait a bit.)
725748
pub fn is_missing_room_key(&self) -> bool {
726-
matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex)
749+
// In case of MissingMegolmSession with a withheld code we return false here
750+
// given that this API is used to decide if waiting a bit will help.
751+
matches!(
752+
self,
753+
Self::MissingMegolmSession { withheld_code: None } | Self::UnknownMegolmMessageIndex
754+
)
755+
}
756+
}
757+
758+
/// A machine-readable code for why a Megolm key was not sent.
759+
///
760+
/// 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.
761+
#[derive(
762+
Clone,
763+
PartialEq,
764+
Eq,
765+
Hash,
766+
AsStrAsRefStr,
767+
AsRefStr,
768+
FromString,
769+
DebugAsRefStr,
770+
SerializeAsRefStr,
771+
DeserializeFromCowStr,
772+
)]
773+
pub enum WithheldCode {
774+
/// the user/device was blacklisted.
775+
#[ruma_enum(rename = "m.blacklisted")]
776+
Blacklisted,
777+
778+
/// the user/devices is unverified.
779+
#[ruma_enum(rename = "m.unverified")]
780+
Unverified,
781+
782+
/// The user/device is not allowed have the key. For example, this would
783+
/// usually be sent in response to a key request if the user was not in
784+
/// the room when the message was sent.
785+
#[ruma_enum(rename = "m.unauthorised")]
786+
Unauthorised,
787+
788+
/// Sent in reply to a key request if the device that the key is requested
789+
/// from does not have the requested key.
790+
#[ruma_enum(rename = "m.unavailable")]
791+
Unavailable,
792+
793+
/// An olm session could not be established.
794+
/// This may happen, for example, if the sender was unable to obtain a
795+
/// one-time key from the recipient.
796+
#[ruma_enum(rename = "m.no_olm")]
797+
NoOlm,
798+
799+
#[doc(hidden)]
800+
_Custom(PrivOwnedStr),
801+
}
802+
803+
impl fmt::Display for WithheldCode {
804+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
805+
let string = match self {
806+
WithheldCode::Blacklisted => "The sender has blocked you.",
807+
WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.",
808+
WithheldCode::Unauthorised => "You are not authorised to read the message.",
809+
WithheldCode::Unavailable => "The requested key was not found.",
810+
WithheldCode::NoOlm => "Unable to establish a secure channel.",
811+
_ => self.as_str(),
812+
};
813+
814+
f.write_str(string)
815+
}
816+
}
817+
818+
// The Ruma macro expects the type to have this name.
819+
// The payload is counter intuitively made public in order to avoid having
820+
// multiple copies of this struct.
821+
#[doc(hidden)]
822+
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
823+
pub struct PrivOwnedStr(pub Box<str>);
824+
825+
#[cfg(not(tarpaulin_include))]
826+
impl fmt::Debug for PrivOwnedStr {
827+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828+
self.0.fmt(f)
727829
}
728830
}
729831

@@ -817,7 +919,7 @@ mod tests {
817919
use super::{
818920
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent,
819921
TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
820-
UnsignedEventLocation, VerificationState,
922+
UnsignedEventLocation, VerificationState, WithheldCode,
821923
};
822924
use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel};
823925

@@ -1038,4 +1140,111 @@ mod tests {
10381140
});
10391141
});
10401142
}
1143+
1144+
#[test]
1145+
fn sync_timeline_event_deserialisation_migration_for_withheld() {
1146+
// Old serialized version was
1147+
// "utd_info": {
1148+
// "reason": "MissingMegolmSession",
1149+
// "session_id": "session000"
1150+
// }
1151+
1152+
// The new version would be
1153+
// "utd_info": {
1154+
// "reason": {
1155+
// "MissingMegolmSession": {
1156+
// "withheld_code": null
1157+
// }
1158+
// },
1159+
// "session_id": "session000"
1160+
// }
1161+
1162+
let serialized = json!({
1163+
"kind": {
1164+
"UnableToDecrypt": {
1165+
"event": {
1166+
"content": {
1167+
"algorithm": "m.megolm.v1.aes-sha2",
1168+
"ciphertext": "AwgAEoABzL1JYhqhjW9jXrlT3M6H8mJ4qffYtOQOnPuAPNxsuG20oiD/Fnpv6jnQGhU6YbV9pNM+1mRnTvxW3CbWOPjLKqCWTJTc7Q0vDEVtYePg38ncXNcwMmfhgnNAoW9S7vNs8C003x3yUl6NeZ8bH+ci870BZL+kWM/lMl10tn6U7snNmSjnE3ckvRdO+11/R4//5VzFQpZdf4j036lNSls/WIiI67Fk9iFpinz9xdRVWJFVdrAiPFwb8L5xRZ8aX+e2JDMlc1eW8gk",
1169+
"device_id": "SKCGPNUWAU",
1170+
"sender_key": "Gim/c7uQdSXyrrUbmUOrBT6sMC0gO7QSLmOK6B7NOm0",
1171+
"session_id": "hgLyeSqXfb8vc5AjQLsg6TSHVu0HJ7HZ4B6jgMvxkrs"
1172+
},
1173+
"event_id": "$xxxxx:example.org",
1174+
"origin_server_ts": 2189,
1175+
"room_id": "!someroom:example.com",
1176+
"sender": "@carl:example.com",
1177+
"type": "m.room.message"
1178+
},
1179+
"utd_info": {
1180+
"reason": "MissingMegolmSession",
1181+
"session_id": "session000"
1182+
}
1183+
}
1184+
}
1185+
});
1186+
1187+
let result = serde_json::from_value(serialized);
1188+
assert!(result.is_ok());
1189+
1190+
// should have migrated to the new format
1191+
let event: SyncTimelineEvent = result.unwrap();
1192+
assert_matches!(
1193+
event.kind,
1194+
TimelineEventKind::UnableToDecrypt { utd_info, .. }=> {
1195+
assert_matches!(
1196+
utd_info.reason,
1197+
UnableToDecryptReason::MissingMegolmSession { withheld_code: None }
1198+
);
1199+
}
1200+
)
1201+
}
1202+
1203+
#[test]
1204+
fn unable_to_decrypt_info_migration_for_withheld() {
1205+
let old_format = json!({
1206+
"reason": "MissingMegolmSession",
1207+
"session_id": "session000"
1208+
});
1209+
1210+
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(old_format).unwrap();
1211+
let session_id = Some("session000".to_owned());
1212+
1213+
assert_eq!(deserialized.session_id, session_id);
1214+
assert_eq!(
1215+
deserialized.reason,
1216+
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
1217+
);
1218+
1219+
let new_format = json!({
1220+
"session_id": "session000",
1221+
"reason": {
1222+
"MissingMegolmSession": {
1223+
"withheld_code": null
1224+
}
1225+
}
1226+
});
1227+
1228+
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(new_format).unwrap();
1229+
1230+
assert_eq!(
1231+
deserialized.reason,
1232+
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
1233+
);
1234+
assert_eq!(deserialized.session_id, session_id);
1235+
}
1236+
1237+
#[test]
1238+
fn unable_to_decrypt_reason_is_missing_room_key() {
1239+
let reason = UnableToDecryptReason::MissingMegolmSession { withheld_code: None };
1240+
assert!(reason.is_missing_room_key());
1241+
1242+
let reason = UnableToDecryptReason::MissingMegolmSession {
1243+
withheld_code: Some(WithheldCode::Blacklisted),
1244+
};
1245+
assert!(!reason.is_missing_room_key());
1246+
1247+
let reason = UnableToDecryptReason::UnknownMegolmMessageIndex;
1248+
assert!(reason.is_missing_room_key());
1249+
}
10411250
}

crates/matrix-sdk-crypto/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file.
66

77
## [Unreleased] - ReleaseDate
88

9+
- Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`.
10+
These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors
11+
when the sender either did not wish to share or was unable to share the room_key.
12+
([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305))
13+
914
## [0.8.0] - 2024-11-19
1015

1116
### Features

crates/matrix-sdk-crypto/src/error.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@
1414

1515
use std::collections::BTreeMap;
1616

17-
use matrix_sdk_common::deserialized_responses::VerificationLevel;
17+
use matrix_sdk_common::deserialized_responses::{VerificationLevel, WithheldCode};
1818
use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId};
1919
use serde::{ser::SerializeMap, Serializer};
2020
use serde_json::Error as SerdeError;
2121
use thiserror::Error;
2222
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
2323

2424
use super::store::CryptoStoreError;
25-
use crate::{
26-
olm::SessionExportError,
27-
types::{events::room_key_withheld::WithheldCode, SignedKey},
28-
};
25+
use crate::{olm::SessionExportError, types::SignedKey};
2926
#[cfg(doc)]
3027
use crate::{CollectStrategy, Device, LocalTrust, OtherUserIdentity};
3128

crates/matrix-sdk-crypto/src/identities/device.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::{
2121
},
2222
};
2323

24+
use matrix_sdk_common::deserialized_responses::WithheldCode;
2425
use ruma::{
2526
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
2627
events::{key::verification::VerificationMethod, AnyToDeviceEventContent},
@@ -48,8 +49,7 @@ use crate::{
4849
types::{
4950
events::{
5051
forwarded_room_key::ForwardedRoomKeyContent,
51-
room::encrypted::ToDeviceEncryptedEventContent, room_key_withheld::WithheldCode,
52-
EventType,
52+
room::encrypted::ToDeviceEncryptedEventContent, EventType,
5353
},
5454
requests::{OutgoingVerificationRequest, ToDeviceRequest},
5555
DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures, SignedKey,

crates/matrix-sdk-crypto/src/machine/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2582,7 +2582,9 @@ fn megolm_error_to_utd_info(
25822582
let reason = match error {
25832583
EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent,
25842584
Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent,
2585-
MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession,
2585+
MissingRoomKey(maybe_withheld) => {
2586+
UnableToDecryptReason::MissingMegolmSession { withheld_code: maybe_withheld }
2587+
}
25862588
Decryption(DecryptionError::UnknownMessageIndex(_, _)) => {
25872589
UnableToDecryptReason::UnknownMegolmMessageIndex
25882590
}

0 commit comments

Comments
 (0)