package wecom import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/sha1" "crypto/subtle" "encoding/base64" "encoding/binary" "encoding/hex" "sort" "strings" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // CallbackCrypto 实现企微回调要求的 SHA-1 签名校验和 AES-256-CBC 解密。 type CallbackCrypto struct { token string aesKey []byte receiveID string } // NewCallbackCrypto 创建企业微信回调加密器。 func NewCallbackCrypto(token, encodingAESKey, receiveID string) (*CallbackCrypto, error) { key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encodingAESKey) + "=") if err != nil || len(key) != 32 || strings.TrimSpace(token) == "" || strings.TrimSpace(receiveID) == "" { return nil, errors.New(errors.CodeWeComCredentialInvalid, "企业微信回调 Token、EncodingAESKey 或 receiveid 无效") } return &CallbackCrypto{token: token, aesKey: key, receiveID: receiveID}, nil } // VerifyAndDecrypt 校验签名并解密企业微信回调密文。 func (c *CallbackCrypto) VerifyAndDecrypt(signature, timestamp, nonce, encrypted string) ([]byte, error) { if c == nil || !c.validSignature(signature, timestamp, nonce, encrypted) { return nil, errors.New(errors.CodeUnauthorized, "企业微信回调签名无效") } ciphertext, err := base64.StdEncoding.DecodeString(encrypted) if err != nil || len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 { return nil, errors.New(errors.CodeInvalidParam, "企业微信回调密文无效") } block, err := aes.NewCipher(c.aesKey) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "初始化企业微信回调解密器失败") } plaintext := make([]byte, len(ciphertext)) cipher.NewCBCDecrypter(block, c.aesKey[:aes.BlockSize]).CryptBlocks(plaintext, ciphertext) plaintext, err = removePKCS7Padding(plaintext) if err != nil || len(plaintext) < 20 { return nil, errors.New(errors.CodeInvalidParam, "企业微信回调填充无效") } messageLength := int(binary.BigEndian.Uint32(plaintext[16:20])) if messageLength < 0 || 20+messageLength > len(plaintext) { return nil, errors.New(errors.CodeInvalidParam, "企业微信回调消息长度无效") } message := plaintext[20 : 20+messageLength] receiveID := plaintext[20+messageLength:] if subtle.ConstantTimeCompare(receiveID, []byte(c.receiveID)) != 1 { return nil, errors.New(errors.CodeUnauthorized, "企业微信回调 receiveid 不匹配") } return append([]byte(nil), message...), nil } func (c *CallbackCrypto) validSignature(signature, timestamp, nonce, encrypted string) bool { parts := []string{c.token, timestamp, nonce, encrypted} sort.Strings(parts) hash := sha1.Sum([]byte(strings.Join(parts, ""))) expected := hex.EncodeToString(hash[:]) return subtle.ConstantTimeCompare([]byte(strings.ToLower(signature)), []byte(expected)) == 1 } func removePKCS7Padding(value []byte) ([]byte, error) { if len(value) == 0 { return nil, errors.New(errors.CodeInvalidParam) } padding := int(value[len(value)-1]) if padding <= 0 || padding > 32 || padding > len(value) { return nil, errors.New(errors.CodeInvalidParam) } if !bytes.Equal(value[len(value)-padding:], bytes.Repeat([]byte{byte(padding)}, padding)) { return nil, errors.New(errors.CodeInvalidParam) } return value[:len(value)-padding], nil }