Files
user-system/internal/auth/password.go

161 lines
4.0 KiB
Go
Raw Permalink 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 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
}