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