Skip to main content

ruma_common/canonical_json/
redaction.rs

1use std::{fmt, mem};
2
3use super::value::{CanonicalJsonObject, CanonicalJsonType, CanonicalJsonValue};
4use crate::{room_version_rules::RedactionRules, serde::Raw};
5
6/// Redacts an event using the rules specified in the Matrix client-server specification.
7///
8/// This is part of the process of signing an event.
9///
10/// Redaction is also suggested when verifying an event with `verify_event` returns
11/// `Verified::Signatures`. See the documentation for `Verified` for details.
12///
13/// Returns a new JSON object with all applicable fields redacted.
14///
15/// # Parameters
16///
17/// * `object`: A JSON object to redact.
18/// * `version`: The room version, determines which keys to keep for a few event types.
19/// * `redacted_because`: If this is set, an `unsigned` object with a `redacted_because` field set
20///   to the given value is added to the event after redaction.
21///
22/// # Errors
23///
24/// Returns an error if:
25///
26/// * `object` contains a field called `content` that is not a JSON object.
27/// * `object` contains a field called `hashes` that is not a JSON object.
28/// * `object` contains a field called `signatures` that is not a JSON object.
29/// * `object` is missing the `type` field or the field is not a JSON string.
30pub fn redact(
31    mut object: CanonicalJsonObject,
32    rules: &RedactionRules,
33    redacted_because: Option<RedactedBecause>,
34) -> Result<CanonicalJsonObject, RedactionError> {
35    redact_in_place(&mut object, rules, redacted_because)?;
36    Ok(object)
37}
38
39/// Redacts an event using the rules specified in the Matrix client-server specification.
40///
41/// Functionally equivalent to `redact`, only this'll redact the event in-place.
42pub fn redact_in_place(
43    event: &mut CanonicalJsonObject,
44    rules: &RedactionRules,
45    redacted_because: Option<RedactedBecause>,
46) -> Result<(), RedactionError> {
47    retained_event_keys(event)?.apply(rules, event);
48
49    if let Some(redacted_because) = redacted_because {
50        let unsigned = CanonicalJsonObject::from_iter([(
51            "redacted_because".to_owned(),
52            redacted_because.0.into(),
53        )]);
54        event.insert("unsigned".to_owned(), unsigned.into());
55    }
56
57    Ok(())
58}
59
60/// Redacts the given event content using the given redaction rules for the version of the current
61/// room.
62///
63/// Edits the `content` in-place.
64pub fn redact_content_in_place(
65    content: &mut CanonicalJsonObject,
66    rules: &RedactionRules,
67    event_type: impl AsRef<str>,
68) {
69    retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content);
70}
71
72/// The value to put in `unsigned.redacted_because`.
73#[derive(Clone, Debug)]
74pub struct RedactedBecause(CanonicalJsonObject);
75
76impl RedactedBecause {
77    /// Create a `RedactedBecause` from an arbitrary JSON object.
78    pub fn from_json(obj: CanonicalJsonObject) -> Self {
79        Self(obj)
80    }
81
82    /// Create a `RedactedBecause` from a redaction event.
83    ///
84    /// Fails if the raw event is not valid canonical JSON.
85    pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
86        ev.deserialize_as_unchecked().map(Self)
87    }
88}
89
90/// Marker trait for redaction events.
91pub trait RedactionEvent {}
92
93/// Errors that can happen in redaction.
94#[derive(Debug)]
95#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
96pub enum RedactionError {
97    /// The field at `path` was expected to be of type `expected`, but was received as `found`.
98    InvalidType {
99        /// The path of the invalid field.
100        path: String,
101
102        /// The type that was expected.
103        expected: CanonicalJsonType,
104
105        /// The type that was found.
106        found: CanonicalJsonType,
107    },
108
109    /// A required field is missing from a JSON object.
110    MissingField {
111        /// The path of the missing field.
112        path: String,
113    },
114}
115
116impl fmt::Display for RedactionError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            RedactionError::InvalidType { path, expected, found } => {
120                write!(f, "invalid type at `{path}`: expected {expected:?}, found {found:?}")
121            }
122            RedactionError::MissingField { path } => {
123                write!(f, "missing field: `{path}`")
124            }
125        }
126    }
127}
128
129impl std::error::Error for RedactionError {}
130
131/// A function that takes redaction rules and a key and returns whether the field should be
132/// retained.
133type RetainKeyFn = dyn Fn(&RedactionRules, &str) -> RetainKey;
134
135/// Whether a key should be retained.
136enum RetainKey {
137    /// The key should be retained.
138    Yes {
139        /// The rules to apply to the child keys if the value of this key is an object.
140        ///
141        /// If the value is an object and this is `None`, the default is [`RetainedKeys::All`].
142        child_retained_keys: Option<RetainedKeys>,
143    },
144
145    /// The key should be redacted.
146    No,
147}
148
149impl From<bool> for RetainKey {
150    fn from(value: bool) -> Self {
151        if value { Self::Yes { child_retained_keys: None } } else { Self::No }
152    }
153}
154
155/// Keys to retain on an object.
156enum RetainedKeys {
157    /// All keys are retained.
158    All,
159
160    /// Some keys are retained, they are determined by the inner function.
161    Some(Box<RetainKeyFn>),
162
163    /// No keys are retained.
164    None,
165}
166
167impl RetainedKeys {
168    /// Construct a `RetainedKeys::Some(_)` with the given function.
169    fn some<F>(retain_key_fn: F) -> Self
170    where
171        F: Fn(&RedactionRules, &str) -> RetainKey + Clone + 'static,
172    {
173        Self::Some(Box::new(retain_key_fn))
174    }
175
176    /// Apply this `RetainedKeys` on the given object.
177    fn apply(&self, rules: &RedactionRules, object: &mut CanonicalJsonObject) {
178        match self {
179            Self::All => {}
180            Self::Some(retain_key_fn) => {
181                let old_object = mem::take(object);
182
183                for (key, mut value) in old_object {
184                    if let RetainKey::Yes { child_retained_keys } = retain_key_fn(rules, &key) {
185                        if let Some(child_retained_keys) = child_retained_keys
186                            && let CanonicalJsonValue::Object(child_object) = &mut value
187                        {
188                            child_retained_keys.apply(rules, child_object);
189                        }
190
191                        object.insert(key, value);
192                    }
193                }
194            }
195            Self::None => object.clear(),
196        }
197    }
198}
199
200/// Get the given keys should be retained at the top level of an event.
201fn retained_event_keys(event: &CanonicalJsonObject) -> Result<RetainedKeys, RedactionError> {
202    let event_type = match event.get("type") {
203        Some(CanonicalJsonValue::String(event_type)) => event_type.clone(),
204        Some(value) => {
205            return Err(RedactionError::InvalidType {
206                path: "type".to_owned(),
207                expected: CanonicalJsonType::String,
208                found: value.json_type(),
209            });
210        }
211        None => return Err(RedactionError::MissingField { path: "type".to_owned() }),
212    };
213
214    Ok(RetainedKeys::some(move |rules, key| match key {
215        "content" => RetainKey::Yes {
216            child_retained_keys: Some(retained_event_content_keys(&event_type, rules)),
217        },
218        "event_id" | "type" | "room_id" | "sender" | "state_key" | "hashes" | "signatures"
219        | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true.into(),
220        "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state.into(),
221        _ => false.into(),
222    }))
223}
224
225/// Get the keys that should be retained in the `content` of an event with the given type.
226fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
227    match event_type {
228        "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
229        "m.room.create" => room_create_content_retained_keys(rules),
230        "m.room.join_rules" => RetainedKeys::some(is_room_join_rules_content_key_retained),
231        "m.room.power_levels" => RetainedKeys::some(is_room_power_levels_content_key_retained),
232        "m.room.history_visibility" => {
233            RetainedKeys::some(|_rules, key| is_room_history_visibility_content_key_retained(key))
234        }
235        "m.room.redaction" => room_redaction_content_retained_keys(rules),
236        "m.room.aliases" => room_aliases_content_retained_keys(rules),
237        #[cfg(feature = "unstable-msc2870")]
238        "m.room.server_acl" => RetainedKeys::some(is_room_server_acl_content_key_retained),
239        _ => RetainedKeys::None,
240    }
241}
242
243/// Whether the given key in the `content` of an `m.room.member` event is retained after redaction.
244fn is_room_member_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
245    match key {
246        "membership" => true.into(),
247        "join_authorised_via_users_server" => {
248            rules.keep_room_member_join_authorised_via_users_server.into()
249        }
250        "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
251            RetainKey::Yes {
252                child_retained_keys: Some(RetainedKeys::some(|_rules, key| {
253                    (key == "signed").into()
254                })),
255            }
256        }
257        _ => false.into(),
258    }
259}
260
261/// Get the retained keys in the `content` of an `m.room.create` event.
262fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
263    if rules.keep_room_create_content {
264        RetainedKeys::All
265    } else {
266        RetainedKeys::some(|_rules, field| (field == "creator").into())
267    }
268}
269
270/// Whether the given key in the `content` of an `m.room.join_rules` event is retained after
271/// redaction.
272fn is_room_join_rules_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
273    match key {
274        "join_rule" => true,
275        "allow" => rules.keep_room_join_rules_allow,
276        _ => false,
277    }
278    .into()
279}
280
281/// Whether the given key in the `content` of an `m.room.power_levels` event is retained after
282/// redaction.
283fn is_room_power_levels_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
284    match key {
285        "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
286        | "users_default" => true,
287        "invite" => rules.keep_room_power_levels_invite,
288        _ => false,
289    }
290    .into()
291}
292
293/// Whether the given key in the `content` of an `m.room.history_visibility` event is retained after
294/// redaction.
295fn is_room_history_visibility_content_key_retained(key: &str) -> RetainKey {
296    (key == "history_visibility").into()
297}
298
299/// Get the retained keys in the `content` of an `m.room.redaction` event.
300fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
301    if rules.keep_room_redaction_redacts {
302        RetainedKeys::some(|_rules, field| (field == "redacts").into())
303    } else {
304        RetainedKeys::None
305    }
306}
307
308/// Get the retained keys in the `content` of an `m.room.aliases` event.
309fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
310    if rules.keep_room_aliases_aliases {
311        RetainedKeys::some(|_rules, field| (field == "aliases").into())
312    } else {
313        RetainedKeys::None
314    }
315}
316
317/// Whether the given key in the `content` of an `m.room.server_acl` event is retained after
318/// redaction.
319#[cfg(feature = "unstable-msc2870")]
320fn is_room_server_acl_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
321    match key {
322        "allow" | "deny" | "allow_ip_literals" => {
323            rules.keep_room_server_acl_allow_deny_allow_ip_literals
324        }
325        _ => false,
326    }
327    .into()
328}