password.go
⎇
Raw
1package db
2
3import (
4 "crypto/rand"
5 "crypto/subtle"
6 "encoding/base64"
7 "errors"
8 "fmt"
9 "strings"
10 "sync"
11
12 "golang.org/x/crypto/argon2"
13)
14
15// Parameters of the `argon2` npm package, which produced every hash already
16// stored in the database. Keep them identical so old hashes still verify.
17const (
18 argonTime = 3
19 argonMemory = 64 * 1024
20 argonThreads = 4
21 argonKeyLen = 32
22 argonSaltLen = 16
23)
24
25var errBadHash = errors.New("invalid argon2 hash")
26
27// HashPassword returns a PHC-format argon2id hash string.
28func HashPassword(pw string) (string, error) {
29 salt := make([]byte, argonSaltLen)
30 if _, err := rand.Read(salt); err != nil {
31 return "", err
32 }
33 key := argon2.IDKey([]byte(pw), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
34 b64 := base64.RawStdEncoding
35 return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
36 argon2.Version, argonMemory, argonTime, argonThreads,
37 b64.EncodeToString(salt), b64.EncodeToString(key)), nil
38}
39
40// dummyHash is a real argon2 hash of a fixed string, computed once.
41var dummyHash = sync.OnceValue(func() string {
42 h, err := HashPassword("hearthforge-unknown-user")
43 if err != nil {
44 return ""
45 }
46 return h
47})
48
49// VerifyDummyPassword does the same argon2 work as VerifyPassword and always
50// fails. Call it when the account does not exist, so a login for an unknown
51// username takes about as long as one for a known one.
52func VerifyDummyPassword(pw string) {
53 _, _ = VerifyPassword(dummyHash(), pw)
54}
55
56// VerifyPassword checks a password against a stored PHC hash string.
57func VerifyPassword(hash, pw string) (bool, error) {
58 parts := strings.Split(hash, "$")
59 if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" {
60 return false, errBadHash
61 }
62 var version int
63 if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
64 return false, errBadHash
65 }
66 if version != argon2.Version {
67 return false, fmt.Errorf("%w: unsupported version %d", errBadHash, version)
68 }
69 var memory, time uint32
70 var threads uint8
71 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil {
72 return false, errBadHash
73 }
74 b64 := base64.RawStdEncoding
75 salt, err := b64.DecodeString(parts[4])
76 if err != nil {
77 return false, errBadHash
78 }
79 want, err := b64.DecodeString(parts[5])
80 if err != nil {
81 return false, errBadHash
82 }
83 got := argon2.IDKey([]byte(pw), salt, time, memory, threads, uint32(len(want)))
84 return subtle.ConstantTimeCompare(got, want) == 1, nil
85}
86