package service import ( "fmt" "gorm.io/gorm" ) // secretBearingColumns 列出所有以加密形式落库的密文字段。 // 新增落库密文时务必同步登记,否则密钥轮换会漏表。 var secretBearingColumns = []struct { Table string Column string }{ {"api_clients", "secret_ciphertext"}, {"callback_subscriptions", "secret_ciphertext"}, } // SecretReencryptReport 汇总一次密钥轮换的重加密结果。 type SecretReencryptReport struct { Scanned int // 扫描到的非空密文行数 Reencrypted int // 用新密钥重新写入的行数 AlreadyNewKey int // 已能用新密钥解密的行数(重复执行时出现) Failed int // 新旧密钥都无法解密的行数 } // SecretReencryptTable 单个表的明细,便于输出可读日志。 type SecretReencryptTable struct { Table string Report SecretReencryptReport } // ReencryptSecrets 使用 newCodec 重新加密所有落库密文,用于轮换 DATA_ENCRYPTION_KEY。 // // apply=false 时只统计不写库(预演)。单行解密失败不会中断整轮操作, // 而是计入 Failed 并由调用方决定退出码,避免一行坏数据卡死整个轮换。 // 这里使用原生 SQL 而非 GORM 模型,软删除的行同样会被处理—— // 否则将来恢复一条软删除记录时会因为密钥没换而解密失败。 func ReencryptSecrets(db *gorm.DB, oldCodec, newCodec *SecretCodec, apply bool) ([]SecretReencryptTable, error) { if oldCodec == nil || newCodec == nil { return nil, fmt.Errorf("新旧密钥编解码器都不能为空") } results := make([]SecretReencryptTable, 0, len(secretBearingColumns)) for _, target := range secretBearingColumns { report := SecretReencryptReport{} type row struct { ID uint Ciphertext string } var rows []row query := fmt.Sprintf( "SELECT id, %s AS ciphertext FROM %s WHERE %s IS NOT NULL AND %s <> '' ORDER BY id", target.Column, target.Table, target.Column, target.Column, ) if err := db.Raw(query).Scan(&rows).Error; err != nil { return results, fmt.Errorf("读取 %s.%s 失败: %w", target.Table, target.Column, err) } for _, r := range rows { report.Scanned++ plain, err := oldCodec.Decrypt(r.Ciphertext) if err != nil { // 旧密钥解不开:可能上一轮已经换过,也可能是数据损坏,需区分对待。 if _, newErr := newCodec.Decrypt(r.Ciphertext); newErr == nil { report.AlreadyNewKey++ continue } report.Failed++ continue } ciphertext, err := newCodec.Encrypt(plain) if err != nil { report.Failed++ continue } if !apply { report.Reencrypted++ continue } update := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", target.Table, target.Column) if err := db.Exec(update, ciphertext, r.ID).Error; err != nil { report.Failed++ continue } report.Reencrypted++ } results = append(results, SecretReencryptTable{Table: target.Table, Report: report}) } return results, nil } // TotalFailed 返回所有表中解密失败的总行数,供调用方决定退出码。 func TotalFailed(results []SecretReencryptTable) int { total := 0 for _, r := range results { total += r.Report.Failed } return total }