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