.env 中 JWT_SECRET 仍逐字节等于 .env.example 的示例值,且 DATA_ENCRYPTION_KEY 未设置、按 config.go 的回退逻辑实际等于 JWT_SECRET。任何见过仓库的人都能伪造 管理员 JWT,并解密库中全部 API Client / 回调密钥密文。 - JWT_SECRET 与 DATA_ENCRYPTION_KEY 改为各自独立的强随机值 - 新增 Config.SecurityWarnings():示例密钥 / 未分离 / release 下开调试日志均告警 - 新增 cmd/reencrypt_secrets:默认只预演,-apply 才写库;幂等,坏数据不中断整轮 - 新增 docs/密钥轮换说明.md;.gitignore 忽略 .env.bak* - release 模式下关闭 OPEN_API_DEBUG
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
// Command reencrypt_secrets 在轮换 DATA_ENCRYPTION_KEY 时,
|
|
// 把存量以密文落库的 API Client / 回调密钥从旧密钥重新加密为新密钥。
|
|
//
|
|
// 典型用法:
|
|
//
|
|
// OLD_DATA_ENCRYPTION_KEY=<旧密钥> go run ./cmd/reencrypt_secrets # 预演
|
|
// OLD_DATA_ENCRYPTION_KEY=<旧密钥> go run ./cmd/reencrypt_secrets -apply # 真正写库
|
|
//
|
|
// 详见 docs/密钥轮换说明.md
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"affiliate_dash/internal/config"
|
|
"affiliate_dash/internal/service"
|
|
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
func main() {
|
|
apply := flag.Bool("apply", false, "真正写库;不加该参数时只做预演(dry-run),不修改任何数据")
|
|
flag.Parse()
|
|
|
|
cfg := config.Load()
|
|
|
|
newKey := cfg.DataEncryptionKey
|
|
// 兼容两种历史配置:显式配置过 DATA_ENCRYPTION_KEY,或曾经回退到 JWT_SECRET。
|
|
oldKey := os.Getenv("OLD_DATA_ENCRYPTION_KEY")
|
|
if oldKey == "" {
|
|
oldKey = os.Getenv("OLD_JWT_SECRET")
|
|
}
|
|
|
|
if oldKey == "" {
|
|
log.Fatal("缺少旧密钥:请用 OLD_DATA_ENCRYPTION_KEY(或旧 JWT_SECRET)环境变量提供轮换前的密钥")
|
|
}
|
|
if oldKey == newKey {
|
|
log.Fatal("新旧密钥相同,无需轮换")
|
|
}
|
|
|
|
oldCodec, err := service.NewSecretCodec(oldKey)
|
|
if err != nil {
|
|
log.Fatalf("初始化旧密钥编解码器失败: %v", err)
|
|
}
|
|
newCodec, err := service.NewSecretCodec(newKey)
|
|
if err != nil {
|
|
log.Fatalf("初始化新密钥编解码器失败: %v", err)
|
|
}
|
|
|
|
db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("连接数据库失败: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
log.Fatalf("获取数据库连接失败: %v", err)
|
|
}
|
|
defer sqlDB.Close()
|
|
|
|
mode := "预演(dry-run)"
|
|
if *apply {
|
|
mode = "写入(apply)"
|
|
}
|
|
fmt.Printf("密钥轮换模式: %s\n\n", mode)
|
|
|
|
results, err := service.ReencryptSecrets(db, oldCodec, newCodec, *apply)
|
|
if err != nil {
|
|
log.Fatalf("重加密失败: %v", err)
|
|
}
|
|
|
|
for _, r := range results {
|
|
fmt.Printf("%-24s 扫描=%d 重加密=%d 已是新密钥=%d 失败=%d\n",
|
|
r.Table, r.Report.Scanned, r.Report.Reencrypted, r.Report.AlreadyNewKey, r.Report.Failed)
|
|
}
|
|
|
|
if failed := service.TotalFailed(results); failed > 0 {
|
|
log.Fatalf("有 %d 行新旧密钥都无法解密,请先排查数据来源,未做任何破坏性操作", failed)
|
|
}
|
|
if !*apply {
|
|
fmt.Println("\n预演完成,未修改任何数据。确认无误后加 -apply 执行写入。")
|
|
return
|
|
}
|
|
fmt.Println("\n重加密完成。请确认业务正常后再移除旧密钥。")
|
|
}
|