diff options
| author | historia <[not public]> | 2026-09-15 19:55:21 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 19:55:21 -0400 |
| commit | 6f3c43aca62a7d33a19f14f6ad7c16891952673a (patch) | |
| tree | 0b0665ef624ef442df87ec7131129aaecc3420bd /internal/player/password.go | |
| parent | cd95de60364aba8f5bdf8ec86f7234a9903eacc8 (diff) | |
| download | thehouseoficarus-6f3c43aca62a7d33a19f14f6ad7c16891952673a.tar.gz | |
fix: outstanding-stake ledger, owner backpressure, editor state preservation
Diffstat (limited to 'internal/player/password.go')
| -rw-r--r-- | internal/player/password.go | 77 |
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:") } |
