Prechádzať zdrojové kódy

security: 10-slot Semaphore around password hash functions to prevent resource exhaustion attacks

jamesread 4 mesiacov pred
rodič
commit
a7be68b359

+ 11 - 1
service/internal/api/api.go

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

+ 30 - 9
service/internal/api/local_user_login.go

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