功能说明
口令存储不能使用普通哈希,因为攻击者可以用显卡高速穷举。Argon2id 是口令哈希竞赛的推荐算法,通过可调的内存、时间和并行度抬高单次尝试成本,同时抵抗侧信道和时间内存权衡攻击。每次哈希使用独立的随机盐,使相同口令得到不同结果。
输出采用 PHC 字符串格式,包含算法、版本、参数、盐和哈希,全部信息自描述,校验时不需要额外查询数据库字段。示例哈希一段口令并进行正确与错误两次校验。
关键代码
HashPassword 用 crypto/rand 生成十六字节盐,调用 argon2.IDKey 得到三十二字节派生密钥,再用 RawStdEncoding 编码,避免填充字符出现在格式串中。VerifyPassword 按美元符号切分,校验算法标识,解析内存、时间与并行参数,然后使用同样的参数和盐重新派生,最后用 subtle.ConstantTimeCompare 比较,防止通过比较耗时推测哈希内容。
使用注意
示例参数为六十四兆内存、一次迭代、四线程,需要按服务器规格调整,目标是单次哈希耗时约一百到五百毫秒。参数会写入哈希串,升级参数后旧哈希仍可校验,登录成功时可重新哈希并更新。校验失败与格式错误要区分日志,但对外统一返回失败。口令长度应设上限,避免超长输入消耗内存。盐和哈希都不能记录到日志。
验证方式
哈希串应以 argon2id 标识开头,打印前缀可以看到格式。正确口令校验返回 true,错误口令返回 false,两次调用 err 均为 nil。由于盐随机,同一个口令连续哈希两次得到的字符串不同,但都能通过校验;把哈希串中的参数或哈希字符改掉,校验应返回 false 或格式错误。
代码示例
package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
argonTime = 1
argonMemory = 64 * 1024
argonThreads = 4
argonKeyLen = 32
argonSaltLen = 16
)
func HashPassword(password string) (string, error) {
salt := make([]byte, argonSaltLen)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("read salt: %w", err)
}
key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
return fmt.Sprintf(
"$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
argonMemory,
argonTime,
argonThreads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(key),
), nil
}
func VerifyPassword(password, encoded string) (bool, error) {
parts := strings.Split(encoded, "$")
if len(parts) != 6 || parts[1] != "argon2id" {
return false, fmt.Errorf("unsupported hash format")
}
var memory uint32
var timeCost uint32
var threads uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &timeCost, &threads); err != nil {
return false, fmt.Errorf("parse parameters: %w", err)
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, fmt.Errorf("decode salt: %w", err)
}
want, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, fmt.Errorf("decode hash: %w", err)
}
got := argon2.IDKey([]byte(password), salt, timeCost, memory, threads, uint32(len(want)))
return subtle.ConstantTimeCompare(got, want) == 1, nil
}
func main() {
encoded, err := HashPassword("correct horse battery staple")
if err != nil {
panic(err)
}
fmt.Println("encoded prefix:", encoded[:10])
match, err := VerifyPassword("correct horse battery staple", encoded)
if err != nil {
panic(err)
}
reject, err := VerifyPassword("wrong password", encoded)
if err != nil {
panic(err)
}
fmt.Println("match:", match, "reject:", reject)
}