From d7721e827b3558406b63d4e2798132ee28c96d6c Mon Sep 17 00:00:00 2001 From: eikendev Date: Mon, 8 Feb 2021 00:34:33 +0100 Subject: [PATCH] Add option to enforce strict compatibility Some plugins check for token length. Since PushBits uses longer tokens by default for better security, these plugins are incompatible. With this patch, users can decide if they want an application to have a short token, so that said plugins can talk to PushBits again. --- internal/api/application.go | 35 +++++++++++++++++----- internal/api/interfaces.go | 1 + internal/api/user.go | 2 +- internal/api/util.go | 3 ++ internal/authentication/token.go | 32 ++++++++++++++------ internal/dispatcher/application.go | 48 +++++++++++++++++++++++++----- internal/model/application.go | 7 +++-- internal/model/user.go | 8 ++--- 8 files changed, 106 insertions(+), 30 deletions(-) diff --git a/internal/api/application.go b/internal/api/application.go index 2346f8f..1ee56a7 100644 --- a/internal/api/application.go +++ b/internal/api/application.go @@ -22,6 +22,10 @@ func (h *ApplicationHandler) applicationExists(token string) bool { return application != nil } +func (h *ApplicationHandler) generateToken(compat bool) string { + return authentication.GenerateNotExistingToken(authentication.GenerateApplicationToken, compat, h.applicationExists) +} + func (h *ApplicationHandler) registerApplication(ctx *gin.Context, a *model.Application, u *model.User) error { log.Printf("Registering application %s.\n", a.Name) @@ -36,12 +40,12 @@ func (h *ApplicationHandler) registerApplication(ctx *gin.Context, a *model.Appl return nil } -func (h *ApplicationHandler) createApplication(ctx *gin.Context, name string, u *model.User) (*model.Application, error) { +func (h *ApplicationHandler) createApplication(ctx *gin.Context, u *model.User, name string, compat bool) (*model.Application, error) { log.Printf("Creating application %s.\n", name) application := model.Application{} application.Name = name - application.Token = authentication.GenerateNotExistingToken(authentication.GenerateApplicationToken, h.applicationExists) + application.Token = h.generateToken(compat) application.UserID = u.ID err := h.DB.CreateApplication(&application) @@ -78,18 +82,30 @@ func (h *ApplicationHandler) deleteApplication(ctx *gin.Context, a *model.Applic return nil } -func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Application, updateApplication *model.UpdateApplication) error { - log.Printf("Updating application %s.\n", a.Name) +func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Application, u *model.User, updateApplication *model.UpdateApplication) error { + log.Printf("Updating application %s (ID %d).\n", a.Name, a.ID) if updateApplication.Name != nil { + log.Printf("Updating application name to '%s'.", *updateApplication.Name) a.Name = *updateApplication.Name } + if updateApplication.RefreshToken != nil && (*updateApplication.RefreshToken) { + log.Print("Updating application token.") + compat := updateApplication.StrictCompatibility != nil && (*updateApplication.StrictCompatibility) + a.Token = h.generateToken(compat) + } + err := h.DB.UpdateApplication(a) if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success { return err } + err = h.DP.UpdateApplication(a, u) + if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success { + return err + } + return nil } @@ -106,7 +122,7 @@ func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) { return } - application, err := h.createApplication(ctx, createApplication.Name, user) + application, err := h.createApplication(ctx, user, createApplication.Name, createApplication.StrictCompatibility) if err != nil { return } @@ -180,11 +196,16 @@ func (h *ApplicationHandler) UpdateApplication(ctx *gin.Context) { } var updateApplication model.UpdateApplication - if err := ctx.BindUri(&updateApplication); err != nil { + if err := ctx.Bind(&updateApplication); err != nil { return } - if err := h.updateApplication(ctx, application, &updateApplication); err != nil { + user := authentication.GetUser(ctx) + if user == nil { + return + } + + if err := h.updateApplication(ctx, application, user, &updateApplication); err != nil { return } diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go index d3b98ca..73486c0 100644 --- a/internal/api/interfaces.go +++ b/internal/api/interfaces.go @@ -28,6 +28,7 @@ type Database interface { type Dispatcher interface { RegisterApplication(id uint, name, token, user string) (string, error) DeregisterApplication(a *model.Application, u *model.User) error + UpdateApplication(a *model.Application, u *model.User) error } // The CredentialsManager interface for updating credentials. diff --git a/internal/api/user.go b/internal/api/user.go index f33bd94..ea06ff2 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -205,7 +205,7 @@ func (h *UserHandler) UpdateUser(ctx *gin.Context) { } var updateUser model.UpdateUser - if err := ctx.BindUri(&updateUser); err != nil { + if err := ctx.Bind(&updateUser); err != nil { return } diff --git a/internal/api/util.go b/internal/api/util.go index d40d833..177f537 100644 --- a/internal/api/util.go +++ b/internal/api/util.go @@ -19,6 +19,9 @@ func successOrAbort(ctx *gin.Context, code int, err error) bool { func isCurrentUser(ctx *gin.Context, ID uint) bool { user := authentication.GetUser(ctx) + if user == nil { + return false + } if user.ID != ID { ctx.AbortWithError(http.StatusForbidden, errors.New("only owner can delete application")) diff --git a/internal/authentication/token.go b/internal/authentication/token.go index c9cf283..df057b8 100644 --- a/internal/authentication/token.go +++ b/internal/authentication/token.go @@ -2,13 +2,15 @@ package authentication import ( "crypto/rand" + "log" "math/big" ) var ( - tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - randomTokenLength = 64 - applicationPrefix = "A" + tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + standardTokenLength = 64 // This length includes the prefix (one character). + compatTokenLength = 15 // This length includes the prefix (one character). + applicationPrefix = "A" ) func randIntn(n int) int { @@ -23,9 +25,9 @@ func randIntn(n int) int { } // GenerateNotExistingToken receives a token generation function and a function to check whether the token exists, returns a unique token. -func GenerateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string { +func GenerateNotExistingToken(generateToken func(bool) string, compat bool, tokenExists func(token string) bool) string { for { - token := generateToken() + token := generateToken(compat) if !tokenExists(token) { return token @@ -44,11 +46,23 @@ func generateRandomString(length int) string { return string(res) } -func generateRandomToken(prefix string) string { - return prefix + generateRandomString(randomTokenLength) +func generateRandomToken(prefix string, compat bool) string { + tokenLength := standardTokenLength + + if compat { + tokenLength = compatTokenLength + } + + // Although constant at the time of writing, this check should prevent future changes from generating insecure tokens. + randomLength := tokenLength - len(prefix) + if randomLength < 14 { + log.Fatalf("Tokens should have more than %d random characters", randomLength) + } + + return prefix + generateRandomString(randomLength) } // GenerateApplicationToken generates a token for an application. -func GenerateApplicationToken() string { - return generateRandomToken(applicationPrefix) +func GenerateApplicationToken(compat bool) string { + return generateRandomToken(applicationPrefix, compat) } diff --git a/internal/dispatcher/application.go b/internal/dispatcher/application.go index c2887ab..d6a49da 100644 --- a/internal/dispatcher/application.go +++ b/internal/dispatcher/application.go @@ -9,23 +9,25 @@ import ( "github.com/matrix-org/gomatrix" ) +func buildRoomTopic(id uint) string { + return fmt.Sprintf("Application %d", id) +} + // RegisterApplication creates a channel for an application. func (d *Dispatcher) RegisterApplication(id uint, name, token, user string) (string, error) { log.Printf("Registering application %s, notifications will be relayed to user %s.\n", name, user) - topic := fmt.Sprintf("Application %d, Token %s", id, token) - response, err := d.client.CreateRoom(&gomatrix.ReqCreateRoom{ Invite: []string{user}, IsDirect: true, Name: name, Preset: "private_chat", - Topic: topic, + Topic: buildRoomTopic(id), Visibility: "private", }) if err != nil { - log.Fatal(err) + log.Print(err) return "", err } @@ -44,17 +46,49 @@ func (d *Dispatcher) DeregisterApplication(a *model.Application, u *model.User) } if _, err := d.client.KickUser(a.MatrixID, kickUser); err != nil { - log.Fatal(err) + log.Print(err) return err } if _, err := d.client.LeaveRoom(a.MatrixID); err != nil { - log.Fatal(err) + log.Print(err) return err } if _, err := d.client.ForgetRoom(a.MatrixID); err != nil { - log.Fatal(err) + log.Print(err) + return err + } + + return nil +} + +func (d *Dispatcher) sendRoomEvent(roomID, eventType string, content interface{}) error { + if _, err := d.client.SendStateEvent(roomID, eventType, "", content); err != nil { + log.Print(err) + return err + } + + return nil +} + +// UpdateApplication updates a channel for an application. +func (d *Dispatcher) UpdateApplication(a *model.Application, u *model.User) error { + log.Printf("Updating application %s (ID %d) with Matrix ID %s.\n", a.Name, a.ID, a.MatrixID) + + content := map[string]interface{}{ + "name": a.Name, + } + + if err := d.sendRoomEvent(a.MatrixID, "m.room.name", content); err != nil { + return err + } + + content = map[string]interface{}{ + "topic": buildRoomTopic(a.ID), + } + + if err := d.sendRoomEvent(a.MatrixID, "m.room.topic", content); err != nil { return err } diff --git a/internal/model/application.go b/internal/model/application.go index 729e621..736f79b 100644 --- a/internal/model/application.go +++ b/internal/model/application.go @@ -11,10 +11,13 @@ type Application struct { // CreateApplication is used to process queries for creating applications. type CreateApplication struct { - Name string `form:"name" query:"name" json:"name" binding:"required"` + Name string `form:"name" query:"name" json:"name" binding:"required"` + StrictCompatibility bool `form:"strict_compatibility" query:"strict_compatibility" json:"strict_compatibility"` } // UpdateApplication is used to process queries for updating applications. type UpdateApplication struct { - Name *string `json:"name"` + Name *string `form:"new_name" query:"new_name" json:"new_name"` + RefreshToken *bool `form:"refresh_token" query:"refresh_token" json:"refresh_token"` + StrictCompatibility *bool `form:"strict_compatibility" query:"strict_compatibility" json:"strict_compatibility"` } diff --git a/internal/model/user.go b/internal/model/user.go index ddb77cf..7c7cbfc 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -79,8 +79,8 @@ func (u *User) IntoExternalUser() *ExternalUser { // UpdateUser is used to process queries for updating users. type UpdateUser struct { - Name *string `json:"name"` - Password *string `json:"password"` - IsAdmin *bool `json:"is_admin"` - MatrixID *string `json:"matrix_id"` + Name *string `form:"name" query:"name" json:"name"` + Password *string `form:"password" query:"password" json:"password"` + IsAdmin *bool `form:"is_admin" query:"is_admin" json:"is_admin"` + MatrixID *string `form:"matrix_id" query:"matrix_id" json:"matrix_id"` }