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 ( import (
ctx "context" ctx "context"
"encoding/json" "encoding/json"
"errors"
"os" "os"
"path" "path"
"sort" "sort"
@ -144,6 +145,9 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1
hash, err := createHash(req.Msg.Password) hash, err := createHash(req.Msg.Password)
if err != nil { 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)) 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 }), 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{ response := connect.NewResponse(&apiv1.LocalUserLoginResponse{
Success: match, Success: match,

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"errors"
"runtime" "runtime"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
@ -8,6 +9,12 @@ import (
log "github.com/sirupsen/logrus" 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{ var defaultParams = argon2id.Params{
Memory: 64 * 1024, Memory: 64 * 1024,
Iterations: 4, Iterations: 4,
@ -17,6 +24,12 @@ var defaultParams = argon2id.Params{
} }
func CreateHash(password string) (string, error) { func CreateHash(password string) (string, error) {
select {
case argon2Sem <- struct{}{}:
defer func() { <-argon2Sem }()
default:
return "", ErrArgon2Busy
}
hash, err := argon2id.CreateHash(password, &defaultParams) hash, err := argon2id.CreateHash(password, &defaultParams)
if err != nil { if err != nil {
@ -31,30 +44,38 @@ func createHash(password string) (string, error) {
return CreateHash(password) 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) match, err := argon2id.ComparePasswordAndHash(password, hash)
if err != nil { if err != nil {
log.Errorf("Error comparing password and hash: %v", err) 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 { for _, user := range cfg.AuthLocalUsers.Users {
if user.Username == username { if user.Username == username {
match := comparePasswordAndHash(password, user.Password) match, err := comparePasswordAndHash(password, user.Password)
if err != nil {
return false, err
}
if match { if match {
return true return true, nil
} else { } else {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": username, "username": username,
}).Warn("Password does not match for user") }).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, "username": username,
}).Warn("Failed to check password for user, as username was not found") }).Warn("Failed to check password for user, as username was not found")
return false return false, nil
} }