aboutsummaryrefslogtreecommitdiff
path: root/internal/player/password.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-16 19:57:45 -0400
committerhistoria <[not public]>2026-09-16 19:57:45 -0400
commit6ef40a5739f1723ec26cc32dfc48fe764f776dca (patch)
treee215a4dc96ea639bcea07bb589aae3e31c3609bb /internal/player/password.go
parent1dad15698845d370e998f7cc5feb8aece3fab58f (diff)
downloadthehouseoficarus-6ef40a5739f1723ec26cc32dfc48fe764f776dca.tar.gz
fix: durable payouts survive journal failure, cap bank withdrawals, hash passwords off the owner loop.
Diffstat (limited to 'internal/player/password.go')
-rw-r--r--internal/player/password.go20
1 files changed, 20 insertions, 0 deletions
diff --git a/internal/player/password.go b/internal/player/password.go
index f8ce3cc..9be39fb 100644
--- a/internal/player/password.go
+++ b/internal/player/password.go
@@ -31,13 +31,31 @@ const (
argonSaltLen = 16
)
+// hashLimiter bounds concurrent Argon2id computations. Each hash costs
+// ~64 MiB and tens of milliseconds, so an unbounded burst of logins (or
+// login attempts) would spike memory and CPU in parallel. Password work
+// runs on connection/HTTP goroutines (never the game owner); this only
+// caps how many run at once. Callers acquire per computation, never
+// nested, so the limited channel cannot deadlock.
+var hashLimiter = make(chan struct{}, 4)
+
+func acquireHashSlot() {
+ hashLimiter <- struct{}{}
+}
+
+func releaseHashSlot() {
+ <-hashLimiter
+}
+
// HashPassword derives an Argon2id password hash.
func HashPassword(password string) (string, error) {
salt := make([]byte, argonSaltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
+ acquireHashSlot()
key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
+ releaseHashSlot()
return fmt.Sprintf("argon2id:%d:%d:%d:%s:%s",
argonTime, argonMemory, argonThreads,
base64.RawStdEncoding.EncodeToString(salt), hex.EncodeToString(key)), nil
@@ -63,7 +81,9 @@ func CheckPassword(password, stored string) bool {
if errT != nil || errM != nil || errP != nil || errSalt != nil || errKey != nil {
return false
}
+ acquireHashSlot()
key := argon2.IDKey([]byte(password), salt, uint32(t), uint32(m), uint8(p), uint32(len(expected)))
+ releaseHashSlot()
return subtle.ConstantTimeCompare(key, expected) == 1
case "sha256":
legacy := strings.SplitN(stored, ":", 3)