package wechat import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/binary" "encoding/json" "errors" "fmt" "strconv" "time" ) // 微信消息推送「安全模式」加解密,与公众号/服务号的消息加解密协议一致: // // AESKey = base64.StdEncoding.Decode(EncodingAESKey + "=") // 32 字节 // IV = AESKey[:16](CBC 模式) // 填充 = PKCS#7(块大小 32) // 明文结构 = random(16B) + msgLen(4B, 大端) + msg + appid // 接收签名 = sha1(sort(token, timestamp, nonce, Encrypt)) -> URL 参数 msg_signature // 回包签名 = sha1(sort(token, TimeStamp, Nonce, Encrypt)) -> 回包字段 MsgSignature // // 安全模式下微信推送的包体只含密文(JSON 信封 {"Encrypt":"..."}),开发者的回包也必须为密文; // 未配置 EncodingAESKey 时按明文模式处理(包体即业务明文,回包为明文应答)。 const ( // wxPKCS7BlockSize 微信 PKCS#7 填充块大小(固定 32,与 AES 的 16 字节分组兼容)。 wxPKCS7BlockSize = 32 // wxEncodingAESKeyLen EncodingAESKey 固定长度(43 位,base64 补 "=" 后解出 32 字节)。 wxEncodingAESKeyLen = 43 ) // IsEncryptedNotify 判断本次推送是否为安全模式(微信会带 msg_signature 参数)。 func IsEncryptedNotify(msgSignature string) bool { return msgSignature != "" } // HasMsgEncryptKey 是否已配置 EncodingAESKey(安全模式可用)。 func HasMsgEncryptKey() bool { c, err := getVirtualPayClient() return err == nil && len(c.aesKey) == 32 } // DecodeEchoStr 安全模式 URL 配置验证:校验 msg_signature 并解密 echostr,返回明文供原样回显。 // echostr 仅用于验证地址归属,不做 appid 校验(避免因 appid 形态差异导致后台无法保存配置)。 func DecodeEchoStr(echostr, timestamp, nonce, msgSignature string) (string, error) { c, err := getVirtualPayClient() if err != nil { return "", err } if !c.checkMsgSignature(timestamp, nonce, echostr, msgSignature) { return "", errors.New("xpay verify msg_signature mismatch") } msg, _, derr := c.decrypt(echostr) if derr != nil { return "", fmt.Errorf("decrypt echostr: %w", derr) } return msg, nil } // DecodeEncryptedNotify 安全模式接收:校验 msg_signature 并解密出明文业务报文。 // 任一环节失败都返回错误,调用方应回失败应答让微信重试(fail-closed)。 func DecodeEncryptedNotify(raw []byte, timestamp, nonce, msgSignature string) ([]byte, error) { c, err := getVirtualPayClient() if err != nil { return nil, err } encrypt, err := extractEncryptField(raw) if err != nil { return nil, err } if !c.checkMsgSignature(timestamp, nonce, encrypt, msgSignature) { return nil, errors.New("xpay notify msg_signature mismatch") } msg, appID, derr := c.decrypt(encrypt) if derr != nil { return nil, fmt.Errorf("decrypt xpay notify: %w", derr) } // 防伪造:密文内的 appid 必须是本小程序。 if appID != c.appID { return nil, fmt.Errorf("xpay notify appid mismatch: got %q, want %q", appID, c.appID) } return []byte(msg), nil } // EncodeEncryptedNotify 安全模式回包:把明文业务应答加密为信封 // {"Encrypt":..,"MsgSignature":..,"TimeStamp":..,"Nonce":..}。 func EncodeEncryptedNotify(plain []byte, nonce string) ([]byte, error) { c, err := getVirtualPayClient() if err != nil { return nil, err } encrypt, eerr := c.encrypt(string(plain)) if eerr != nil { return nil, fmt.Errorf("encrypt xpay reply: %w", eerr) } now := time.Now().Unix() return json.Marshal(map[string]any{ "Encrypt": encrypt, "MsgSignature": sortedSHA1Hex(c.token, strconv.FormatInt(now, 10), nonce, encrypt), "TimeStamp": now, "Nonce": nonce, }) } // extractEncryptField 从安全模式信封中取出 Encrypt 字段(只支持 JSON 信封)。 func extractEncryptField(raw []byte) (string, error) { if trimmed := bytes.TrimLeft(raw, " \t\r\n"); len(trimmed) > 0 && trimmed[0] == '<' { return "", errors.New("xpay notify envelope is XML but only JSON is supported; " + "please set 消息推送 → 数据格式 = JSON in MP console") } var env struct { Encrypt string `json:"Encrypt"` } if err := json.Unmarshal(raw, &env); err != nil { return "", fmt.Errorf("parse xpay notify envelope: %w", err) } if env.Encrypt == "" { return "", errors.New("xpay notify envelope missing Encrypt") } return env.Encrypt, nil } // checkMsgSignature 校验安全模式接收签名:sha1(sort(token, timestamp, nonce, encrypt))。 func (c *virtualPayClient) checkMsgSignature(timestamp, nonce, encrypt, msgSignature string) bool { if c.token == "" || msgSignature == "" { return false } return sortedSHA1Hex(c.token, timestamp, nonce, encrypt) == msgSignature } // decrypt 解密微信密文:AES-256-CBC(IV=AESKey[:16])→ 去 PKCS#7 → 切出 msg 与 appid。 func (c *virtualPayClient) decrypt(encrypt string) (msg, appID string, err error) { if len(c.aesKey) != 32 { return "", "", errors.New("virtual pay encoding_aes_key not configured") } cipherText, err := base64.StdEncoding.DecodeString(encrypt) if err != nil { return "", "", fmt.Errorf("base64 decode cipher text: %w", err) } block, err := aes.NewCipher(c.aesKey) if err != nil { return "", "", fmt.Errorf("new aes cipher: %w", err) } if len(cipherText) == 0 || len(cipherText)%block.BlockSize() != 0 { return "", "", fmt.Errorf("invalid cipher text length %d", len(cipherText)) } plain := make([]byte, len(cipherText)) cipher.NewCBCDecrypter(block, c.aesKey[:block.BlockSize()]).CryptBlocks(plain, cipherText) plain, err = pkcs7Unpad(plain, wxPKCS7BlockSize) if err != nil { return "", "", err } if len(plain) < 20 { return "", "", fmt.Errorf("decrypted payload too short: %d bytes", len(plain)) } msgLen := int(binary.BigEndian.Uint32(plain[16:20])) if 20+msgLen > len(plain) { return "", "", fmt.Errorf("invalid msg length %d in decrypted payload %d", msgLen, len(plain)) } return string(plain[20 : 20+msgLen]), string(plain[20+msgLen:]), nil } // encrypt 加密:random(16B) + msgLen(4B, 大端) + msg + appid → PKCS#7 → AES-256-CBC → base64。 func (c *virtualPayClient) encrypt(msg string) (string, error) { if len(c.aesKey) != 32 { return "", errors.New("virtual pay encoding_aes_key not configured") } random := make([]byte, 16) if _, err := rand.Read(random); err != nil { return "", fmt.Errorf("read random bytes: %w", err) } var buf bytes.Buffer buf.Write(random) msgBytes := []byte(msg) var lenBuf [4]byte binary.BigEndian.PutUint32(lenBuf[:], uint32(len(msgBytes))) buf.Write(lenBuf[:]) buf.Write(msgBytes) buf.WriteString(c.appID) plain := pkcs7Pad(buf.Bytes(), wxPKCS7BlockSize) block, err := aes.NewCipher(c.aesKey) if err != nil { return "", fmt.Errorf("new aes cipher: %w", err) } out := make([]byte, len(plain)) cipher.NewCBCEncrypter(block, c.aesKey[:block.BlockSize()]).CryptBlocks(out, plain) return base64.StdEncoding.EncodeToString(out), nil } // pkcs7Pad 按微信约定填充到 blockSize 的整数倍(每个填充字节的值即填充长度)。 func pkcs7Pad(data []byte, blockSize int) []byte { padding := blockSize - len(data)%blockSize return append(data, bytes.Repeat([]byte{byte(padding)}, padding)...) } // pkcs7Unpad 去除 PKCS#7 填充并校验填充合法性。 func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) { if len(data) == 0 || len(data)%blockSize != 0 { return nil, fmt.Errorf("invalid padded data length %d", len(data)) } padding := int(data[len(data)-1]) if padding == 0 || padding > blockSize { return nil, fmt.Errorf("invalid pkcs7 padding value %d", padding) } for _, b := range data[len(data)-padding:] { if int(b) != padding { return nil, errors.New("invalid pkcs7 padding bytes") } } return data[:len(data)-padding], nil }