Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
// Package settings 密码哈希与校验:
|
||
// 新密码使用 bcrypt(格式 bcrypt$<bcrypt原生哈希串>);同时兼容两代旧格式——
|
||
// sha256$salt$hash(上一版)与旧版明文(迁移校验)。
|
||
// 安全审计 M1:单轮 SHA256+盐抗 GPU 爆破不足,新哈希统一升级 bcrypt。
|
||
// 兼容策略:VerifyPassword 支持全部三代格式;调用方可用 NeedsRehash 判定
|
||
// 登录成功后是否需要用新算法重哈希写回(登录升级路径见 api.adminLogin)。
|
||
package settings
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
// bcryptCost bcrypt 工作因子:12(2026 年桌面 CPU 单次校验约 100-250ms,
|
||
// 离线爆破成本相比单轮 SHA256 提升数个数量级)。
|
||
const bcryptCost = 12
|
||
|
||
// bcryptMaxLen bcrypt 算法只取前 72 字节;超长输入统一截断,
|
||
// 避免 GenerateFromPassword/CompareHashAndPassword 对 >72 字节返回错误。
|
||
const bcryptMaxLen = 72
|
||
|
||
func bcryptBytes(password string) []byte {
|
||
b := []byte(password)
|
||
if len(b) > bcryptMaxLen {
|
||
b = b[:bcryptMaxLen]
|
||
}
|
||
return b
|
||
}
|
||
|
||
// HashPassword 生成 bcrypt$<hash> 格式密码哈希(<hash> 为 bcrypt 原生串
|
||
// `$2a$<cost>$<salt><hash>`,cost 内嵌于哈希串中)。
|
||
func HashPassword(password string) string {
|
||
sum, err := bcrypt.GenerateFromPassword(bcryptBytes(password), bcryptCost)
|
||
if err != nil {
|
||
// 截断后仅剩非法 cost 等实现级错误:确定性失败优于弱哈希回落
|
||
panic("settings: bcrypt 哈希失败: " + err.Error())
|
||
}
|
||
return "bcrypt$" + string(sum)
|
||
}
|
||
|
||
// VerifyPassword 校验密码:支持 bcrypt$、sha256$salt$hash 与旧版明文三种格式。
|
||
func VerifyPassword(password, hashed string) bool {
|
||
if hashed == "" {
|
||
return false
|
||
}
|
||
switch {
|
||
case strings.HasPrefix(hashed, "bcrypt$"):
|
||
return bcrypt.CompareHashAndPassword([]byte(hashed[len("bcrypt$"):]), bcryptBytes(password)) == nil
|
||
case strings.HasPrefix(hashed, "sha256$"):
|
||
parts := strings.Split(hashed, "$")
|
||
if len(parts) != 3 {
|
||
return false
|
||
}
|
||
salt, stored := parts[1], parts[2]
|
||
sum := sha256.Sum256([]byte(salt + password))
|
||
return hmac.Equal([]byte(hex.EncodeToString(sum[:])), []byte(stored))
|
||
}
|
||
// 旧版明文比较(兼容迁移)
|
||
return hmac.Equal([]byte(password), []byte(hashed))
|
||
}
|
||
|
||
// NeedsRehash 判断哈希是否需要升级为当前算法/成本(登录成功后判定,透明迁移)。
|
||
// sha256 与明文一律 true;bcrypt 成本低于当前 bcryptCost 时 true。
|
||
func NeedsRehash(hashed string) bool {
|
||
if !strings.HasPrefix(hashed, "bcrypt$") {
|
||
return true
|
||
}
|
||
// bcrypt 原生串格式:$2a$<cost>$<salt><hash>
|
||
parts := strings.Split(hashed[len("bcrypt$"):], "$")
|
||
if len(parts) < 4 {
|
||
return true
|
||
}
|
||
cost, err := strconv.Atoi(parts[2])
|
||
if err != nil {
|
||
return true
|
||
}
|
||
return cost < bcryptCost
|
||
}
|
||
|
||
// IsPasswordHashed 判断是否为受支持的哈希格式(bcrypt / sha256)。
|
||
func IsPasswordHashed(s string) bool {
|
||
return strings.HasPrefix(s, "bcrypt$") || strings.HasPrefix(s, "sha256$")
|
||
}
|
||
|
||
// GenerateJWTSecret 生成 64 字符十六进制随机密钥。
|
||
func GenerateJWTSecret() string {
|
||
b := make([]byte, 32)
|
||
if _, err := rand.Read(b); err != nil {
|
||
panic("settings: crypto/rand 不可用: " + err.Error())
|
||
}
|
||
return hex.EncodeToString(b)
|
||
}
|