├── 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 | --------------------------------------------------------------------------------