Files
junhong_cmp_fiber/internal/infrastructure/wecom/callback_crypto.go
break 73f5125d3d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
2026-07-25 17:06:58 +08:00

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
}