161 lines
4.0 KiB
Go
161 lines
4.0 KiB
Go
package auth
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"crypto/subtle"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"golang.org/x/crypto/argon2"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
var defaultPasswordManager = NewPassword()
|
||
|
||
// Password 密码管理器(Argon2id)
|
||
type Password struct {
|
||
memory uint32
|
||
iterations uint32
|
||
parallelism uint8
|
||
saltLength uint32
|
||
keyLength uint32
|
||
}
|
||
|
||
// NewPassword 创建密码管理器
|
||
func NewPassword() *Password {
|
||
return &Password{
|
||
memory: 64 * 1024, // 64MB(符合 OWASP 建议)
|
||
iterations: 5, // 5 次迭代(保守值,高于 OWASP 建议的 3)
|
||
parallelism: 4, // 4 并行(符合 OWASP 建议,防御 GPU 破解)
|
||
saltLength: 16, // 16 字节盐(符合 OWASP 最低要求)
|
||
keyLength: 32, // 32 字节密钥
|
||
}
|
||
}
|
||
|
||
// Hash 哈希密码(使用Argon2id + 随机盐)
|
||
func (p *Password) Hash(password string) (string, error) {
|
||
// 使用 crypto/rand 生成真正随机的盐
|
||
salt := make([]byte, p.saltLength)
|
||
if _, err := rand.Read(salt); err != nil {
|
||
return "", fmt.Errorf("生成随机盐失败: %w", err)
|
||
}
|
||
|
||
// 使用Argon2id哈希密码
|
||
hash := argon2.IDKey(
|
||
[]byte(password),
|
||
salt,
|
||
p.iterations,
|
||
p.memory,
|
||
p.parallelism,
|
||
p.keyLength,
|
||
)
|
||
|
||
// 格式: $argon2id$v=<version>$m=<memory>,t=<iterations>,p=<parallelism>$<salt_hex>$<hash_hex>
|
||
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||
argon2.Version,
|
||
p.memory,
|
||
p.iterations,
|
||
p.parallelism,
|
||
hex.EncodeToString(salt),
|
||
hex.EncodeToString(hash),
|
||
)
|
||
|
||
return encoded, nil
|
||
}
|
||
|
||
// Verify 验证密码
|
||
func (p *Password) Verify(hashedPassword, password string) bool {
|
||
// 支持 bcrypt 格式(兼容旧数据)
|
||
if strings.HasPrefix(hashedPassword, "$2a$") || strings.HasPrefix(hashedPassword, "$2b$") {
|
||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||
return err == nil
|
||
}
|
||
|
||
// 解析 Argon2id 格式
|
||
parts := strings.Split(hashedPassword, "$")
|
||
// 格式: ["", "argon2id", "v=<version>", "m=<mem>,t=<iter>,p=<par>", "<salt_hex>", "<hash_hex>"]
|
||
if len(parts) != 6 || parts[1] != "argon2id" {
|
||
return false
|
||
}
|
||
|
||
// 解析参数
|
||
var memory, iterations uint32
|
||
var parallelism uint8
|
||
params := strings.Split(parts[3], ",")
|
||
if len(params) != 3 {
|
||
return false
|
||
}
|
||
for _, param := range params {
|
||
kv := strings.SplitN(param, "=", 2)
|
||
if len(kv) != 2 {
|
||
return false
|
||
}
|
||
val, err := strconv.ParseUint(kv[1], 10, 64)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
switch kv[0] {
|
||
case "m":
|
||
memory = uint32(val)
|
||
case "t":
|
||
iterations = uint32(val)
|
||
case "p":
|
||
parallelism = uint8(val)
|
||
}
|
||
}
|
||
|
||
// 解码盐和存储的哈希
|
||
salt, err := hex.DecodeString(parts[4])
|
||
if err != nil {
|
||
return false
|
||
}
|
||
storedHash, err := hex.DecodeString(parts[5])
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
// 用相同参数重新计算哈希
|
||
computedHash := argon2.IDKey(
|
||
[]byte(password),
|
||
salt,
|
||
iterations,
|
||
memory,
|
||
parallelism,
|
||
uint32(len(storedHash)),
|
||
)
|
||
|
||
// 常数时间比较,防止时序攻击
|
||
return subtle.ConstantTimeCompare(storedHash, computedHash) == 1
|
||
}
|
||
|
||
// HashPassword hashes passwords with Argon2id for new credentials.
|
||
func HashPassword(password string) (string, error) {
|
||
return defaultPasswordManager.Hash(password)
|
||
}
|
||
|
||
// VerifyPassword verifies both Argon2id and legacy bcrypt password hashes.
|
||
func VerifyPassword(hashedPassword, password string) bool {
|
||
return defaultPasswordManager.Verify(hashedPassword, password)
|
||
}
|
||
|
||
// ErrInvalidPassword 密码无效错误
|
||
var ErrInvalidPassword = errors.New("密码无效")
|
||
|
||
// BcryptHash 使用bcrypt哈希密码(兼容性支持)
|
||
func BcryptHash(password string) (string, error) {
|
||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return "", fmt.Errorf("bcrypt加密失败: %w", err)
|
||
}
|
||
return string(hash), nil
|
||
}
|
||
|
||
// BcryptVerify 使用bcrypt验证密码
|
||
func BcryptVerify(hashedPassword, password string) bool {
|
||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||
return err == nil
|
||
}
|