Skip to main content

ruma_common/canonical_json/
redaction.rs

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