refactor(payment): 支付配置接入统一 secretbox 信封加密
CI / backend (push) Canceled after 0s

- 丢弃支付模块自带的 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
This commit is contained in:
Logan.Li committed 2026-11-01 06:03:24 +08:00
1 parent 3a98ccc631
commit b0b611eee2
3 files changed
+26 -138

No files matched your search

+14 -125
View File
@@ -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:<base64url(nonce||ciphertext)>,前缀便于未来平滑升级算法。 // 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
@@ -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) {