federation/eventauth: add most of v3-v12 event auth rules

This commit is contained in:
Tulir Asokan 2025-08-23 03:14:40 +03:00
commit 7778f0ce7e
2 changed files with 681 additions and 0 deletions

View file

@ -0,0 +1,640 @@
// Copyright (c) 2025 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build goexperiment.jsonv2
package eventauth
import (
"encoding/json"
"encoding/json/jsontext"
"errors"
"fmt"
"strconv"
"strings"
"github.com/tidwall/gjson"
"go.mau.fi/util/exgjson"
"go.mau.fi/util/exstrings"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/federation/pdu"
"maunium.net/go/mautrix/federation/signutil"
"maunium.net/go/mautrix/id"
)
var (
ErrInvalidUserID = errors.New("invalid user ID")
)
type ErrAuthFail struct {
Index string
Message string
Wrapped error
}
func (eaf ErrAuthFail) Error() string {
if eaf.Message != "" {
return fmt.Sprintf("fail %s: %s", eaf.Index, eaf.Message)
} else if eaf.Wrapped != nil {
return fmt.Sprintf("fail %s: %s", eaf.Index, eaf.Wrapped.Error())
}
return fmt.Sprintf("fail %s", eaf.Index)
}
func (eaf ErrAuthFail) Unwrap() error {
return eaf.Wrapped
}
var mFederatePath = exgjson.Path("m.federate")
var (
ErrCreateHasPrevEvents = ErrAuthFail{Index: "1.1", Message: "m.room.create event has prev_events"}
ErrCreateHasRoomID = ErrAuthFail{Index: "1.2", Message: "m.room.create event has room_id set"}
ErrRoomIDDoesntMatchSender = ErrAuthFail{Index: "1.2", Message: "room ID server doesn't match sender server"}
ErrUnknownRoomVersion = ErrAuthFail{Index: "1.3", Wrapped: id.ErrUnknownRoomVersion}
ErrInvalidAdditionalCreators = ErrAuthFail{Index: "1.4", Message: "m.room.create event has invalid additional_creators"}
ErrMissingCreator = ErrAuthFail{Index: "1.4", Message: "m.room.create event is missing creator field"}
ErrInvalidRoomIDLength = ErrAuthFail{Index: "2", Message: "room ID length is invalid"}
ErrFailedToGetCreateEvent = ErrAuthFail{Index: "2", Message: "failed to get m.room.create event"}
ErrCreateEventNotFound = ErrAuthFail{Index: "2", Message: "m.room.create event not found using room ID as event ID"}
ErrRejectedCreateEvent = ErrAuthFail{Index: "2", Message: "m.room.create event was rejected"}
ErrFailedToGetAuthEvents = ErrAuthFail{Index: "3", Message: "failed to get auth events"}
ErrFailedToParsePowerLevels = ErrAuthFail{Index: "?", Message: "failed to parse power levels"}
ErrDuplicateAuthEvent = ErrAuthFail{Index: "3.1", Message: "duplicate type/state key pair in auth events"}
ErrNonStateAuthEvent = ErrAuthFail{Index: "3.2", Message: "non-state event in auth events"}
ErrUnexpectedAuthEvent = ErrAuthFail{Index: "3.2", Message: "unexpected type/state key pair in auth events"}
ErrNoCreateEvent = ErrAuthFail{Index: "3.2", Message: "no m.room.create event found in auth events"}
ErrRejectedAuthEvent = ErrAuthFail{Index: "3.3", Message: "auth event was rejected"}
ErrMismatchingRoomIDInAuthEvent = ErrAuthFail{Index: "3.4", Message: "auth event room ID does not match event room ID"}
ErrFederationDisabled = ErrAuthFail{Index: "4", Message: "federation is disabled for this room"}
ErrMemberNotState = ErrAuthFail{Index: "5.1", Message: "m.room.member event is not a state event"}
ErrNotSignedByAuthoriser = ErrAuthFail{Index: "5.2", Message: "m.room.member event is not signed by server of join_authorised_via_users_server"}
ErrCantJoinOtherUser = ErrAuthFail{Index: "5.3.2", Message: "can't send join event with different state key"}
ErrCantJoinBanned = ErrAuthFail{Index: "5.3.3", Message: "user is banned from the room"}
ErrAuthoriserCantInvite = ErrAuthFail{Index: "5.3.5.2", Message: "authoriser doesn't have sufficient power level to invite"}
ErrCantJoinWithoutInvite = ErrAuthFail{Index: "5.3.7", Message: "can't join invite-only room without invite"}
ErrInvalidJoinRule = ErrAuthFail{Index: "5.3.7", Message: "invalid join rule in room"}
ErrThirdPartyInviteBanned = ErrAuthFail{Index: "5.4.1.1", Message: "third party invite target user is banned"}
ErrThirdPartyInviteMissingFields = ErrAuthFail{Index: "5.4.1.3", Message: "third party invite is missing mxid or token fields"}
ErrThirdPartyInviteMXIDMismatch = ErrAuthFail{Index: "5.4.1.4", Message: "mxid in signed third party invite doesn't match event state key"}
ErrThirdPartyInviteNotFound = ErrAuthFail{Index: "5.4.1.5", Message: "matching m.room.third_party_invite event not found in auth events"}
ErrThirdPartyInviteSenderMismatch = ErrAuthFail{Index: "5.4.1.6", Message: "sender of third party invite doesn't match sender of member event"}
ErrThirdPartyInviteNotSigned = ErrAuthFail{Index: "5.4.1.8", Message: "no valid signatures found for third party invite"}
ErrInviterNotInRoom = ErrAuthFail{Index: "5.4.2", Message: "inviter's membership is not join"}
ErrInviteTargetAlreadyInRoom = ErrAuthFail{Index: "5.4.3", Message: "invite target user is already in the room"}
ErrInviteTargetBanned = ErrAuthFail{Index: "5.4.3", Message: "invite target user is banned"}
ErrInsufficientPermissionForInvite = ErrAuthFail{Index: "5.4.5", Message: "inviter does not have sufficient permission to send invites"}
ErrCantLeaveWithoutBeingInRoom = ErrAuthFail{Index: "5.5.1", Message: "can't leave room without being in it"}
ErrCantKickWithoutBeingInRoom = ErrAuthFail{Index: "5.5.2", Message: "can't kick another user without being in the room"}
ErrInsufficientPermissionForUnban = ErrAuthFail{Index: "5.5.3", Message: "sender does not have sufficient permission to unban users"}
ErrInsufficientPermissionForKick = ErrAuthFail{Index: "5.5.5", Message: "sender does not have sufficient permission to kick the user"}
ErrCantBanWithoutBeingInRoom = ErrAuthFail{Index: "5.6.1", Message: "can't ban another user without being in the room"}
ErrInsufficientPermissionForBan = ErrAuthFail{Index: "5.6.3", Message: "sender does not have sufficient permission to ban the user"}
ErrNotKnockableRoom = ErrAuthFail{Index: "5.7.1", Message: "join rule doesn't allow knocking"}
ErrCantKnockOtherUser = ErrAuthFail{Index: "5.7.1", Message: "can't send knock event with different state key"}
ErrCantKnockWhileInRoom = ErrAuthFail{Index: "5.7.2", Message: "can't knock while joined, invited or banned"}
ErrUnknownMembership = ErrAuthFail{Index: "5.8", Message: "unknown membership in m.room.member event"}
ErrNotInRoom = ErrAuthFail{Index: "6", Message: "sender is not a member of the room"}
ErrInsufficientPowerForThirdPartyInvite = ErrAuthFail{Index: "7.1", Message: "sender does not have sufficient power level to send third party invite"}
ErrInsufficientPowerLevel = ErrAuthFail{Index: "8", Message: "sender does not have sufficient power level to send event"}
ErrMismatchingPrivateStateKey = ErrAuthFail{Index: "9", Message: "state keys starting with @ must match sender user ID"}
)
func isRejected(evt *pdu.PDU) bool {
// TODO figure out what's the best way for the caller of Authorize to provide this information
panic("not implemented yet")
}
type GetEventsFunc = func(ids []id.EventID) ([]*pdu.PDU, error)
func Authorize(roomVersion id.RoomVersion, evt *pdu.PDU, getEvents GetEventsFunc, getKey pdu.GetKeyFunc) error {
if evt.Type == event.StateCreate.Type {
// 1. If type is m.room.create:
return authorizeCreate(roomVersion, evt)
}
var createEvt *pdu.PDU
if roomVersion.RoomIDIsCreateEventID() {
// 2. If the events room_id is not an event ID for an accepted (not rejected) m.room.create event,
// with the sigil ! instead of $, reject.
if len(evt.RoomID) != 44 {
return fmt.Errorf("%w (%d)", ErrInvalidRoomIDLength, len(evt.RoomID))
} else if createEvts, err := getEvents([]id.EventID{id.EventID("$" + evt.RoomID[1:])}); err != nil {
return fmt.Errorf("%w: %w", ErrFailedToGetCreateEvent, err)
} else if len(createEvts) != 1 {
return fmt.Errorf("%w (%s)", ErrCreateEventNotFound, evt.RoomID)
} else if isRejected(createEvts[0]) {
return ErrRejectedCreateEvent
} else {
createEvt = createEvts[0]
}
}
authEvents, err := getEvents(evt.AuthEvents)
if err != nil {
return fmt.Errorf("%w: %w", ErrFailedToGetAuthEvents, err)
}
expectedAuthEvents := evt.AuthEventSelection(roomVersion)
deduplicator := make(map[pdu.StateKey]id.EventID, len(expectedAuthEvents))
// 3. Considering the events auth_events:
for i, ae := range authEvents {
authEvtID := evt.AuthEvents[i]
if ae.StateKey == nil {
// This approximately falls under rule 3.2.
return fmt.Errorf("%w (%s)", ErrNonStateAuthEvent, authEvtID)
}
key := pdu.StateKey{Type: ae.Type, StateKey: *ae.StateKey}
if prevEvtID, alreadyFound := deduplicator[key]; alreadyFound {
// 3.1. If there are duplicate entries for a given type and state_key pair, reject.
return fmt.Errorf("%w for %s/%s: found %s and %s", ErrDuplicateAuthEvent, ae.Type, *ae.StateKey, prevEvtID, authEvtID)
} else if !expectedAuthEvents.Has(key) {
// 3.2. If there are entries whose type and state_key dont match those specified by
// the auth events selection algorithm described in the server specification, reject.
return fmt.Errorf("%w: found %s with key %s/%s", ErrUnexpectedAuthEvent, authEvtID, ae.Type, *ae.StateKey)
} else if isRejected(ae) {
// 3.3. If there are entries which were themselves rejected under the checks performed on receipt of a PDU, reject.
return fmt.Errorf("%w (%s)", ErrRejectedAuthEvent, authEvtID)
} else if ae.RoomID != evt.RoomID {
// 3.4. If any event in auth_events has a room_id which does not match that of the event being authorised, reject.
return fmt.Errorf("%w (%s)", ErrMismatchingRoomIDInAuthEvent, authEvtID)
} else {
deduplicator[key] = authEvtID
}
if ae.Type == event.StateCreate.Type {
if createEvt == nil {
createEvt = ae
} else {
// Duplicates are prevented by deduplicator, AuthEventSelection also won't allow a create event at all for v12+
panic(fmt.Errorf("impossible case: multiple create events found in auth events"))
}
}
}
if createEvt == nil {
// This comes either from auth_events or room_id depending on the room version.
// The checks above make sure it's from the right source.
return ErrNoCreateEvent
}
if federateVal := gjson.GetBytes(createEvt.Content, mFederatePath); federateVal.Type == gjson.False && createEvt.Sender.Homeserver() != evt.Sender.Homeserver() {
// 4. If the content of the m.room.create event in the room state has the property m.federate set to false,
// and the sender domain of the event does not match the sender domain of the create event, reject.
return ErrFederationDisabled
}
if evt.Type == event.StateMember.Type {
// 5. If type is m.room.member:
return authorizeMember(roomVersion, evt, createEvt, authEvents, getKey)
}
senderMembership := event.Membership(findEventAndReadString(authEvents, event.StateMember.Type, evt.Sender.String(), "membership", "leave"))
if senderMembership != event.MembershipJoin {
// 6. If the senders current membership state is not join, reject.
return ErrNotInRoom
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
senderPL := powerLevels.GetUserLevel(evt.Sender)
if evt.Type == event.StateThirdPartyInvite.Type {
// 7.1. Allow if and only if senders current power level is greater than or equal to the invite level.
if senderPL >= powerLevels.Invite() {
return nil
}
return ErrInsufficientPowerForThirdPartyInvite
}
typeClass := event.MessageEventType
if evt.StateKey != nil {
typeClass = event.StateEventType
}
evtLevel := powerLevels.GetEventLevel(event.Type{Type: evt.Type, Class: typeClass})
if evtLevel > senderPL {
// 8. If the event types required power level is greater than the senders power level, reject.
return fmt.Errorf("%w (%d > %d)", ErrInsufficientPowerLevel, evtLevel, senderPL)
}
if evt.StateKey != nil && strings.HasPrefix(*evt.StateKey, "@") && *evt.StateKey != evt.Sender.String() {
// 9. If the event has a state_key that starts with an @ and does not match the sender, reject.
return ErrMismatchingPrivateStateKey
}
if evt.Type == event.StatePowerLevels.Type {
// 10. If type is m.room.power_levels:
return authorizePowerLevels(roomVersion, evt, createEvt, authEvents)
}
// 11. Otherwise, allow.
return nil
}
func authorizeCreate(roomVersion id.RoomVersion, evt *pdu.PDU) error {
if len(evt.PrevEvents) > 0 {
// 1.1. If it has any prev_events, reject.
return ErrCreateHasPrevEvents
}
if roomVersion.RoomIDIsCreateEventID() {
if evt.RoomID != "" {
// 1.2. If the event has a room_id, reject.
return ErrCreateHasRoomID
}
} else {
_, _, server := id.ParseCommonIdentifier(evt.RoomID)
if server == "" || server != evt.Sender.Homeserver() {
// 1.2. (v11 and below) If the domain of the room_id does not match the domain of the sender, reject.
return ErrRoomIDDoesntMatchSender
}
}
if !roomVersion.IsKnown() {
// 1.3. If content.room_version is present and is not a recognised version, reject.
return fmt.Errorf("%w %s", ErrUnknownRoomVersion, roomVersion)
}
if roomVersion.PrivilegedRoomCreators() {
additionalCreators := gjson.GetBytes(evt.Content, "additional_creators")
if additionalCreators.Exists() {
if !additionalCreators.IsArray() {
return fmt.Errorf("%w: not an array", ErrInvalidAdditionalCreators)
}
for i, item := range additionalCreators.Array() {
// 1.4. If additional_creators is present in content and is not an array of strings
// where each string passes the same user ID validation applied to sender, reject.
if item.Type != gjson.String {
return fmt.Errorf("%w: item #%d is not a string", ErrInvalidAdditionalCreators, i+1)
}
_, _, err := id.UserID(item.Str).Parse()
if err != nil {
return fmt.Errorf("%w: item #%d is not a valid user ID", ErrInvalidAdditionalCreators, i+1)
}
}
}
}
if roomVersion.CreatorInContent() {
// 1.4. (v10 and below) If content has no creator property, reject.
if !gjson.GetBytes(evt.Content, "creator").Exists() {
return ErrMissingCreator
}
}
// 1.5. Otherwise, allow.
return nil
}
func authorizeMember(roomVersion id.RoomVersion, evt *pdu.PDU, createEvt *pdu.PDU, authEvents []*pdu.PDU, getKey pdu.GetKeyFunc) error {
membership := event.Membership(gjson.GetBytes(evt.Content, "membership").Str)
if evt.StateKey == nil {
// 5.1. If there is no state_key property, or no membership property in content, reject.
return ErrMemberNotState
}
authorizedVia := id.UserID(gjson.GetBytes(evt.Content, "authorized_via_users_server").Str)
if authorizedVia != "" {
homeserver := authorizedVia.Homeserver()
err := evt.VerifySignature(roomVersion, homeserver, getKey)
if err != nil {
// 5.2. If content has a join_authorised_via_users_server key:
// 5.2.1. If the event is not validly signed by the homeserver of the user ID denoted by the key, reject.
return fmt.Errorf("%w: %w", ErrNotSignedByAuthoriser, err)
}
}
targetPrevMembership := event.Membership(findEventAndReadString(authEvents, event.StateMember.Type, *evt.StateKey, "membership", "leave"))
senderMembership := event.Membership(findEventAndReadString(authEvents, event.StateMember.Type, evt.Sender.String(), "membership", "leave"))
switch membership {
case event.MembershipJoin:
createEvtID, err := createEvt.GetEventID(roomVersion)
if err != nil {
return fmt.Errorf("failed to get create event ID: %w", err)
}
creator := createEvt.Sender.String()
if roomVersion.CreatorInContent() {
creator = gjson.GetBytes(evt.Content, "creator").Str
}
if len(evt.PrevEvents) == 1 &&
len(evt.AuthEvents) <= 1 &&
evt.PrevEvents[0] == createEvtID &&
*evt.StateKey == creator {
// 5.3.1. If the only previous event is an m.room.create and the state_key is the sender of the m.room.create, allow.
return nil
}
// Spec wart: this would make more sense before the check above.
// Now you can set anyone as the sender of the first join.
if evt.Sender.String() != *evt.StateKey {
// 5.3.2. If the sender does not match state_key, reject.
return ErrCantJoinOtherUser
}
if senderMembership == event.MembershipBan {
// 5.3.3. If the sender is banned, reject.
return ErrCantJoinBanned
}
joinRule := event.JoinRule(findEventAndReadString(authEvents, event.StateJoinRules.Type, "", "join_rule", "invite"))
switch joinRule {
case event.JoinRuleKnock:
if !roomVersion.Knocks() {
return ErrInvalidJoinRule
}
fallthrough
case event.JoinRuleInvite:
// 5.3.4. If the join_rule is invite or knock then allow if membership state is invite or join.
if targetPrevMembership == event.MembershipJoin || targetPrevMembership == event.MembershipLeave {
return nil
}
return ErrCantJoinWithoutInvite
case event.JoinRuleKnockRestricted:
if !roomVersion.KnockRestricted() {
return ErrInvalidJoinRule
}
fallthrough
case event.JoinRuleRestricted:
if joinRule == event.JoinRuleRestricted && !roomVersion.RestrictedJoins() {
return ErrInvalidJoinRule
}
if targetPrevMembership == event.MembershipJoin || targetPrevMembership == event.MembershipLeave {
// 5.3.5.1. If membership state is join or invite, allow.
return nil
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
if powerLevels.GetUserLevel(authorizedVia) < powerLevels.Invite() {
// 5.3.5.2. If the join_authorised_via_users_server key in content is not a user with sufficient permission to invite other users, reject.
return ErrAuthoriserCantInvite
}
// 5.3.5.3. Otherwise, allow.
return nil
case event.JoinRulePublic:
// 5.3.6. If the join_rule is public, allow.
return nil
default:
// 5.3.7. Otherwise, reject.
return ErrInvalidJoinRule
}
case event.MembershipInvite:
tpiVal := gjson.GetBytes(evt.Content, "third_party_invite")
if tpiVal.Exists() {
if targetPrevMembership == event.MembershipBan {
return ErrThirdPartyInviteBanned
}
signed := gjson.Get(tpiVal.Raw, "signed")
mxid := gjson.Get(signed.Raw, "mxid").Str
token := gjson.Get(signed.Raw, "token").Str
if mxid == "" || token == "" {
// 5.4.1.2. If content.third_party_invite does not have a signed property, reject.
// 5.4.1.3. If signed does not have mxid and token properties, reject.
return ErrThirdPartyInviteMissingFields
}
if mxid != *evt.StateKey {
// 5.4.1.4. If mxid does not match state_key, reject.
return ErrThirdPartyInviteMXIDMismatch
}
tpiEvt := findEvent(authEvents, event.StateThirdPartyInvite.Type, token)
if tpiEvt == nil {
// 5.4.1.5. If there is no m.room.third_party_invite event in the current room state with state_key matching token, reject.
return ErrThirdPartyInviteNotFound
}
if tpiEvt.Sender != evt.Sender {
// 5.4.1.6. If sender does not match sender of the m.room.third_party_invite, reject.
return ErrThirdPartyInviteSenderMismatch
}
var keys []id.Ed25519
const ed25519Base64Len = 43
oldPubKey := gjson.GetBytes(evt.Content, "public_key.token")
if oldPubKey.Type == gjson.String && len(oldPubKey.Str) == ed25519Base64Len {
keys = append(keys, id.Ed25519(oldPubKey.Str))
}
gjson.GetBytes(evt.Content, "public_keys").ForEach(func(key, value gjson.Result) bool {
if key.Type != gjson.Number {
return false
}
if value.Type == gjson.String && len(value.Str) == ed25519Base64Len {
keys = append(keys, id.Ed25519(value.Str))
}
return true
})
rawSigned := jsontext.Value(exstrings.UnsafeBytes(signed.Str))
var validated bool
for _, key := range keys {
if signutil.VerifyJSONAny(key, rawSigned) == nil {
validated = true
}
}
if validated {
// 4.4.1.7. If any signature in signed matches any public key in the m.room.third_party_invite event, allow.
return nil
}
// 4.4.1.8. Otherwise, reject.
return ErrThirdPartyInviteNotSigned
}
if senderMembership != event.MembershipJoin {
// 5.4.2. If the senders current membership state is not join, reject.
return ErrInviterNotInRoom
}
// 5.4.3. If target users current membership state is join or ban, reject.
if targetPrevMembership == event.MembershipJoin {
return ErrInviteTargetAlreadyInRoom
} else if targetPrevMembership == event.MembershipBan {
return ErrInviteTargetBanned
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
if powerLevels.GetUserLevel(evt.Sender) >= powerLevels.Invite() {
// 5.4.4. If the senders power level is greater than or equal to the invite level, allow.
return nil
}
// 5.4.5. Otherwise, reject.
return ErrInsufficientPermissionForInvite
case event.MembershipLeave:
if evt.Sender.String() == *evt.StateKey {
// 5.5.1. If the sender matches state_key, allow if and only if that users current membership state is invite, join, or knock.
if senderMembership == event.MembershipInvite ||
senderMembership == event.MembershipJoin ||
(senderMembership == event.MembershipKnock && roomVersion.Knocks()) {
return nil
}
return ErrCantLeaveWithoutBeingInRoom
}
if senderMembership != event.MembershipLeave {
// 5.5.2. If the senders current membership state is not join, reject.
return ErrCantKickWithoutBeingInRoom
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
senderLevel := powerLevels.GetUserLevel(evt.Sender)
if targetPrevMembership == event.MembershipBan && senderLevel < powerLevels.Ban() {
// 5.5.3. If the target users current membership state is ban, and the senders power level is less than the ban level, reject.
return ErrInsufficientPermissionForUnban
}
if senderLevel >= powerLevels.Kick() && powerLevels.GetUserLevel(id.UserID(*evt.StateKey)) < senderLevel {
// 5.5.4. If the senders power level is greater than or equal to the kick level, and the target users power level is less than the senders power level, allow.
return nil
}
// TODO separate errors for < kick and < target user level?
// 5.5.5. Otherwise, reject.
return ErrInsufficientPermissionForKick
case event.MembershipBan:
if senderMembership != event.MembershipLeave {
// 5.6.1. If the senders current membership state is not join, reject.
return ErrCantBanWithoutBeingInRoom
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
senderLevel := powerLevels.GetUserLevel(evt.Sender)
if senderLevel >= powerLevels.Ban() && powerLevels.GetUserLevel(id.UserID(*evt.StateKey)) < senderLevel {
// 5.6.2. If the senders power level is greater than or equal to the ban level, and the target users power level is less than the senders power level, allow.
return nil
}
// 5.6.3. Otherwise, reject.
return ErrInsufficientPermissionForBan
case event.MembershipKnock:
joinRule := event.JoinRule(findEventAndReadString(authEvents, event.StateJoinRules.Type, "", "join_rule", "invite"))
validKnockRule := roomVersion.Knocks() && joinRule == event.JoinRuleKnock
validKnockRestrictedRule := roomVersion.KnockRestricted() && joinRule == event.JoinRuleKnockRestricted
if !validKnockRule && !validKnockRestrictedRule {
// 5.7.1. If the join_rule is anything other than knock or knock_restricted, reject.
return ErrNotKnockableRoom
}
if evt.Sender.String() != *evt.StateKey {
// 5.7.2. If the sender does not match state_key, reject.
return ErrCantKnockOtherUser
}
if senderMembership != event.MembershipBan && senderMembership != event.MembershipInvite && senderMembership != event.MembershipJoin {
// 5.7.3. If the senders current membership is not ban, invite, or join, allow.
return nil
}
// 5.7.4. Otherwise, reject.
return ErrCantKnockWhileInRoom
default:
// 5.8. Otherwise, the membership is unknown. Reject.
return ErrUnknownMembership
}
}
func authorizePowerLevels(version id.RoomVersion, evt *pdu.PDU, evt2 *pdu.PDU, events []*pdu.PDU) error {
panic("not implemented")
}
func findEvent(events []*pdu.PDU, evtType, stateKey string) *pdu.PDU {
for _, evt := range events {
if evt.Type == evtType && *evt.StateKey == stateKey {
return evt
}
}
return nil
}
func findEventAndReadData[T any](events []*pdu.PDU, evtType, stateKey string, reader func(evt *pdu.PDU) T) T {
return reader(findEvent(events, evtType, stateKey))
}
func findEventAndReadString(events []*pdu.PDU, evtType, stateKey, fieldPath, defVal string) string {
return findEventAndReadData(events, evtType, stateKey, func(evt *pdu.PDU) string {
if evt == nil {
return defVal
}
res := gjson.GetBytes(evt.Content, fieldPath)
if res.Type != gjson.String {
return defVal
}
return res.Str
})
}
func getPowerLevels(roomVersion id.RoomVersion, authEvents []*pdu.PDU, createEvt *pdu.PDU) (*event.PowerLevelsEventContent, error) {
var err error
powerLevels := findEventAndReadData(authEvents, event.StatePowerLevels.Type, "", func(evt *pdu.PDU) (out event.PowerLevelsEventContent) {
if evt == nil {
return
}
content := evt.Content
if !roomVersion.ValidatePowerLevelInts() {
safeParsePowerLevels(content, &out)
} else {
err = json.Unmarshal(content, &out)
}
return
})
if err != nil {
// This should never happen thanks to safeParsePowerLevels for v1-9 and strict validation in v10+
return nil, fmt.Errorf("%w: %w", ErrFailedToParsePowerLevels, err)
}
if roomVersion.PrivilegedRoomCreators() {
powerLevels.CreateEvent, err = createEvt.ToClientEvent(roomVersion)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrFailedToParsePowerLevels, err)
}
}
return &powerLevels, nil
}
func parsePythonInt(val gjson.Result) *int {
switch val.Type {
case gjson.True:
return ptr.Ptr(1)
case gjson.False:
return ptr.Ptr(0)
case gjson.Number:
return ptr.Ptr(int(val.Int()))
case gjson.String:
// strconv.Atoi accepts signs as well as leading zeroes, so we just need to trim spaces beforehand
num, err := strconv.Atoi(strings.TrimSpace(val.Str))
if err != nil {
return nil
}
return &num
default:
// Python int() doesn't accept nulls, arrays or dicts
return nil
}
}
func safeParsePowerLevels(content jsontext.Value, into *event.PowerLevelsEventContent) {
*into = event.PowerLevelsEventContent{
Users: make(map[id.UserID]int),
UsersDefault: ptr.Val(parsePythonInt(gjson.GetBytes(content, "users_default"))),
Events: make(map[string]int),
EventsDefault: ptr.Val(parsePythonInt(gjson.GetBytes(content, "events_default"))),
Notifications: nil, // irrelevant for event auth
StateDefaultPtr: parsePythonInt(gjson.GetBytes(content, "state_default")),
InvitePtr: parsePythonInt(gjson.GetBytes(content, "invite")),
KickPtr: parsePythonInt(gjson.GetBytes(content, "kick")),
BanPtr: parsePythonInt(gjson.GetBytes(content, "ban")),
RedactPtr: parsePythonInt(gjson.GetBytes(content, "redact")),
}
gjson.GetBytes(content, "events").ForEach(func(key, value gjson.Result) bool {
if key.Type != gjson.String {
return false
}
val := parsePythonInt(value)
if val != nil {
into.Events[key.Str] = *val
}
return true
})
gjson.GetBytes(content, "users").ForEach(func(key, value gjson.Result) bool {
if key.Type != gjson.String {
return false
}
val := parsePythonInt(value)
if val == nil {
return false
}
userID := id.UserID(key.Str)
if _, _, err := userID.Parse(); err != nil {
return false
}
into.Users[userID] = *val
return true
})
}

View file

@ -48,6 +48,47 @@ func VerifyJSON(serverName string, keyID id.KeyID, key id.SigningKey, data any)
return VerifyJSONRaw(key, sigVal.Str, message)
}
func VerifyJSONAny(key id.SigningKey, data any) error {
var err error
message, ok := data.(json.RawMessage)
if !ok {
message, err = json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
}
sigs := gjson.GetBytes(message, "signatures")
if !sigs.IsObject() {
return ErrSignatureNotFound
}
message, err = sjson.DeleteBytes(message, "signatures")
if err != nil {
return fmt.Errorf("failed to delete signatures: %w", err)
}
message, err = sjson.DeleteBytes(message, "unsigned")
if err != nil {
return fmt.Errorf("failed to delete unsigned: %w", err)
}
var validated bool
sigs.ForEach(func(_, value gjson.Result) bool {
if !value.IsObject() {
return true
}
value.ForEach(func(_, value gjson.Result) bool {
if value.Type != gjson.String {
return true
}
validated = VerifyJSONRaw(key, value.Str, message) == nil
return !validated
})
return !validated
})
if !validated {
return ErrInvalidSignature
}
return nil
}
func VerifyJSONRaw(key id.SigningKey, sig string, message json.RawMessage) error {
sigBytes, err := base64.RawStdEncoding.DecodeString(sig)
if err != nil {