Skip to main content

ruma_common/canonical_json/
redaction.rs

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