// 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/base64" "encoding/json" "errors" "regexp" "strings" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "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/tidwall/gjson" ) type LoginPublicKeyEthereum struct { // Todo: See https://... Type string `json:"type"` Address string `json:"address"` Session string `json:"session"` Message string `json:"message"` Signature string `json:"signature"` HashFields publicKeyEthereumHashFields `json:"hashFields"` HashFieldsRaw string // Raw base64 encoded string of MessageFields for hash verification userAPI userApi.UserRegisterAPI config *config.ClientAPI } type publicKeyEthereumHashFields struct { // Todo: See https://... Domain string `json:"domain"` // home server domain Address string `json:"address"` // Ethereum address. 0x... Nonce string `json:"nonce"` // session ID Version string `json:"version"` // version of the Matrix public key spec that the client is complying with ChainId string `json:"chainId"` // blockchain network ID. } type publicKeyEthereumRequiredFields struct { From string // Sender To string // Recipient Hash string // Hash of JSON representation of the message fields } func CreatePublicKeyEthereumHandler( reqBytes []byte, userAPI userApi.UserRegisterAPI, config *config.ClientAPI, ) (*LoginPublicKeyEthereum, *jsonerror.MatrixError) { var pk LoginPublicKeyEthereum if err := json.Unmarshal(reqBytes, &pk); err != nil { return nil, jsonerror.BadJSON("auth") } hashFields := gjson.GetBytes(reqBytes, "hashFields") if !hashFields.Exists() { return nil, jsonerror.BadJSON("auth.hashFields") } pk.config = config pk.userAPI = userAPI // Save raw bytes for hash verification later. pk.HashFieldsRaw = hashFields.Raw // Case-insensitive pk.Address = strings.ToLower(pk.Address) 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.Address, &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 } func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) { // Check signature to verify message was not tempered isVerified := verifySignature(pk.Address, []byte(pk.Message), pk.Signature) if !isVerified { return false, jsonerror.InvalidSignature("") } // Extract the required message fields for validation requiredFields, err := extractRequiredMessageFields(pk.Message) if err != nil { return false, jsonerror.MissingParam("message does not contain domain, address, or hash") } // Verify that the hash is valid for the message fields. if !verifyHash(pk.HashFieldsRaw, requiredFields.Hash) { return false, jsonerror.InvalidParam("error verifying message hash") } // Unmarshal the hashFields for further validation var authData publicKeyEthereumHashFields if err := json.Unmarshal([]byte(pk.HashFieldsRaw), &authData); err != nil { return false, jsonerror.BadJSON("auth.hashFields") } // Error if the message is not from the expected public address if pk.Address != requiredFields.From || requiredFields.From != pk.HashFields.Address { return false, jsonerror.InvalidParam("address") } // Error if the message is not for the home server if requiredFields.To != pk.HashFields.Domain { return false, jsonerror.InvalidParam("domain") } // No errors. return true, nil } func (pk LoginPublicKeyEthereum) CreateLogin() *Login { identifier := LoginIdentifier{ Type: "m.id.user", User: pk.Address, } login := Login{ Identifier: identifier, } return &login } // The required fields in the signed message are: // 1. Domain -- home server. First non-whitespace characters in the first line. // 2. Address -- public address of the user. Starts with 0x... in the second line on its own. // 3. Hash -- Base64-encoded hash string of the metadata that represents the message. // The rest of the fields are informational, and will be used in the future. var regexpAuthority = regexp.MustCompile(`^\S+`) var regexpAddress = regexp.MustCompile(`\n(?P
0x\w+)\n`) var regexpHash = regexp.MustCompile(`\nHash: (?P