// 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" ) var checkChannelsCmd = &cobra.Command{ Use: "check-channels", Short: "诊断渠道上游密钥能否用当前主密钥解密", Long: `诊断渠道上游密钥能否用当前主密钥(OT_MASTER_KEY)解密。 出现 "failed to decrypt channel key" 时先跑这个:它会把当前主密钥的派生路径和 指纹打出来,并逐个渠道试解密,从而区分两种情况: 1. 主密钥被换过/没加载到(常见:换了 OT_MASTER_KEY、从无 .env 变成有 .env、 密钥长度变化导致从「原始字节」切到「SHA-256 派生」)——需要换回原主密钥; 2. 主密钥对、但个别渠道密文被外部改写——该渠道重填上游 key 即可。 主密钥一旦丢失无法恢复(AES-GCM 没有后门),只能重新录入每个渠道的上游 key。 退出码:全部可解密为 0;存在无法解密的渠道为 1(便于脚本告警)。`, Run: func(cmd *cobra.Command, args []string) { runCheckChannels() }, } func init() { rootCmd.AddCommand(checkChannelsCmd) } func runCheckChannels() { cfg, err := config.Load() if err != nil { log.Fatalf("加载配置失败: %v", err) } db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN) if err != nil { log.Fatalf("连接数据库失败: %v", err) } enc := crypto.NewEncryptor(cfg.Master) fmt.Printf("数据库: %s (%s)\n", cfg.DB.DSN, cfg.DB.Driver) fmt.Printf("主密钥: %s\n", describeMaster(cfg.Master)) fmt.Printf("密钥指纹: %s(同指纹=同一把密钥;换过主密钥后这里会变)\n", enc.KeyFingerprint()) fmt.Printf("运行环境: OT_ENV=%s\n\n", cfg.Env) 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 } okCount, failCount := 0, 0 for _, ch := range chs { state := "" if !ch.Enabled { state = " [停用]" } plain, derr := enc.Decrypt(ch.APIKeyEnc) switch { case derr != nil: failCount++ fmt.Printf(" [失败] #%-3d %-20s%s %v\n", ch.ID, ch.Name, state, derr) case plain == "": okCount++ fmt.Printf(" [空值] #%-3d %-20s%s 解密成功但内容为空\n", ch.ID, ch.Name, state) default: okCount++ fmt.Printf(" [正常] #%-3d %-20s%s 解密正常(key 长度 %d,前缀 %s)\n", ch.ID, ch.Name, state, len(plain), maskKey(plain)) } } fmt.Printf("\n合计:%d 个渠道,%d 个可解密,%d 个失败\n", len(chs), okCount, failCount) if failCount > 0 { fmt.Print(` 诊断建议: · 如果「全部」渠道都失败:几乎一定是主密钥不对。核对 OT_MASTER_KEY: - 是否换了值,或者启动目录变了导致 .env 没被读到(.env 按当前工作目录读取); - 主密钥长度是否变了——16/24/32 字节直接用原始字节,其他长度走 SHA-256 派生, 改长度等于换了一把密钥。 用原主密钥重启即可恢复;原值丢失则只能重新录入各渠道上游 key(AES-GCM 无法逆向)。 · 如果只有「个别」渠道失败:这些渠道的密文被改过或从别的库导入, 在管理后台重新填一次上游 key 即可。 `) os.Exit(1) } } func describeMaster(master string) string { if master == "" { return "未设置!用的是 sha256(\"\") 派生(公开可还原,生产必须配置 OT_MASTER_KEY)" } enc := crypto.NewEncryptor(master) return fmt.Sprintf("已设置,%d 字节,%s", len(master), enc.KeyMode()) } // maskKey 只露上游 key 的首尾各 4 个字符(排障时确认是不是填错了 key)。 func maskKey(s string) string { if len(s) <= 8 { return "****" } return s[:4] + "…" + s[len(s)-4:] }