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
|
package kate
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
// Argon2Config configures Argon2id password hashing parameters.
//
// Leave any fields at their zero value to use the default.
type Argon2Config struct {
// Time is the number of iterations (default: 1)
Time uint32
// Memory is the memory usage in KiB (default: 64*1024 = 64MB)
Memory uint32
// Threads is the number of parallel threads (default: 4)
Threads uint8
// KeyLen is the length of the derived key in bytes (default: 32)
KeyLen uint32
}
var defaultArgon2Config = Argon2Config{
Time: 1,
Memory: 64 * 1024,
Threads: 4,
KeyLen: 32,
}
// HashPassword hashes a password using Argon2id.
//
// Returns a PHC-formatted string containing all parameters needed for verification.
// If config is nil, secure defaults are used.
func HashPassword(password string, config *Argon2Config) (string, error) {
// Use defaults if no config provided
if config == nil {
config = &defaultArgon2Config
}
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("HashPassword: %w", err)
}
// Use defaults for zero values in config
time := config.Time
if time == 0 {
time = defaultArgon2Config.Time
}
memory := config.Memory
if memory == 0 {
memory = defaultArgon2Config.Memory
}
threads := config.Threads
if threads == 0 {
threads = defaultArgon2Config.Threads
}
keyLen := config.KeyLen
if keyLen == 0 {
keyLen = defaultArgon2Config.KeyLen
}
hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen)
return buildPHC(salt, hash, memory, time, threads), nil
}
// ComparePassword verifies a password against a stored hash in PHC format.
//
// Returns (false, nil) for incorrect passwords and (false, error) for malformed hashes.
// Uses constant-time comparison to prevent timing attacks.
func ComparePassword(loginpass, storedhash string) (bool, error) {
salt, hash, memory, time, threads, err := parsePHC(storedhash)
if err != nil {
return false, fmt.Errorf("ComparePassword: %w", err)
}
loginHash := argon2.IDKey([]byte(loginpass), salt, time, memory, threads, uint32(len(hash)))
return subtle.ConstantTimeCompare(loginHash, hash) == 1, nil
}
func buildPHC(salt, hash []byte, memory, time uint32, threads uint8) string {
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, memory, time, threads, b64Salt, b64Hash)
}
func parsePHC(phc string) (salt, hash []byte, memory, time uint32, threads uint8, err error) {
parts := strings.Split(phc, "$")
if len(parts) != 6 {
err = errors.New("invalid PHC format")
return
}
if parts[1] != "argon2id" {
err = errors.New("unsupported algorithm")
return
}
var version int
if _, err = fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
return
}
if version > argon2.Version {
err = errors.New("unsupported argon2 version")
return
}
var p uint32
if _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &p); err != nil {
return
}
threads = uint8(p)
if salt, err = base64.RawStdEncoding.DecodeString(parts[4]); err != nil {
return
}
if hash, err = base64.RawStdEncoding.DecodeString(parts[5]); err != nil {
return
}
return
}
|