package event import ( "encoding/json" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" ) // Redact returns the event with everything a redaction removes stripped // off, leaving the properties the room version protects. // // The redacted event keeps the same event ID, because the ID is the // reference hash of the redacted form in the first place — redacting an // event does not rename it, and the signature on it still verifies. That is // the whole point of the algorithm: a server can prove a redacted event is // genuine without ever having seen its content. // // Redact says nothing about whether the redaction is *allowed*. That // depends on the sender's power level or on their server having sent the // original, and belongs to the roomserver along with the decision to apply // it at all. func (e *Event) Redact() (*Event, error) { redacted, err := redactJSON(e.raw, e.rules) if err != nil { return nil, err } out := &Event{rules: e.rules, raw: redacted, id: e.id} if err := json.Unmarshal(redacted, &out.fields); err != nil { return nil, fmt.Errorf("%w: redacted event: %w", ErrInvalidEvent, err) } if e.rules.RedactsInContent && out.fields.Type == TypeRedaction { var content struct { Redacts string `json:"redacts"` } if err := json.Unmarshal(out.fields.Content, &content); err != nil { return nil, fmt.Errorf("%w: redacted m.room.redaction content: %w", ErrInvalidEvent, err) } out.fields.Redacts = content.Redacts } return out, nil } // redactJSON applies the redaction algorithm to raw event JSON and returns // canonical JSON. func redactJSON(raw []byte, rules roomversion.Rules) ([]byte, error) { obj, err := decodeObject(raw) if err != nil { return nil, err } var header struct { Type string `json:"type"` } if err := json.Unmarshal(raw, &header); err != nil { return nil, fmt.Errorf("%w: %w", ErrInvalidEvent, err) } keep := protectedTopLevelKeys(rules) for k := range obj { if !keep[k] { delete(obj, k) } } content, err := redactContent(header.Type, obj["content"], rules) if err != nil { return nil, err } obj["content"] = content out, err := canonicaljson.Marshal(obj) if err != nil { return nil, fmt.Errorf("%w: redact: %w", ErrInvalidEvent, err) } return out, nil } // protectedTopLevelKeys returns the event properties a redaction keeps. func protectedTopLevelKeys(rules roomversion.Rules) map[string]bool { keep := map[string]bool{ "event_id": true, "type": true, "room_id": true, "sender": true, "state_key": true, "content": true, "hashes": true, "signatures": true, "depth": true, "prev_events": true, "auth_events": true, "origin_server_ts": true, } if rules.RedactionProtectsLegacyKeys { // Room versions up to 10 also protected these. None was // load-bearing, and the top-level membership in particular let a // redacted event keep asserting a membership its content no longer // supported, so version 11 stopped protecting them. keep["prev_state"] = true keep["origin"] = true keep["membership"] = true } return keep } // redactContent strips the content of an event down to the keys its type is // allowed to keep. func redactContent(eventType string, content json.RawMessage, rules roomversion.Rules) (json.RawMessage, error) { empty := json.RawMessage("{}") if len(content) == 0 { return empty, nil } // m.room.create keeps all of its content from room version 11, so // there is nothing to strip and nothing to re-encode. if eventType == TypeCreate && rules.RedactionKeepsWholeCreateContent { return content, nil } keep := protectedContentKeys(eventType, rules) keepsThirdPartyInvite := eventType == TypeMember && rules.RedactionKeepsThirdPartyInviteSigned if len(keep) == 0 && !keepsThirdPartyInvite { return empty, nil } var obj map[string]json.RawMessage if err := json.Unmarshal(content, &obj); err != nil { return nil, fmt.Errorf("%w: content: %w", ErrInvalidEvent, err) } for k := range obj { if !keep[k] { delete(obj, k) } } if keepsThirdPartyInvite { if signed, ok := thirdPartyInviteSigned(content); ok { obj["third_party_invite"] = signed } } out, err := json.Marshal(obj) if err != nil { return nil, fmt.Errorf("%w: redact content: %w", ErrInvalidEvent, err) } return out, nil } // protectedContentKeys returns the content keys a redaction keeps for an // event type. An unlisted type keeps nothing. func protectedContentKeys(eventType string, rules roomversion.Rules) map[string]bool { keep := map[string]bool{} switch eventType { case TypeMember: keep["membership"] = true if rules.RedactionKeepsJoinAuthorisedViaUsersServer { // Without this, a redacted join in a restricted room could // never be re-authorised, because the proof that somebody with // the power to invite had allowed it was in the key that went // away. keep["join_authorised_via_users_server"] = true } case TypeCreate: // Before room version 11 only the creator survived, which threw // away room_version — the one property a server needs in order to // read the rest of the room. keep["creator"] = true case TypeJoinRules: keep["join_rule"] = true if rules.RedactionKeepsJoinRulesAllow { keep["allow"] = true } case TypePowerLevels: for _, k := range []string{ "ban", "events", "events_default", "kick", "redact", "state_default", "users", "users_default", } { keep[k] = true } if rules.RedactionKeepsPowerLevelsInvite { keep["invite"] = true } case TypeHistoryVisibility: keep["history_visibility"] = true case TypeRedaction: if rules.RedactionKeepsRedacts { keep["redacts"] = true } case TypeAliases: if rules.RedactionKeepsAliases { keep["aliases"] = true } } return keep } // thirdPartyInviteSigned returns {"signed": ...} from an m.room.member // event's third_party_invite, if it has one. // // From room version 11 this much of a third-party invite survives // redaction: it is the signature chain that proves the invite was issued by // an identity server, and an event that loses it can never be authorised // again. func thirdPartyInviteSigned(content json.RawMessage) (json.RawMessage, bool) { var parsed struct { ThirdPartyInvite map[string]json.RawMessage `json:"third_party_invite"` } if err := json.Unmarshal(content, &parsed); err != nil { return nil, false } signed, ok := parsed.ThirdPartyInvite["signed"] if !ok { return nil, false } out, err := json.Marshal(map[string]json.RawMessage{"signed": signed}) if err != nil { return nil, false } return out, true }