1package password
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/subtle"
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "runtime"
11 "strconv"
12 "strings"
13 "sync"
14
15 "golang.org/x/crypto/argon2"
16)
17
18var (
19 ErrMalformed = errors.New("password: malformed stored hash")
20 ErrBusy = errors.New("password: verifier busy")
21)
22
23var errEmptyPassword = errors.New("password: empty password refused")
24
25const (
26 defaultMemory = 19456
27 defaultTime = 2
28 defaultThreads = 1
29 keyLength = 32
30 saltLength = 16
31
32 minMemory = 8192
33 maxMemory = 65536
34 minTime = 1
35 maxTime = 16
36 minThreads = 1
37 maxThreads = 16
38 minBytes = 16
39 maxBytes = 64
40 maxDigits = 7
41)
42
43const variantField = "argon2id"
44
45var versionField = "v=" + strconv.Itoa(argon2.Version)
46
47var b64 = base64.RawStdEncoding.Strict()
48
49var kdf = argon2.IDKey
50
51var sem = make(chan struct{}, semaphoreSize(runtime.GOMAXPROCS(0)))
52
53var dummy = sync.OnceValue(newDummy)
54
55type phc struct {
56 memory uint32
57 time uint32
58 threads uint8
59 salt []byte
60 key []byte
61}
62
63func semaphoreSize(gomaxprocs int) int {
64 return min(gomaxprocs, 3)
65}
66
67func Hash(password string) ([]byte, error) {
68 if password == "" {
69 return nil, errEmptyPassword
70 }
71 salt := make([]byte, saltLength)
72 rand.Read(salt)
73 key := kdf([]byte(password), salt, defaultTime, defaultMemory, defaultThreads, keyLength)
74 return encode(phc{memory: defaultMemory, time: defaultTime, threads: defaultThreads, salt: salt, key: key}), nil
75}
76
77func HashContext(ctx context.Context, password string) ([]byte, error) {
78 if password == "" {
79 return nil, errEmptyPassword
80 }
81 if err := acquire(ctx); err != nil {
82 return nil, err
83 }
84 defer release()
85 return Hash(password)
86}
87
88func Verify(ctx context.Context, stored []byte, password string) (bool, error) {
89 h, ok := decode(stored)
90 if !ok {
91 return false, ErrMalformed
92 }
93 if err := acquire(ctx); err != nil {
94 return false, err
95 }
96 defer release()
97 got := kdf([]byte(password), h.salt, h.time, h.memory, h.threads, uint32(len(h.key)))
98 return subtle.ConstantTimeCompare(got, h.key) == 1, nil
99}
100
101func VerifyAbsent(ctx context.Context, password string) error {
102 _, err := Verify(ctx, dummy(), password)
103 return err
104}
105
106func Unusable() []byte {
107 return newDummy()
108}
109
110func newDummy() []byte {
111 salt := make([]byte, saltLength)
112 key := make([]byte, keyLength)
113 rand.Read(salt)
114 rand.Read(key)
115 return encode(phc{memory: defaultMemory, time: defaultTime, threads: defaultThreads, salt: salt, key: key})
116}
117
118func acquire(ctx context.Context) error {
119 if ctx.Err() != nil {
120 busy.Add(1)
121 return ErrBusy
122 }
123 select {
124 case sem <- struct{}{}:
125 return nil
126 default:
127 }
128 waits.Add(1)
129 select {
130 case sem <- struct{}{}:
131 return nil
132 case <-ctx.Done():
133 busy.Add(1)
134 return ErrBusy
135 }
136}
137
138func release() {
139 <-sem
140}
141
142func encode(h phc) []byte {
143 return fmt.Appendf(nil, "$%s$%s$m=%d,t=%d,p=%d$%s$%s", variantField, versionField,
144 h.memory, h.time, h.threads, b64.EncodeToString(h.salt), b64.EncodeToString(h.key))
145}
146
147func decode(stored []byte) (phc, bool) {
148 fields := strings.Split(string(stored), "$")
149 if len(fields) != 6 || fields[0] != "" || fields[1] != variantField || fields[2] != versionField {
150 return phc{}, false
151 }
152 params := strings.Split(fields[3], ",")
153 if len(params) != 3 {
154 return phc{}, false
155 }
156 m, okM := parseParam(params[0], "m=")
157 t, okT := parseParam(params[1], "t=")
158 p, okP := parseParam(params[2], "p=")
159 if !okM || !okT || !okP {
160 return phc{}, false
161 }
162 if m < minMemory || m > maxMemory || t < minTime || t > maxTime || p < minThreads || p > maxThreads {
163 return phc{}, false
164 }
165 salt, okS := decodeSegment(fields[4])
166 key, okK := decodeSegment(fields[5])
167 if !okS || !okK {
168 return phc{}, false
169 }
170 return phc{memory: uint32(m), time: uint32(t), threads: uint8(p), salt: salt, key: key}, true
171}
172
173func parseParam(field, prefix string) (uint64, bool) {
174 digits, ok := strings.CutPrefix(field, prefix)
175 if !ok || digits == "" || len(digits) > maxDigits || (len(digits) > 1 && digits[0] == '0') {
176 return 0, false
177 }
178 for i := 0; i < len(digits); i++ {
179 if digits[i] < '0' || digits[i] > '9' {
180 return 0, false
181 }
182 }
183 v, err := strconv.ParseUint(digits, 10, 32)
184 return v, err == nil
185}
186
187func decodeSegment(segment string) ([]byte, bool) {
188 raw, err := b64.DecodeString(segment)
189 if err != nil || len(raw) < minBytes || len(raw) > maxBytes || b64.EncodeToString(raw) != segment {
190 return nil, false
191 }
192 return raw, true
193}