security: 10-slot Semaphore around password hash functions to prevent… (#904)

This commit is contained in:
James Read 2026-02-26 12:12:06 -05:00 committed by GitHub
commit 6dfffd1170
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 10 deletions

View File

@ -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,

View File

@ -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
}