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