From 8dabca0f079417dad6375ba865d16bfc88df7a52 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 22 Sep 2017 16:13:19 +0100 Subject: [PATCH] Implement shared secret registration (#257) * Implement shared secret registration * Use HexString from gomatrixserverlib * Correctly check username validility --- .../clientapi/auth/authtypes/logintypes.go | 3 +- .../dendrite/clientapi/jsonerror/jsonerror.go | 6 + .../dendrite/clientapi/routing/routing.go | 8 +- .../dendrite/clientapi/writers/register.go | 163 +++++++++++++++++- .../dendrite/common/config/config.go | 3 + vendor/manifest | 2 +- .../gomatrixserverlib/hex_string.go | 44 +++++ .../gomatrixserverlib/hex_string_test.go | 82 +++++++++ 8 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string.go create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string_test.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go b/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go index 6a90b295..ca9c8b38 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/authtypes/logintypes.go @@ -5,5 +5,6 @@ type LoginType string // The relevant login types implemented in Dendrite const ( - LoginTypeDummy = "m.login.dummy" + LoginTypeDummy = "m.login.dummy" + LoginTypeSharedSecret = "org.matrix.login.shared_secret" ) diff --git a/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go b/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go index d267355e..8f168f4f 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go +++ b/src/github.com/matrix-org/dendrite/clientapi/jsonerror/jsonerror.go @@ -85,6 +85,12 @@ func WeakPassword(msg string) *MatrixError { return &MatrixError{"M_WEAK_PASSWORD", msg} } +// InvalidUsername is an error returned when the client tries to register an +// invalid username +func InvalidUsername(msg string) *MatrixError { + return &MatrixError{"M_INVALID_USERNAME", msg} +} + // GuestAccessForbidden is an error which is returned when the client is // forbidden from accessing a resource as a guest. func GuestAccessForbidden(msg string) *MatrixError { diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index 28284a27..607b8ef4 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -34,6 +34,7 @@ import ( "github.com/matrix-org/util" ) +const pathPrefixV1 = "/_matrix/client/api/v1" const pathPrefixR0 = "/_matrix/client/r0" const pathPrefixUnstable = "/_matrix/client/unstable" @@ -67,6 +68,7 @@ func Setup( ).Methods("GET") r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() + v1mux := apiMux.PathPrefix(pathPrefixV1).Subrouter() unstableMux := apiMux.PathPrefix(pathPrefixUnstable).Subrouter() r0mux.Handle("/createRoom", @@ -115,7 +117,11 @@ func Setup( ).Methods("PUT", "OPTIONS") r0mux.Handle("/register", common.MakeAPI("register", func(req *http.Request) util.JSONResponse { - return writers.Register(req, accountDB, deviceDB) + return writers.Register(req, accountDB, deviceDB, &cfg) + })).Methods("POST", "OPTIONS") + + v1mux.Handle("/register", common.MakeAPI("register", func(req *http.Request) util.JSONResponse { + return writers.LegacyRegister(req, accountDB, deviceDB, &cfg) })).Methods("POST", "OPTIONS") r0mux.Handle("/directory/room/{roomAlias}", diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/register.go b/src/github.com/matrix-org/dendrite/clientapi/writers/register.go index dfebaf6d..9be1ba91 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/register.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/register.go @@ -2,10 +2,17 @@ package writers import ( "context" + "crypto/hmac" + "crypto/sha1" + "errors" "fmt" "net/http" + "regexp" + "strings" "time" + "github.com/matrix-org/dendrite/common/config" + log "github.com/Sirupsen/logrus" "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" @@ -23,6 +30,8 @@ const ( maxUsernameLength = 254 // http://matrix.org/speculator/spec/HEAD/intro.html#user-identifiers TODO account for domain ) +var validUsernameRegex = regexp.MustCompile(`^[0-9a-zA-Z_\-./]+$`) + // registerRequest represents the submitted registration request. // It can be broken down into 2 sections: the auth dictionary and registration parameters. // Registration parameters vary depending on the request, and will need to remembered across @@ -33,13 +42,15 @@ type registerRequest struct { // registration parameters. Password string `json:"password"` Username string `json:"username"` + Admin bool `json:"admin"` // user-interactive auth params Auth authDict `json:"auth"` } type authDict struct { - Type authtypes.LoginType `json:"type"` - Session string `json:"session"` + Type authtypes.LoginType `json:"type"` + Session string `json:"session"` + Mac gomatrixserverlib.HexString `json:"mac"` // TODO: Lots of custom keys depending on the type } @@ -57,6 +68,15 @@ type authFlow struct { Stages []authtypes.LoginType `json:"stages"` } +// legacyRegisterRequest represents the submitted registration request for v1 API. +type legacyRegisterRequest struct { + Password string `json:"password"` + Username string `json:"user"` + Admin bool `json:"admin"` + Type authtypes.LoginType `json:"type"` + Mac gomatrixserverlib.HexString `json:"mac"` +} + func newUserInteractiveResponse(sessionID string, fs []authFlow) userInteractiveResponse { return userInteractiveResponse{ fs, []authtypes.LoginType{}, make(map[string]interface{}), sessionID, @@ -71,36 +91,51 @@ type registerResponse struct { DeviceID string `json:"device_id"` } -// Validate returns an error response if the request fails to validate. -func (r *registerRequest) Validate() *util.JSONResponse { +// Validate returns an error response if the username/password are invalid +func validate(username, password string) *util.JSONResponse { // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161 - if len(r.Password) > maxPasswordLength { + if len(password) > maxPasswordLength { return &util.JSONResponse{ Code: 400, JSON: jsonerror.BadJSON(fmt.Sprintf("'password' >%d characters", maxPasswordLength)), } - } else if len(r.Username) > maxUsernameLength { + } else if len(username) > maxUsernameLength { return &util.JSONResponse{ Code: 400, JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)), } - } else if len(r.Password) > 0 && len(r.Password) < minPasswordLength { + } else if len(password) > 0 && len(password) < minPasswordLength { return &util.JSONResponse{ Code: 400, JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", minPasswordLength)), } + } else if !validUsernameRegex.MatchString(username) { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.InvalidUsername("User ID can only contain characters a-z, 0-9, or '_-./'"), + } + } else if username[0] == '_' { // Regex checks its not a zero length string + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.InvalidUsername("User ID can't start with a '_'"), + } } return nil } // Register processes a /register request. http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register -func Register(req *http.Request, accountDB *accounts.Database, deviceDB *devices.Database) util.JSONResponse { +func Register( + req *http.Request, + accountDB *accounts.Database, + deviceDB *devices.Database, + cfg *config.Dendrite, +) util.JSONResponse { var r registerRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { return *resErr } - if resErr = r.Validate(); resErr != nil { + if resErr = validate(r.Username, r.Password); resErr != nil { return *resErr } @@ -124,6 +159,7 @@ func Register(req *http.Request, accountDB *accounts.Database, deviceDB *devices // Server admins should be able to change things around (eg enable captcha) JSON: newUserInteractiveResponse(time.Now().String(), []authFlow{ {[]authtypes.LoginType{authtypes.LoginTypeDummy}}, + {[]authtypes.LoginType{authtypes.LoginTypeSharedSecret}}, }), } } @@ -133,6 +169,79 @@ func Register(req *http.Request, accountDB *accounts.Database, deviceDB *devices // TODO: email / msisdn / recaptcha auth types. switch r.Auth.Type { + case authtypes.LoginTypeSharedSecret: + if cfg.Matrix.RegistrationSharedSecret == "" { + return util.MessageResponse(400, "Shared secret registration is disabled") + } + + valid, err := isValidMacLogin(r.Username, r.Password, r.Admin, r.Auth.Mac, cfg.Matrix.RegistrationSharedSecret) + + if err != nil { + return httputil.LogThenError(req, err) + } + + if !valid { + return util.MessageResponse(403, "HMAC incorrect") + } + + return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) + case authtypes.LoginTypeDummy: + // there is nothing to do + return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) + default: + return util.JSONResponse{ + Code: 501, + JSON: jsonerror.Unknown("unknown/unimplemented auth type"), + } + } +} + +// LegacyRegister process register requests from the legacy v1 API +func LegacyRegister( + req *http.Request, + accountDB *accounts.Database, + deviceDB *devices.Database, + cfg *config.Dendrite, +) util.JSONResponse { + var r legacyRegisterRequest + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + if resErr = validate(r.Username, r.Password); resErr != nil { + return *resErr + } + + logger := util.GetLogger(req.Context()) + logger.WithFields(log.Fields{ + "username": r.Username, + "auth.type": r.Type, + }).Info("Processing registration request") + + // All registration requests must specify what auth they are using to perform this request + if r.Type == "" { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.BadJSON("invalid type"), + } + } + + switch r.Type { + case authtypes.LoginTypeSharedSecret: + if cfg.Matrix.RegistrationSharedSecret == "" { + return util.MessageResponse(400, "Shared secret registration is disabled") + } + + valid, err := isValidMacLogin(r.Username, r.Password, r.Admin, r.Mac, cfg.Matrix.RegistrationSharedSecret) + if err != nil { + return httputil.LogThenError(req, err) + } + + if !valid { + return util.MessageResponse(403, "HMAC incorrect") + } + + return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) case authtypes.LoginTypeDummy: // there is nothing to do return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password) @@ -198,3 +307,39 @@ func completeRegistration( }, } } + +// Used for shared secret registration. +// Checks if the username, password and isAdmin flag matches the given mac. +func isValidMacLogin( + username, password string, + isAdmin bool, + givenMac []byte, + sharedSecret string, +) (bool, error) { + // Double check that username/passowrd don't contain the HMAC delimiters. We should have + // already checked this. + if strings.Contains(username, "\x00") { + return false, errors.New("Username contains invalid character") + } + if strings.Contains(password, "\x00") { + return false, errors.New("Password contains invalid character") + } + if sharedSecret == "" { + return false, errors.New("Shared secret registration is disabled") + } + + adminString := "notadmin" + if isAdmin { + adminString = "admin" + } + joined := strings.Join([]string{username, password, adminString}, "\x00") + + mac := hmac.New(sha1.New, []byte(sharedSecret)) + _, err := mac.Write([]byte(joined)) + if err != nil { + return false, err + } + expectedMAC := mac.Sum(nil) + + return hmac.Equal(givenMac, expectedMAC), nil +} diff --git a/src/github.com/matrix-org/dendrite/common/config/config.go b/src/github.com/matrix-org/dendrite/common/config/config.go index 019774b4..2324d513 100644 --- a/src/github.com/matrix-org/dendrite/common/config/config.go +++ b/src/github.com/matrix-org/dendrite/common/config/config.go @@ -74,6 +74,9 @@ type Dendrite struct { // verify third-party identifiers. // Defaults to an empty array. TrustedIDServers []string `yaml:"trusted_third_party_id_servers"` + // If set, allows registration by anyone who also has the shared + // secret, even if registration is otherwise disabled. + RegistrationSharedSecret string `yaml:"registration_shared_secret"` } `yaml:"matrix"` // The configuration specific to the media repostitory. diff --git a/vendor/manifest b/vendor/manifest index e30b2f2e..9200690e 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -116,7 +116,7 @@ { "importpath": "github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib", - "revision": "40b35e1c997fc7e35342aeb39187ff6bf3e10b2e", + "revision": "ce6f4766251e31487906dfaaebd7d7cfea147252", "branch": "master" }, { diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string.go new file mode 100644 index 00000000..883307d5 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string.go @@ -0,0 +1,44 @@ +/* Copyright 2017 New Vector Ltd + * + * 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 gomatrixserverlib + +import ( + "encoding/hex" + "encoding/json" +) + +// A HexString is a string of bytes that are hex encoded when used in JSON. +// The bytes encoded using hex when marshalled as JSON. +// When the bytes are unmarshalled from JSON they are decoded from hex. +type HexString []byte + +// MarshalJSON encodes the bytes as hex and then encodes the hex as a JSON string. +// This takes a value receiver so that maps and slices of HexString encode correctly. +func (h HexString) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(h)) +} + +// UnmarshalJSON decodes a JSON string and then decodes the resulting hex. +// This takes a pointer receiver because it needs to write the result of decoding. +func (h *HexString) UnmarshalJSON(raw []byte) (err error) { + var str string + if err = json.Unmarshal(raw, &str); err != nil { + return + } + + *h, err = hex.DecodeString(str) + return +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string_test.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string_test.go new file mode 100644 index 00000000..6cb04862 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/hex_string_test.go @@ -0,0 +1,82 @@ +/* Copyright 2016-2017 Vector Creations Ltd + * + * 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 gomatrixserverlib + +import ( + "encoding/json" + "testing" +) + +func TestMarshalHex(t *testing.T) { + input := HexString("this\xffis\xffa\xfftest") + want := `"74686973ff6973ff61ff74657374"` + got, err := json.Marshal(input) + if err != nil { + t.Fatal(err) + } + if string(got) != want { + t.Fatalf("json.Marshal(HexString(%q)): wanted %q got %q", string(input), want, string(got)) + } +} + +func TestUnmarshalHex(t *testing.T) { + input := []byte(`"74686973ff6973ff61ff74657374"`) + want := "this\xffis\xffa\xfftest" + var got HexString + err := json.Unmarshal(input, &got) + if err != nil { + t.Fatal(err) + } + if string(got) != want { + t.Fatalf("json.Unmarshal(%q): wanted %q got %q", string(input), want, string(got)) + } +} + +func TestMarshalHexStruct(t *testing.T) { + input := struct{ Value HexString }{HexString("this\xffis\xffa\xfftest")} + want := `{"Value":"74686973ff6973ff61ff74657374"}` + got, err := json.Marshal(input) + if err != nil { + t.Fatal(err) + } + if string(got) != want { + t.Fatalf("json.Marshal(%v): wanted %q got %q", input, want, string(got)) + } +} + +func TestMarshalHexMap(t *testing.T) { + input := map[string]HexString{"Value": HexString("this\xffis\xffa\xfftest")} + want := `{"Value":"74686973ff6973ff61ff74657374"}` + got, err := json.Marshal(input) + if err != nil { + t.Fatal(err) + } + if string(got) != want { + t.Fatalf("json.Marshal(%v): wanted %q got %q", input, want, string(got)) + } +} + +func TestMarshalHexSlice(t *testing.T) { + input := []HexString{HexString("this\xffis\xffa\xfftest")} + want := `["74686973ff6973ff61ff74657374"]` + got, err := json.Marshal(input) + if err != nil { + t.Fatal(err) + } + if string(got) != want { + t.Fatalf("json.Marshal(%v): wanted %q got %q", input, want, string(got)) + } +}