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
108
109
|
package player
import (
"crypto/rand"
"crypto/sha256"
"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>
//
// Legacy accounts may still carry the old single-pass salted SHA-256 hash
// ("sha256:<b64salt>:<hexhash>"); CheckPassword verifies both, and the
// login paths rehash to Argon2id on successful verification (see
// NeedsRehash).
const (
argonTime = 1
argonMemory = 64 * 1024 // KiB
argonThreads = 2
argonKeyLen = 32
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
}
// CheckPassword verifies a password against a stored hash in either the
// current Argon2id format or the legacy salted SHA-256 format.
func CheckPassword(password, stored string) bool {
parts := strings.SplitN(stored, ":", 6)
if len(parts) == 0 {
return false
}
switch parts[0] {
case "argon2id":
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
}
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)
if len(legacy) != 3 {
return false
}
salt, err := base64.RawStdEncoding.DecodeString(legacy[1])
if err != nil {
return false
}
hash := sha256.Sum256(append(salt, []byte(password)...))
return legacy[2] == fmt.Sprintf("%x", hash)
default:
return false
}
}
// NeedsRehash reports whether a stored hash uses the legacy single-pass
// SHA-256 scheme. Callers rehash with HashPassword after a successful
// verification and persist the new hash (transparent upgrade on login).
func NeedsRehash(stored string) bool {
return strings.HasPrefix(stored, "sha256:")
}
|