1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
package player
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/argon2"
)
// Argon2id parameters for password hashing. Memory cost dominates the
// GPU/ASIC attack economics; 64 MiB per hash is affordable for a MUD's
// login rate while making brute force expensive. Stored format:
//
// argon2id:<time>:<memoryKiB>:<threads>:<b64salt>:<hexkey>
//
// Only Argon2id is accepted: the prototype has no password-hash migration and
// accounts are recreated rather than upgraded.
const (
argonTime = 1
argonMemory = 64 * 1024 // KiB
argonThreads = 2
argonKeyLen = 32
argonSaltLen = 16
)
// Sanity bounds applied to Argon2id parameters parsed from a stored hash.
// HashPassword always writes the constants above, but a hand-edited or
// corrupted account file can carry arbitrary numbers; without these bounds a
// value like 4294967295 (or a negative that wraps through uint32) makes
// argon2.IDKey request a multi-TiB allocation and kills the process instead of
// failing the login. Anything outside these ranges is rejected before hashing.
const (
argonMinTime = 1
argonMaxTime = 8
argonMinMemory = 8 * 1024 // KiB (8 MiB)
argonMaxMemory = 256 * 1024 // KiB (256 MiB)
argonMinThreads = 1
argonMaxThreads = 16
argonMinKeyLen = 16
argonMaxKeyLen = 64
)
// 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
}
// CheckPassword verifies a password against a stored Argon2id hash.
func CheckPassword(password, stored string) bool {
parts := strings.SplitN(stored, ":", 6)
if len(parts) == 0 || parts[0] != "argon2id" {
return false
}
if len(parts) != 6 {
return false
}
t, errT := strconv.Atoi(parts[1])
m, errM := strconv.Atoi(parts[2])
p, errP := strconv.Atoi(parts[3])
salt, errSalt := base64.RawStdEncoding.DecodeString(parts[4])
expected, errKey := hex.DecodeString(parts[5])
if errT != nil || errM != nil || errP != nil || errSalt != nil || errKey != nil {
return false
}
if t < argonMinTime || t > argonMaxTime ||
m < argonMinMemory || m > argonMaxMemory ||
p < argonMinThreads || p > argonMaxThreads ||
len(expected) < argonMinKeyLen || len(expected) > argonMaxKeyLen {
// Out-of-range parameters: fail the check instead of trusting the
// stored values and requesting an enormous allocation.
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
}
|