后端基础层:模块定义、配置解析、SQLite 存储层、Argon2id 认证与 Markdown 渲染

- 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 同源校验
This commit is contained in:
2026-09-08 08:14:12 +08:00
parent 685f628e26
commit 247e88c4fb
17 changed files with 1786 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
// 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[:])
}