Skip to main content

ruma_events/room_key/
withheld.rs

1//! Types for the [`m.room_key.withheld`] event.
2//!
3//! [`m.room_key.withheld`]: https://spec.matrix.org/v1.19/client-server-api/#mroom_keywithheld
4
5use std::borrow::Cow;
6
7use as_variant::as_variant;
8use ruma_common::{
9    EventEncryptionAlgorithm, OwnedRoomId,
10    serde::{Base64, JsonObject, from_raw_json_value},
11};
12use ruma_macros::{EventContent, StringEnum};
13use serde::{Deserialize, Serialize, de};
14use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
15
16use crate::PrivOwnedStr;
17
18/// The content of an [`m.room_key.withheld`] event.
19///
20/// Typically encrypted as an `m.room.encrypted` event, then sent as a to-device event.
21///
22/// [`m.room_key.withheld`]: https://spec.matrix.org/v1.19/client-server-api/#mroom_keywithheld
23#[derive(Clone, Debug, Serialize, EventContent)]
24#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
25#[ruma_event(type = "m.room_key.withheld", kind = ToDevice)]
26pub struct ToDeviceRoomKeyWithheldEventContent {
27    /// The encryption algorithm the key in this event is to be used with.
28    ///
29    /// Must be `m.megolm.v1.aes-sha2`.
30    pub algorithm: EventEncryptionAlgorithm,
31
32    /// A machine-readable code for why the megolm key was not sent.
33    #[serde(flatten)]
34    pub code: RoomKeyWithheldCodeInfo,
35
36    /// A human-readable reason for why the key was not sent.
37    ///
38    /// The receiving client should only use this string if it does not understand the code.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub reason: Option<String>,
41
42    /// The unpadded base64-encoded device curve25519 key of the event's sender.
43    pub sender_key: Base64,
44}
45
46impl ToDeviceRoomKeyWithheldEventContent {
47    /// Creates a new `ToDeviceRoomKeyWithheldEventContent` with the given algorithm, code and
48    /// sender key.
49    pub fn new(
50        algorithm: EventEncryptionAlgorithm,
51        code: RoomKeyWithheldCodeInfo,
52        sender_key: Base64,
53    ) -> Self {
54        Self { algorithm, code, reason: None, sender_key }
55    }
56}
57
58impl<'de> Deserialize<'de> for ToDeviceRoomKeyWithheldEventContent {
59    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60    where
61        D: de::Deserializer<'de>,
62    {
63        #[derive(Deserialize)]
64        struct ToDeviceRoomKeyWithheldEventContentDeHelper {
65            algorithm: EventEncryptionAlgorithm,
66            reason: Option<String>,
67            sender_key: Base64,
68        }
69
70        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
71
72        let ToDeviceRoomKeyWithheldEventContentDeHelper { algorithm, reason, sender_key } =
73            from_raw_json_value(&json)?;
74        let code = from_raw_json_value(&json)?;
75
76        Ok(Self { algorithm, code, reason, sender_key })
77    }
78}
79
80/// The possible codes for why a megolm key was not sent, and the associated session data.
81#[derive(Debug, Clone, Serialize)]
82#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
83#[serde(tag = "code")]
84pub enum RoomKeyWithheldCodeInfo {
85    /// `m.blacklisted`
86    ///
87    /// The user or device was blacklisted.
88    #[serde(rename = "m.blacklisted")]
89    Blacklisted(Box<RoomKeyWithheldSessionData>),
90
91    /// `m.unverified`
92    ///
93    /// The user or device was not verified, and the sender is only sharing keys with verified
94    /// users or devices.
95    #[serde(rename = "m.unverified")]
96    Unverified(Box<RoomKeyWithheldSessionData>),
97
98    /// `m.unauthorised`
99    ///
100    /// The user or device is not allowed to have the key. For example, this could be sent in
101    /// response to a key request if the user or device was not in the room when the original
102    /// message was sent.
103    #[serde(rename = "m.unauthorised")]
104    Unauthorized(Box<RoomKeyWithheldSessionData>),
105
106    /// `m.unavailable`
107    ///
108    /// Sent in reply to a key request if the device that the key is requested from does not have
109    /// the requested key.
110    #[serde(rename = "m.unavailable")]
111    Unavailable(Box<RoomKeyWithheldSessionData>),
112
113    /// `m.no_olm`
114    ///
115    /// An olm session could not be established.
116    #[serde(rename = "m.no_olm")]
117    NoOlm,
118
119    /// `m.history_not_shared`
120    ///
121    /// The megolm session does not have the `shared_history` flag set.
122    #[serde(rename = "m.history_not_shared")]
123    HistoryNotShared,
124
125    #[doc(hidden)]
126    #[serde(untagged)]
127    _Custom(Box<CustomRoomKeyWithheldCodeInfo>),
128}
129
130impl RoomKeyWithheldCodeInfo {
131    /// Get the code of this `RoomKeyWithheldCodeInfo`.
132    pub fn code(&self) -> RoomKeyWithheldCode {
133        match self {
134            Self::Blacklisted(_) => RoomKeyWithheldCode::Blacklisted,
135            Self::Unverified(_) => RoomKeyWithheldCode::Unverified,
136            Self::Unauthorized(_) => RoomKeyWithheldCode::Unauthorized,
137            Self::Unavailable(_) => RoomKeyWithheldCode::Unavailable,
138            Self::NoOlm => RoomKeyWithheldCode::NoOlm,
139            Self::HistoryNotShared => RoomKeyWithheldCode::HistoryNotShared,
140            Self::_Custom(info) => info.code.as_str().into(),
141        }
142    }
143}
144
145impl<'de> Deserialize<'de> for RoomKeyWithheldCodeInfo {
146    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147    where
148        D: de::Deserializer<'de>,
149    {
150        #[derive(Debug, Deserialize)]
151        struct ExtractCode<'a> {
152            #[serde(borrow)]
153            code: Cow<'a, str>,
154        }
155
156        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
157        let ExtractCode { code } = from_raw_json_value(&json)?;
158
159        Ok(match code.as_ref() {
160            "m.blacklisted" => Self::Blacklisted(from_raw_json_value(&json)?),
161            "m.unverified" => Self::Unverified(from_raw_json_value(&json)?),
162            "m.unauthorised" => Self::Unauthorized(from_raw_json_value(&json)?),
163            "m.unavailable" => Self::Unavailable(from_raw_json_value(&json)?),
164            "m.no_olm" => Self::NoOlm,
165            _ => {
166                let mut data = from_raw_json_value::<JsonObject, _>(&json)?;
167
168                // Probably due to the `#[serde(flatten)]` attribute, we deserialize fields that
169                // should be caught by `ToDeviceRoomKeyWithheldEventContent`. Let's remove them to
170                // fix re-serialization.
171                data.remove("algorithm");
172                data.remove("sender_key");
173                data.remove("reason");
174
175                let code = as_variant!(
176                    data.remove("code").expect("we already checked that the code field is present"),
177                    JsonValue::String
178                )
179                .expect("we already checked that the code is a string");
180
181                Self::_Custom(CustomRoomKeyWithheldCodeInfo { code, data }.into())
182            }
183        })
184    }
185}
186
187/// The session data associated to a withheld room key.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
190pub struct RoomKeyWithheldSessionData {
191    /// The room for the key.
192    pub room_id: OwnedRoomId,
193
194    /// The session ID of the key.
195    pub session_id: String,
196}
197
198impl RoomKeyWithheldSessionData {
199    /// Construct a new `RoomKeyWithheldSessionData` with the given room ID and session ID.
200    pub fn new(room_id: OwnedRoomId, session_id: String) -> Self {
201        Self { room_id, session_id }
202    }
203}
204
205/// The payload for a custom room key withheld code.
206#[doc(hidden)]
207#[derive(Clone, Debug, Serialize)]
208pub struct CustomRoomKeyWithheldCodeInfo {
209    /// A custom code.
210    code: String,
211
212    /// Remaining event content.
213    #[serde(flatten)]
214    data: JsonObject,
215}
216
217/// The possible codes for why a megolm key was not sent.
218#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
219#[derive(Clone, StringEnum)]
220#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
221#[non_exhaustive]
222pub enum RoomKeyWithheldCode {
223    /// `m.blacklisted`
224    ///
225    /// The user or device was blacklisted.
226    Blacklisted,
227
228    /// `m.unverified`
229    ///
230    /// The user or device was not verified, and the sender is only sharing keys with verified
231    /// users or devices.
232    Unverified,
233
234    /// `m.unauthorised`
235    ///
236    /// The user or device is not allowed to have the key. For example, this could be sent in
237    /// response to a key request if the user or device was not in the room when the original
238    /// message was sent.
239    Unauthorized,
240
241    /// `m.unavailable`
242    ///
243    /// Sent in reply to a key request if the device that the key is requested from does not have
244    /// the requested key.
245    Unavailable,
246
247    /// `m.no_olm`
248    ///
249    /// An olm session could not be established.
250    NoOlm,
251
252    /// `m.history_not_shared`
253    ///
254    /// The megolm session does not have the `shared_history` flag set.
255    HistoryNotShared,
256
257    #[doc(hidden)]
258    _Custom(PrivOwnedStr),
259}
260
261#[cfg(test)]
262mod tests {
263    use assert_matches2::assert_matches;
264    use ruma_common::{
265        EventEncryptionAlgorithm, canonical_json::assert_to_canonical_json_eq, owned_room_id,
266        serde::Base64,
267    };
268    use serde_json::{from_value as from_json_value, json};
269
270    use super::{
271        RoomKeyWithheldCodeInfo, RoomKeyWithheldSessionData, ToDeviceRoomKeyWithheldEventContent,
272    };
273
274    const PUBLIC_KEY: &[u8] = b"key";
275    const BASE64_ENCODED_PUBLIC_KEY: &str = "a2V5";
276
277    #[test]
278    fn serialization_no_olm() {
279        let content = ToDeviceRoomKeyWithheldEventContent::new(
280            EventEncryptionAlgorithm::MegolmV1AesSha2,
281            RoomKeyWithheldCodeInfo::NoOlm,
282            Base64::new(PUBLIC_KEY.to_owned()),
283        );
284
285        assert_to_canonical_json_eq!(
286            content,
287            json!({
288                "algorithm": "m.megolm.v1.aes-sha2",
289                "code": "m.no_olm",
290                "sender_key": BASE64_ENCODED_PUBLIC_KEY,
291            })
292        );
293    }
294
295    #[test]
296    fn serialization_blacklisted() {
297        let room_id = owned_room_id!("!roomid:localhost");
298        let content = ToDeviceRoomKeyWithheldEventContent::new(
299            EventEncryptionAlgorithm::MegolmV1AesSha2,
300            RoomKeyWithheldCodeInfo::Blacklisted(
301                RoomKeyWithheldSessionData::new(room_id.clone(), "unique_id".to_owned()).into(),
302            ),
303            Base64::new(PUBLIC_KEY.to_owned()),
304        );
305
306        assert_to_canonical_json_eq!(
307            content,
308            json!({
309                "algorithm": "m.megolm.v1.aes-sha2",
310                "code": "m.blacklisted",
311                "sender_key": BASE64_ENCODED_PUBLIC_KEY,
312                "room_id": room_id,
313                "session_id": "unique_id",
314            })
315        );
316    }
317
318    #[test]
319    fn deserialization_no_olm() {
320        let json = json!({
321            "algorithm": "m.megolm.v1.aes-sha2",
322            "code": "m.no_olm",
323            "sender_key": BASE64_ENCODED_PUBLIC_KEY,
324            "reason": "Could not find an olm session",
325        });
326
327        let content = from_json_value::<ToDeviceRoomKeyWithheldEventContent>(json).unwrap();
328        assert_eq!(content.algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
329        assert_eq!(content.sender_key, Base64::new(PUBLIC_KEY.to_owned()));
330        assert_eq!(content.reason.as_deref(), Some("Could not find an olm session"));
331        assert_matches!(content.code, RoomKeyWithheldCodeInfo::NoOlm);
332    }
333
334    #[test]
335    fn deserialization_blacklisted() {
336        let room_id = owned_room_id!("!roomid:localhost");
337        let json = json!({
338            "algorithm": "m.megolm.v1.aes-sha2",
339            "code": "m.blacklisted",
340            "sender_key": BASE64_ENCODED_PUBLIC_KEY,
341            "room_id": room_id,
342            "session_id": "unique_id",
343        });
344
345        let content = from_json_value::<ToDeviceRoomKeyWithheldEventContent>(json).unwrap();
346        assert_eq!(content.algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
347        assert_eq!(content.sender_key, Base64::new(PUBLIC_KEY.to_owned()));
348        assert_eq!(content.reason, None);
349        assert_matches!(content.code, RoomKeyWithheldCodeInfo::Blacklisted(session_data));
350        assert_eq!(session_data.room_id, room_id);
351        assert_eq!(session_data.session_id, "unique_id");
352    }
353
354    #[test]
355    fn custom_room_key_withheld_code_info_round_trip() {
356        let room_id = owned_room_id!("!roomid:localhost");
357        let json = json!({
358            "algorithm": "m.megolm.v1.aes-sha2",
359            "code": "dev.ruma.custom_code",
360            "sender_key": BASE64_ENCODED_PUBLIC_KEY,
361            "room_id": room_id,
362            "key": "value",
363        });
364
365        let content = from_json_value::<ToDeviceRoomKeyWithheldEventContent>(json.clone()).unwrap();
366        assert_eq!(content.code.code().as_str(), "dev.ruma.custom_code");
367
368        assert_to_canonical_json_eq!(content, json);
369    }
370}