security: 10-slot Semaphore around password hash functions to prevent… (#904)
This commit is contained in:
commit
6dfffd1170
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue