- 丢弃支付模块自带的 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:
1 parent
3a98ccc631
commit
b0b611eee2
3 files changed
+26
-138
No files matched your search
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user