// Copyright 2022 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package auth import ( "context" "encoding/json" "fmt" "regexp" "strings" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/userutil" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/spruceid/siwe-go" ) type LoginPublicKeyEthereum struct { // https://github.com/tak-hntlabs/matrix-spec-proposals/blob/main/proposals/3782-matrix-publickey-login-spec.md#client-sends-login-request-with-authentication-data Type string `json:"type"` UserId string `json:"user_id"` Session string `json:"session"` Message string `json:"message"` Signature string `json:"signature"` userAPI userapi.ClientUserAPI config *config.ClientAPI } func CreatePublicKeyEthereumHandler( reqBytes []byte, userAPI userapi.ClientUserAPI, config *config.ClientAPI, ) (*LoginPublicKeyEthereum, *jsonerror.MatrixError) { var pk LoginPublicKeyEthereum if err := json.Unmarshal(reqBytes, &pk); err != nil { return nil, jsonerror.BadJSON("auth") } pk.config = config pk.userAPI = userAPI // Case-insensitive pk.UserId = strings.ToLower(pk.UserId) return &pk, nil } func (pk LoginPublicKeyEthereum) GetSession() string { return pk.Session } func (pk LoginPublicKeyEthereum) GetType() string { return pk.Type } func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *jsonerror.MatrixError) { localPart, err := userutil.ParseUsernameParam(pk.UserId, &pk.config.Matrix.ServerName) if err != nil { // userId does not exist return "", jsonerror.Forbidden("the address is incorrect, or the account does not exist.") } res := userapi.QueryAccountAvailabilityResponse{} if err := pk.userAPI.QueryAccountAvailability(ctx, &userapi.QueryAccountAvailabilityRequest{ Localpart: localPart, }, &res); err != nil { return "", jsonerror.Unknown("failed to check availability: " + err.Error()) } if res.Available { return "", jsonerror.Forbidden("the address is incorrect, account does not exist") } return localPart, nil } var validChainAgnosticIdRegex = regexp.MustCompile("^eip155=3a[0-9]+=3a0x[0-9a-fA-F]+$") func (pk LoginPublicKeyEthereum) IsValidUserIdForRegistration(userId string) bool { // Verify that the user ID is a valid one according to spec. // https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md // Matrix ID has additional grammar requirements for user ID. // https://spec.matrix.org/v1.1/appendices/#user-identifiers // Make sure disallowed characters are escaped. // E.g. ":" is replaced with "=3a". isValid := validChainAgnosticIdRegex.MatchString(userId) // In addition, double check that the user ID for registration // matches the authentication data in the request. return isValid && userId == pk.UserId } func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) { // Parse the message to extract all the fields. message, err := siwe.ParseMessage(pk.Message) if err != nil { return false, jsonerror.InvalidParam("auth.message") } // Check signature to verify message was not tempered _, err = message.Verify(pk.Signature, (*string)(&pk.config.Matrix.ServerName), nil, nil) if err != nil { return false, jsonerror.InvalidSignature(err.Error()) } // Error if the user ID does not match the signed message. isVerifiedUserId := pk.verifyMessageUserId(message) if !isVerifiedUserId { return false, jsonerror.InvalidUsername(pk.UserId) } // Error if the chainId is not supported by the server. if !contains(pk.config.PublicKeyAuthentication.Ethereum.ChainIDs, message.GetChainID()) { return false, jsonerror.Forbidden("chainId") } // No errors. return true, nil } func (pk LoginPublicKeyEthereum) CreateLogin() *Login { identifier := LoginIdentifier{ Type: "m.id.decentralizedid", User: pk.UserId, } login := Login{ Identifier: identifier, } return &login } func (pk LoginPublicKeyEthereum) verifyMessageUserId(message *siwe.Message) bool { // Use info in the signed message to derive the expected user ID. expectedUserId := fmt.Sprintf("eip155=3a%d=3a%s", message.GetChainID(), message.GetAddress()) // Case-insensitive comparison to make sure the user ID matches the expected // one derived from the signed message. return pk.UserId == strings.ToLower(expectedUserId) } func contains(list []int, element int) bool { for _, i := range list { if i == element { return true } } return false }