package db import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "sync" "golang.org/x/crypto/argon2" ) // Parameters of the `argon2` npm package, which produced every hash already // stored in the database. Keep them identical so old hashes still verify. const ( argonTime = 3 argonMemory = 64 * 1024 argonThreads = 4 argonKeyLen = 32 argonSaltLen = 16 ) var errBadHash = errors.New("invalid argon2 hash") // argonSlots caps parallel argon2 runs. Each run allocates 64 MiB, so a // burst of logins could otherwise exhaust memory. var argonSlots = make(chan struct{}, 2) func idKey(pw, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte { argonSlots <- struct{}{} defer func() { <-argonSlots }() return argon2.IDKey(pw, salt, time, memory, threads, keyLen) } // HashPassword returns a PHC-format argon2id hash string. func HashPassword(pw string) (string, error) { salt := make([]byte, argonSaltLen) if _, err := rand.Read(salt); err != nil { return "", err } key := idKey([]byte(pw), salt, argonTime, argonMemory, argonThreads, argonKeyLen) b64 := base64.RawStdEncoding return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argonMemory, argonTime, argonThreads, b64.EncodeToString(salt), b64.EncodeToString(key)), nil } // dummyHash is a real argon2 hash of a fixed string, computed once. var dummyHash = sync.OnceValue(func() string { h, err := HashPassword("hearthforge-unknown-user") if err != nil { return "" } return h }) // VerifyDummyPassword does the same argon2 work as VerifyPassword and always // fails. Call it when the account does not exist, so a login for an unknown // username takes about as long as one for a known one. func VerifyDummyPassword(pw string) { _, _ = VerifyPassword(dummyHash(), pw) } // VerifyPassword checks a password against a stored PHC hash string. func VerifyPassword(hash, pw string) (bool, error) { parts := strings.Split(hash, "$") if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" { return false, errBadHash } var version int if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { return false, errBadHash } if version != argon2.Version { return false, fmt.Errorf("%w: unsupported version %d", errBadHash, version) } var memory, time uint32 var threads uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil { return false, errBadHash } b64 := base64.RawStdEncoding salt, err := b64.DecodeString(parts[4]) if err != nil { return false, errBadHash } want, err := b64.DecodeString(parts[5]) if err != nil { return false, errBadHash } got := idKey([]byte(pw), salt, time, memory, threads, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1, nil }