Files
yunzerwebsiteallinone/go/pkg/passwordutil/password.go
T
2026-09-19 21:44:04 +08:00

150 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, &parallelism); 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[:])
}