diff --git a/pay/notify/refund.go b/pay/notify/refund.go new file mode 100644 index 0000000..af04ca4 --- /dev/null +++ b/pay/notify/refund.go @@ -0,0 +1,77 @@ +package notify + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "errors" + + "github.com/silenceper/wechat/v2/util" +) + +// reference: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10 + +// RefundedResult 退款回调 +type RefundedResult struct { + ReturnCode *string `xml:"return_code"` + ReturnMsg *string `xml:"return_msg"` + + AppID *string `xml:"appid"` + MchID *string `xml:"mch_id"` + NonceStr *string `xml:"nonce_str"` + ReqInfo *string `xml:"req_info"` +} + +// RefundedReqInfo 退款结果(明文) +type RefundedReqInfo struct { + TransactionID *string `xml:"transaction_id"` + OutTradeNO *string `xml:"out_trade_no"` + RefundID *string `xml:"refund_id"` + OutRefundNO *string `xml:"out_refund_no"` + TotalFee *int `xml:"total_fee"` + SettlementTotalFee *int `xml:"settlement_total_fee"` + RefundFee *int `xml:"refund_fee"` + SettlementRefundFee *int `xml:"settlement_refund_fee"` + RefundStatus *string `xml:"refund_status"` + SuccessTime *string `xml:"success_time"` + RefundRecvAccount *string `xml:"refund_recv_account"` + RefundAccount *string `xml:"refund_account"` + RefundRequestSource *string `xml:"refund_request_source"` +} + +// RefundedResp 消息通知返回 +type RefundedResp struct { + ReturnCode string `xml:"return_code"` + ReturnMsg string `xml:"return_msg"` +} + +// DecryptReqInfo 对退款结果进行解密 +func (notify *Notify) DecryptReqInfo(result *RefundedResult) (*RefundedReqInfo, error) { + var err error + if result == nil || result.ReqInfo == nil { + return nil, errors.New("empty refunded_result or req_info") + } + + base64Decode, err := base64.StdEncoding.DecodeString(*result.ReqInfo) + if err != nil { + return nil, err + } + + hash := md5.New() + if _, err = hash.Write([]byte(notify.Key)); err != nil { + return nil, err + } + md5APIKey := hex.EncodeToString(hash.Sum(nil)) + + data, err := util.AesECBDecrypt(base64Decode, []byte(md5APIKey)) + if err != nil { + return nil, err + } + + res := &RefundedReqInfo{} + if err = xml.Unmarshal(data, res); err != nil { + return nil, err + } + return res, nil +} diff --git a/pay/notify/refund_test.go b/pay/notify/refund_test.go new file mode 100644 index 0000000..f926e51 --- /dev/null +++ b/pay/notify/refund_test.go @@ -0,0 +1,26 @@ +package notify + +import ( + "encoding/xml" + "testing" + + "github.com/silenceper/wechat/v2/pay/config" +) + +func TestNotify_DecryptReqInfo(t *testing.T) { + // data_source: https://studygolang.com/articles/11811 + notify := &Notify{Config: &config.Config{Key: "ziR0QKsTUfMOuochC9RfCdmfHECorQAP"}} + info := "YYwp8C48th0wnQzTqeI+41pflB26v+smFj9z6h9RPBgxTyZyxc+4YNEz7QEgZNWj/6rIb2MfyWMZmCc41CfjKSssoSZPXxOhUayb6KvNSZ1p6frOX1PDWzhyruXK7ouNND+gDsG4yZ0XXzsL4/pYNwLLba/71QrnkJ/BHcByk4EXnglju5DLup9pJQSnTxjomI9Rxu57m9jg5lLQFxMWXyeASZJNvof0ulnHlWJswS4OxKOkmW7VEyKyLGV6npoOm03Qsx2wkRxLsSa9gPpg4hdaReeUqh1FMbm7aWjyrVYT/MEZWg98p4GomEIYvz34XfDncTezX4bf/ZiSLXt79aE1/YTZrYfymXeCrGjlbe0rg/T2ezJHAC870u2vsVbY1/KcE2A443N+DEnAziXlBQ1AeWq3Rqk/O6/TMM0lomzgctAOiAMg+bh5+Gu1ubA9O3E+vehULydD5qx2o6i3+qA9ORbH415NyRrQdeFq5vmCiRikp5xYptWiGZA0tkoaLKMPQ4ndE5gWHqiBbGPfULZWokI+QjjhhBmwgbd6J0VqpRorwOuzC/BHdkP72DCdNcm7IDUpggnzBIy0+seWIkcHEryKjge3YDHpJeQCqrAH0CgxXHDt1xtbQbST1VqFyuhPhUjDXMXrknrGPN/oE1t0rLRq+78cI+k8xe5E6seeUXQsEe8r3358mpcDYSmXWSXVZxK6er9EF98APqHwcndyEJD2YyCh/mMVhERuX+7kjlRXSiNUWa/Cv/XAKFQuvUYA5ea2eYWtPRHa4DpyuF1SNsaqVKfgqKXZrJHfAgslVpSVqUpX4zkKszHF4kwMZO3M7J1P94Mxa7Tm9mTOJePOoHPXeEB+m9rX6pSfoi3mJDQ5inJ+Vc4gOkg/Wd/lqiy6TTyP/dHDN6/v+AuJx5AXBo/2NDD3fWhHjkqEKIuARr2ClZt9ZRQO4HkXdZo7CN06sGCHk48Tg8PmxnxKcMZm7Aoquv5yMIM2gWSWIRJhwJ8cUpafIHc+GesDlbF6Zbt+/KXkafJAQq2RklEN+WvZ/zFz113EPgWPjp16TwBoziq96MMekvWKY/vdhjol8VFtGH9F61Oy1Xwf6DJtPw==" + res, err := notify.DecryptReqInfo(&RefundedResult{ReqInfo: &info}) + if err != nil { + t.Error(err) + return + } + + bytes, err := xml.Marshal(res) + if err != nil { + t.Error(err) + return + } + t.Log(string(bytes)) +} diff --git a/pay/refund/refund.go b/pay/refund/refund.go index 4ac3e7a..d64a91d 100644 --- a/pay/refund/refund.go +++ b/pay/refund/refund.go @@ -29,6 +29,7 @@ type Params struct { RefundFee string RefundDesc string RootCa string //ca证书 + NotifyURL string } //request 接口请求参数 @@ -43,7 +44,7 @@ type request struct { TotalFee string `xml:"total_fee"` RefundFee string `xml:"refund_fee"` RefundDesc string `xml:"refund_desc,omitempty"` - //NotifyUrl string `xml:"notify_url,omitempty"` + NotifyURL string `xml:"notify_url,omitempty"` } //Response 接口返回 @@ -83,13 +84,16 @@ func (refund *Refund) Refund(p *Params) (rsp Response, err error) { param["total_fee"] = p.TotalFee param["sign_type"] = util.SignTypeMD5 param["transaction_id"] = p.TransactionID + if p.NotifyURL != "" { + param["notify_url"] = p.NotifyURL + } sign, err := util.ParamSign(param, refund.Key) if err != nil { return } - request := request{ + req := request{ AppID: refund.AppID, MchID: refund.MchID, NonceStr: nonceStr, @@ -101,7 +105,7 @@ func (refund *Refund) Refund(p *Params) (rsp Response, err error) { RefundFee: p.RefundFee, RefundDesc: p.RefundDesc, } - rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, refund.MchID) + rawRet, err := util.PostXMLWithTLS(refundGateway, req, p.RootCa, refund.MchID) if err != nil { return } diff --git a/util/crypto.go b/util/crypto.go index 4a9296b..1c98d73 100644 --- a/util/crypto.go +++ b/util/crypto.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" @@ -227,3 +228,105 @@ func ParamSign(p map[string]string, key string) (string, error) { return CalculateSign(str, signType, key) } + +// ECB provides confidentiality by assigning a fixed ciphertext block to each plaintext block. +// See NIST SP 800-38A, pp 08-09 +// reference: https://codereview.appspot.com/7860047/patch/23001/24001 +type ecb struct { + b cipher.Block + blockSize int +} + +func newECB(b cipher.Block) *ecb { + return &ecb{ + b: b, + blockSize: b.BlockSize(), + } +} + +// ECBEncryptor - +type ECBEncryptor ecb + +// NewECBEncryptor returns a BlockMode which encrypts in electronic code book mode, using the given Block. +func NewECBEncryptor(b cipher.Block) cipher.BlockMode { + return (*ECBEncryptor)(newECB(b)) +} + +// BlockSize implement BlockMode.BlockSize +func (x *ECBEncryptor) BlockSize() int { + return x.blockSize +} + +// CryptBlocks implement BlockMode.CryptBlocks +func (x *ECBEncryptor) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Encrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} + +// ECBDecryptor - +type ECBDecryptor ecb + +// NewECBDecryptor returns a BlockMode which decrypts in electronic code book mode, using the given Block. +func NewECBDecryptor(b cipher.Block) cipher.BlockMode { + return (*ECBDecryptor)(newECB(b)) +} + +// BlockSize implement BlockMode.BlockSize +func (x *ECBDecryptor) BlockSize() int { + return x.blockSize +} + +// CryptBlocks implement BlockMode.CryptBlocks +func (x *ECBDecryptor) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Decrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} + +// AesECBDecrypt will decrypt data with PKCS5Padding +func AesECBDecrypt(ciphertext []byte, aesKey []byte) ([]byte, error) { + if len(ciphertext) < aes.BlockSize { + return nil, errors.New("ciphertext too short") + } + // ECB mode always works in whole blocks. + if len(ciphertext)%aes.BlockSize != 0 { + return nil, errors.New("ciphertext is not a multiple of the block size") + } + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + NewECBDecryptor(block).CryptBlocks(ciphertext, ciphertext) + return PKCS5UnPadding(ciphertext), nil +} + +// PKCS5Padding - +func PKCS5Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padText...) +} + +// PKCS5UnPadding - +func PKCS5UnPadding(origData []byte) []byte { + length := len(origData) + unPadding := int(origData[length-1]) + return origData[:(length - unPadding)] +}