package auth import ( "context" "crypto/ed25519" "encoding/json" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/matrix/signing" ) // memberContent is the part of an m.room.member event's content the rules // read. The pointer and json.RawMessage fields are there so that "absent" // can be told from "empty", which several of the rules turn on. type memberContent struct { Membership string `json:"membership"` JoinAuthorisedViaUsersServer string `json:"join_authorised_via_users_server"` ThirdPartyInvite *thirdPartyInvite `json:"third_party_invite"` } type thirdPartyInvite struct { Signed json.RawMessage `json:"signed"` } // checkMember applies rule 5, the membership rules. // // These are the only rules a user who is not in the room may satisfy, so // they carry the whole of who can get in: invitations, public rooms, // knocking, restricted rooms, and who may throw whom out. func checkMember(ctx context.Context, e *event.Event, st State, create *event.Event, pl *PowerLevels, verifier Verifier) error { rules := e.Rules() // Rule 5.1. target, ok := e.StateKey() if !ok { return reject("an m.room.member event must have a state_key") } var content memberContent if err := json.Unmarshal(e.Content(), &content); err != nil { return reject("m.room.member content: %v", err) } if content.Membership == "" { return reject("an m.room.member event must have a membership") } // Rule 5.2: a join authorised by another user must be signed by that // user's server, not merely by the joining user's. Without it, anybody // could claim to have been let into a restricted room. if rules.RestrictedJoinRule && content.JoinAuthorisedViaUsersServer != "" { if err := checkJoinAuthorisation(ctx, e, content.JoinAuthorisedViaUsersServer, verifier); err != nil { return err } } senderMembership := st.Membership(e.Sender()) targetMembership := st.Membership(target) switch content.Membership { case event.MembershipJoin: return checkJoin(e, st, create, pl, target, targetMembership, content) case event.MembershipInvite: return checkInvite(e, st, pl, target, senderMembership, targetMembership, content) case event.MembershipLeave: return checkLeave(e, pl, target, senderMembership, targetMembership) case event.MembershipBan: return checkBan(e, pl, target, senderMembership) case event.MembershipKnock: if !rules.KnockJoinRule { return reject("room version %s does not allow knocking", rules.Version) } return checkKnock(e, st, target, senderMembership) default: // Rule 5.8. return reject("unknown membership %q", content.Membership) } } // checkJoin applies rule 5.3. func checkJoin(e *event.Event, st State, create *event.Event, pl *PowerLevels, target, targetMembership string, content memberContent) error { rules := e.Rules() // Rule 5.3.1: the creator's own join, the second event in the room. // There is no state yet to authorise it against, so it is recognised // by its shape: it follows the create event and nothing else. prev := e.PrevEventIDs() if len(prev) == 1 && prev[0] == create.EventID() && target == create.Sender() { return nil } // Rule 5.3.2: you join yourself. Being dragged into a room is an // invite, which the invited user then accepts with a join of their own. if e.Sender() != target { return reject("the sender may not join %q on their behalf", target) } // Rule 5.3.3. if targetMembership == event.MembershipBan { return reject("the user is banned from the room") } joinRule := st.JoinRule() switch joinRule { case JoinRuleInvite, JoinRuleKnock: // Rule 5.3.4. A knock does not admit anybody by itself: it asks, // and somebody with the power to invite still has to answer. if targetMembership == event.MembershipInvite || targetMembership == event.MembershipJoin { return nil } return reject("the room's join rule is %q and the user has not been invited (membership is %q)", joinRule, targetMembership) case JoinRuleRestricted, JoinRuleKnockRestricted: if joinRule == JoinRuleRestricted && !rules.RestrictedJoinRule { return reject("room version %s does not have the restricted join rule", rules.Version) } if joinRule == JoinRuleKnockRestricted && !rules.KnockRestrictedJoinRule { return reject("room version %s does not have the knock_restricted join rule", rules.Version) } // Rule 5.3.5.1: an existing invite or join still works, so a // restricted room is never *harder* to enter than an invite room. if targetMembership == event.MembershipInvite || targetMembership == event.MembershipJoin { return nil } // Rule 5.3.5.2. The authorising user has to be in the room and // able to invite, because what they are really doing is vouching // that the joining user is a member of one of the allowed rooms — // something this server may be unable to see for itself. authoriser := content.JoinAuthorisedViaUsersServer if authoriser == "" { return reject("the room's join rule is %q and the join is not authorised by any user", joinRule) } if m := st.Membership(authoriser); m != event.MembershipJoin { return reject("the authorising user %q is not joined to the room (membership is %q)", authoriser, m) } if level := pl.UserLevel(authoriser); level < pl.Invite { return reject("the authorising user %q has power level %s, below the invite level %d", authoriser, levelString(level), pl.Invite) } // Rule 5.3.5.3. return nil case JoinRulePublic: // Rule 5.3.6. return nil default: // Rule 5.3.7. "private" lands here, and so does any join rule // from a future room version: a rule we cannot evaluate must keep // people out rather than let them in. return reject("the room's join rule %q does not allow joining", joinRule) } } // checkInvite applies rule 5.4. func checkInvite(e *event.Event, st State, pl *PowerLevels, target, senderMembership, targetMembership string, content memberContent) error { // Rule 5.4.1: a third-party invite is an invitation issued to an email // address or phone number before the server knew which user it was. // The identity server's signature stands in for the inviter's power. if content.ThirdPartyInvite != nil { return checkThirdPartyInvite(e, st, target, targetMembership, content) } // Rule 5.4.2. if senderMembership != event.MembershipJoin { return reject("the sender is not joined to the room (membership is %q)", senderMembership) } // Rule 5.4.3. if targetMembership == event.MembershipJoin || targetMembership == event.MembershipBan { return reject("the user is already %q in the room", targetMembership) } // Rule 5.4.4. if level := pl.UserLevel(e.Sender()); level < pl.Invite { return reject("the sender's power level %s is below the invite level %d", levelString(level), pl.Invite) } // Rule 5.4.5 is the negation of the above. return nil } // checkThirdPartyInvite applies rule 5.4.1. func checkThirdPartyInvite(e *event.Event, st State, target, targetMembership string, content memberContent) error { // Rule 5.4.1.1. if targetMembership == event.MembershipBan { return reject("the user is banned from the room") } // Rule 5.4.1.2. if len(content.ThirdPartyInvite.Signed) == 0 { return reject("the third_party_invite has no signed property") } var signed struct { MXID string `json:"mxid"` Token string `json:"token"` } if err := json.Unmarshal(content.ThirdPartyInvite.Signed, &signed); err != nil { return reject("the third_party_invite signed property: %v", err) } // Rule 5.4.1.3. if signed.MXID == "" || signed.Token == "" { return reject("the third_party_invite signed property needs both mxid and token") } // Rule 5.4.1.4. if signed.MXID != target { return reject("the third_party_invite names %q but the event invites %q", signed.MXID, target) } // Rule 5.4.1.5. inviteEvent := st.ThirdPartyInvite(signed.Token) if inviteEvent == nil { return reject("there is no m.room.third_party_invite in the room state for token %q", signed.Token) } // Rule 5.4.1.6: the same user who issued the invitation must be the // one redeeming it. if inviteEvent.Sender() != e.Sender() { return reject("the third-party invite was issued by %q, not by %q", inviteEvent.Sender(), e.Sender()) } // Rules 5.4.1.7 and 5.4.1.8. if !VerifyThirdPartyInviteSignature(content.ThirdPartyInvite.Signed, inviteEvent) { return reject("no signature on the third-party invite matches a public key in %s", inviteEvent.EventID()) } return nil } // checkLeave applies rule 5.5, which covers both leaving and being kicked. func checkLeave(e *event.Event, pl *PowerLevels, target, senderMembership, targetMembership string) error { rules := e.Rules() // Rule 5.5.1: leaving on your own account, which also covers turning // down an invitation and withdrawing a knock. if e.Sender() == target { switch targetMembership { case event.MembershipInvite, event.MembershipJoin: return nil case event.MembershipKnock: if rules.KnockJoinRule { return nil } } return reject("the user cannot leave from membership %q", targetMembership) } // Rule 5.5.2. if senderMembership != event.MembershipJoin { return reject("the sender is not joined to the room (membership is %q)", senderMembership) } senderLevel := pl.UserLevel(e.Sender()) // Rule 5.5.3: unbanning is a leave over a ban, and it takes the power // to have banned in the first place. if targetMembership == event.MembershipBan && senderLevel < pl.Ban { return reject("the sender's power level %s is below the ban level %d needed to unban", levelString(senderLevel), pl.Ban) } // Rule 5.5.4. if senderLevel < pl.Kick { return reject("the sender's power level %s is below the kick level %d", levelString(senderLevel), pl.Kick) } if targetLevel := pl.UserLevel(target); targetLevel >= senderLevel { return reject("the sender's power level %s does not exceed %q's power level %s", levelString(senderLevel), target, levelString(targetLevel)) } // Rule 5.5.5 is the negation of the above. return nil } // checkBan applies rule 5.6. func checkBan(e *event.Event, pl *PowerLevels, target, senderMembership string) error { // Rule 5.6.1. if senderMembership != event.MembershipJoin { return reject("the sender is not joined to the room (membership is %q)", senderMembership) } // Rule 5.6.2. senderLevel := pl.UserLevel(e.Sender()) if senderLevel < pl.Ban { return reject("the sender's power level %s is below the ban level %d", levelString(senderLevel), pl.Ban) } if targetLevel := pl.UserLevel(target); targetLevel >= senderLevel { return reject("the sender's power level %s does not exceed %q's power level %s", levelString(senderLevel), target, levelString(targetLevel)) } // Rule 5.6.3 is the negation of the above. return nil } // checkKnock applies rule 5.7. func checkKnock(e *event.Event, st State, target, senderMembership string) error { // Rule 5.7.1. joinRule := st.JoinRule() if joinRule != JoinRuleKnock && joinRule != JoinRuleKnockRestricted { return reject("the room's join rule is %q, so it cannot be knocked on", joinRule) } // Rule 5.7.2. if e.Sender() != target { return reject("the sender may not knock on behalf of %q", target) } // Rule 5.7.3: knocking is allowed from anything but banned, invited or // joined — there is no point knocking on a door that is already open. switch senderMembership { case event.MembershipBan, event.MembershipInvite, event.MembershipJoin: // Rule 5.7.4. return reject("the user cannot knock while their membership is %q", senderMembership) default: return nil } } // checkJoinAuthorisation applies rule 5.2. func checkJoinAuthorisation(ctx context.Context, e *event.Event, authoriser string, verifier Verifier) error { serverName, err := id.ServerNameOf(authoriser) if err != nil { return reject("join_authorised_via_users_server: %v", err) } if verifier == nil { return fmt.Errorf("%w: %s is authorised by %q, whose server's signature cannot be checked", ErrNoVerifier, e.EventID(), authoriser) } if err := verifier.VerifyEventSignature(ctx, serverName, e); err != nil { return reject("the join is authorised by %q but carries no valid signature from %q: %v", authoriser, serverName, err) } return nil } // thirdPartyInviteKeys returns the public keys an m.room.third_party_invite // event published, from either of the two places the spec allows. func thirdPartyInviteKeys(e *event.Event) ([]ed25519.PublicKey, error) { var content struct { PublicKey string `json:"public_key"` PublicKeys []struct { PublicKey string `json:"public_key"` } `json:"public_keys"` } if err := json.Unmarshal(e.Content(), &content); err != nil { return nil, reject("m.room.third_party_invite content: %v", err) } encoded := make([]string, 0, len(content.PublicKeys)+1) if content.PublicKey != "" { encoded = append(encoded, content.PublicKey) } for _, k := range content.PublicKeys { if k.PublicKey != "" { encoded = append(encoded, k.PublicKey) } } keys := make([]ed25519.PublicKey, 0, len(encoded)) for _, s := range encoded { raw, err := signing.DecodeBase64(s) if err != nil || len(raw) != ed25519.PublicKeySize { // A key we cannot read cannot match a signature, and the rule // asks whether *any* key matches — so a bad one is skipped // rather than fatal. continue } keys = append(keys, ed25519.PublicKey(raw)) } return keys, nil }