// Package cli 命令行入口。 package cli import ( "fmt" "log" "os" "github.com/openteam/server/internal/config" "github.com/openteam/server/internal/pkg/crypto" "github.com/openteam/server/internal/store" "github.com/spf13/cobra" "gorm.io/gorm" ) var ( rotateOldKey string rotateNewKey string rotateDryRun bool rotateForce bool ) var rotateMasterKeyCmd = &cobra.Command{ Use: "rotate-master-key", Short: "用旧主密钥解密渠道密钥、再用新主密钥重新加密(主密钥迁移)", Long: `主密钥迁移:把渠道上游 key 从旧主密钥改为用新主密钥加密。 典型场景:实例最初没有配置 OT_MASTER_KEY(用的是 sha256("") 派生的空密钥加密), 后来补上了 OT_MASTER_KEY,于是所有渠道在运行时报 "failed to decrypt channel key"。 运行本命令后重启服务即可恢复,不需要重新录入各渠道的上游 key。 用法: # 旧密钥为空(历史默认 sha256("")),新密钥取当前配置的 OT_MASTER_KEY go run ./cmd/server rotate-master-key # 显式指定旧密钥 go run ./cmd/server rotate-master-key --old "旧主密钥" # 只验证不改库 go run ./cmd/server rotate-master-key --dry-run 新主密钥默认用当前环境/配置里的 OT_MASTER_KEY(必须非空)。全部渠道在同一个事务里 更新:任一渠道用旧密钥解不开就整体回滚(除非 --force 跳过这些渠道),避免库里 出现新旧密钥混杂、下次还得再猜一遍的状态。 注意:命令成功只改了库,跑完必须用新主密钥重启服务!`, Run: func(cmd *cobra.Command, args []string) { runRotateMasterKey() }, } func init() { rootCmd.AddCommand(rotateMasterKeyCmd) rotateMasterKeyCmd.Flags().StringVar(&rotateOldKey, "old", "", "旧主密钥(留空表示历史默认的空密钥,即 sha256(\"\"))") rotateMasterKeyCmd.Flags().StringVar(&rotateNewKey, "new", "", "新主密钥(留空则用配置里的 OT_MASTER_KEY)") rotateMasterKeyCmd.Flags().BoolVar(&rotateDryRun, "dry-run", false, "只验证能否用旧密钥解密,不改库") rotateMasterKeyCmd.Flags().BoolVar(&rotateForce, "force", false, "跳过旧密钥解不开的渠道(默认整体回滚)") } func runRotateMasterKey() { cfg, err := config.Load() if err != nil { log.Fatalf("加载配置失败: %v", err) } newMaster := rotateNewKey if newMaster == "" { newMaster = cfg.Master } if newMaster == "" { log.Fatal("新主密钥为空:请用 --new 指定,或先配置 OT_MASTER_KEY") } oldEnc := crypto.NewEncryptor(rotateOldKey) newEnc := crypto.NewEncryptor(newMaster) if oldEnc.KeyFingerprint() == newEnc.KeyFingerprint() { fmt.Println("旧密钥与新密钥指纹相同(同一把密钥),无需迁移。") return } db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN) if err != nil { log.Fatalf("连接数据库失败: %v", err) } fmt.Printf("数据库: %s (%s)\n", cfg.DB.DSN, cfg.DB.Driver) fmt.Printf("旧密钥: %s,指纹 %s\n", describeOldKey(rotateOldKey), oldEnc.KeyFingerprint()) fmt.Printf("新密钥: %s,指纹 %s\n", describeMaster(newMaster), newEnc.KeyFingerprint()) if rotateDryRun { fmt.Println("模式: dry-run(只验证,不改库)") } fmt.Println() var chs []store.Channel if err := db.Order("id").Find(&chs).Error; err != nil { log.Fatalf("读取渠道失败: %v", err) } if len(chs) == 0 { fmt.Println("没有渠道,无需迁移。") return } type pending struct { id uint64 name string newEncV string } var todo []pending var failed []string for _, ch := range chs { plain, derr := oldEnc.Decrypt(ch.APIKeyEnc) if derr != nil { failed = append(failed, fmt.Sprintf("#%d %s: %v", ch.ID, ch.Name, derr)) continue } re, eerr := newEnc.Encrypt(plain) if eerr != nil { failed = append(failed, fmt.Sprintf("#%d %s: 重新加密失败: %v", ch.ID, ch.Name, eerr)) continue } // 回读校验:写进库之前确认新密文能被新密钥解开,避免把渠道写成谁也解不开的状态 if back, berr := newEnc.Decrypt(re); berr != nil || back != plain { failed = append(failed, fmt.Sprintf("#%d %s: 新密文回读校验失败", ch.ID, ch.Name)) continue } todo = append(todo, pending{id: ch.ID, name: ch.Name, newEncV: re}) } for _, f := range failed { fmt.Printf(" [失败] %s\n", f) } if len(todo) > 0 { fmt.Printf(" [就绪] %d 个渠道可用旧密钥解密并已用新密钥重新加密\n", len(todo)) } if len(failed) > 0 && !rotateForce { fmt.Printf("\n%d 个渠道无法用旧密钥解密,已整体回滚(未改动任何数据)。\n", len(failed)) fmt.Println("如果这些渠道本就该用另一把密钥,可用 --force 跳过它们只迁移其余渠道;") fmt.Println("如果全部失败,说明 --old 猜错了:用 check-channels 的指纹逐个核对候选旧密钥。") os.Exit(1) } if len(todo) == 0 { fmt.Println("\n没有可迁移的渠道。") return } if rotateDryRun { fmt.Printf("\ndry-run:以上 %d 个渠道迁移可行,未改动数据库。\n", len(todo)) return } if err := db.Transaction(func(tx *gorm.DB) error { for _, p := range todo { if err := tx.Model(&store.Channel{}).Where("id = ?", p.id). Update("api_key_enc", p.newEncV).Error; err != nil { return fmt.Errorf("渠道 #%d %s: %w", p.id, p.name, err) } } return nil }); err != nil { log.Fatalf("写入失败,已回滚: %v", err) } fmt.Printf("\n迁移完成:%d 个渠道已改用新主密钥加密。\n", len(todo)) fmt.Println("现在用新主密钥重启服务,然后运行 check-channels 确认全部「正常」。") if len(failed) > 0 { fmt.Printf("仍有 %d 个渠道未迁移(用 --force 跳过的),需要在管理后台重填上游 key。\n", len(failed)) } } // describeOldKey 说明旧密钥形态;空值单独提示,因为那正是历史默认行为。 func describeOldKey(master string) string { if master == "" { return "空(历史默认:sha256(\"\") 派生)" } return describeMaster(master) }