ruma_common/
canonical_json.rs

1//! Canonical JSON types and related functions.
2
3use std::{fmt, mem};
4
5use serde::Serialize;
6use serde_json::Value as JsonValue;
7
8mod value;
9
10pub use self::value::{CanonicalJsonObject, CanonicalJsonValue};
11use crate::{room_version_rules::RedactionRules, serde::Raw};
12
13/// The set of possible errors when serializing to canonical JSON.
14#[derive(Debug)]
15#[allow(clippy::exhaustive_enums)]
16pub enum CanonicalJsonError {
17    /// The numeric value failed conversion to js_int::Int.
18    IntConvert,
19
20    /// An error occurred while serializing/deserializing.
21    SerDe(serde_json::Error),
22}
23
24impl fmt::Display for CanonicalJsonError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            CanonicalJsonError::IntConvert => {
28                f.write_str("number found is not a valid `js_int::Int`")
29            }
30            CanonicalJsonError::SerDe(err) => write!(f, "serde Error: {err}"),
31        }
32    }
33}
34
35impl std::error::Error for CanonicalJsonError {}
36
37/// Errors that can happen in redaction.
38#[derive(Debug)]
39#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
40pub enum RedactionError {
41    /// The field `field` is not of the correct type `of_type` ([`JsonType`]).
42    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
43    NotOfType {
44        /// The field name.
45        field: String,
46        /// The expected JSON type.
47        of_type: JsonType,
48    },
49
50    /// The given required field is missing from a JSON object.
51    JsonFieldMissingFromObject(String),
52}
53
54impl fmt::Display for RedactionError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            RedactionError::NotOfType { field, of_type } => {
58                write!(f, "Value in {field:?} must be a JSON {of_type:?}")
59            }
60            RedactionError::JsonFieldMissingFromObject(field) => {
61                write!(f, "JSON object must contain the field {field:?}")
62            }
63        }
64    }
65}
66
67impl std::error::Error for RedactionError {}
68
69impl RedactionError {
70    fn not_of_type(target: &str, of_type: JsonType) -> Self {
71        Self::NotOfType { field: target.to_owned(), of_type }
72    }
73
74    fn field_missing_from_object(target: &str) -> Self {
75        Self::JsonFieldMissingFromObject(target.to_owned())
76    }
77}
78
79/// A JSON type enum for [`RedactionError`] variants.
80#[derive(Debug)]
81#[allow(clippy::exhaustive_enums)]
82pub enum JsonType {
83    /// A JSON Object.
84    Object,
85
86    /// A JSON String.
87    String,
88
89    /// A JSON Integer.
90    Integer,
91
92    /// A JSON Array.
93    Array,
94
95    /// A JSON Boolean.
96    Boolean,
97
98    /// JSON Null.
99    Null,
100}
101
102/// Fallible conversion from a `serde_json::Map` to a `CanonicalJsonObject`.
103pub fn try_from_json_map(
104    json: serde_json::Map<String, JsonValue>,
105) -> Result<CanonicalJsonObject, CanonicalJsonError> {
106    json.into_iter().map(|(k, v)| Ok((k, v.try_into()?))).collect()
107}
108
109/// Fallible conversion from any value that impl's `Serialize` to a `CanonicalJsonValue`.
110pub fn to_canonical_value<T: Serialize>(
111    value: T,
112) -> Result<CanonicalJsonValue, CanonicalJsonError> {
113    serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)?.try_into()
114}
115
116/// The value to put in `unsigned.redacted_because`.
117#[derive(Clone, Debug)]
118pub struct RedactedBecause(CanonicalJsonObject);
119
120impl RedactedBecause {
121    /// Create a `RedactedBecause` from an arbitrary JSON object.
122    pub fn from_json(obj: CanonicalJsonObject) -> Self {
123        Self(obj)
124    }
125
126    /// Create a `RedactedBecause` from a redaction event.
127    ///
128    /// Fails if the raw event is not valid canonical JSON.
129    pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
130        ev.deserialize_as_unchecked().map(Self)
131    }
132}
133
134/// Marker trait for redaction events.
135pub trait RedactionEvent {}
136
137/// Redacts an event using the rules specified in the Matrix client-server specification.
138///
139/// This is part of the process of signing an event.
140///
141/// Redaction is also suggested when verifying an event with `verify_event` returns
142/// `Verified::Signatures`. See the documentation for `Verified` for details.
143///
144/// Returns a new JSON object with all applicable fields redacted.
145///
146/// # Parameters
147///
148/// * `object`: A JSON object to redact.
149/// * `version`: The room version, determines which keys to keep for a few event types.
150/// * `redacted_because`: If this is set, an `unsigned` object with a `redacted_because` field set
151///   to the given value is added to the event after redaction.
152///
153/// # Errors
154///
155/// Returns an error if:
156///
157/// * `object` contains a field called `content` that is not a JSON object.
158/// * `object` contains a field called `hashes` that is not a JSON object.
159/// * `object` contains a field called `signatures` that is not a JSON object.
160/// * `object` is missing the `type` field or the field is not a JSON string.
161pub fn redact(
162    mut object: CanonicalJsonObject,
163    rules: &RedactionRules,
164    redacted_because: Option<RedactedBecause>,
165) -> Result<CanonicalJsonObject, RedactionError> {
166    redact_in_place(&mut object, rules, redacted_because)?;
167    Ok(object)
168}
169
170/// Redacts an event using the rules specified in the Matrix client-server specification.
171///
172/// Functionally equivalent to `redact`, only this'll redact the event in-place.
173pub fn redact_in_place(
174    event: &mut CanonicalJsonObject,
175    rules: &RedactionRules,
176    redacted_because: Option<RedactedBecause>,
177) -> Result<(), RedactionError> {
178    // Get the content keys here even if they're only needed inside the branch below, because we
179    // can't teach rust that this is a disjoint borrow with `get_mut("content")`.
180    let retained_event_content_keys = match event.get("type") {
181        Some(CanonicalJsonValue::String(event_type)) => {
182            retained_event_content_keys(event_type.as_ref(), rules)
183        }
184        Some(_) => return Err(RedactionError::not_of_type("type", JsonType::String)),
185        None => return Err(RedactionError::field_missing_from_object("type")),
186    };
187
188    if let Some(content_value) = event.get_mut("content") {
189        let CanonicalJsonValue::Object(content) = content_value else {
190            return Err(RedactionError::not_of_type("content", JsonType::Object));
191        };
192
193        retained_event_content_keys.apply(rules, content)?;
194    }
195
196    let retained_event_keys =
197        RetainedKeys::some(|rules, key, _value| Ok(is_event_key_retained(rules, key)));
198    retained_event_keys.apply(rules, event)?;
199
200    if let Some(redacted_because) = redacted_because {
201        let unsigned = CanonicalJsonObject::from_iter([(
202            "redacted_because".to_owned(),
203            redacted_because.0.into(),
204        )]);
205        event.insert("unsigned".to_owned(), unsigned.into());
206    }
207
208    Ok(())
209}
210
211/// Redacts the given event content using the given redaction rules for the version of the current
212/// room.
213///
214/// Edits the `content` in-place.
215pub fn redact_content_in_place(
216    content: &mut CanonicalJsonObject,
217    rules: &RedactionRules,
218    event_type: impl AsRef<str>,
219) -> Result<(), RedactionError> {
220    retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content)
221}
222
223/// A function that takes redaction rules, a key and its value, and returns whether the field
224/// should be retained.
225type RetainKeyFn =
226    dyn Fn(&RedactionRules, &str, &mut CanonicalJsonValue) -> Result<bool, RedactionError>;
227
228/// Keys to retain on an object.
229enum RetainedKeys {
230    /// All keys are retained.
231    All,
232
233    /// Some keys are retained, they are determined by the inner function.
234    Some(Box<RetainKeyFn>),
235
236    /// No keys are retained.
237    None,
238}
239
240impl RetainedKeys {
241    /// Construct a `RetainedKeys::Some(_)` with the given function.
242    fn some<F>(retain_key_fn: F) -> Self
243    where
244        F: Fn(&RedactionRules, &str, &mut CanonicalJsonValue) -> Result<bool, RedactionError>
245            + 'static,
246    {
247        Self::Some(Box::new(retain_key_fn))
248    }
249
250    /// Apply this `RetainedKeys` on the given object.
251    fn apply(
252        &self,
253        rules: &RedactionRules,
254        object: &mut CanonicalJsonObject,
255    ) -> Result<(), RedactionError> {
256        match self {
257            Self::All => {}
258            Self::Some(allow_field_fn) => {
259                let old_object = mem::take(object);
260
261                for (key, mut value) in old_object {
262                    if allow_field_fn(rules, &key, &mut value)? {
263                        object.insert(key, value);
264                    }
265                }
266            }
267            Self::None => object.clear(),
268        }
269
270        Ok(())
271    }
272}
273
274/// Get the given keys should be retained at the top level of an event.
275fn is_event_key_retained(rules: &RedactionRules, key: &str) -> bool {
276    match key {
277        "event_id" | "type" | "room_id" | "sender" | "state_key" | "content" | "hashes"
278        | "signatures" | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true,
279        "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state,
280        _ => false,
281    }
282}
283
284/// Get the keys that should be retained in the `content` of an event with the given type.
285fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
286    match event_type {
287        "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
288        "m.room.create" => room_create_content_retained_keys(rules),
289        "m.room.join_rules" => RetainedKeys::some(|rules, key, _value| {
290            is_room_join_rules_content_key_retained(rules, key)
291        }),
292        "m.room.power_levels" => RetainedKeys::some(|rules, key, _value| {
293            is_room_power_levels_content_key_retained(rules, key)
294        }),
295        "m.room.history_visibility" => RetainedKeys::some(|_rules, key, _value| {
296            is_room_history_visibility_content_key_retained(key)
297        }),
298        "m.room.redaction" => room_redaction_content_retained_keys(rules),
299        "m.room.aliases" => room_aliases_content_retained_keys(rules),
300        #[cfg(feature = "unstable-msc2870")]
301        "m.room.server_acl" => RetainedKeys::some(|rules, key, _value| {
302            is_room_server_acl_content_key_retained(rules, key)
303        }),
304        _ => RetainedKeys::None,
305    }
306}
307
308/// Whether the given key in the `content` of an `m.room.member` event is retained after redaction.
309fn is_room_member_content_key_retained(
310    rules: &RedactionRules,
311    key: &str,
312    value: &mut CanonicalJsonValue,
313) -> Result<bool, RedactionError> {
314    Ok(match key {
315        "membership" => true,
316        "join_authorised_via_users_server" => {
317            rules.keep_room_member_join_authorised_via_users_server
318        }
319        "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
320            let Some(third_party_invite) = value.as_object_mut() else {
321                return Err(RedactionError::not_of_type("third_party_invite", JsonType::Object));
322            };
323
324            third_party_invite.retain(|key, _| key == "signed");
325
326            // Keep the field only if it's not empty.
327            !third_party_invite.is_empty()
328        }
329        _ => false,
330    })
331}
332
333/// Get the retained keys in the `content` of an `m.room.create` event.
334fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
335    if rules.keep_room_create_content {
336        RetainedKeys::All
337    } else {
338        RetainedKeys::some(|_rules, field, _value| Ok(field == "creator"))
339    }
340}
341
342/// Whether the given key in the `content` of an `m.room.join_rules` event is retained after
343/// redaction.
344fn is_room_join_rules_content_key_retained(
345    rules: &RedactionRules,
346    key: &str,
347) -> Result<bool, RedactionError> {
348    Ok(match key {
349        "join_rule" => true,
350        "allow" => rules.keep_room_join_rules_allow,
351        _ => false,
352    })
353}
354
355/// Whether the given key in the `content` of an `m.room.power_levels` event is retained after
356/// redaction.
357fn is_room_power_levels_content_key_retained(
358    rules: &RedactionRules,
359    key: &str,
360) -> Result<bool, RedactionError> {
361    Ok(match key {
362        "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
363        | "users_default" => true,
364        "invite" => rules.keep_room_power_levels_invite,
365        _ => false,
366    })
367}
368
369/// Whether the given key in the `content` of an `m.room.history_visibility` event is retained after
370/// redaction.
371fn is_room_history_visibility_content_key_retained(key: &str) -> Result<bool, RedactionError> {
372    Ok(key == "history_visibility")
373}
374
375/// Get the retained keys in the `content` of an `m.room.redaction` event.
376fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
377    if rules.keep_room_redaction_redacts {
378        RetainedKeys::some(|_rules, field, _value| Ok(field == "redacts"))
379    } else {
380        RetainedKeys::None
381    }
382}
383
384/// Get the retained keys in the `content` of an `m.room.aliases` event.
385fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
386    if rules.keep_room_aliases_aliases {
387        RetainedKeys::some(|_rules, field, _value| Ok(field == "aliases"))
388    } else {
389        RetainedKeys::None
390    }
391}
392
393/// Whether the given key in the `content` of an `m.room.server_acl` event is retained after
394/// redaction.
395#[cfg(feature = "unstable-msc2870")]
396fn is_room_server_acl_content_key_retained(
397    rules: &RedactionRules,
398    key: &str,
399) -> Result<bool, RedactionError> {
400    Ok(match key {
401        "allow" | "deny" | "allow_ip_literals" => {
402            rules.keep_room_server_acl_allow_deny_allow_ip_literals
403        }
404        _ => false,
405    })
406}
407
408#[cfg(test)]
409mod tests {
410    use std::collections::BTreeMap;
411
412    use assert_matches2::assert_matches;
413    use js_int::int;
414    use serde_json::{
415        from_str as from_json_str, json, to_string as to_json_string, to_value as to_json_value,
416    };
417
418    use super::{
419        redact_in_place, to_canonical_value, try_from_json_map, value::CanonicalJsonValue,
420    };
421    use crate::room_version_rules::RedactionRules;
422
423    #[test]
424    fn serialize_canon() {
425        let json: CanonicalJsonValue = json!({
426            "a": [1, 2, 3],
427            "other": { "stuff": "hello" },
428            "string": "Thing"
429        })
430        .try_into()
431        .unwrap();
432
433        let ser = to_json_string(&json).unwrap();
434        let back = from_json_str::<CanonicalJsonValue>(&ser).unwrap();
435
436        assert_eq!(json, back);
437    }
438
439    #[test]
440    fn check_canonical_sorts_keys() {
441        let json: CanonicalJsonValue = json!({
442            "auth": {
443                "success": true,
444                "mxid": "@john.doe:example.com",
445                "profile": {
446                    "display_name": "John Doe",
447                    "three_pids": [
448                        {
449                            "medium": "email",
450                            "address": "john.doe@example.org"
451                        },
452                        {
453                            "medium": "msisdn",
454                            "address": "123456789"
455                        }
456                    ]
457                }
458            }
459        })
460        .try_into()
461        .unwrap();
462
463        assert_eq!(
464            to_json_string(&json).unwrap(),
465            r#"{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}"#
466        );
467    }
468
469    #[test]
470    fn serialize_map_to_canonical() {
471        let mut expected = BTreeMap::new();
472        expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
473        expected.insert(
474            "bar".into(),
475            CanonicalJsonValue::Array(vec![
476                CanonicalJsonValue::Integer(int!(0)),
477                CanonicalJsonValue::Integer(int!(1)),
478                CanonicalJsonValue::Integer(int!(2)),
479            ]),
480        );
481
482        let mut map = serde_json::Map::new();
483        map.insert("foo".into(), json!("string"));
484        map.insert("bar".into(), json!(vec![0, 1, 2,]));
485
486        assert_eq!(try_from_json_map(map).unwrap(), expected);
487    }
488
489    #[test]
490    fn to_canonical() {
491        #[derive(Debug, serde::Serialize)]
492        struct Thing {
493            foo: String,
494            bar: Vec<u8>,
495        }
496        let t = Thing { foo: "string".into(), bar: vec![0, 1, 2] };
497
498        let mut expected = BTreeMap::new();
499        expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
500        expected.insert(
501            "bar".into(),
502            CanonicalJsonValue::Array(vec![
503                CanonicalJsonValue::Integer(int!(0)),
504                CanonicalJsonValue::Integer(int!(1)),
505                CanonicalJsonValue::Integer(int!(2)),
506            ]),
507        );
508
509        assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected));
510    }
511
512    #[test]
513    fn redact_allowed_keys_some() {
514        let original_event = json!({
515            "content": {
516                "ban": 50,
517                "events": {
518                    "m.room.avatar": 50,
519                    "m.room.canonical_alias": 50,
520                    "m.room.history_visibility": 100,
521                    "m.room.name": 50,
522                    "m.room.power_levels": 100
523                },
524                "events_default": 0,
525                "invite": 0,
526                "kick": 50,
527                "redact": 50,
528                "state_default": 50,
529                "users": {
530                    "@example:localhost": 100
531                },
532                "users_default": 0
533            },
534            "event_id": "$15139375512JaHAW:localhost",
535            "origin_server_ts": 45,
536            "sender": "@example:localhost",
537            "room_id": "!room:localhost",
538            "state_key": "",
539            "type": "m.room.power_levels",
540            "unsigned": {
541                "age": 45
542            }
543        });
544
545        assert_matches!(
546            CanonicalJsonValue::try_from(original_event),
547            Ok(CanonicalJsonValue::Object(mut object))
548        );
549
550        redact_in_place(&mut object, &RedactionRules::V1, None).unwrap();
551
552        let redacted_event = to_json_value(&object).unwrap();
553
554        assert_eq!(
555            redacted_event,
556            json!({
557                "content": {
558                    "ban": 50,
559                    "events": {
560                        "m.room.avatar": 50,
561                        "m.room.canonical_alias": 50,
562                        "m.room.history_visibility": 100,
563                        "m.room.name": 50,
564                        "m.room.power_levels": 100
565                    },
566                    "events_default": 0,
567                    "kick": 50,
568                    "redact": 50,
569                    "state_default": 50,
570                    "users": {
571                        "@example:localhost": 100
572                    },
573                    "users_default": 0
574                },
575                "event_id": "$15139375512JaHAW:localhost",
576                "origin_server_ts": 45,
577                "sender": "@example:localhost",
578                "room_id": "!room:localhost",
579                "state_key": "",
580                "type": "m.room.power_levels",
581            })
582        );
583    }
584
585    #[test]
586    fn redact_allowed_keys_none() {
587        let original_event = json!({
588            "content": {
589                "aliases": ["#somewhere:localhost"]
590            },
591            "event_id": "$152037280074GZeOm:localhost",
592            "origin_server_ts": 1,
593            "sender": "@example:localhost",
594            "state_key": "room.com",
595            "room_id": "!room:room.com",
596            "type": "m.room.aliases",
597            "unsigned": {
598                "age": 1
599            }
600        });
601
602        assert_matches!(
603            CanonicalJsonValue::try_from(original_event),
604            Ok(CanonicalJsonValue::Object(mut object))
605        );
606
607        redact_in_place(&mut object, &RedactionRules::V9, None).unwrap();
608
609        let redacted_event = to_json_value(&object).unwrap();
610
611        assert_eq!(
612            redacted_event,
613            json!({
614                "content": {},
615                "event_id": "$152037280074GZeOm:localhost",
616                "origin_server_ts": 1,
617                "sender": "@example:localhost",
618                "state_key": "room.com",
619                "room_id": "!room:room.com",
620                "type": "m.room.aliases",
621            })
622        );
623    }
624
625    #[test]
626    fn redact_allowed_keys_all() {
627        let original_event = json!({
628            "content": {
629              "m.federate": true,
630              "predecessor": {
631                "event_id": "$something",
632                "room_id": "!oldroom:example.org"
633              },
634              "room_version": "11",
635            },
636            "event_id": "$143273582443PhrSn",
637            "origin_server_ts": 1_432_735,
638            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
639            "sender": "@example:example.org",
640            "state_key": "",
641            "type": "m.room.create",
642            "unsigned": {
643              "age": 1234,
644            },
645        });
646
647        assert_matches!(
648            CanonicalJsonValue::try_from(original_event),
649            Ok(CanonicalJsonValue::Object(mut object))
650        );
651
652        redact_in_place(&mut object, &RedactionRules::V11, None).unwrap();
653
654        let redacted_event = to_json_value(&object).unwrap();
655
656        assert_eq!(
657            redacted_event,
658            json!({
659                "content": {
660                  "m.federate": true,
661                  "predecessor": {
662                    "event_id": "$something",
663                    "room_id": "!oldroom:example.org"
664                  },
665                  "room_version": "11",
666                },
667                "event_id": "$143273582443PhrSn",
668                "origin_server_ts": 1_432_735,
669                "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
670                "sender": "@example:example.org",
671                "state_key": "",
672                "type": "m.room.create",
673            })
674        );
675    }
676}