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