-
Notifications
You must be signed in to change notification settings - Fork 7
/
password.go
332 lines (277 loc) · 9.25 KB
/
password.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// Package argonpass provides passphrase hashing and hash verification using the Argon2
// password hashing method.
//
// The default Argon2 function is ```Argon2id```, which is a hybrid version of Argon2 combining
// Argon2i and Argon2d. Argon2id is side-channel resistant and provides better brute- force cost
// savings due to time-memory tradeoffs than Argon2i, but Argon2i is still plenty secure.
//
// The string input/output format was designed to be compatible with
// [Passlib for Python](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.argon2.html) and
// [Argon2 PHP](https://wiki.php.net/rfc/argon2_password_hash), and you should have full compatibility
// using the ```argon2i``` function, but will not be able to use ```argon2id```, which is the default
// for this package until those libraries are updated to support it. I encourage you to find the parameters
// that work best for your application, but the defaults are resonable for an interactive use
// such as a web application login.
package argonpass
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"regexp"
"strconv"
"strings"
"golang.org/x/crypto/argon2"
)
// ArgonVariant describes an Argon2 hashing variant.
type ArgonVariant string
const (
// ArgonVariant2i describe the Argon2i variant.
ArgonVariant2i ArgonVariant = "argon2i"
// ArgonVariant2id describe the Argon2id variant.
ArgonVariant2id ArgonVariant = "argon2id"
)
// Security levels from bits.
const (
Sec128b = 128 >> 3 // Until year 2022
Sec192b = 192 >> 3
Sec224b = 224 >> 3
Sec256b = 256 >> 3
Sec384b = 384 >> 3
Sec512b = 512 >> 3
)
const (
currentVersion = argon2.Version
minPassLength = 8
minSaltSize = Sec128b
maxSaltSize = Sec512b
minTime = 1
minMemory = 1 << 10
minParallelism = 1
minOutputSize = Sec128b // minimum Argon2 digest output size only
maxOutputSize = Sec512b
maxParallelism = 64
)
const (
// DefaultMemory ...
DefaultMemory = 64 * 1024
// DefaultParallelism ...
DefaultParallelism = 4
// DefaultOutputSize ...
DefaultOutputSize = Sec128b
// DefaultFunction ...
DefaultFunction = ArgonVariant2id
// DefaultSaltSize ...
DefaultSaltSize = Sec128b
// DefaultTime ...
DefaultTime = 1
)
// NewDefaultParams returns the parameters used by default.
func NewDefaultParams() *ArgonParams {
return &ArgonParams{
Time: DefaultTime,
Memory: DefaultMemory,
Parallelism: DefaultParallelism,
OutputSize: DefaultOutputSize,
Function: DefaultFunction,
SaltSize: DefaultSaltSize,
}
}
// hashFormatRegExpCompiled is used to verify hash string format.
var hashFormatRegExpCompiled = regexp.MustCompile(`[$]argon2(?:id|i)[$]v=\d{1,3}[$]m=\d{3,20},t=\d{1,4},p=\d{1,2}[$][^$]{1,100}[$][^$]{1,768}`)
// ArgonParams control how the Argon2 function creates the digest output.
type ArgonParams struct {
Function ArgonVariant
Time uint32
Memory uint32
OutputSize uint32
Parallelism uint8
SaltSize uint8
}
// Hash generates a argon2id hash of the input pass string and returns the
// output in the specified string format and error value.
// Whether 'ArgonParams' is null, then it used default settings.
func Hash(pass string, customParams *ArgonParams) (string, error) {
// Check input pass length
if len(pass) < minPassLength {
return "", ErrPassphraseInputTooShort
}
// Check for custom params, if not use default.
params := new(ArgonParams) // nolint:staticcheck // false positive
if customParams != nil {
params = checkParams(customParams)
} else {
params = NewDefaultParams()
}
// Generate random salt
salt, err := generateSalt(params.SaltSize)
if err != nil {
return "", err
}
// Generate hash
passHash, err := generateHash([]byte(pass), salt, params)
if err != nil {
return "", err
}
// Encode hash to base64
encodedHash := base64.StdEncoding.EncodeToString(passHash)
// Encode salt to base64
encodedSalt := base64.StdEncoding.EncodeToString(salt)
// Format output string
// $argon2{function(i or id)}$v={version}$m={memory},t={time},p={parallelism}${salt(base64)}${digest(base64)}
// example: $argon2id$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
hashOut := generateOutputString(params.Function, currentVersion, params.Memory, params.Time, params.Parallelism, encodedSalt, encodedHash)
return hashOut, nil
}
// Verify regenerates the hash using the supplied pass and compares the value returning an error if the password
// is invalid or another error occurs. Any error should be considered a validation failure.
func Verify(pass, hash string) error {
// Get Parameters
hashParams, err := GetParams(hash)
if err != nil {
return err
}
// Split hash into parts
part := strings.Split(hash, "$")
// Get & Check Version
hashVersion, err := strconv.Atoi(strings.Trim(part[2], "v="))
if err != nil {
return err
}
// Verify version is not greater than current version or less than 0
if hashVersion > currentVersion || hashVersion < 0 {
return ErrVersion
}
// Get salt
salt, err := base64.StdEncoding.DecodeString(part[4])
if err != nil {
return ErrDecodingSalt
}
// Get argon digest
decodedHash, err := base64.StdEncoding.DecodeString(part[5])
if err != nil {
return ErrDecodingDigest
}
// Generate hash for comparison using user input with stored parameters
comparisonHash, err := generateHash([]byte(pass), salt, hashParams)
if err != nil {
return fmt.Errorf("unable to generate hash for comparison using inputs, error: %w", err)
}
// Compare given hash input to generated hash
if res := subtle.ConstantTimeCompare(decodedHash, comparisonHash); res == 1 {
// return nil only if supplied hash and computed hash from passphrase match
return nil
}
return ErrHashMismatch
}
// GetParams takes hash sting as input and returns parameters as ArgonParams along with error.
func GetParams(hash string) (hashParams *ArgonParams, err error) {
// Check valid input
if err = checkHashFormat(hash); err != nil {
return
}
// Split hash into parts
part := strings.Split(hash, "$")
// Get Parameters
hashParams, err = parseParams(part[3])
if err != nil {
return
}
// Get hash function
hashParams.Function = ArgonVariant(part[1])
// Get salt size
salt, err := base64.StdEncoding.DecodeString(part[4])
if err != nil {
return hashParams, ErrDecodingSalt
}
hashParams.SaltSize = uint8(len(salt))
// Get argon digest size
decodedHash, err := base64.StdEncoding.DecodeString(part[5])
if err != nil {
return hashParams, ErrDecodingDigest
}
hashParams.OutputSize = uint32(len(decodedHash))
return hashParams, nil
}
// checkHashFormat uses regex to validate hash string pattern and returns error.
func checkHashFormat(hash string) error {
// Check valid input
if !hashFormatRegExpCompiled.MatchString(hash) {
return ErrInvalidHashFormat
}
return nil
}
// generateSalt uses int input to return a random a salt for use in crypto operations.
func generateSalt(saltLen uint8) ([]byte, error) {
salt := make([]byte, saltLen)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
return salt, nil
}
// generateHash takes passphrase and salt as bytes with parameters to provide Argon2 digest output.
func generateHash(pass, salt []byte, params *ArgonParams) ([]byte, error) {
switch params.Function { // nolint:exhaustive // false positive
case ArgonVariant2i:
return argon2.Key(pass, salt, params.Time, params.Memory, params.Parallelism, params.OutputSize), nil
case ArgonVariant2id:
return argon2.IDKey(pass, salt, params.Time, params.Memory, params.Parallelism, params.OutputSize), nil
default:
return nil, ErrFunctionMismatch
}
}
func generateOutputString(argonVariant ArgonVariant, version int, memory, time uint32, parallelism uint8, salt, hash string) string {
return fmt.Sprintf("$%s$v=%v$m=%v,t=%v,p=%v$%s$%s", argonVariant, version, memory, time, parallelism, salt, hash)
}
// parseParams takes parameters from a slice of hash string and returns ArgonParam.
func parseParams(inputParams string) (out *ArgonParams, err error) {
// expected format: m=65536,t=2,p=4
part := strings.Split(inputParams, ",")
mem, err := strconv.ParseUint(strings.TrimPrefix(part[0], "m="), 10, 32)
if err != nil {
return out, ErrParseMemory
}
timeCost, err := strconv.ParseUint(strings.TrimPrefix(part[1], "t="), 10, 32)
if err != nil {
return out, ErrParseTime
}
parallelism, err := strconv.ParseUint(strings.TrimPrefix(part[2], "p="), 10, 8)
if err != nil {
return out, ErrParseParallelism
}
return &ArgonParams{
Memory: uint32(mem),
Time: uint32(timeCost),
Parallelism: uint8(parallelism),
}, nil
}
// checkParams verifies that parameters fall within min and max allowed values.
func checkParams(params *ArgonParams) *ArgonParams {
// Enforce Minimum Params
if params.SaltSize < minSaltSize {
params.SaltSize = minSaltSize
}
if params.Time < minTime {
params.Time = minTime
}
if params.Memory < minMemory {
params.Memory = minMemory
}
if params.Parallelism < minParallelism {
params.Parallelism = minParallelism
}
if params.OutputSize < minOutputSize {
params.OutputSize = minOutputSize
}
// Enforce Max Params
if params.SaltSize > maxSaltSize {
params.SaltSize = maxSaltSize
}
if params.OutputSize > maxOutputSize {
params.OutputSize = maxOutputSize
}
if params.Parallelism > maxParallelism {
params.Parallelism = maxParallelism
}
return params
}