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.
This commit is contained in:
eikendev 2021-02-08 00:34:33 +01:00
parent fb90808288
commit d7721e827b
No known key found for this signature in database
GPG key ID: A1BDB1B28C8EF694
8 changed files with 106 additions and 30 deletions

View file

@ -22,6 +22,10 @@ func (h *ApplicationHandler) applicationExists(token string) bool {
return application != nil 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 { func (h *ApplicationHandler) registerApplication(ctx *gin.Context, a *model.Application, u *model.User) error {
log.Printf("Registering application %s.\n", a.Name) log.Printf("Registering application %s.\n", a.Name)
@ -36,12 +40,12 @@ func (h *ApplicationHandler) registerApplication(ctx *gin.Context, a *model.Appl
return nil 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) log.Printf("Creating application %s.\n", name)
application := model.Application{} application := model.Application{}
application.Name = name application.Name = name
application.Token = authentication.GenerateNotExistingToken(authentication.GenerateApplicationToken, h.applicationExists) application.Token = h.generateToken(compat)
application.UserID = u.ID application.UserID = u.ID
err := h.DB.CreateApplication(&application) err := h.DB.CreateApplication(&application)
@ -78,18 +82,30 @@ func (h *ApplicationHandler) deleteApplication(ctx *gin.Context, a *model.Applic
return nil return nil
} }
func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Application, updateApplication *model.UpdateApplication) error { func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Application, u *model.User, updateApplication *model.UpdateApplication) error {
log.Printf("Updating application %s.\n", a.Name) log.Printf("Updating application %s (ID %d).\n", a.Name, a.ID)
if updateApplication.Name != nil { if updateApplication.Name != nil {
log.Printf("Updating application name to '%s'.", *updateApplication.Name)
a.Name = *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) err := h.DB.UpdateApplication(a)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success { if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err return err
} }
err = h.DP.UpdateApplication(a, u)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
return nil return nil
} }
@ -106,7 +122,7 @@ func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) {
return return
} }
application, err := h.createApplication(ctx, createApplication.Name, user) application, err := h.createApplication(ctx, user, createApplication.Name, createApplication.StrictCompatibility)
if err != nil { if err != nil {
return return
} }
@ -180,11 +196,16 @@ func (h *ApplicationHandler) UpdateApplication(ctx *gin.Context) {
} }
var updateApplication model.UpdateApplication var updateApplication model.UpdateApplication
if err := ctx.BindUri(&updateApplication); err != nil { if err := ctx.Bind(&updateApplication); err != nil {
return 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 return
} }

View file

@ -28,6 +28,7 @@ type Database interface {
type Dispatcher interface { type Dispatcher interface {
RegisterApplication(id uint, name, token, user string) (string, error) RegisterApplication(id uint, name, token, user string) (string, error)
DeregisterApplication(a *model.Application, u *model.User) error DeregisterApplication(a *model.Application, u *model.User) error
UpdateApplication(a *model.Application, u *model.User) error
} }
// The CredentialsManager interface for updating credentials. // The CredentialsManager interface for updating credentials.

View file

@ -205,7 +205,7 @@ func (h *UserHandler) UpdateUser(ctx *gin.Context) {
} }
var updateUser model.UpdateUser var updateUser model.UpdateUser
if err := ctx.BindUri(&updateUser); err != nil { if err := ctx.Bind(&updateUser); err != nil {
return return
} }

View file

@ -19,6 +19,9 @@ func successOrAbort(ctx *gin.Context, code int, err error) bool {
func isCurrentUser(ctx *gin.Context, ID uint) bool { func isCurrentUser(ctx *gin.Context, ID uint) bool {
user := authentication.GetUser(ctx) user := authentication.GetUser(ctx)
if user == nil {
return false
}
if user.ID != ID { if user.ID != ID {
ctx.AbortWithError(http.StatusForbidden, errors.New("only owner can delete application")) ctx.AbortWithError(http.StatusForbidden, errors.New("only owner can delete application"))

View file

@ -2,13 +2,15 @@ package authentication
import ( import (
"crypto/rand" "crypto/rand"
"log"
"math/big" "math/big"
) )
var ( var (
tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
randomTokenLength = 64 standardTokenLength = 64 // This length includes the prefix (one character).
applicationPrefix = "A" compatTokenLength = 15 // This length includes the prefix (one character).
applicationPrefix = "A"
) )
func randIntn(n int) int { 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. // 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 { for {
token := generateToken() token := generateToken(compat)
if !tokenExists(token) { if !tokenExists(token) {
return token return token
@ -44,11 +46,23 @@ func generateRandomString(length int) string {
return string(res) return string(res)
} }
func generateRandomToken(prefix string) string { func generateRandomToken(prefix string, compat bool) string {
return prefix + generateRandomString(randomTokenLength) 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. // GenerateApplicationToken generates a token for an application.
func GenerateApplicationToken() string { func GenerateApplicationToken(compat bool) string {
return generateRandomToken(applicationPrefix) return generateRandomToken(applicationPrefix, compat)
} }

View file

@ -9,23 +9,25 @@ import (
"github.com/matrix-org/gomatrix" "github.com/matrix-org/gomatrix"
) )
func buildRoomTopic(id uint) string {
return fmt.Sprintf("Application %d", id)
}
// RegisterApplication creates a channel for an application. // RegisterApplication creates a channel for an application.
func (d *Dispatcher) RegisterApplication(id uint, name, token, user string) (string, error) { 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) 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{ response, err := d.client.CreateRoom(&gomatrix.ReqCreateRoom{
Invite: []string{user}, Invite: []string{user},
IsDirect: true, IsDirect: true,
Name: name, Name: name,
Preset: "private_chat", Preset: "private_chat",
Topic: topic, Topic: buildRoomTopic(id),
Visibility: "private", Visibility: "private",
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Print(err)
return "", 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 { if _, err := d.client.KickUser(a.MatrixID, kickUser); err != nil {
log.Fatal(err) log.Print(err)
return err return err
} }
if _, err := d.client.LeaveRoom(a.MatrixID); err != nil { if _, err := d.client.LeaveRoom(a.MatrixID); err != nil {
log.Fatal(err) log.Print(err)
return err return err
} }
if _, err := d.client.ForgetRoom(a.MatrixID); err != nil { 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 return err
} }

View file

@ -11,10 +11,13 @@ type Application struct {
// CreateApplication is used to process queries for creating applications. // CreateApplication is used to process queries for creating applications.
type CreateApplication struct { 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. // UpdateApplication is used to process queries for updating applications.
type UpdateApplication struct { 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"`
} }

View file

@ -79,8 +79,8 @@ func (u *User) IntoExternalUser() *ExternalUser {
// UpdateUser is used to process queries for updating users. // UpdateUser is used to process queries for updating users.
type UpdateUser struct { type UpdateUser struct {
Name *string `json:"name"` Name *string `form:"name" query:"name" json:"name"`
Password *string `json:"password"` Password *string `form:"password" query:"password" json:"password"`
IsAdmin *bool `json:"is_admin"` IsAdmin *bool `form:"is_admin" query:"is_admin" json:"is_admin"`
MatrixID *string `json:"matrix_id"` MatrixID *string `form:"matrix_id" query:"matrix_id" json:"matrix_id"`
} }