From b0b611eee261c24c0dd9e978335472d7a3469e82 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Sun, 1 Nov 2026 06:03:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(payment):=20=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8E=A5=E5=85=A5=E7=BB=9F=E4=B8=80=20secret?= =?UTF-8?q?box=20=E4=BF=A1=E5=B0=81=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 丢弃支付模块自带的 PAYMENT_SECRET_KEY/JWT_SECRET 派生双密钥体系, EncryptSecret/DecryptSecret 直接走 internal/secretbox(OPSLIC_MASTER_KEY) - 全站只有一把主密钥:Waffo 私钥、节点 shared_key、支付 APIv3 密钥/应用私钥同格式同前缀 - 测试改由 TestMain 装载 secretbox 主密钥,密文前缀统一引用 secretbox.Prefix - .env.example 移除 PAYMENT_SECRET_KEY,改为说明复用 OPSLIC_MASTER_KEY --- .env.example | 17 +-- .../internal/service/payment/config_crypto.go | 139 ++---------------- .../service/payment/config_crypto_test.go | 8 +- 3 files changed, 26 insertions(+), 138 deletions(-) diff --git a/.env.example b/.env.example index 5d5de35..337f1e9 100644 --- a/.env.example +++ b/.env.example @@ -109,20 +109,15 @@ SUCCESS_URL=https://l123.91ai.io/api/v1/purchase/success # ============================================ # ============================================ -# 支付配置加密(后台「支付配置」页 · 微信支付 / 支付宝) +# 支付配置(后台「支付配置」页 · 微信支付 / 支付宝) # ============================================ -# 敏感字段(微信 APIv3 密钥、支付宝应用私钥/公钥)入库前用 AES-256-GCM 加密, -# 主密钥只从环境变量读取,绝不写入数据库或仓库。生成方式二选一: -# openssl rand -hex 32 # 64 位 hex -# openssl rand -base64 32 # 32 字节 base64 -# 未设置时系统会退化为用 JWT_SECRET 派生主密钥(可开箱使用,但轮换 JWT_SECRET 会导致 -# 已保存的支付密钥无法解密,必须重新填写)——生产环境请显式设置本变量。 -# 注意:主密钥一旦更换,已保存的密文全部失效。 -# PAYMENT_SECRET_KEY= - -# 支付证书/私钥落地目录(容器内数据卷,位于仓库之外); +# 敏感字段(微信 APIv3 密钥、支付宝应用私钥/公钥)不单独设密钥, +# 统一走 internal/secretbox 信封加密,主密钥即上面的 OPSLIC_MASTER_KEY。 +# 证书/私钥落地目录(容器内数据卷,位于仓库之外); # 目录权限 0700、文件 0600,不通过任何静态路由对外暴露。默认 /data/payment-certs # PAYMENT_CERT_DIR=/data/payment-certs +# 对外基址(用于生成微信/支付宝回调地址展示) +# PAYMENT_PUBLIC_BASE_URL=https://licence.rlwj.cn # 宿主机映射端口(容器内固定 9001) HOST_PORT=9001 VERSION=latest diff --git a/backend/internal/service/payment/config_crypto.go b/backend/internal/service/payment/config_crypto.go index fbdeb8b..0335b5a 100644 --- a/backend/internal/service/payment/config_crypto.go +++ b/backend/internal/service/payment/config_crypto.go @@ -1,96 +1,29 @@ package payment import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" + "backend/internal/secretbox" "crypto/sha256" - "encoding/base64" "encoding/hex" - "errors" - "io" - "log" - "os" "strings" - "sync" ) -// 支付配置敏感字段的对称加密实现(AES-256-GCM)。 +// 支付配置敏感字段的加密出口:统一走项目级信封加密(internal/secretbox,AES-256-GCM)。 // -// 设计要点: -// 1. 主密钥来自环境变量 PAYMENT_SECRET_KEY(32 字节 hex / base64,或任意口令经 SHA-256 派生); -// 2. 未设置时退化为用 JWT_SECRET 做域分离派生,保证开箱可用,但轮换 JWT_SECRET 会失去解密能力; -// 3. 密文格式 enc:v1:,前缀便于未来平滑升级算法。 // Logan.Li +// 这样全站只有一把主密钥(环境变量 OPSLIC_MASTER_KEY,启动时 fail-fast 装载), +// 支付配置不再自带第二套密钥体系:微信 APIv3 密钥、支付宝应用私钥/公钥 +// 与 Waffo 私钥、节点 shared_key 共用同一套密文格式(enc:v1: 前缀)。 // Logan.Li + const ( - // EnvPaymentMasterKey 加密主密钥环境变量名 // Logan.Li - EnvPaymentMasterKey = "PAYMENT_SECRET_KEY" - // cipherPrefix 密文前缀 // Logan.Li - cipherPrefix = "enc:v1:" - // cipherAAD 附加认证数据,避免密文被跨用途搬运 // Logan.Li - cipherAAD = "opsx-licence/payment-config/v1" + // EnvPaymentMasterKey 主密钥环境变量名(与 secretbox 一致,保留常量供文档/提示引用) // Logan.Li + EnvPaymentMasterKey = "OPSLIC_MASTER_KEY" ) -var ( - masterKeyOnce sync.Once - masterKeyBytes []byte - masterKeyErr error - masterKeyOrigin string -) - -// PaymentMasterKeyOrigin 返回主密钥来源(env / jwt_secret),供页面提示 // Logan.Li +// PaymentMasterKeyOrigin 返回加密主密钥来源,供后台页面提示 // Logan.Li func PaymentMasterKeyOrigin() string { - _, _ = masterKey() - return masterKeyOrigin -} - -// masterKey 惰性解析主密钥,仅解析一次 // Logan.Li -func masterKey() ([]byte, error) { - masterKeyOnce.Do(func() { - if raw := strings.TrimSpace(os.Getenv(EnvPaymentMasterKey)); raw != "" { - key, err := parseKeyMaterial(raw) - if err != nil { - masterKeyErr = err - return - } - masterKeyBytes = key - masterKeyOrigin = "env" - return - } - if jwt := strings.TrimSpace(os.Getenv("JWT_SECRET")); jwt != "" { - sum := sha256.Sum256([]byte(cipherAAD + "|jwt:" + jwt)) - masterKeyBytes = sum[:] - masterKeyOrigin = "jwt_secret" - log.Printf("[支付配置] 未设置 %s,已用 JWT_SECRET 派生加密主密钥;生产环境建议显式设置独立主密钥", EnvPaymentMasterKey) - return - } - masterKeyOrigin = "none" - masterKeyErr = errors.New("未配置加密主密钥:请设置环境变量 " + EnvPaymentMasterKey + "(32 字节 hex/base64,或任意口令)") - }) - return masterKeyBytes, masterKeyErr -} - -// parseKeyMaterial 把环境变量值解析成 32 字节密钥 // Logan.Li -func parseKeyMaterial(raw string) ([]byte, error) { - // 1) 64 位 hex - if len(raw) == 64 { - if b, err := hex.DecodeString(raw); err == nil && len(b) == 32 { - return b, nil - } + if secretbox.Ready() { + return "env" } - // 2) base64(标准 / URL / 无填充) - for _, enc := range []*base64.Encoding{ - base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding, - } { - if b, err := enc.DecodeString(raw); err == nil && len(b) == 32 { - return b, nil - } - } - // 3) 任意口令:SHA-256 派生 - if len(raw) >= 8 { - sum := sha256.Sum256([]byte(raw)) - return sum[:], nil - } - return nil, errors.New("PAYMENT_SECRET_KEY 过短(至少 8 个字符,推荐 openssl rand -hex 32)") + return "none" } // EncryptSecret 加密敏感字段;空串直接返回空串 // Logan.Li @@ -98,24 +31,7 @@ func EncryptSecret(plain string) (string, error) { if strings.TrimSpace(plain) == "" { return "", nil } - key, err := masterKey() - if err != nil { - return "", err - } - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return "", err - } - sealed := gcm.Seal(nonce, nonce, []byte(plain), []byte(cipherAAD)) - return cipherPrefix + base64.RawURLEncoding.EncodeToString(sealed), nil + return secretbox.Encrypt(plain) } // DecryptSecret 解密敏感字段;无前缀视为历史明文直接返回 // Logan.Li @@ -124,34 +40,7 @@ func DecryptSecret(stored string) (string, error) { if raw == "" { return "", nil } - if !strings.HasPrefix(raw, cipherPrefix) { - return raw, nil - } - key, err := masterKey() - if err != nil { - return "", err - } - payload, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(raw, cipherPrefix)) - if err != nil { - return "", errors.New("密文格式错误") - } - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - if len(payload) < gcm.NonceSize() { - return "", errors.New("密文长度异常") - } - nonce, cipherText := payload[:gcm.NonceSize()], payload[gcm.NonceSize():] - plain, err := gcm.Open(nil, nonce, cipherText, []byte(cipherAAD)) - if err != nil { - return "", errors.New("解密失败:加密主密钥(PAYMENT_SECRET_KEY)与保存时不一致") - } - return string(plain), nil + return secretbox.Decrypt(raw) } // MaskTail 脱敏展示:仅保留末尾 keep 位,其余用掩码填充到 total 位 // Logan.Li diff --git a/backend/internal/service/payment/config_crypto_test.go b/backend/internal/service/payment/config_crypto_test.go index f041ba2..d4965fa 100644 --- a/backend/internal/service/payment/config_crypto_test.go +++ b/backend/internal/service/payment/config_crypto_test.go @@ -1,15 +1,19 @@ package payment import ( + "backend/internal/secretbox" "os" "strings" "testing" ) -// TestMain 固定测试用加密主密钥(masterKey 只会解析一次) // Logan.Li +// TestMain 固定测试用加密主密钥并装载信封加密(全站共用 OPSLIC_MASTER_KEY) // Logan.Li func TestMain(m *testing.M) { os.Setenv(EnvPaymentMasterKey, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") os.Setenv("PAYMENT_CERT_DIR", os.TempDir()+"/opsx-licence-test-certs") + if err := secretbox.LoadMasterKey(); err != nil { + panic(err) + } os.Exit(m.Run()) } @@ -20,7 +24,7 @@ func TestEncryptDecryptRoundTrip(t *testing.T) { if err != nil { t.Fatalf("加密失败: %v", err) } - if !strings.HasPrefix(enc, cipherPrefix) { + if !strings.HasPrefix(enc, secretbox.Prefix) { t.Fatalf("密文缺少前缀: %s", enc) } if strings.Contains(enc, plain) {