150 lines
4.4 KiB
Go
150 lines
4.4 KiB
Go
package passwordutil
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"crypto/subtle"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"golang.org/x/crypto/argon2"
|
||
)
|
||
|
||
// 算法标识。
|
||
// 历史数据用 legacy(sha256 单轮,无迭代拉伸,抗 GPU 爆破能力弱);
|
||
// 新密码统一用 argon2id,登录成功检测到 legacy 时应重新哈希升级。
|
||
const (
|
||
AlgoArgon2id = "argon2id"
|
||
AlgoLegacy = "legacy"
|
||
)
|
||
|
||
// argon2id 参数(OWASP 推荐起步配置:64MB / 3 轮 / 并行 2)
|
||
const (
|
||
argonTime = 3
|
||
argonMemory = 64 * 1024 // 单位 KB,即 64MB
|
||
argonThreads = 2
|
||
argonKeyLen = 32
|
||
argonSaltLen = 16
|
||
)
|
||
|
||
// legacy 兼容参数
|
||
const (
|
||
saltBytes = 16
|
||
separator = "$"
|
||
hashLength = 64 // sha256 hex length
|
||
)
|
||
|
||
// Hash 使用当前默认算法(argon2id)生成密码存储串。
|
||
//
|
||
// 返回 PHC 标准格式(盐与参数内联,无需独立 salt 列):
|
||
//
|
||
// $argon2id$v=19$m=65536,t=3,p=2$<base64(salt)>$<base64(hash)>
|
||
//
|
||
// 注意:函数签名与旧版一致,全部调用点无需改动即可切换到新算法。
|
||
func Hash(plain string) (string, error) {
|
||
plain = strings.TrimSpace(plain)
|
||
if plain == "" {
|
||
return "", errors.New("password 不能为空")
|
||
}
|
||
salt := make([]byte, argonSaltLen)
|
||
if _, err := rand.Read(salt); err != nil {
|
||
return "", err
|
||
}
|
||
key := argon2.IDKey([]byte(plain), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
|
||
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||
argon2.Version,
|
||
argonMemory, argonTime, argonThreads,
|
||
base64.RawStdEncoding.EncodeToString(salt),
|
||
base64.RawStdEncoding.EncodeToString(key),
|
||
), nil
|
||
}
|
||
|
||
// Verify 校验明文密码是否匹配存储串,自动识别算法:
|
||
// - $argon2id$... → argon2id(新)
|
||
// - <salt>$<hash> → legacy sha256(旧,兼容)
|
||
func Verify(stored, plain string) bool {
|
||
stored = strings.TrimSpace(stored)
|
||
plain = strings.TrimSpace(plain)
|
||
if stored == "" || plain == "" {
|
||
return false
|
||
}
|
||
if strings.HasPrefix(stored, "$argon2id$") {
|
||
return verifyArgon2id(stored, plain)
|
||
}
|
||
return verifyLegacy(stored, plain)
|
||
}
|
||
|
||
// NeedsRehash 判断已存储的密码是否需要用当前算法重新哈希。
|
||
// 登录成功后调用:返回 true 时应拿当次登录的明文重新 Hash 并落库,
|
||
// 实现旧算法用户「首次登录自动升级」,无需强制全员重置密码。
|
||
func NeedsRehash(stored string) bool {
|
||
return !strings.HasPrefix(strings.TrimSpace(stored), "$argon2id$")
|
||
}
|
||
|
||
// AlgoOf 返回存储串使用的算法标识,用于审计/统计旧算法存量。
|
||
func AlgoOf(stored string) string {
|
||
if strings.HasPrefix(strings.TrimSpace(stored), "$argon2id$") {
|
||
return AlgoArgon2id
|
||
}
|
||
return AlgoLegacy
|
||
}
|
||
|
||
// verifyArgon2id 解析 PHC 串并用相同参数重算比对(恒定时间比较)。
|
||
func verifyArgon2id(stored, plain string) bool {
|
||
parts := strings.Split(stored, "$")
|
||
// ["", "argon2id", "v=19", "m=...,t=...,p=...", salt, hash]
|
||
if len(parts) != 6 {
|
||
return false
|
||
}
|
||
var memory, iterations, parallelism uint32
|
||
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, ¶llelism); err != nil {
|
||
return false
|
||
}
|
||
if memory == 0 || iterations == 0 || parallelism == 0 || parallelism > 255 {
|
||
return false
|
||
}
|
||
threads := uint8(parallelism)
|
||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||
if err != nil || len(salt) == 0 {
|
||
return false
|
||
}
|
||
want, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||
if err != nil || len(want) == 0 {
|
||
return false
|
||
}
|
||
got := argon2.IDKey([]byte(plain), salt, iterations, memory, threads, uint32(len(want)))
|
||
return subtle.ConstantTimeCompare(got, want) == 1
|
||
}
|
||
|
||
// verifyLegacy 校验旧格式:salt$hash(均为 hex),sha256(salt+plain)。
|
||
// 仅用于兼容历史数据,不再用于新密码。
|
||
func verifyLegacy(stored, plain string) bool {
|
||
stored = strings.TrimSpace(stored)
|
||
plain = strings.TrimSpace(plain)
|
||
if stored == "" || plain == "" {
|
||
return false
|
||
}
|
||
parts := strings.Split(stored, separator)
|
||
if len(parts) != 2 {
|
||
return false
|
||
}
|
||
saltHex := strings.TrimSpace(parts[0])
|
||
hashHexStored := strings.TrimSpace(parts[1])
|
||
if saltHex == "" || len(hashHexStored) != hashLength {
|
||
return false
|
||
}
|
||
if _, err := hex.DecodeString(saltHex); err != nil {
|
||
return false
|
||
}
|
||
return hashHex(saltHex, plain) == strings.ToLower(hashHexStored)
|
||
}
|
||
|
||
// hashHex 旧算法:sha256(saltHex + plain),供 verifyLegacy 使用。
|
||
func hashHex(saltHex, plain string) string {
|
||
sum := sha256.Sum256([]byte(saltHex + plain))
|
||
return hex.EncodeToString(sum[:])
|
||
}
|