AES 字段加密迁配置并兼容旧密文

移除字段加密硬编码主密钥,改为 FIELD_ENCRYPTION_KEY 注入。

保留 FIELD_ENCRYPTION_LEGACY_KEY 透明回退旧密文,新写入统一使用主密钥;生产环境校验主密钥和 legacy 密钥长度、占位符及相等关系,并统一生产环境判断口径。

补充配置与旧密文兼容回归测试。
This commit is contained in:
yml
2026-06-14 17:53:24 +08:00
parent 64f7ed3753
commit 8fef037be3
15 changed files with 519 additions and 82 deletions
+52 -10
View File
@@ -34,6 +34,7 @@ import (
"hfb_sys/backend/internal/modules/withdrawal"
_ "hfb_sys/backend/docs" // Swagger 文档
"hfb_sys/backend/pkg/crypto"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
@@ -42,7 +43,7 @@ import (
)
func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
if cfg.AppEnv == "production" {
if config.IsProductionEnv(cfg.AppEnv) {
gin.SetMode(gin.ReleaseMode)
}
@@ -58,13 +59,53 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
engine.GET("/health", health.Check)
// Swagger 文档路由(仅在非生产环境)
if cfg.AppEnv != "production" {
if !config.IsProductionEnv(cfg.AppEnv) {
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
if cfg.RateLimit.Enabled {
engine.Use(middleware.RateLimitPerMinute(cfg.RateLimit.RequestsPerMinute, deps.Redis))
}
// 业务字段加密器(实名/收款账号):主密钥 + legacy 回退,用于密钥轮换期间透明解出旧密文。
// 生产环境必须配置 FIELD_ENCRYPTION_KEYValidateProductionSecurity 已校验)。
// 开发/测试缺 FIELD_ENCRYPTION_KEY 时用 legacy 密钥构造单密钥加密器,
// 保证零配置兼容历史硬编码密钥加密的存量密文(非生产 legacy 默认填历史硬编码值)。
var fieldEncryptor crypto.Encryptor
primary := cfg.FieldEncryptionKey
legacy := cfg.FieldEncryptionLegacyKey
switch {
case primary != "":
encryptor, err := crypto.NewFieldEncryptor(primary, legacy)
if err != nil {
if config.IsProductionEnv(cfg.AppEnv) {
logger.Fatal("FIELD_ENCRYPTION_KEY invalid", zap.Error(err))
}
logger.Warn("FIELD_ENCRYPTION_KEY invalid, fallback to legacy-only", zap.Error(err))
// primary 非法时退回 legacy 单密钥(若有),否则 MockEncryptor
if legacy != "" {
if e, eErr := crypto.NewFieldEncryptor(legacy); eErr == nil {
fieldEncryptor = e
}
}
} else {
fieldEncryptor = encryptor
}
case legacy != "":
// 开发态零配置:用 legacy 单密钥,能解历史存量密文。
encryptor, err := crypto.NewFieldEncryptor(legacy)
if err == nil {
fieldEncryptor = encryptor
logger.Warn("FIELD_ENCRYPTION_KEY not set, using legacy-only encryptor for dev compatibility")
}
}
if fieldEncryptor == nil {
if config.IsProductionEnv(cfg.AppEnv) {
logger.Fatal("FIELD_ENCRYPTION_KEY not set")
}
fieldEncryptor = &crypto.MockEncryptor{}
logger.Warn("FIELD_ENCRYPTION_KEY and legacy both unset, using MockEncryptor")
}
jwtManager := auth.NewJWTManager(cfg.JWTSecret)
var userRepo *auth.UserRepository
if deps.DB != nil {
@@ -108,7 +149,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
if deps.DB != nil {
realnameRepo = realname.NewRepository(deps.DB)
}
realnameService := realname.NewService(realnameRepo, newRealnameProvider(cfg, logger), logger)
realnameService := realname.NewService(realnameRepo, newRealnameProvider(cfg, fieldEncryptor, logger), logger)
realnameHandler := realname.NewHandler(realnameService)
var listingRepo *listing.Repository
if deps.DB != nil {
@@ -149,13 +190,13 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
walletHandler := wallet.NewHandler(walletService)
var paymentAccountRepo *paymentaccount.Repository
if deps.DB != nil {
paymentAccountRepo = paymentaccount.NewRepository(deps.DB)
paymentAccountRepo = paymentaccount.NewRepository(deps.DB, fieldEncryptor)
}
paymentAccountService := paymentaccount.NewService(paymentAccountRepo)
paymentAccountHandler := paymentaccount.NewHandler(paymentAccountService)
var withdrawalRepo *withdrawal.Repository
if deps.DB != nil {
withdrawalRepo = withdrawal.NewRepository(deps.DB, walletRepo)
withdrawalRepo = withdrawal.NewRepository(deps.DB, walletRepo, fieldEncryptor)
}
withdrawalService := withdrawal.NewService(withdrawalRepo)
withdrawalHandler := withdrawal.NewHandler(withdrawalService)
@@ -174,7 +215,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
}
}
if encryptor == nil {
if cfg.AppEnv == "production" {
if config.IsProductionEnv(cfg.AppEnv) {
logger.Fatal("PAYMENT_CONFIG_ENCRYPTION_KEY not set or invalid")
}
encryptor = &paymentconfig.MockEncryptor{}
@@ -556,12 +597,13 @@ func newSMSProvider(cfg config.Config, logger *zap.Logger) smsintegration.Provid
}
}
func newRealnameProvider(cfg config.Config, logger *zap.Logger) realname.Provider {
func newRealnameProvider(cfg config.Config, encryptor crypto.Encryptor, logger *zap.Logger) realname.Provider {
switch strings.ToLower(strings.TrimSpace(cfg.Realname.Provider)) {
case "cloudmarket", "aliyun_cloudmarket":
provider, err := realname.NewCloudMarketProvider(realname.CloudMarketConfig{
URL: cfg.Realname.CloudMarketURL,
AppCode: cfg.Realname.CloudMarketAppCode,
URL: cfg.Realname.CloudMarketURL,
AppCode: cfg.Realname.CloudMarketAppCode,
Encryptor: encryptor,
})
if err != nil {
logger.Warn("cloud market realname provider unavailable; realname verify will fail", zap.Error(err))
@@ -569,6 +611,6 @@ func newRealnameProvider(cfg config.Config, logger *zap.Logger) realname.Provide
}
return provider
default:
return realname.NewMockProvider()
return realname.NewMockProvider(encryptor)
}
}