Files
2026-09-15 12:46:29 +08:00

107 lines
3.1 KiB
Go

package payment
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
"strings"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 渠道敏感参数的对称加密与掩码
//
// yz_platform_payment_channel.config_json 中的密钥/私钥/Secret 以 AES-256-GCM 加密后存储,
// 格式:base64( "YZP1:" + nonce + ciphertext )。
// 密钥来源:app.conf 的 payment_secret_key(任意长度字符串,内部做 SHA-256 派生);
// 未配置时退回内置兜底密钥(保证开箱可用),上线前务必在 app.conf 配置该值,
// 且注意:更换密钥后,历史密文将无法解密,需要重新保存各渠道配置。
// =============================================================
const configCipherPrefix = "YZP1:"
// fallbackSecretKey 内置兜底密钥,仅用于开发/未配置场景
const fallbackSecretKey = "yunzer_payment_default_secret_key_v1"
func configKey() []byte {
raw, _ := beego.AppConfig.String("payment_secret_key")
if strings.TrimSpace(raw) == "" {
raw = fallbackSecretKey
}
sum := sha256.Sum256([]byte(raw))
return sum[:]
}
// EncryptConfig 加密渠道参数 JSON
func EncryptConfig(plain string) (string, error) {
if plain == "" {
return "", nil
}
block, err := aes.NewCipher(configKey())
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
}
ciphertext := gcm.Seal(nil, nonce, []byte(plain), nil)
buf := append([]byte(configCipherPrefix), nonce...)
buf = append(buf, ciphertext...)
return base64.StdEncoding.EncodeToString(buf), nil
}
// DecryptConfig 解密渠道参数 JSON;空串原样返回
func DecryptConfig(encoded string) (string, error) {
if encoded == "" {
return "", nil
}
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return "", errors.New("渠道参数不是有效的 base64")
}
if len(raw) < len(configCipherPrefix) || string(raw[:len(configCipherPrefix)]) != configCipherPrefix {
return "", errors.New("渠道参数格式不正确(缺少加密前缀)")
}
block, err := aes.NewCipher(configKey())
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := raw[len(configCipherPrefix) : len(configCipherPrefix)+gcm.NonceSize()]
ciphertext := raw[len(configCipherPrefix)+gcm.NonceSize():]
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", errors.New("渠道参数解密失败(payment_secret_key 是否被更换过?)")
}
return string(plain), nil
}
// MaskSecret 生成掩码:保留末 4 位
func MaskSecret(v string) string {
if v == "" {
return ""
}
if len(v) <= 4 {
return "******"
}
return "******" + v[len(v)-4:]
}
// IsMasked 判断前端回传的值是否仍是掩码(表示「不修改原值」)
func IsMasked(v string) bool {
return strings.HasPrefix(v, "******")
}