Files
pure-note/internal/auth/auth.go
T
wangairnan ba7d5dd01f fix(auth): VerifyPassword 校验 PHC 参数上下界
t/p=0 会使 argon2.IDKey panic、m 超大可 OOM;越界一律返回 false(round2 P2-8),补畸形参数用例。
2026-09-08 17:32:52 +08:00

109 lines
3.2 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 提供 Argon2id 口令哈希(PHC 串)、随机 token 与 token 摘要。
package auth
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
// OWASP 现行推荐 Argon2id 参数:m=19MiB、t=2、p=1(设计 §3.1/§7.3)。
const (
ArgonTime = 2
ArgonMemory = 19456
ArgonThreads = 1
SaltLen = 16
KeyLen = 32
// MinPasswordLength 新口令最小长度(§7.1 改密)。
MinPasswordLength = 12
)
// HashPassword 生成 PHC 格式串:
// $argon2id$v=19$m=19456,t=2,p=1$<b64salt>$<b64hash>(参数随哈希走)。
func HashPassword(password string) (string, error) {
salt := make([]byte, SaltLen)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("生成盐失败: %w", err)
}
key := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, KeyLen)
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
ArgonMemory, ArgonTime, ArgonThreads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(key),
), nil
}
// PHC 参数上下界(防畸形哈希触发 argon2 panic 或 OOM,评审 round2 P2-8):
// t/p 过小使 argon2.IDKey panic,m 过大直接耗尽内存。
const (
maxArgonTime = 10
maxArgonThreads = 8
maxArgonMemory = 1 << 20 // KiB(1 GiB),远大于现行 19456
)
// VerifyPassword 按 PHC 串内参数重派生并常量时间比较。任何解析失败或参数越界均返回 false。
func VerifyPassword(encoded, password string) bool {
parts := strings.Split(encoded, "$")
// ["", "argon2id", "v=19", "m=..,t=..,p=..", salt, hash]
if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" {
return false
}
var version int
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil || version != argon2.Version {
return false
}
var m uint32
var t uint32
var p uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p); err != nil {
return false
}
if t < 1 || t > maxArgonTime || p < 1 || p > maxArgonThreads ||
m < 8 || m > maxArgonMemory {
return false
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false
}
want, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil || len(want) != KeyLen {
return false
}
got := argon2.IDKey([]byte(password), salt, t, m, p, uint32(len(want)))
return subtle.ConstantTimeCompare(got, want) == 1
}
// ErrWeakPassword 新口令不满足强度要求。
var ErrWeakPassword = errors.New("密码长度至少 12 个字符")
// CheckPasswordStrength 校验新口令强度。
func CheckPasswordStrength(password string) error {
if len(password) < MinPasswordLength {
return ErrWeakPassword
}
return nil
}
// NewToken 生成 256bit 随机 token(base64url,43 字符)。
func NewToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// HashToken token 明文的 SHA-256 十六进制摘要(库中只存摘要)。
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}