@@ -17,7 +17,10 @@ use std::{collections::BTreeMap, fmt};
1717use 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} ;
2326use 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
673676fn 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 ) ]
679700pub 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}
0 commit comments