- internal/config:serve/init/backup/gc 子命令参数解析(--dev 强制 loopback 守卫) - internal/store:user_version 版本化迁移(仅追加式 + 越界拒启 + --allow-newer)、 笔记/图片/引用/会话/设置 DAO、并集可见性查询、VACUUM INTO 在线备份、 回收站 30 天 + 孤儿图 7 天宽限 gc - internal/auth:Argon2id PHC 串(m=19456,t=2,p=1)、256bit token 与 SHA-256 摘要 - internal/markdown:goldmark(默认转义)+ bluemonday 双保险,摘要纯文本提取 - internal/middleware:安全头(CSP/HSTS/nosniff 等)、错误日志、 有界令牌桶限流(per-IP 桶上限 + TTL 逐出)、Origin/Referer 同源校验
97 lines
2.7 KiB
Go
97 lines
2.7 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||
if err != nil {
|
||
return false
|
||
}
|
||
want, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||
if err != nil {
|
||
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[:])
|
||
}
|