// Package settings 密码哈希与校验: // 新密码使用 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$ 格式密码哈希( 为 bcrypt 原生串 // `$2a$$`,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$$ 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) }