package event import ( "bytes" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/matrix/signing" ) // ErrContentHashMismatch means an event's content does not match the hash // it carries. // // Over federation this is not necessarily an attack and not necessarily // fatal: the usual cause is being sent a redacted copy of an event whose // signature still verifies, because the signature covers only the keys // redaction keeps. The caller's answer is to use the redacted form rather // than to drop the event. var ErrContentHashMismatch = errors.New("event content does not match its content hash") // contentHash computes the content hash of an event: SHA-256 over the // canonical JSON of the event with unsigned, signatures and hashes removed. // // Each of the three is excluded for its own reason. unsigned is explicitly // rewritable in transit; signatures depend on the hash, so including it // would be circular; and hashes is left out so that a future algorithm can // be added alongside sha256 without changing what sha256 covers. func contentHash(raw []byte) ([sha256.Size]byte, error) { obj, err := decodeObject(raw) if err != nil { return [sha256.Size]byte{}, err } delete(obj, "unsigned") delete(obj, "signatures") delete(obj, "hashes") encoded, err := canonicaljson.Marshal(obj) if err != nil { return [sha256.Size]byte{}, fmt.Errorf("%w: content hash: %w", ErrInvalidEvent, err) } return sha256.Sum256(encoded), nil } // referenceHash computes the reference hash of an event, which is what its // ID is made of: the event is redacted, then signatures and unsigned are // removed, then it is canonicalised and hashed. // // Redacting first is what makes an event ID survive redaction — the ID // covers only the keys a redaction keeps, so redacting an event does not // rename it. func referenceHash(raw []byte, rules roomversion.Rules) ([sha256.Size]byte, error) { redacted, err := redactJSON(raw, rules) if err != nil { return [sha256.Size]byte{}, err } obj, err := decodeObject(redacted) if err != nil { return [sha256.Size]byte{}, err } delete(obj, "signatures") delete(obj, "unsigned") encoded, err := canonicaljson.Marshal(obj) if err != nil { return [sha256.Size]byte{}, fmt.Errorf("%w: reference hash: %w", ErrInvalidEvent, err) } return sha256.Sum256(encoded), nil } // computeEventID derives an event ID from the event's reference hash. func computeEventID(raw []byte, rules roomversion.Rules) (string, error) { sum, err := referenceHash(raw, rules) if err != nil { return "", err } switch rules.EventIDFormat { case roomversion.EventIDFormatSHA256: return "$" + base64.RawStdEncoding.EncodeToString(sum[:]), nil case roomversion.EventIDFormatSHA256URLSafe: // URL-safe, because an ID with a '/' in it cannot be put in a path // — which is where event IDs ended up in the federation API. return "$" + base64.RawURLEncoding.EncodeToString(sum[:]), nil default: return "", fmt.Errorf("%w: room version %s does not derive event IDs from a hash", ErrInvalidEvent, rules.Version) } } // ReferenceHash returns the event's reference hash, unencoded. Federation // needs it for the room version 1 and 2 event format and for auth chain // comparisons. func (e *Event) ReferenceHash() ([]byte, error) { sum, err := referenceHash(e.raw, e.rules) if err != nil { return nil, err } return sum[:], nil } // VerifyContentHash checks the event against the content hash it carries. // // It returns ErrContentHashMismatch when the hash does not match, which a // federation caller should answer by using the redacted copy of the event // rather than by rejecting it. func (e *Event) VerifyContentHash() error { want, err := signing.DecodeBase64(e.fields.Hashes.SHA256) if err != nil { return fmt.Errorf("%w: hashes.sha256: %w", ErrInvalidEvent, err) } got, err := contentHash(e.raw) if err != nil { return err } if !bytes.Equal(want, got[:]) { return ErrContentHashMismatch } return nil } // decodeObject parses an event into its properties, leaving every value as // the bytes it arrived as so that nothing we do not understand is lost. func decodeObject(raw []byte) (map[string]json.RawMessage, error) { var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return nil, fmt.Errorf("%w: %w", ErrInvalidEvent, err) } if obj == nil { return nil, fmt.Errorf("%w: event is null, not an object", ErrInvalidEvent) } return obj, nil }