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