// Package id validates the Matrix identifiers that appear inside events. // // The checks here are deliberately laxer than the ones in userapi, which // guard account creation on this server and enforce the current, strict // localpart grammar. Events are different: a room can contain user IDs // minted years ago by servers that allowed upper case and other characters // the grammar has since dropped, and the spec requires receivers to accept // them. Rejecting them would mean refusing to authorise perfectly valid // history, so the rules below are the "historical" ones — a sigil, a // non-empty localpart, a colon, a server name, and a length limit. package id import ( "errors" "fmt" "strings" ) // MaxLength is the spec's cap on a user ID, room ID, alias or event ID, // sigil and server name included. const MaxLength = 255 // ErrInvalidID is the root of every error this package returns. var ErrInvalidID = errors.New("invalid Matrix identifier") // ValidateUserID checks a user ID against the historical grammar: // "@localpart:server_name", at most 255 bytes, with no NUL anywhere. // // It is the check behind the authorisation rules that call for a "valid // user ID", such as the keys of m.room.power_levels users. func ValidateUserID(s string) error { _, _, err := SplitUserID(s) return err } // SplitUserID splits a user ID into its localpart and server name, without // the sigil or the colon. func SplitUserID(s string) (localpart, serverName string, err error) { if err := checkCommon("user ID", s, '@'); err != nil { return "", "", err } localpart, serverName, ok := strings.Cut(s[1:], ":") if !ok { return "", "", fmt.Errorf("%w: user ID %q has no server name; expected @localpart:server", ErrInvalidID, s) } if localpart == "" { return "", "", fmt.Errorf("%w: user ID %q has an empty localpart", ErrInvalidID, s) } if serverName == "" { return "", "", fmt.Errorf("%w: user ID %q has an empty server name", ErrInvalidID, s) } return localpart, serverName, nil } // ServerNameOf returns the server name from a user ID. func ServerNameOf(userID string) (string, error) { _, serverName, err := SplitUserID(userID) return serverName, err } // SameServer reports whether two user IDs belong to the same homeserver. // // Server names are compared byte for byte: the spec says they are // case-sensitive, and treating "Example.org" and "example.org" as one // server here would quietly widen rules that turn on a domain match, such // as the m.federate check and the right to redact another user's event. func SameServer(userIDA, userIDB string) bool { a, errA := ServerNameOf(userIDA) b, errB := ServerNameOf(userIDB) if errA != nil || errB != nil { return false } return a == b } // ValidateRoomID checks a room ID: a '!' sigil and at most 255 bytes. // // Whether the rest of it is "opaque:server" or the hash of the create // event depends on the room version, and in the hashed form there is // nothing left to check beyond the sigil and the length — so the // version-specific part of the check lives with the authorisation rules, // which have the create event to compare against. func ValidateRoomID(s string) error { if err := checkCommon("room ID", s, '!'); err != nil { return err } if len(s) == 1 { return fmt.Errorf("%w: room ID %q is only a sigil", ErrInvalidID, s) } return nil } // ServerNameOfRoomID returns the server name from a room ID of the // "!opaque:server" form used up to room version 11. // // It fails for a room version 12 room ID, which is a hash of the create // event and names no server at all — the caller should only ask in the // versions where the answer exists. func ServerNameOfRoomID(roomID string) (string, error) { if err := ValidateRoomID(roomID); err != nil { return "", err } _, serverName, ok := strings.Cut(roomID[1:], ":") if !ok || serverName == "" { return "", fmt.Errorf("%w: room ID %q names no server", ErrInvalidID, roomID) } return serverName, nil } // ValidateEventID checks an event ID: a '$' sigil and at most 255 bytes. func ValidateEventID(s string) error { if err := checkCommon("event ID", s, '$'); err != nil { return err } if len(s) == 1 { return fmt.Errorf("%w: event ID %q is only a sigil", ErrInvalidID, s) } return nil } // checkCommon applies the three rules every identifier shares. func checkCommon(kind, s string, sigil byte) error { if s == "" { return fmt.Errorf("%w: %s must not be empty", ErrInvalidID, kind) } if s[0] != sigil { return fmt.Errorf("%w: %s %q does not start with %q", ErrInvalidID, kind, s, string(sigil)) } if len(s) > MaxLength { return fmt.Errorf("%w: %s is %d bytes, the limit is %d", ErrInvalidID, kind, len(s), MaxLength) } // A NUL would terminate the string in most of the software that ever // sees it, which makes two identifiers that differ after it look the // same to some servers and not to others. if strings.IndexByte(s, 0) >= 0 { return fmt.Errorf("%w: %s %q contains a NUL byte", ErrInvalidID, kind, s) } return nil }