Skip to main content

ruma_events/
room.rs

1//! Modules for events in the `m.room` namespace.
2//!
3//! This module also contains types shared by events in its child namespaces.
4
5use std::{
6    borrow::Cow,
7    collections::{BTreeMap, btree_map},
8    fmt,
9    ops::Deref,
10};
11
12use js_int::UInt;
13use ruma_common::{
14    OwnedMxcUri,
15    serde::{
16        Base64, JsonObject,
17        base64::{Standard, UrlSafe},
18    },
19};
20use ruma_macros::StringEnum;
21use serde::{Deserialize, Serialize, de};
22use serde_json::Value as JsonValue;
23use zeroize::Zeroize;
24
25use crate::PrivOwnedStr;
26
27pub mod avatar;
28pub mod canonical_alias;
29pub mod create;
30pub mod encrypted;
31mod encrypted_file_serde;
32pub mod encryption;
33pub mod guest_access;
34pub mod history_visibility;
35pub mod join_rules;
36#[cfg(feature = "unstable-msc4334")]
37pub mod language;
38pub mod member;
39pub mod message;
40pub mod name;
41pub mod pinned_events;
42pub mod policy;
43pub mod power_levels;
44pub mod redaction;
45pub mod server_acl;
46pub mod third_party_invite;
47mod thumbnail_source_serde;
48pub mod tombstone;
49pub mod topic;
50
51/// The source of a media file.
52#[derive(Clone, Debug, Serialize)]
53#[allow(clippy::exhaustive_enums)]
54pub enum MediaSource {
55    /// The MXC URI to the unencrypted media file.
56    #[serde(rename = "url")]
57    Plain(OwnedMxcUri),
58
59    /// The encryption info of the encrypted media file.
60    #[serde(rename = "file")]
61    Encrypted(Box<EncryptedFile>),
62}
63
64// Custom implementation of `Deserialize`, because serde doesn't guarantee what variant will be
65// deserialized for "externally tagged"¹ enums where multiple "tag" fields exist.
66//
67// ¹ https://serde.rs/enum-representations.html
68impl<'de> Deserialize<'de> for MediaSource {
69    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
70    where
71        D: serde::Deserializer<'de>,
72    {
73        #[derive(Deserialize)]
74        struct MediaSourceJsonRepr {
75            url: Option<OwnedMxcUri>,
76            file: Option<Box<EncryptedFile>>,
77        }
78
79        match MediaSourceJsonRepr::deserialize(deserializer)? {
80            MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
81            // Prefer file if it is set
82            MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
83            MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
84        }
85    }
86}
87
88/// Metadata about an image.
89#[derive(Clone, Debug, Default, Deserialize, Serialize)]
90#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
91pub struct ImageInfo {
92    /// The height of the image in pixels.
93    #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
94    pub height: Option<UInt>,
95
96    /// The width of the image in pixels.
97    #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
98    pub width: Option<UInt>,
99
100    /// The MIME type of the image, e.g. "image/png."
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub mimetype: Option<String>,
103
104    /// The file size of the image in bytes.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub size: Option<UInt>,
107
108    /// Metadata about the image referred to in `thumbnail_source`.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub thumbnail_info: Option<Box<ThumbnailInfo>>,
111
112    /// The source of the thumbnail of the image.
113    #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
114    pub thumbnail_source: Option<MediaSource>,
115
116    /// The [BlurHash](https://blurha.sh) for this image.
117    ///
118    /// This uses the unstable prefix in
119    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
120    #[cfg(feature = "unstable-msc2448")]
121    #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
122    pub blurhash: Option<String>,
123
124    /// The [ThumbHash](https://evanw.github.io/thumbhash/) for this image.
125    ///
126    /// This uses the unstable prefix in
127    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
128    #[cfg(feature = "unstable-msc2448")]
129    #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
130    pub thumbhash: Option<Base64>,
131
132    /// If this flag is `true`, the original image SHOULD be assumed to be animated. If this flag
133    /// is `false`, the original image SHOULD be assumed to NOT be animated.
134    ///
135    /// If a sending client is unable to determine whether an image is animated, it SHOULD leave
136    /// the flag unset.
137    ///
138    /// Receiving clients MAY use this flag to optimize whether to download the original image
139    /// rather than a thumbnail if it is animated, but they SHOULD NOT trust this flag.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub is_animated: Option<bool>,
142}
143
144impl ImageInfo {
145    /// Creates an empty `ImageInfo`.
146    pub fn new() -> Self {
147        Self::default()
148    }
149}
150
151/// Metadata about a thumbnail.
152#[derive(Clone, Debug, Default, Deserialize, Serialize)]
153#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
154pub struct ThumbnailInfo {
155    /// The height of the thumbnail in pixels.
156    #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
157    pub height: Option<UInt>,
158
159    /// The width of the thumbnail in pixels.
160    #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
161    pub width: Option<UInt>,
162
163    /// The MIME type of the thumbnail, e.g. "image/png."
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub mimetype: Option<String>,
166
167    /// The file size of the thumbnail in bytes.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub size: Option<UInt>,
170}
171
172impl ThumbnailInfo {
173    /// Creates an empty `ThumbnailInfo`.
174    pub fn new() -> Self {
175        Self::default()
176    }
177}
178
179/// A file sent to a room with end-to-end encryption enabled.
180#[derive(Clone, Debug, Deserialize, Serialize)]
181#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
182pub struct EncryptedFile {
183    /// The URL to the file.
184    pub url: OwnedMxcUri,
185
186    /// Information about the encryption of the file.
187    #[serde(flatten)]
188    pub info: EncryptedFileInfo,
189
190    /// A map from an algorithm name to a hash of the ciphertext.
191    ///
192    /// Clients should support the SHA-256 hash.
193    pub hashes: EncryptedFileHashes,
194}
195
196impl EncryptedFile {
197    /// Construct a new `EncryptedFile` with the given URL, encryption info and hashes.
198    pub fn new(url: OwnedMxcUri, info: EncryptedFileInfo, hashes: EncryptedFileHashes) -> Self {
199        Self { url, info, hashes }
200    }
201}
202
203/// Information about the encryption of a file.
204#[derive(Debug, Clone, Serialize)]
205#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
206#[serde(tag = "v", rename_all = "lowercase")]
207pub enum EncryptedFileInfo {
208    /// Information about a file encrypted using version 2 of the attachment encryption protocol.
209    V2(V2EncryptedFileInfo),
210
211    #[doc(hidden)]
212    #[serde(untagged)]
213    _Custom(CustomEncryptedFileInfo),
214}
215
216impl EncryptedFileInfo {
217    /// Get the version of the attachment encryption protocol.
218    ///
219    /// This matches the `v` field in the serialized data.
220    pub fn version(&self) -> &str {
221        match self {
222            Self::V2(_) => "v2",
223            Self::_Custom(info) => &info.v,
224        }
225    }
226
227    /// Get the data of the attachment encryption protocol.
228    ///
229    /// The returned JSON object won't contain the `v` field, use [`.version()`][Self::version] to
230    /// access it.
231    ///
232    /// Prefer to use the public variants of `EncryptedFileInfo` where possible; this method is
233    /// meant to be used for custom versions only.
234    pub fn data(&self) -> Cow<'_, JsonObject> {
235        fn serialize<T: Serialize>(obj: &T) -> JsonObject {
236            match serde_json::to_value(obj).expect("encrypted file info serialization to succeed") {
237                JsonValue::Object(mut obj) => {
238                    obj.remove("body");
239                    obj
240                }
241                _ => panic!("all encrypted file info variants must serialize to objects"),
242            }
243        }
244
245        match self {
246            Self::V2(i) => Cow::Owned(serialize(i)),
247            Self::_Custom(i) => Cow::Borrowed(&i.data),
248        }
249    }
250}
251
252impl From<V2EncryptedFileInfo> for EncryptedFileInfo {
253    fn from(value: V2EncryptedFileInfo) -> Self {
254        Self::V2(value)
255    }
256}
257
258/// A file encrypted with the AES-CTR algorithm with a 256-bit key.
259#[derive(Clone)]
260#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
261pub struct V2EncryptedFileInfo {
262    /// The 256-bit key used to encrypt or decrypt the file.
263    pub k: Base64<UrlSafe, [u8; 32]>,
264
265    /// The 128-bit unique counter block used by AES-CTR.
266    pub iv: Base64<Standard, [u8; 16]>,
267}
268
269impl V2EncryptedFileInfo {
270    /// Construct a new `V2EncryptedFileInfo` with the given encoded key and initialization vector.
271    pub fn new(k: Base64<UrlSafe, [u8; 32]>, iv: Base64<Standard, [u8; 16]>) -> Self {
272        Self { k, iv }
273    }
274
275    /// Construct a new `V2EncryptedFileInfo` by base64-encoding the given key and initialization
276    /// vector bytes.
277    pub fn encode(k: [u8; 32], iv: [u8; 16]) -> Self {
278        Self::new(Base64::new(k), Base64::new(iv))
279    }
280}
281
282impl fmt::Debug for V2EncryptedFileInfo {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        f.debug_struct("V2EncryptedFileInfo").finish_non_exhaustive()
285    }
286}
287
288impl Drop for V2EncryptedFileInfo {
289    fn drop(&mut self) {
290        self.k.zeroize();
291    }
292}
293
294/// Information about a file encrypted using a custom version of the attachment encryption protocol.
295#[doc(hidden)]
296#[derive(Debug, Clone, Serialize)]
297pub struct CustomEncryptedFileInfo {
298    /// The version of the protocol.
299    v: String,
300
301    /// Extra data about the encryption.
302    #[serde(flatten)]
303    data: JsonObject,
304}
305
306/// A map of [`EncryptedFileHashAlgorithm`] to the associated [`EncryptedFileHash`].
307///
308/// This type is used to ensure that a supported [`EncryptedFileHash`] always matches the
309/// appropriate [`EncryptedFileHashAlgorithm`].
310#[derive(Clone, Debug, Default)]
311#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
312pub struct EncryptedFileHashes(BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>);
313
314impl EncryptedFileHashes {
315    /// Construct an empty `EncryptedFileHashes`.
316    pub fn new() -> Self {
317        Self::default()
318    }
319
320    /// Construct an `EncryptedFileHashes` that includes the given SHA-256 hash.
321    pub fn with_sha256(hash: [u8; 32]) -> Self {
322        std::iter::once(EncryptedFileHash::Sha256(Base64::new(hash))).collect()
323    }
324
325    /// Insert the given [`EncryptedFileHash`].
326    ///
327    /// If a map with the same [`EncryptedFileHashAlgorithm`] was already present, it is returned.
328    pub fn insert(&mut self, hash: EncryptedFileHash) -> Option<EncryptedFileHash> {
329        self.0.insert(hash.algorithm(), hash)
330    }
331}
332
333impl Deref for EncryptedFileHashes {
334    type Target = BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>;
335
336    fn deref(&self) -> &Self::Target {
337        &self.0
338    }
339}
340
341impl FromIterator<EncryptedFileHash> for EncryptedFileHashes {
342    fn from_iter<T: IntoIterator<Item = EncryptedFileHash>>(iter: T) -> Self {
343        Self(iter.into_iter().map(|hash| (hash.algorithm(), hash)).collect())
344    }
345}
346
347impl Extend<EncryptedFileHash> for EncryptedFileHashes {
348    fn extend<T: IntoIterator<Item = EncryptedFileHash>>(&mut self, iter: T) {
349        self.0.extend(iter.into_iter().map(|hash| (hash.algorithm(), hash)));
350    }
351}
352
353impl IntoIterator for EncryptedFileHashes {
354    type Item = EncryptedFileHash;
355    type IntoIter = btree_map::IntoValues<EncryptedFileHashAlgorithm, EncryptedFileHash>;
356
357    fn into_iter(self) -> Self::IntoIter {
358        self.0.into_values()
359    }
360}
361
362/// An algorithm used to generate the hash of an [`EncryptedFile`].
363#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
364#[derive(Clone, StringEnum)]
365#[ruma_enum(rename_all = "lowercase")]
366#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
367pub enum EncryptedFileHashAlgorithm {
368    /// The SHA-256 algorithm
369    Sha256,
370
371    #[doc(hidden)]
372    _Custom(PrivOwnedStr),
373}
374
375/// The hash of an encrypted file's ciphertext.
376#[derive(Clone, Debug)]
377#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
378pub enum EncryptedFileHash {
379    /// A hash computed with the SHA-256 algorithm.
380    Sha256(Base64<Standard, [u8; 32]>),
381
382    #[doc(hidden)]
383    _Custom(CustomEncryptedFileHash),
384}
385
386impl EncryptedFileHash {
387    /// The key that was used to group this map.
388    pub fn algorithm(&self) -> EncryptedFileHashAlgorithm {
389        match self {
390            Self::Sha256(_) => EncryptedFileHashAlgorithm::Sha256,
391            Self::_Custom(custom) => custom.algorithm.as_str().into(),
392        }
393    }
394
395    /// Get a reference to the decoded bytes of the hash.
396    pub fn as_bytes(&self) -> &[u8] {
397        match self {
398            Self::Sha256(hash) => hash.as_bytes(),
399            Self::_Custom(custom) => custom.hash.as_bytes(),
400        }
401    }
402
403    /// Get the decoded bytes of the hash.
404    pub fn into_bytes(self) -> Vec<u8> {
405        match self {
406            Self::Sha256(hash) => hash.into_inner().into(),
407            Self::_Custom(custom) => custom.hash.into_inner(),
408        }
409    }
410}
411
412/// A map of results grouped by custom key type.
413#[doc(hidden)]
414#[derive(Clone, Debug)]
415pub struct CustomEncryptedFileHash {
416    /// The algorithm that was used to generate the hash.
417    algorithm: String,
418
419    /// The hash.
420    hash: Base64,
421}
422
423#[cfg(test)]
424mod tests {
425    use assert_matches2::assert_matches;
426    use ruma_common::owned_mxc_uri;
427    use serde::Deserialize;
428    use serde_json::{from_value as from_json_value, json};
429
430    use super::{EncryptedFile, MediaSource, V2EncryptedFileInfo};
431    use crate::room::EncryptedFileHashes;
432
433    #[derive(Deserialize)]
434    struct MsgWithAttachment {
435        #[allow(dead_code)]
436        body: String,
437        #[serde(flatten)]
438        source: MediaSource,
439    }
440
441    #[test]
442    fn prefer_encrypted_attachment_over_plain() {
443        let msg: MsgWithAttachment = from_json_value(json!({
444            "body": "",
445            "file": EncryptedFile::new(
446                owned_mxc_uri!("mxc://localhost/encryptedfile"),
447                V2EncryptedFileInfo::encode([0;32], [1;16]).into(),
448                EncryptedFileHashes::new(),
449            ),
450            "url": "mxc://localhost/file",
451        }))
452        .unwrap();
453
454        assert_matches!(msg.source, MediaSource::Encrypted(_));
455    }
456}