aboutsummaryrefslogtreecommitdiff
path: root/internal/player/password.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/player/password.go')
-rw-r--r--internal/player/password.go77
1 files changed, 67 insertions, 10 deletions
diff --git a/internal/player/password.go b/internal/player/password.go
index 17e4218..f8ce3cc 100644
--- a/internal/player/password.go
+++ b/internal/player/password.go
@@ -3,30 +3,87 @@ 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
)
+// HashPassword derives an Argon2id password hash.
func HashPassword(password string) (string, error) {
- salt := make([]byte, 16)
+ salt := make([]byte, argonSaltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
- hash := sha256.Sum256(append(salt, []byte(password)...))
- return fmt.Sprintf("sha256:%s:%x", base64.RawStdEncoding.EncodeToString(salt), hash), nil
+ key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
+ 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, ":", 3)
- if len(parts) != 3 || parts[0] != "sha256" {
+ parts := strings.SplitN(stored, ":", 6)
+ if len(parts) == 0 {
return false
}
- salt, err := base64.RawStdEncoding.DecodeString(parts[1])
- if err != nil {
+ 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
+ }
+ key := argon2.IDKey([]byte(password), salt, uint32(t), uint32(m), uint8(p), uint32(len(expected)))
+ 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
}
- hash := sha256.Sum256(append(salt, []byte(password)...))
- expected := fmt.Sprintf("%x", hash)
- return parts[2] == expected
+}
+
+// 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:")
}