ruma_events/room/
encrypted.rs

1//! Types for the [`m.room.encrypted`] event.
2//!
3//! [`m.room.encrypted`]: https://spec.matrix.org/latest/client-server-api/#mroomencrypted
4
5use std::{borrow::Cow, collections::BTreeMap};
6
7use js_int::UInt;
8use ruma_common::{serde::JsonObject, OwnedDeviceId, OwnedEventId};
9use ruma_macros::EventContent;
10use serde::{Deserialize, Serialize};
11
12use super::message;
13use crate::relation::{Annotation, CustomRelation, InReplyTo, Reference, RelationType, Thread};
14
15mod relation_serde;
16
17/// The content of an `m.room.encrypted` event.
18#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
19#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
20#[ruma_event(type = "m.room.encrypted", kind = MessageLike)]
21pub struct RoomEncryptedEventContent {
22    /// Algorithm-specific fields.
23    #[serde(flatten)]
24    pub scheme: EncryptedEventScheme,
25
26    /// Information about related events.
27    #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
28    pub relates_to: Option<Relation>,
29}
30
31impl RoomEncryptedEventContent {
32    /// Creates a new `RoomEncryptedEventContent` with the given scheme and relation.
33    pub fn new(scheme: EncryptedEventScheme, relates_to: Option<Relation>) -> Self {
34        Self { scheme, relates_to }
35    }
36}
37
38impl From<EncryptedEventScheme> for RoomEncryptedEventContent {
39    fn from(scheme: EncryptedEventScheme) -> Self {
40        Self { scheme, relates_to: None }
41    }
42}
43
44/// The to-device content of an `m.room.encrypted` event.
45#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
46#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
47#[ruma_event(type = "m.room.encrypted", kind = ToDevice)]
48pub struct ToDeviceRoomEncryptedEventContent {
49    /// Algorithm-specific fields.
50    #[serde(flatten)]
51    pub scheme: EncryptedEventScheme,
52}
53
54impl ToDeviceRoomEncryptedEventContent {
55    /// Creates a new `ToDeviceRoomEncryptedEventContent` with the given scheme.
56    pub fn new(scheme: EncryptedEventScheme) -> Self {
57        Self { scheme }
58    }
59}
60
61impl From<EncryptedEventScheme> for ToDeviceRoomEncryptedEventContent {
62    fn from(scheme: EncryptedEventScheme) -> Self {
63        Self { scheme }
64    }
65}
66
67/// The encryption scheme for `RoomEncryptedEventContent`.
68#[derive(Clone, Debug, Deserialize, Serialize)]
69#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
70#[serde(tag = "algorithm")]
71pub enum EncryptedEventScheme {
72    /// An event encrypted with `m.olm.v1.curve25519-aes-sha2`.
73    #[serde(rename = "m.olm.v1.curve25519-aes-sha2")]
74    OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content),
75
76    /// An event encrypted with `m.megolm.v1.aes-sha2`.
77    #[serde(rename = "m.megolm.v1.aes-sha2")]
78    MegolmV1AesSha2(MegolmV1AesSha2Content),
79}
80
81/// Relationship information about an encrypted event.
82///
83/// Outside of the encrypted payload to support server aggregation.
84#[derive(Clone, Debug)]
85#[allow(clippy::manual_non_exhaustive)]
86#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
87pub enum Relation {
88    /// An `m.in_reply_to` relation indicating that the event is a reply to another event.
89    Reply {
90        /// Information about another message being replied to.
91        in_reply_to: InReplyTo,
92    },
93
94    /// An event that replaces another event.
95    Replacement(Replacement),
96
97    /// A reference to another event.
98    Reference(Reference),
99
100    /// An annotation to an event.
101    Annotation(Annotation),
102
103    /// An event that belongs to a thread.
104    Thread(Thread),
105
106    #[doc(hidden)]
107    _Custom(CustomRelation),
108}
109
110impl Relation {
111    /// The type of this `Relation`.
112    ///
113    /// Returns an `Option` because the `Reply` relation does not have a`rel_type` field.
114    pub fn rel_type(&self) -> Option<RelationType> {
115        match self {
116            Relation::Reply { .. } => None,
117            Relation::Replacement(_) => Some(RelationType::Replacement),
118            Relation::Reference(_) => Some(RelationType::Reference),
119            Relation::Annotation(_) => Some(RelationType::Annotation),
120            Relation::Thread(_) => Some(RelationType::Thread),
121            Relation::_Custom(c) => c.rel_type(),
122        }
123    }
124
125    /// The associated data.
126    ///
127    /// The returned JSON object holds the contents of `m.relates_to`, including `rel_type` and
128    /// `event_id` if present, but not things like `m.new_content` for `m.replace` relations that
129    /// live next to `m.relates_to`.
130    ///
131    /// Prefer to use the public variants of `Relation` where possible; this method is meant to
132    /// be used for custom relations only.
133    pub fn data(&self) -> Cow<'_, JsonObject> {
134        if let Relation::_Custom(CustomRelation(data)) = self {
135            Cow::Borrowed(data)
136        } else {
137            Cow::Owned(self.serialize_data())
138        }
139    }
140}
141
142impl<C> From<message::Relation<C>> for Relation {
143    fn from(rel: message::Relation<C>) -> Self {
144        match rel {
145            message::Relation::Reply { in_reply_to } => Self::Reply { in_reply_to },
146            message::Relation::Replacement(re) => {
147                Self::Replacement(Replacement { event_id: re.event_id })
148            }
149            message::Relation::Thread(t) => Self::Thread(Thread {
150                event_id: t.event_id,
151                in_reply_to: t.in_reply_to,
152                is_falling_back: t.is_falling_back,
153            }),
154            message::Relation::_Custom(c) => Self::_Custom(c),
155        }
156    }
157}
158
159/// The event this relation belongs to [replaces another event].
160///
161/// In contrast to [`relation::Replacement`](crate::relation::Replacement), this
162/// struct doesn't store the new content, since that is part of the encrypted content of an
163/// `m.room.encrypted` events.
164///
165/// [replaces another event]: https://spec.matrix.org/latest/client-server-api/#event-replacements
166#[derive(Clone, Debug, Deserialize, Serialize)]
167#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
168#[serde(tag = "rel_type", rename = "m.replace")]
169pub struct Replacement {
170    /// The ID of the event being replaced.
171    pub event_id: OwnedEventId,
172}
173
174impl Replacement {
175    /// Creates a new `Replacement` with the given event ID.
176    pub fn new(event_id: OwnedEventId) -> Self {
177        Self { event_id }
178    }
179}
180
181/// The content of an `m.room.encrypted` event using the `m.olm.v1.curve25519-aes-sha2` algorithm.
182#[derive(Clone, Debug, Serialize, Deserialize)]
183#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
184pub struct OlmV1Curve25519AesSha2Content {
185    /// A map from the recipient Curve25519 identity key to ciphertext information.
186    pub ciphertext: BTreeMap<String, CiphertextInfo>,
187
188    /// The Curve25519 key of the sender.
189    pub sender_key: String,
190}
191
192impl OlmV1Curve25519AesSha2Content {
193    /// Creates a new `OlmV1Curve25519AesSha2Content` with the given ciphertext and sender key.
194    pub fn new(ciphertext: BTreeMap<String, CiphertextInfo>, sender_key: String) -> Self {
195        Self { ciphertext, sender_key }
196    }
197}
198
199/// Ciphertext information holding the ciphertext and message type.
200///
201/// Used for messages encrypted with the `m.olm.v1.curve25519-aes-sha2` algorithm.
202#[derive(Clone, Debug, Deserialize, Serialize)]
203#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
204pub struct CiphertextInfo {
205    /// The encrypted payload.
206    pub body: String,
207
208    /// The Olm message type.
209    #[serde(rename = "type")]
210    pub message_type: UInt,
211}
212
213impl CiphertextInfo {
214    /// Creates a new `CiphertextInfo` with the given body and type.
215    pub fn new(body: String, message_type: UInt) -> Self {
216        Self { body, message_type }
217    }
218}
219
220/// The content of an `m.room.encrypted` event using the `m.megolm.v1.aes-sha2` algorithm.
221///
222/// To create an instance of this type, first create a `MegolmV1AesSha2ContentInit` and convert it
223/// via `MegolmV1AesSha2Content::from` / `.into()`.
224#[derive(Clone, Debug, Serialize, Deserialize)]
225#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
226pub struct MegolmV1AesSha2Content {
227    /// The encrypted content of the event.
228    pub ciphertext: String,
229
230    /// The Curve25519 key of the sender.
231    #[deprecated = "this field still needs to be sent but should not be used when received"]
232    pub sender_key: String,
233
234    /// The ID of the sending device.
235    #[deprecated = "this field still needs to be sent but should not be used when received"]
236    pub device_id: OwnedDeviceId,
237
238    /// The ID of the session used to encrypt the message.
239    pub session_id: String,
240}
241
242/// Mandatory initial set of fields of `MegolmV1AesSha2Content`.
243///
244/// This struct will not be updated even if additional fields are added to `MegolmV1AesSha2Content`
245/// in a new (non-breaking) release of the Matrix specification.
246#[derive(Debug)]
247#[allow(clippy::exhaustive_structs)]
248pub struct MegolmV1AesSha2ContentInit {
249    /// The encrypted content of the event.
250    pub ciphertext: String,
251
252    /// The Curve25519 key of the sender.
253    pub sender_key: String,
254
255    /// The ID of the sending device.
256    pub device_id: OwnedDeviceId,
257
258    /// The ID of the session used to encrypt the message.
259    pub session_id: String,
260}
261
262impl From<MegolmV1AesSha2ContentInit> for MegolmV1AesSha2Content {
263    /// Creates a new `MegolmV1AesSha2Content` from the given init struct.
264    fn from(init: MegolmV1AesSha2ContentInit) -> Self {
265        let MegolmV1AesSha2ContentInit { ciphertext, sender_key, device_id, session_id } = init;
266        #[allow(deprecated)]
267        Self { ciphertext, sender_key, device_id, session_id }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use assert_matches2::assert_matches;
274    use js_int::uint;
275    use ruma_common::{owned_event_id, serde::Raw};
276    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
277
278    use super::{
279        EncryptedEventScheme, InReplyTo, MegolmV1AesSha2ContentInit, Relation,
280        RoomEncryptedEventContent,
281    };
282
283    #[test]
284    fn serialization() {
285        let key_verification_start_content = RoomEncryptedEventContent {
286            scheme: EncryptedEventScheme::MegolmV1AesSha2(
287                MegolmV1AesSha2ContentInit {
288                    ciphertext: "ciphertext".into(),
289                    sender_key: "sender_key".into(),
290                    device_id: "device_id".into(),
291                    session_id: "session_id".into(),
292                }
293                .into(),
294            ),
295            relates_to: Some(Relation::Reply {
296                in_reply_to: InReplyTo { event_id: owned_event_id!("$h29iv0s8:example.com") },
297            }),
298        };
299
300        let json_data = json!({
301            "algorithm": "m.megolm.v1.aes-sha2",
302            "ciphertext": "ciphertext",
303            "sender_key": "sender_key",
304            "device_id": "device_id",
305            "session_id": "session_id",
306            "m.relates_to": {
307                "m.in_reply_to": {
308                    "event_id": "$h29iv0s8:example.com"
309                }
310            },
311        });
312
313        assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
314    }
315
316    #[test]
317    #[allow(deprecated)]
318    fn deserialization() {
319        let json_data = json!({
320            "algorithm": "m.megolm.v1.aes-sha2",
321            "ciphertext": "ciphertext",
322            "sender_key": "sender_key",
323            "device_id": "device_id",
324            "session_id": "session_id",
325            "m.relates_to": {
326                "m.in_reply_to": {
327                    "event_id": "$h29iv0s8:example.com"
328                }
329            },
330        });
331
332        let content: RoomEncryptedEventContent = from_json_value(json_data).unwrap();
333
334        assert_matches!(content.scheme, EncryptedEventScheme::MegolmV1AesSha2(scheme));
335        assert_eq!(scheme.ciphertext, "ciphertext");
336        assert_eq!(scheme.sender_key, "sender_key");
337        assert_eq!(scheme.device_id, "device_id");
338        assert_eq!(scheme.session_id, "session_id");
339
340        assert_matches!(content.relates_to, Some(Relation::Reply { in_reply_to }));
341        assert_eq!(in_reply_to.event_id, "$h29iv0s8:example.com");
342    }
343
344    #[test]
345    fn deserialization_olm() {
346        let json_data = json!({
347            "sender_key": "test_key",
348            "ciphertext": {
349                "test_curve_key": {
350                    "body": "encrypted_body",
351                    "type": 1
352                }
353            },
354            "algorithm": "m.olm.v1.curve25519-aes-sha2"
355        });
356        let content: RoomEncryptedEventContent = from_json_value(json_data).unwrap();
357
358        assert_matches!(content.scheme, EncryptedEventScheme::OlmV1Curve25519AesSha2(c));
359        assert_eq!(c.sender_key, "test_key");
360        assert_eq!(c.ciphertext.len(), 1);
361        assert_eq!(c.ciphertext["test_curve_key"].body, "encrypted_body");
362        assert_eq!(c.ciphertext["test_curve_key"].message_type, uint!(1));
363
364        assert_matches!(content.relates_to, None);
365    }
366
367    #[test]
368    fn deserialization_failure() {
369        from_json_value::<Raw<RoomEncryptedEventContent>>(
370            json!({ "algorithm": "m.megolm.v1.aes-sha2" }),
371        )
372        .unwrap()
373        .deserialize()
374        .unwrap_err();
375    }
376}