109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
// 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[:])
|
||
}
|