1use 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::{serde::Raw, RoomVersionId};
12
13#[cfg(feature = "canonical-json")]
15#[derive(Debug)]
16#[allow(clippy::exhaustive_enums)]
17pub enum CanonicalJsonError {
18 IntConvert,
20
21 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#[cfg(feature = "canonical-json")]
40#[derive(Debug)]
41#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
42pub enum RedactionError {
43 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
45 NotOfType {
46 field: String,
48 of_type: JsonType,
50 },
51
52 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#[derive(Debug)]
83#[allow(clippy::exhaustive_enums)]
84pub enum JsonType {
85 Object,
87
88 String,
90
91 Integer,
93
94 Array,
96
97 Boolean,
99
100 Null,
102}
103
104pub 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
111pub 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#[derive(Clone, Debug)]
120pub struct RedactedBecause(CanonicalJsonObject);
121
122impl RedactedBecause {
123 pub fn from_json(obj: CanonicalJsonObject) -> Self {
125 Self(obj)
126 }
127
128 pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
132 ev.deserialize_as().map(Self)
133 }
134}
135
136pub trait RedactionEvent {}
138
139pub fn redact(
164 mut object: CanonicalJsonObject,
165 version: &RoomVersionId,
166 redacted_because: Option<RedactedBecause>,
167) -> Result<CanonicalJsonObject, RedactionError> {
168 redact_in_place(&mut object, version, redacted_because)?;
169 Ok(object)
170}
171
172pub fn redact_in_place(
176 event: &mut CanonicalJsonObject,
177 version: &RoomVersionId,
178 redacted_because: Option<RedactedBecause>,
179) -> Result<(), RedactionError> {
180 let allowed_content_keys = match event.get("type") {
183 Some(CanonicalJsonValue::String(event_type)) => {
184 allowed_content_keys_for(event_type, version)
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 object_retain_keys(content, allowed_content_keys)?;
197 }
198
199 let mut old_event = mem::take(event);
200
201 for &key in allowed_event_keys_for(version) {
202 if let Some(value) = old_event.remove(key) {
203 event.insert(key.to_owned(), value);
204 }
205 }
206
207 if let Some(redacted_because) = redacted_because {
208 let unsigned = CanonicalJsonObject::from_iter([(
209 "redacted_because".to_owned(),
210 redacted_because.0.into(),
211 )]);
212 event.insert("unsigned".to_owned(), unsigned.into());
213 }
214
215 Ok(())
216}
217
218pub fn redact_content_in_place(
222 object: &mut CanonicalJsonObject,
223 version: &RoomVersionId,
224 event_type: impl AsRef<str>,
225) -> Result<(), RedactionError> {
226 object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version))
227}
228
229fn object_retain_keys(
230 object: &mut CanonicalJsonObject,
231 allowed_keys: &AllowedKeys,
232) -> Result<(), RedactionError> {
233 match *allowed_keys {
234 AllowedKeys::All => {}
235 AllowedKeys::Some { keys, nested } => {
236 object_retain_some_keys(object, keys, nested)?;
237 }
238 AllowedKeys::None => {
239 object.clear();
240 }
241 }
242
243 Ok(())
244}
245
246fn object_retain_some_keys(
247 object: &mut CanonicalJsonObject,
248 keys: &[&str],
249 nested: &[(&str, &AllowedKeys)],
250) -> Result<(), RedactionError> {
251 let mut old_object = mem::take(object);
252
253 for &(nested_key, nested_allowed_keys) in nested {
254 if let Some((key, mut nested_object_value)) = old_object.remove_entry(nested_key) {
255 let nested_object = match &mut nested_object_value {
256 CanonicalJsonValue::Object(map) => map,
257 _ => return Err(RedactionError::not_of_type(nested_key, JsonType::Object)),
258 };
259
260 object_retain_keys(nested_object, nested_allowed_keys)?;
261
262 if !nested_object.is_empty() {
265 object.insert(key, nested_object_value);
266 }
267 }
268 }
269
270 for &key in keys {
271 if let Some((key, value)) = old_object.remove_entry(key) {
272 object.insert(key, value);
273 }
274 }
275
276 Ok(())
277}
278
279fn allowed_event_keys_for(version: &RoomVersionId) -> &'static [&'static str] {
282 match version {
283 RoomVersionId::V1
284 | RoomVersionId::V2
285 | RoomVersionId::V3
286 | RoomVersionId::V4
287 | RoomVersionId::V5
288 | RoomVersionId::V6
289 | RoomVersionId::V7
290 | RoomVersionId::V8
291 | RoomVersionId::V9
292 | RoomVersionId::V10 => &[
293 "event_id",
294 "type",
295 "room_id",
296 "sender",
297 "state_key",
298 "content",
299 "hashes",
300 "signatures",
301 "depth",
302 "prev_events",
303 "prev_state",
304 "auth_events",
305 "origin",
306 "origin_server_ts",
307 "membership",
308 ],
309 _ => &[
310 "event_id",
311 "type",
312 "room_id",
313 "sender",
314 "state_key",
315 "content",
316 "hashes",
317 "signatures",
318 "depth",
319 "prev_events",
320 "auth_events",
321 "origin_server_ts",
322 ],
323 }
324}
325
326#[derive(Clone, Copy)]
328enum AllowedKeys {
329 All,
331 Some {
333 keys: &'static [&'static str],
335
336 nested: &'static [(&'static str, &'static AllowedKeys)],
340 },
341 None,
343}
344
345impl AllowedKeys {
346 const fn some(keys: &'static [&'static str]) -> Self {
348 Self::Some { keys, nested: &[] }
349 }
350
351 const fn some_nested(
353 keys: &'static [&'static str],
354 nested: &'static [(&'static str, &'static AllowedKeys)],
355 ) -> Self {
356 Self::Some { keys, nested }
357 }
358}
359
360static ROOM_MEMBER_V1: AllowedKeys = AllowedKeys::some(&["membership"]);
362static ROOM_MEMBER_V9: AllowedKeys =
364 AllowedKeys::some(&["membership", "join_authorised_via_users_server"]);
365static ROOM_MEMBER_V11: AllowedKeys = AllowedKeys::some_nested(
367 &["membership", "join_authorised_via_users_server"],
368 &[("third_party_invite", &ROOM_MEMBER_THIRD_PARTY_INVITE_V11)],
369);
370static ROOM_MEMBER_THIRD_PARTY_INVITE_V11: AllowedKeys = AllowedKeys::some(&["signed"]);
373
374static ROOM_CREATE_V1: AllowedKeys = AllowedKeys::some(&["creator"]);
376
377static ROOM_JOIN_RULES_V1: AllowedKeys = AllowedKeys::some(&["join_rule"]);
379static ROOM_JOIN_RULES_V8: AllowedKeys = AllowedKeys::some(&["join_rule", "allow"]);
381
382static ROOM_POWER_LEVELS_V1: AllowedKeys = AllowedKeys::some(&[
384 "ban",
385 "events",
386 "events_default",
387 "kick",
388 "redact",
389 "state_default",
390 "users",
391 "users_default",
392]);
393static ROOM_POWER_LEVELS_V11: AllowedKeys = AllowedKeys::some(&[
395 "ban",
396 "events",
397 "events_default",
398 "invite",
399 "kick",
400 "redact",
401 "state_default",
402 "users",
403 "users_default",
404]);
405
406static ROOM_ALIASES_V1: AllowedKeys = AllowedKeys::some(&["aliases"]);
408
409#[cfg(feature = "unstable-msc2870")]
411static ROOM_SERVER_ACL_MSC2870: AllowedKeys =
412 AllowedKeys::some(&["allow", "deny", "allow_ip_literals"]);
413
414static ROOM_HISTORY_VISIBILITY_V1: AllowedKeys = AllowedKeys::some(&["history_visibility"]);
416
417static ROOM_REDACTION_V11: AllowedKeys = AllowedKeys::some(&["redacts"]);
419
420fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'static AllowedKeys {
421 match event_type {
422 "m.room.member" => match version {
423 RoomVersionId::V1
424 | RoomVersionId::V2
425 | RoomVersionId::V3
426 | RoomVersionId::V4
427 | RoomVersionId::V5
428 | RoomVersionId::V6
429 | RoomVersionId::V7
430 | RoomVersionId::V8 => &ROOM_MEMBER_V1,
431 RoomVersionId::V9 | RoomVersionId::V10 => &ROOM_MEMBER_V9,
432 _ => &ROOM_MEMBER_V11,
433 },
434 "m.room.create" => match version {
435 RoomVersionId::V1
436 | RoomVersionId::V2
437 | RoomVersionId::V3
438 | RoomVersionId::V4
439 | RoomVersionId::V5
440 | RoomVersionId::V6
441 | RoomVersionId::V7
442 | RoomVersionId::V8
443 | RoomVersionId::V9
444 | RoomVersionId::V10 => &ROOM_CREATE_V1,
445 _ => &AllowedKeys::All,
446 },
447 "m.room.join_rules" => match version {
448 RoomVersionId::V1
449 | RoomVersionId::V2
450 | RoomVersionId::V3
451 | RoomVersionId::V4
452 | RoomVersionId::V5
453 | RoomVersionId::V6
454 | RoomVersionId::V7 => &ROOM_JOIN_RULES_V1,
455 _ => &ROOM_JOIN_RULES_V8,
456 },
457 "m.room.power_levels" => match version {
458 RoomVersionId::V1
459 | RoomVersionId::V2
460 | RoomVersionId::V3
461 | RoomVersionId::V4
462 | RoomVersionId::V5
463 | RoomVersionId::V6
464 | RoomVersionId::V7
465 | RoomVersionId::V8
466 | RoomVersionId::V9
467 | RoomVersionId::V10 => &ROOM_POWER_LEVELS_V1,
468 _ => &ROOM_POWER_LEVELS_V11,
469 },
470 "m.room.aliases" => match version {
471 RoomVersionId::V1
472 | RoomVersionId::V2
473 | RoomVersionId::V3
474 | RoomVersionId::V4
475 | RoomVersionId::V5 => &ROOM_ALIASES_V1,
476 _ => &AllowedKeys::None,
479 },
480 #[cfg(feature = "unstable-msc2870")]
481 "m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => &ROOM_SERVER_ACL_MSC2870,
482 "m.room.history_visibility" => &ROOM_HISTORY_VISIBILITY_V1,
483 "m.room.redaction" => match version {
484 RoomVersionId::V1
485 | RoomVersionId::V2
486 | RoomVersionId::V3
487 | RoomVersionId::V4
488 | RoomVersionId::V5
489 | RoomVersionId::V6
490 | RoomVersionId::V7
491 | RoomVersionId::V8
492 | RoomVersionId::V9
493 | RoomVersionId::V10 => &AllowedKeys::None,
494 _ => &ROOM_REDACTION_V11,
495 },
496 _ => &AllowedKeys::None,
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use std::collections::BTreeMap;
503
504 use assert_matches2::assert_matches;
505 use js_int::int;
506 use serde_json::{
507 from_str as from_json_str, json, to_string as to_json_string, to_value as to_json_value,
508 };
509
510 use super::{
511 redact_in_place, to_canonical_value, try_from_json_map, value::CanonicalJsonValue,
512 };
513 use crate::RoomVersionId;
514
515 #[test]
516 fn serialize_canon() {
517 let json: CanonicalJsonValue = json!({
518 "a": [1, 2, 3],
519 "other": { "stuff": "hello" },
520 "string": "Thing"
521 })
522 .try_into()
523 .unwrap();
524
525 let ser = to_json_string(&json).unwrap();
526 let back = from_json_str::<CanonicalJsonValue>(&ser).unwrap();
527
528 assert_eq!(json, back);
529 }
530
531 #[test]
532 fn check_canonical_sorts_keys() {
533 let json: CanonicalJsonValue = json!({
534 "auth": {
535 "success": true,
536 "mxid": "@john.doe:example.com",
537 "profile": {
538 "display_name": "John Doe",
539 "three_pids": [
540 {
541 "medium": "email",
542 "address": "john.doe@example.org"
543 },
544 {
545 "medium": "msisdn",
546 "address": "123456789"
547 }
548 ]
549 }
550 }
551 })
552 .try_into()
553 .unwrap();
554
555 assert_eq!(
556 to_json_string(&json).unwrap(),
557 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}}"#
558 );
559 }
560
561 #[test]
562 fn serialize_map_to_canonical() {
563 let mut expected = BTreeMap::new();
564 expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
565 expected.insert(
566 "bar".into(),
567 CanonicalJsonValue::Array(vec![
568 CanonicalJsonValue::Integer(int!(0)),
569 CanonicalJsonValue::Integer(int!(1)),
570 CanonicalJsonValue::Integer(int!(2)),
571 ]),
572 );
573
574 let mut map = serde_json::Map::new();
575 map.insert("foo".into(), json!("string"));
576 map.insert("bar".into(), json!(vec![0, 1, 2,]));
577
578 assert_eq!(try_from_json_map(map).unwrap(), expected);
579 }
580
581 #[test]
582 fn to_canonical() {
583 #[derive(Debug, serde::Serialize)]
584 struct Thing {
585 foo: String,
586 bar: Vec<u8>,
587 }
588 let t = Thing { foo: "string".into(), bar: vec![0, 1, 2] };
589
590 let mut expected = BTreeMap::new();
591 expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
592 expected.insert(
593 "bar".into(),
594 CanonicalJsonValue::Array(vec![
595 CanonicalJsonValue::Integer(int!(0)),
596 CanonicalJsonValue::Integer(int!(1)),
597 CanonicalJsonValue::Integer(int!(2)),
598 ]),
599 );
600
601 assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected));
602 }
603
604 #[test]
605 fn redact_allowed_keys_some() {
606 let original_event = json!({
607 "content": {
608 "ban": 50,
609 "events": {
610 "m.room.avatar": 50,
611 "m.room.canonical_alias": 50,
612 "m.room.history_visibility": 100,
613 "m.room.name": 50,
614 "m.room.power_levels": 100
615 },
616 "events_default": 0,
617 "invite": 0,
618 "kick": 50,
619 "redact": 50,
620 "state_default": 50,
621 "users": {
622 "@example:localhost": 100
623 },
624 "users_default": 0
625 },
626 "event_id": "$15139375512JaHAW:localhost",
627 "origin_server_ts": 45,
628 "sender": "@example:localhost",
629 "room_id": "!room:localhost",
630 "state_key": "",
631 "type": "m.room.power_levels",
632 "unsigned": {
633 "age": 45
634 }
635 });
636
637 assert_matches!(
638 CanonicalJsonValue::try_from(original_event),
639 Ok(CanonicalJsonValue::Object(mut object))
640 );
641
642 redact_in_place(&mut object, &RoomVersionId::V1, None).unwrap();
643
644 let redacted_event = to_json_value(&object).unwrap();
645
646 assert_eq!(
647 redacted_event,
648 json!({
649 "content": {
650 "ban": 50,
651 "events": {
652 "m.room.avatar": 50,
653 "m.room.canonical_alias": 50,
654 "m.room.history_visibility": 100,
655 "m.room.name": 50,
656 "m.room.power_levels": 100
657 },
658 "events_default": 0,
659 "kick": 50,
660 "redact": 50,
661 "state_default": 50,
662 "users": {
663 "@example:localhost": 100
664 },
665 "users_default": 0
666 },
667 "event_id": "$15139375512JaHAW:localhost",
668 "origin_server_ts": 45,
669 "sender": "@example:localhost",
670 "room_id": "!room:localhost",
671 "state_key": "",
672 "type": "m.room.power_levels",
673 })
674 );
675 }
676
677 #[test]
678 fn redact_allowed_keys_none() {
679 let original_event = json!({
680 "content": {
681 "aliases": ["#somewhere:localhost"]
682 },
683 "event_id": "$152037280074GZeOm:localhost",
684 "origin_server_ts": 1,
685 "sender": "@example:localhost",
686 "state_key": "room.com",
687 "room_id": "!room:room.com",
688 "type": "m.room.aliases",
689 "unsigned": {
690 "age": 1
691 }
692 });
693
694 assert_matches!(
695 CanonicalJsonValue::try_from(original_event),
696 Ok(CanonicalJsonValue::Object(mut object))
697 );
698
699 redact_in_place(&mut object, &RoomVersionId::V10, None).unwrap();
700
701 let redacted_event = to_json_value(&object).unwrap();
702
703 assert_eq!(
704 redacted_event,
705 json!({
706 "content": {},
707 "event_id": "$152037280074GZeOm:localhost",
708 "origin_server_ts": 1,
709 "sender": "@example:localhost",
710 "state_key": "room.com",
711 "room_id": "!room:room.com",
712 "type": "m.room.aliases",
713 })
714 );
715 }
716
717 #[test]
718 fn redact_allowed_keys_all() {
719 let original_event = json!({
720 "content": {
721 "m.federate": true,
722 "predecessor": {
723 "event_id": "$something",
724 "room_id": "!oldroom:example.org"
725 },
726 "room_version": "11",
727 },
728 "event_id": "$143273582443PhrSn",
729 "origin_server_ts": 1_432_735,
730 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
731 "sender": "@example:example.org",
732 "state_key": "",
733 "type": "m.room.create",
734 "unsigned": {
735 "age": 1234,
736 },
737 });
738
739 assert_matches!(
740 CanonicalJsonValue::try_from(original_event),
741 Ok(CanonicalJsonValue::Object(mut object))
742 );
743
744 redact_in_place(&mut object, &RoomVersionId::V11, None).unwrap();
745
746 let redacted_event = to_json_value(&object).unwrap();
747
748 assert_eq!(
749 redacted_event,
750 json!({
751 "content": {
752 "m.federate": true,
753 "predecessor": {
754 "event_id": "$something",
755 "room_id": "!oldroom:example.org"
756 },
757 "room_version": "11",
758 },
759 "event_id": "$143273582443PhrSn",
760 "origin_server_ts": 1_432_735,
761 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
762 "sender": "@example:example.org",
763 "state_key": "",
764 "type": "m.room.create",
765 })
766 );
767 }
768}