// 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$$(参数随哈希走)。 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[:]) }