feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
160
internal/auth/password.go
Normal file
160
internal/auth/password.go
Normal file
@@ -0,0 +1,160 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user