diff options
| author | historia <[not public]> | 2026-09-16 19:57:45 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-16 19:57:45 -0400 |
| commit | 6ef40a5739f1723ec26cc32dfc48fe764f776dca (patch) | |
| tree | e215a4dc96ea639bcea07bb589aae3e31c3609bb /internal/player | |
| parent | 1dad15698845d370e998f7cc5feb8aece3fab58f (diff) | |
| download | thehouseoficarus-6ef40a5739f1723ec26cc32dfc48fe764f776dca.tar.gz | |
fix: durable payouts survive journal failure, cap bank withdrawals, hash passwords off the owner loop.
Diffstat (limited to 'internal/player')
| -rw-r--r-- | internal/player/password.go | 20 | ||||
| -rw-r--r-- | internal/player/player.go | 10 |
2 files changed, 29 insertions, 1 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) diff --git a/internal/player/player.go b/internal/player/player.go index 708588c..0a48b04 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -176,7 +176,15 @@ type Player struct { // amounts are ALREADY INCLUDED in this file's credit balance. Recovery // uses membership to prove a payout became durable instead of guessing // from balance checkpoints, which unrelated credit changes can fake. - OpenPayouts []string `yaml:"open_payouts,omitempty"` + OpenPayouts []string `yaml:"open_payouts,omitempty"` + // ResolvedStakes is durable evidence for the economy journal (see + // internal/game/economy.go): the IDs of outstanding wager records the + // session RESOLVED (won, lost, pushed, refunded) but whose settled + // markers could not be appended to the journal (e.g. a broken economy + // path). Recovery treats membership as proof the round resolved — the + // stake must not be refunded and its chip part must not return in kind + // on top of a settlement that already returned it as credits. + ResolvedStakes []string `yaml:"resolved_stakes,omitempty"` Description string `yaml:"description"` AttackStyle AttackStyle `yaml:"attack_style"` Prompt string `yaml:"prompt"` |
