All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
86 lines
3.3 KiB
Go
86 lines
3.3 KiB
Go
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
|
|
}
|