diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 02781d2..911f2ab 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -3,6 +3,7 @@ package api import ( ctx "context" "encoding/json" + "errors" "os" "path" "sort" @@ -144,6 +145,9 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 hash, err := createHash(req.Msg.Password) if err != nil { + if errors.Is(err, ErrArgon2Busy) { + return nil, connect.NewError(connect.CodeResourceExhausted, err) + } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err)) } @@ -162,7 +166,13 @@ func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[api }), nil } - match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) + match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) + if err != nil { + if errors.Is(err, ErrArgon2Busy) { + return nil, connect.NewError(connect.CodeResourceExhausted, err) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err)) + } response := connect.NewResponse(&apiv1.LocalUserLoginResponse{ Success: match, diff --git a/service/internal/api/local_user_login.go b/service/internal/api/local_user_login.go index db6e99e..13e77ce 100644 --- a/service/internal/api/local_user_login.go +++ b/service/internal/api/local_user_login.go @@ -1,6 +1,7 @@ package api import ( + "errors" "runtime" config "github.com/OliveTin/OliveTin/internal/config" @@ -8,6 +9,12 @@ import ( log "github.com/sirupsen/logrus" ) +var ErrArgon2Busy = errors.New("too many concurrent password operations") + +const argon2MaxConcurrent = 10 + +var argon2Sem = make(chan struct{}, argon2MaxConcurrent) + var defaultParams = argon2id.Params{ Memory: 64 * 1024, Iterations: 4, @@ -17,6 +24,12 @@ var defaultParams = argon2id.Params{ } func CreateHash(password string) (string, error) { + select { + case argon2Sem <- struct{}{}: + defer func() { <-argon2Sem }() + default: + return "", ErrArgon2Busy + } hash, err := argon2id.CreateHash(password, &defaultParams) if err != nil { @@ -31,30 +44,38 @@ func createHash(password string) (string, error) { return CreateHash(password) } -func comparePasswordAndHash(password, hash string) bool { +func comparePasswordAndHash(password, hash string) (bool, error) { + select { + case argon2Sem <- struct{}{}: + defer func() { <-argon2Sem }() + default: + return false, ErrArgon2Busy + } match, err := argon2id.ComparePasswordAndHash(password, hash) if err != nil { log.Errorf("Error comparing password and hash: %v", err) - return false + return false, nil } - return match + return match, nil } -func checkUserPassword(cfg *config.Config, username, password string) bool { +func checkUserPassword(cfg *config.Config, username, password string) (bool, error) { for _, user := range cfg.AuthLocalUsers.Users { if user.Username == username { - match := comparePasswordAndHash(password, user.Password) - + match, err := comparePasswordAndHash(password, user.Password) + if err != nil { + return false, err + } if match { - return true + return true, nil } else { log.WithFields(log.Fields{ "username": username, }).Warn("Password does not match for user") - return false + return false, nil } } } @@ -63,5 +84,5 @@ func checkUserPassword(cfg *config.Config, username, password string) bool { "username": username, }).Warn("Failed to check password for user, as username was not found") - return false + return false, nil }