// Package wecom provides cryptographic utilities for WeChat Work (WeCom) message encryption and decryption. // It implements the WXBizMsgCrypt algorithm for secure message handling with WeChat Work APIs. package wecom import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha1" "encoding/base64" "encoding/binary" "encoding/json" "errors" "fmt" "math/big" "sort" "strings" "time" ) const ( WXBizMsgCrypt_OK = 0 WXBizMsgCrypt_ValidateSignature_Error = 40001 WXBizMsgCrypt_ParseJson_Error = 40002 WXBizMsgCrypt_ComputeSignature_Error = 40003 WXBizMsgCrypt_IllegalAesKey = 40004 WXBizMsgCrypt_EncryptAES_Error = 40005 WXBizMsgCrypt_DecryptAES_Error = 40006 WXBizMsgCrypt_IllegalBuffer = 40007 WXBizMsgCrypt_ValidateCorpid_Error = 40008 WXBizMsgCrypt_ValidateCorpid_Receive_Id = 40009 WXBizMsgCrypt_ValidateCorpid_Mismatch = 40010 ) var wecomErrorMessages = map[int]string{ WXBizMsgCrypt_OK: "success", WXBizMsgCrypt_ValidateSignature_Error: "signature validation failed", WXBizMsgCrypt_ParseJson_Error: "invalid JSON format", WXBizMsgCrypt_ComputeSignature_Error: "signature computation failed", WXBizMsgCrypt_IllegalAesKey: "illegal AES key", WXBizMsgCrypt_EncryptAES_Error: "AES encryption failed", WXBizMsgCrypt_DecryptAES_Error: "AES decryption failed", WXBizMsgCrypt_IllegalBuffer: "illegal buffer format", WXBizMsgCrypt_ValidateCorpid_Error: "corp ID validation failed", WXBizMsgCrypt_ValidateCorpid_Receive_Id: "receive ID validation failed", WXBizMsgCrypt_ValidateCorpid_Mismatch: "corp ID mismatch", } func (c *AIBotClient) getErrorMessage(code int) error { if msg, ok := wecomErrorMessages[code]; ok { return fmt.Errorf("wecom error (code %d): %s", code, msg) } return fmt.Errorf("unknown wecom error: %d", code) } var ErrFormat = errors.New("format error") // SHA1 负责生成安全签名(sha1) type SHA1 struct{} // GetSHA1 : 对 token, timestamp, nonce, encrypt 排序后 sha1 // 返回 (code, signature) func (s *SHA1) GetSHA1(token, timestamp, nonce string, encrypt interface{}) (int, string) { defer func() { // no panic propagation in this helper; but keep signature simple }() encStr := "" switch v := encrypt.(type) { case string: encStr = v case []byte: encStr = string(v) case nil: encStr = "" default: encStr = fmt.Sprint(v) } list := []string{token, timestamp, nonce, encStr} sort.Strings(list) joined := strings.Join(list, "") h := sha1.New() _, err := h.Write([]byte(joined)) if err != nil { return WXBizMsgCrypt_ComputeSignature_Error, "" } return WXBizMsgCrypt_OK, fmt.Sprintf("%x", h.Sum(nil)) } // JsonParse 提取/生成 json 消息 type JsonParse struct{} type aesTextResponse struct { Encrypt string `json:"encrypt"` MsgSignature string `json:"msgsignature"` Timestamp string `json:"timestamp"` Nonce string `json:"nonce"` } // Extract 从 json 字符串中提取 encrypt 字段 // 返回 (code, encrypt) func (jp *JsonParse) Extract(jsonText string) (int, string) { var m map[string]interface{} if err := json.Unmarshal([]byte(jsonText), &m); err != nil { return WXBizMsgCrypt_ParseJson_Error, "" } if v, ok := m["encrypt"].(string); ok { return WXBizMsgCrypt_OK, v } return WXBizMsgCrypt_ParseJson_Error, "" } // Generate 根据参数生成 json 字符串 func (jp *JsonParse) Generate(encrypt, signature, timestamp, nonce string) string { resp := aesTextResponse{ Encrypt: encrypt, MsgSignature: signature, Timestamp: timestamp, Nonce: nonce, } bs, _ := json.Marshal(resp) return string(bs) } // PKCS7Encoder 提供基于 PKCS7 的填充/去填充 type PKCS7Encoder struct { BlockSize int // 使用 32 与 Python 示例一致 } func NewPKCS7Encoder() *PKCS7Encoder { return &PKCS7Encoder{BlockSize: 32} } func (p *PKCS7Encoder) Encode(src []byte) []byte { if src == nil { src = []byte{} } n := len(src) amountToPad := p.BlockSize - (n % p.BlockSize) if amountToPad == 0 { amountToPad = p.BlockSize } pad := byte(amountToPad) padtext := bytes.Repeat([]byte{pad}, amountToPad) return append(src, padtext...) } func (p *PKCS7Encoder) Decode(decrypted []byte) ([]byte, error) { if len(decrypted) == 0 { return nil, nil } pad := int(decrypted[len(decrypted)-1]) if pad < 1 || pad > p.BlockSize { // 同 Python 逻辑:当 pad 值不合理时,视为 0(或 error) return decrypted, fmt.Errorf("invalid padding") } return decrypted[:len(decrypted)-pad], nil } // Prpcrypt 提供 AES 加解密功能 type Prpcrypt struct { Key []byte Mode string // not used but kept for parity } func NewPrpcrypt(key []byte) *Prpcrypt { return &Prpcrypt{Key: key, Mode: "CBC"} } // Encrypt 对明文加密,返回 (code, base64Ciphertext) func (pc *Prpcrypt) Encrypt(plainText string, receiveID string) (int, string) { // 将明文转换为 bytes txt := []byte(plainText) // 随机 16 字节数字字符串 rand16, err := getRandom16BytesAsDigits() if err != nil { return WXBizMsgCrypt_EncryptAES_Error, "" } // 包装: 16 bytes random + 4 bytes network-order(len) + txt + receiveid buf := bytes.NewBuffer(nil) buf.Write(rand16) // len(txt) 网络字节序 lenBuf := make([]byte, 4) // Python 示例使用 socket.htonl(len(text)),即 network order (big endian) binary.BigEndian.PutUint32(lenBuf, uint32(len(txt))) buf.Write(lenBuf) buf.Write(txt) buf.Write([]byte(receiveID)) raw := buf.Bytes() // PKCS7 pad 到 blocksize=32 encoder := NewPKCS7Encoder() padded := encoder.Encode(raw) // AES-CBC block, err := aes.NewCipher(pc.Key) if err != nil { return WXBizMsgCrypt_EncryptAES_Error, "" } iv := pc.Key[:16] if len(iv) < 16 { return WXBizMsgCrypt_IllegalAesKey, "" } mode := cipher.NewCBCEncrypter(block, iv) if len(padded)%block.BlockSize() != 0 { // 应该已经经过 pad return WXBizMsgCrypt_EncryptAES_Error, "" } ciphertext := make([]byte, len(padded)) mode.CryptBlocks(ciphertext, padded) enc := base64.StdEncoding.EncodeToString(ciphertext) return WXBizMsgCrypt_OK, enc } // Decrypt 解密 base64 文本,返回 (code, jsonContent) func (pc *Prpcrypt) Decrypt(base64Cipher string, receiveID string) (int, string) { cipherData, err := base64.StdEncoding.DecodeString(base64Cipher) if err != nil { return WXBizMsgCrypt_DecryptAES_Error, "" } block, err := aes.NewCipher(pc.Key) if err != nil { return WXBizMsgCrypt_DecryptAES_Error, "" } if len(cipherData)%block.BlockSize() != 0 { return WXBizMsgCrypt_DecryptAES_Error, "" } iv := pc.Key[:16] mode := cipher.NewCBCDecrypter(block, iv) plain := make([]byte, len(cipherData)) mode.CryptBlocks(plain, cipherData) // 去 PKCS7 填充 (blocksize=32) encoder := NewPKCS7Encoder() unpadded, err := encoder.Decode(plain) if err != nil { // Python 里如果 pad 错误会继续尝试并最后返回 IllegalBuffer // 这里直接返回 IllegalBuffer return WXBizMsgCrypt_IllegalBuffer, "" } // 去掉前 16 字节随机字符串 if len(unpadded) < 16 { return WXBizMsgCrypt_IllegalBuffer, "" } content := unpadded[16:] if len(content) < 4 { return WXBizMsgCrypt_IllegalBuffer, "" } // 前 4 字节为 network order 的 json length jsonLen := binary.BigEndian.Uint32(content[:4]) if int(jsonLen) > len(content)-4 { return WXBizMsgCrypt_IllegalBuffer, "" } jsonContent := string(content[4 : 4+jsonLen]) fromReceiveID := string(content[4+jsonLen:]) if fromReceiveID != receiveID { // receiveid 不匹配 return WXBizMsgCrypt_ValidateCorpid_Error, "" } return WXBizMsgCrypt_OK, jsonContent } // getRandom16BytesAsDigits 产生一个 16 字节的 ASCII 数字字符串(与 Python 版本行为一致) func getRandom16BytesAsDigits() ([]byte, error) { const digits = "0123456789" out := make([]byte, 16) for i := 0; i < 16; i++ { nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits)))) if err != nil { return nil, err } out[i] = digits[nBig.Int64()] } return out, nil } // WXBizJsonMsgCrypt 将整个流程封装:初始化时传入 token, encodingAESKey, receiveID type WXBizJsonMsgCrypt struct { Token string EncodingKey []byte ReceiveID string encodingAES string // 原始 sEncodingAESKey } // NewWXBizJsonMsgCrypt 构造:sToken, sEncodingAESKey, sReceiveID func NewWXBizJsonMsgCrypt(sToken, sEncodingAESKey, sReceiveID string) (*WXBizJsonMsgCrypt, int, error) { // Python 里是 base64.b64decode(sEncodingAESKey + "=") dec, err := base64.StdEncoding.DecodeString(sEncodingAESKey + "=") if err != nil { return nil, WXBizMsgCrypt_IllegalAesKey, fmt.Errorf("EncodingAESKey base64 decode fail: %w", err) } if len(dec) != 32 { return nil, WXBizMsgCrypt_IllegalAesKey, fmt.Errorf("EncodingAESKey decoded length must be 32 (got %d)", len(dec)) } return &WXBizJsonMsgCrypt{ Token: sToken, EncodingKey: dec, ReceiveID: sReceiveID, encodingAES: sEncodingAESKey, }, WXBizMsgCrypt_OK, nil } // VerifyURL 校验并解密 sEchoStr(用于首次验证 URL) // 返回 (code, sReplyEchoStr) func (w *WXBizJsonMsgCrypt) VerifyURL(sMsgSignature, sTimeStamp, sNonce, sEchoStr string) (int, string) { sha1 := &SHA1{} ret, signature := sha1.GetSHA1(w.Token, sTimeStamp, sNonce, sEchoStr) if ret != WXBizMsgCrypt_OK { return ret, "" } if signature != sMsgSignature { return WXBizMsgCrypt_ValidateSignature_Error, "" } pc := NewPrpcrypt(w.EncodingKey) ret, reply := pc.Decrypt(sEchoStr, w.ReceiveID) return ret, reply } // EncryptMsg 对要回复的消息 sReplyMsg(json 字符串)进行加密并生成外层 JSON 包装 // 返回 (code, generatedJson) func (w *WXBizJsonMsgCrypt) EncryptMsg(sReplyMsg, sNonce string, timestamp ...string) (int, string) { pc := NewPrpcrypt(w.EncodingKey) ret, encrypt := pc.Encrypt(sReplyMsg, w.ReceiveID) if ret != WXBizMsgCrypt_OK { return ret, "" } // encrypt 是 base64 字符串(已经),确保是字符串 encryptStr := encrypt ts := "" if len(timestamp) > 0 && timestamp[0] != "" { ts = timestamp[0] } else { ts = fmt.Sprintf("%d", time.Now().Unix()) } sha1 := &SHA1{} ret, signature := sha1.GetSHA1(w.Token, ts, sNonce, encryptStr) if ret != WXBizMsgCrypt_OK { return ret, "" } jp := &JsonParse{} jsonStr := jp.Generate(encryptStr, signature, ts, sNonce) return WXBizMsgCrypt_OK, jsonStr } // DecryptMsg 验证签名并解密 POST 的 json 数据包 // sPostData: POST 的 json 数据字符串(包含 encrypt 字段) // sMsgSignature: URL param msg_signature // sTimeStamp: timestamp // sNonce: nonce // 返回 (code, jsonContent) func (w *WXBizJsonMsgCrypt) DecryptMsg(sPostData, sMsgSignature, sTimeStamp, sNonce string) (int, string) { jp := &JsonParse{} ret, encrypt := jp.Extract(sPostData) if ret != WXBizMsgCrypt_OK { return ret, "" } sha1 := &SHA1{} ret, signature := sha1.GetSHA1(w.Token, sTimeStamp, sNonce, encrypt) if ret != WXBizMsgCrypt_OK { return ret, "" } if signature != sMsgSignature { return WXBizMsgCrypt_ValidateSignature_Error, "" } pc := NewPrpcrypt(w.EncodingKey) return pc.Decrypt(encrypt, w.ReceiveID) }