aboutsummaryrefslogtreecommitdiff
path: root/internal/game/core_login_account.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/core_login_account.go')
-rw-r--r--internal/game/core_login_account.go10
1 files changed, 10 insertions, 0 deletions
diff --git a/internal/game/core_login_account.go b/internal/game/core_login_account.go
index a5106ea..e9fa67f 100644
--- a/internal/game/core_login_account.go
+++ b/internal/game/core_login_account.go
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"strings"
+ "time"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
@@ -156,6 +157,8 @@ func (g *Game) handlePasswordOffOwner(sess *net.Session, input string) {
})
return
}
+ throttle := g.ensureLoginThrottle()
+ key := loginThrottleKey(name, sess.RemoteAddr())
auth := passwordAuth{}
acc, err := g.AccountStore.LoadAccount(name)
switch {
@@ -163,6 +166,7 @@ func (g *Game) handlePasswordOffOwner(sess *net.Session, input string) {
auth.loadErr = true
case player.CheckPassword(input, acc.PasswordHash):
auth.ok, auth.stored = true, acc.PasswordHash
+ throttle.clear(key)
// Transparent upgrade: legacy single-pass SHA-256 hashes rehash to
// Argon2id after a successful verification. A failed rehash keeps
// the legacy hash and retries on the next login.
@@ -173,6 +177,12 @@ func (g *Game) handlePasswordOffOwner(sess *net.Session, input string) {
log.Printf("login: password rehash for %s: %v", acc.Name, hashErr)
}
}
+ default:
+ // Wrong password: apply an escalating per-(account, IP) delay so a
+ // brute-force attempt costs real wall-clock time. This is the
+ // connection's own goroutine, never the owner, so the world is not
+ // stalled by an attacker.
+ time.Sleep(throttle.delayFor(key))
}
g.RunOnOwner(func() {
if g.isShuttingDown() {