Files
2026-09-16 18:06:18 +08:00

107 lines
3.0 KiB
Go

package wechatmp
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
"strings"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 微信公众号敏感字段(AppSecret / EncodingAESKey)的对称加密与掩码
//
// yz_platform_wechat_mp_config.app_secret / aes_key 以 AES-256-GCM 加密后存储,
// 格式:base64( "YZW1:" + nonce + ciphertext )。
// 密钥来源:app.conf 的 wechat_mp_secret_key(任意长度字符串,内部做 SHA-256 派生);
// 未配置时退回内置兜底密钥(保证开箱可用),上线前建议在 app.conf 配置该值。
// 注意:更换密钥后,历史密文将无法解密,需要重新保存一次配置。
// =============================================================
const (
cipherPrefix = "YZW1:"
fallbackSecretKey = "yunzer_wechat_mp_default_secret_key_v1"
)
func configKey() []byte {
raw, _ := beego.AppConfig.String("wechat_mp_secret_key")
if strings.TrimSpace(raw) == "" {
raw = fallbackSecretKey
}
sum := sha256.Sum256([]byte(raw))
return sum[:]
}
// EncryptSecret 加密敏感字段;空串原样返回
func EncryptSecret(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(cipherPrefix), nonce...)
buf = append(buf, ciphertext...)
return base64.StdEncoding.EncodeToString(buf), nil
}
// DecryptSecret 解密敏感字段;空串原样返回
func DecryptSecret(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(cipherPrefix) || string(raw[:len(cipherPrefix)]) != cipherPrefix {
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(cipherPrefix) : len(cipherPrefix)+gcm.NonceSize()]
ciphertext := raw[len(cipherPrefix)+gcm.NonceSize():]
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", errors.New("解密失败(wechat_mp_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, "******")
}