├── go.mod
├── .gitignore
├── README.md
├── sample.go
└── wxbizmsgcrypt
└── wxbizmsgcrypt.go
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sbzhu/weworkapi_golang
2 |
3 | go 1.14
4 |
5 | replace github.com/sbzhu/weworkapi_golang/wxbizmsgcrypt => ./wxbizmsgcrypt
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # IDE
15 | .idea
16 | .vs
17 | .vscode
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## weworkapi_golang
2 |
3 | weworkapi_golang 是为了简化开发者对企业微信API接口的使用而设计的,API调用加解密库之golang版本
4 |
5 | ## Usage
6 |
7 | 将本项目下载到你的目录,既可直接引用相关文件
8 | 详细使用方法参考[sample.go](https://github.com/sbzhu/weworkapi_golang/blob/master/sample.go)代码
9 |
10 | ## About
11 |
12 | **本库仅做示范用,并不保证完全无bug**
13 |
14 | 作者会不定期更新本库,但不保证与官方API接口文档同步,因此一切以[官方文档](https://work.weixin.qq.com/api/doc)为准。
15 |
16 | 更多来自个人开发者的其它语言的库推荐:
17 |
18 | python:
19 |
20 | * https://github.com/sbzhu/weworkapi_python abelzhu@tencent.com(企业微信团队)
21 |
22 | ruby:
23 |
24 | * https://github.com/mycolorway/wework MyColorway(个人开发者)
25 |
26 | php:
27 |
28 | * https://github.com/sbzhu/weworkapi_php abelzhu@tencent.com; xiqunpan@tencent.com(企业微信团队)
29 |
30 | golang:
31 |
32 | * https://github.com/sbzhu/weworkapi_golang ryanjelin@tencent.com(企业微信团队)
33 | * https://github.com/doubliekill/EnterpriseWechatSDK 1006401052yh@gmail.com(个人开发者)
34 |
35 | ## Contact us
36 | ryanjelin@tencent.com
37 |
--------------------------------------------------------------------------------
/sample.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "github.com/sbzhu/weworkapi_golang/wxbizmsgcrypt"
7 | )
8 |
9 | type MsgContent struct {
10 | ToUsername string `xml:"ToUserName"`
11 | FromUsername string `xml:"FromUserName"`
12 | CreateTime uint32 `xml:"CreateTime"`
13 | MsgType string `xml:"MsgType"`
14 | Content string `xml:"Content"`
15 | Msgid string `xml:"MsgId"`
16 | Agentid uint32 `xml:"AgentId"`
17 | }
18 |
19 | func main() {
20 | token := "xxxxxxxxxx"
21 | receiverId := "wx5823bf96d3bd56c7"
22 | encodingAeskey := "xxxxxxxxxxxxxxxx"
23 | wxcpt := wxbizmsgcrypt.NewWXBizMsgCrypt(token, encodingAeskey, receiverId, wxbizmsgcrypt.XmlType)
24 | /*
25 | ------------使用示例一:验证回调URL---------------
26 | *企业开启回调模式时,企业微信会向验证url发送一个get请求
27 | 假设点击验证时,企业收到类似请求:
28 | * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
29 | * HTTP/1.1 Host: qy.weixin.qq.com
30 |
31 | 接收到该请求时,企业应
32 | 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
33 | 这一步注意作URL解码。
34 | 2.验证消息体签名的正确性
35 | 3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
36 | 第2,3步可以用企业微信提供的库函数VerifyURL来实现。
37 |
38 | */
39 | // 解析出url上的参数值如下:
40 | // verifyMsgSign := HttpUtils.ParseUrl("msg_signature")
41 | verifyMsgSign := "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3"
42 | // verifyTimestamp := HttpUtils.ParseUrl("timestamp")
43 | verifyTimestamp := "1409659589"
44 | // verifyNonce := HttpUtils.ParseUrl("nonce")
45 | verifyNonce := "263014780"
46 | // verifyEchoStr := HttpUtils.ParseUrl("echoStr")
47 | verifyEchoStr := "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ=="
48 | echoStr, cryptErr := wxcpt.VerifyURL(verifyMsgSign, verifyTimestamp, verifyNonce, verifyEchoStr)
49 | if nil != cryptErr {
50 | fmt.Println("verifyUrl fail", cryptErr)
51 | }
52 | fmt.Println("verifyUrl success echoStr", string(echoStr))
53 | // 验证URL成功,将sEchoStr返回
54 | // HttpUtils.SetResponse(sEchoStr)
55 |
56 | /*
57 | ------------使用示例二:对用户回复的消息解密---------------
58 | 用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
59 | 假设企业收到企业微信的回调消息如下:
60 | POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1
61 | Host: qy.weixin.qq.com
62 | Content-Length: 613
63 |
64 |
65 |
66 |
67 | 企业收到post请求之后应该:
68 | 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
69 | 2.验证消息体签名的正确性。
70 | 3.将post请求的数据进行xml解析,并将标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
71 | 第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
72 | */
73 | // reqMsgSign := HttpUtils.ParseUrl("msg_signature")
74 | reqMsgSign := "477715d11cdb4164915debcba66cb864d751f3e6"
75 | // reqTimestamp := HttpUtils.ParseUrl("timestamp")
76 | reqTimestamp := "1409659813"
77 | // reqNonce := HttpUtils.ParseUrl("nonce")
78 | reqNonce := "1372623149"
79 | // post请求的密文数据
80 | // reqData = HttpUtils.PostData()
81 | reqData := []byte("")
82 |
83 | msg, cryptErr := wxcpt.DecryptMsg(reqMsgSign, reqTimestamp, reqNonce, reqData)
84 | if nil != cryptErr {
85 | fmt.Println("DecryptMsg fail", cryptErr)
86 | }
87 | fmt.Println("after decrypt msg: ", string(msg))
88 | // TODO: 解析出明文xml标签的内容进行处理
89 | // For example:
90 |
91 | var msgContent MsgContent
92 | err := xml.Unmarshal(msg, &msgContent)
93 | if nil != err {
94 | fmt.Println("Unmarshal fail")
95 | } else {
96 | fmt.Println("struct", msgContent)
97 | }
98 |
99 | /*
100 | ------------使用示例三:企业回复用户消息的加密---------------
101 | 企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的xml串。
102 | 假设企业需要回复用户的明文如下:
103 |
104 |
105 |
106 | 1348831860
107 |
108 |
109 | 1234567890123456
110 | 128
111 |
112 |
113 | 为了将此段明文回复给用户,企业应:
114 | 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
115 | 2.将明文加密得到密文。
116 | 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。
117 | 4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业。
118 | 以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
119 | */
120 | respData := "13488318601234567890123456128"
121 | encryptMsg, cryptErr := wxcpt.EncryptMsg(respData, reqTimestamp, reqNonce)
122 | if nil != cryptErr {
123 | fmt.Println("DecryptMsg fail", cryptErr)
124 | }
125 |
126 | sEncryptMsg := string(encryptMsg)
127 | fmt.Println("after encrypt sEncryptMsg: ", sEncryptMsg)
128 | // 加密成功
129 | // TODO:
130 | // HttpUtils.SetResponse(sEncryptMsg)
131 | }
132 |
--------------------------------------------------------------------------------
/wxbizmsgcrypt/wxbizmsgcrypt.go:
--------------------------------------------------------------------------------
1 | package wxbizmsgcrypt
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/sha1"
8 | "encoding/base64"
9 | "encoding/binary"
10 | "encoding/xml"
11 | "fmt"
12 | "math/rand"
13 | "sort"
14 | "strings"
15 | )
16 |
17 | const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
18 |
19 | const (
20 | ValidateSignatureError int = -40001
21 | ParseXmlError int = -40002
22 | ComputeSignatureError int = -40003
23 | IllegalAesKey int = -40004
24 | ValidateCorpidError int = -40005
25 | EncryptAESError int = -40006
26 | DecryptAESError int = -40007
27 | IllegalBuffer int = -40008
28 | EncodeBase64Error int = -40009
29 | DecodeBase64Error int = -40010
30 | GenXmlError int = -40010
31 | ParseJsonError int = -40012
32 | GenJsonError int = -40013
33 | IllegalProtocolType int = -40014
34 | )
35 |
36 | type ProtocolType int
37 |
38 | const (
39 | XmlType ProtocolType = 1
40 | )
41 |
42 | type CryptError struct {
43 | ErrCode int
44 | ErrMsg string
45 | }
46 |
47 | func NewCryptError(err_code int, err_msg string) *CryptError {
48 | return &CryptError{ErrCode: err_code, ErrMsg: err_msg}
49 | }
50 |
51 | type WXBizMsg4Recv struct {
52 | Tousername string `xml:"ToUserName"`
53 | Encrypt string `xml:"Encrypt"`
54 | Agentid string `xml:"AgentID"`
55 | }
56 |
57 | type CDATA struct {
58 | Value string `xml:",cdata"`
59 | }
60 |
61 | type WXBizMsg4Send struct {
62 | XMLName xml.Name `xml:"xml"`
63 | Encrypt CDATA `xml:"Encrypt"`
64 | Signature CDATA `xml:"MsgSignature"`
65 | Timestamp string `xml:"TimeStamp"`
66 | Nonce CDATA `xml:"Nonce"`
67 | }
68 |
69 | func NewWXBizMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizMsg4Send {
70 | return &WXBizMsg4Send{Encrypt: CDATA{Value: encrypt}, Signature: CDATA{Value: signature}, Timestamp: timestamp, Nonce: CDATA{Value: nonce}}
71 | }
72 |
73 | type ProtocolProcessor interface {
74 | parse(src_data []byte) (*WXBizMsg4Recv, *CryptError)
75 | serialize(msg_send *WXBizMsg4Send) ([]byte, *CryptError)
76 | }
77 |
78 | type WXBizMsgCrypt struct {
79 | token string
80 | encoding_aeskey string
81 | receiver_id string
82 | protocol_processor ProtocolProcessor
83 | }
84 |
85 | type XmlProcessor struct {
86 | }
87 |
88 | func (self *XmlProcessor) parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) {
89 | var msg4_recv WXBizMsg4Recv
90 | err := xml.Unmarshal(src_data, &msg4_recv)
91 | if nil != err {
92 | return nil, NewCryptError(ParseXmlError, "xml to msg fail")
93 | }
94 | return &msg4_recv, nil
95 | }
96 |
97 | func (self *XmlProcessor) serialize(msg4_send *WXBizMsg4Send) ([]byte, *CryptError) {
98 | xml_msg, err := xml.Marshal(msg4_send)
99 | if nil != err {
100 | return nil, NewCryptError(GenXmlError, err.Error())
101 | }
102 | return xml_msg, nil
103 | }
104 |
105 | func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt {
106 | var protocol_processor ProtocolProcessor
107 | if protocol_type != XmlType {
108 | panic("unsupport protocal")
109 | } else {
110 | protocol_processor = new(XmlProcessor)
111 | }
112 |
113 | return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor}
114 | }
115 |
116 | func (self *WXBizMsgCrypt) randString(n int) string {
117 | b := make([]byte, n)
118 | for i := range b {
119 | b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
120 | }
121 | return string(b)
122 | }
123 |
124 | func (self *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte {
125 | padding := block_size - (len(plaintext) % block_size)
126 | padtext := bytes.Repeat([]byte{byte(padding)}, padding)
127 | var buffer bytes.Buffer
128 | buffer.WriteString(plaintext)
129 | buffer.Write(padtext)
130 | return buffer.Bytes()
131 | }
132 |
133 | func (self *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) {
134 | plaintext_len := len(plaintext)
135 | if nil == plaintext || plaintext_len == 0 {
136 | return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero")
137 | }
138 | if plaintext_len%block_size != 0 {
139 | return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size")
140 | }
141 | padding_len := int(plaintext[plaintext_len-1])
142 | return plaintext[:plaintext_len-padding_len], nil
143 | }
144 |
145 | func (self *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) {
146 | aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
147 | if nil != err {
148 | return nil, NewCryptError(DecodeBase64Error, err.Error())
149 | }
150 | const block_size = 32
151 | pad_msg := self.pKCS7Padding(plaintext, block_size)
152 |
153 | block, err := aes.NewCipher(aeskey)
154 | if err != nil {
155 | return nil, NewCryptError(EncryptAESError, err.Error())
156 | }
157 |
158 | ciphertext := make([]byte, len(pad_msg))
159 | iv := aeskey[:aes.BlockSize]
160 |
161 | mode := cipher.NewCBCEncrypter(block, iv)
162 |
163 | mode.CryptBlocks(ciphertext, pad_msg)
164 | base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext)))
165 | base64.StdEncoding.Encode(base64_msg, ciphertext)
166 |
167 | return base64_msg, nil
168 | }
169 |
170 | func (self *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) {
171 | aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
172 | if nil != err {
173 | return nil, NewCryptError(DecodeBase64Error, err.Error())
174 | }
175 |
176 | encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg)
177 | if nil != err {
178 | return nil, NewCryptError(DecodeBase64Error, err.Error())
179 | }
180 |
181 | block, err := aes.NewCipher(aeskey)
182 | if err != nil {
183 | return nil, NewCryptError(DecryptAESError, err.Error())
184 | }
185 |
186 | if len(encrypt_msg) < aes.BlockSize {
187 | return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid")
188 | }
189 |
190 | iv := aeskey[:aes.BlockSize]
191 |
192 | if len(encrypt_msg)%aes.BlockSize != 0 {
193 | return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size")
194 | }
195 |
196 | mode := cipher.NewCBCDecrypter(block, iv)
197 |
198 | mode.CryptBlocks(encrypt_msg, encrypt_msg)
199 |
200 | return encrypt_msg, nil
201 | }
202 |
203 | func (self *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string {
204 | sort_arr := []string{self.token, timestamp, nonce, data}
205 | sort.Strings(sort_arr)
206 | var buffer bytes.Buffer
207 | for _, value := range sort_arr {
208 | buffer.WriteString(value)
209 | }
210 |
211 | sha := sha1.New()
212 | sha.Write(buffer.Bytes())
213 | signature := fmt.Sprintf("%x", sha.Sum(nil))
214 | return string(signature)
215 | }
216 |
217 | func (self *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) {
218 | const block_size = 32
219 | plaintext, err := self.pKCS7Unpadding(plaintext, block_size)
220 | if nil != err {
221 | return nil, 0, nil, nil, err
222 | }
223 |
224 | text_len := uint32(len(plaintext))
225 | if text_len < 20 {
226 | return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1")
227 | }
228 | random := plaintext[:16]
229 | msg_len := binary.BigEndian.Uint32(plaintext[16:20])
230 | if text_len < (20 + msg_len) {
231 | return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2")
232 | }
233 |
234 | msg := plaintext[20 : 20+msg_len]
235 | receiver_id := plaintext[20+msg_len:]
236 |
237 | return random, msg_len, msg, receiver_id, nil
238 | }
239 |
240 | func (self *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) {
241 | signature := self.calSignature(timestamp, nonce, echostr)
242 |
243 | if strings.Compare(signature, msg_signature) != 0 {
244 | return nil, NewCryptError(ValidateSignatureError, "signature not equal")
245 | }
246 |
247 | plaintext, err := self.cbcDecrypter(echostr)
248 | if nil != err {
249 | return nil, err
250 | }
251 |
252 | _, _, msg, receiver_id, err := self.ParsePlainText(plaintext)
253 | if nil != err {
254 | return nil, err
255 | }
256 |
257 | if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
258 | fmt.Println(string(receiver_id), self.receiver_id, len(receiver_id), len(self.receiver_id))
259 | return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
260 | }
261 |
262 | return msg, nil
263 | }
264 |
265 | func (self *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) {
266 | rand_str := self.randString(16)
267 | var buffer bytes.Buffer
268 | buffer.WriteString(rand_str)
269 |
270 | msg_len_buf := make([]byte, 4)
271 | binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg)))
272 | buffer.Write(msg_len_buf)
273 | buffer.WriteString(reply_msg)
274 | buffer.WriteString(self.receiver_id)
275 |
276 | tmp_ciphertext, err := self.cbcEncrypter(buffer.String())
277 | if nil != err {
278 | return nil, err
279 | }
280 | ciphertext := string(tmp_ciphertext)
281 |
282 | signature := self.calSignature(timestamp, nonce, ciphertext)
283 |
284 | msg4_send := NewWXBizMsg4Send(ciphertext, signature, timestamp, nonce)
285 | return self.protocol_processor.serialize(msg4_send)
286 | }
287 |
288 | func (self *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) {
289 | msg4_recv, crypt_err := self.protocol_processor.parse(post_data)
290 | if nil != crypt_err {
291 | return nil, crypt_err
292 | }
293 |
294 | signature := self.calSignature(timestamp, nonce, msg4_recv.Encrypt)
295 |
296 | if strings.Compare(signature, msg_signature) != 0 {
297 | return nil, NewCryptError(ValidateSignatureError, "signature not equal")
298 | }
299 |
300 | plaintext, crypt_err := self.cbcDecrypter(msg4_recv.Encrypt)
301 | if nil != crypt_err {
302 | return nil, crypt_err
303 | }
304 |
305 | _, _, msg, receiver_id, crypt_err := self.ParsePlainText(plaintext)
306 | if nil != crypt_err {
307 | return nil, crypt_err
308 | }
309 |
310 | if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
311 | return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
312 | }
313 |
314 | return msg, nil
315 | }
316 |
--------------------------------------------------------------------------------