Skip to content

事件安全校验

协作开放平台推送的事件消息,均会经过加密。应用接收到的消息体参数如下:

参数参数类型是否必带说明
idstringtrue消息 id
topicstringtrue消息主题
operationstringtrue消息变更动作
timeint64true时间(秒为单位的时间戳)
noncestringtrueiv 向量(解密时使用)
signaturestringtrue消息签名
encrypted_datastringtrue消息变更的加密字段

应用需根据协作官方提供的解密算法,对加密消息体进行解密,方可得到消息内容。

解密算法(Golang)

go
import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/hmac"
    "crypto/md5"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "errors"
)

func Encrypt(rawData []byte, cipher, nonce string) (string, error) {
    data, err := AESCBCEncrypt(rawData, []byte(cipher), []byte(nonce))
    if err != nil {
        return "", err
    }

    return base64.StdEncoding.EncodeToString(data), nil
}

func Decrypt(encryptedData, cipher, nonce string) (string, error) {
    data, err := base64.StdEncoding.DecodeString(encryptedData)
    if err != nil {
        return "", err
    }
    rawData, err := AESCBCPKCS7Decrypt(data, []byte(cipher), []byte(nonce))
    if err != nil {
        return "", err
    }

    return string(rawData), nil
}

func AESCBCEncrypt(rawData, key, nonce []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }

    blockSize := block.BlockSize()
    rawData = PKCS7Padding(rawData, blockSize)
    cipherText := make([]byte, len(rawData))

    iv := nonce[:blockSize]
    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(cipherText, rawData)

    return cipherText, nil
}

func AESCBCPKCS7Decrypt(encryptData, key, nonce []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }

    blockSize := block.BlockSize()
    if len(encryptData) < blockSize {
        return nil, errors.New("cipher text too short")
    }
    if len(encryptData)%blockSize != 0 {
        return nil, errors.New("cipher text is not a multiple of the block size")
    }

    iv := nonce[:blockSize]
    mode := cipher.NewCBCDecrypter(block, iv)
    mode.CryptBlocks(encryptData, encryptData)

    encryptData = PKCS7UnPadding(encryptData)
    return encryptData, nil
}

func PKCS7Padding(cipherText []byte, blockSize int) []byte {
    padding := blockSize - len(cipherText)%blockSize
    paddingText := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(cipherText, paddingText...)
}

func PKCS7UnPadding(origData []byte) []byte {
    length := len(origData)
    unPadding := int(origData[length-1])
    return origData[:(length - unPadding)]
}

func Md5(s string) string {
    h := md5.New()
    h.Write([]byte(s))
    return hex.EncodeToString(h.Sum(nil))
}

func HmacSha256(message string, secret string) string {
    key := []byte(secret)
    h := hmac.New(sha256.New, key)
    h.Write([]byte(message))
    return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

解密算法(Python)

python
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import common
import hashlib
import base64
import hmac
from Crypto.Cipher import AES


app_id  = "xxxxxx"
app_key = "xxxxxxxxxx"

def testParsePlusCompanyMsg():
    data = {
        "topic": "wps.open.plus.company",
        "operation": "update",
        "time": 1592275396,
        "nonce": "aae******3bb2a6",
        "signature": "zbwP0rFm7T******CMbNQIHuX-UU",
        "encrypted_data": "79Nsnsdq******fK2lZZFMQ==",
    }

    content = app_id + ":" + data.get("topic") + ":" + data.get("nonce") + ":" + str(data.get("time")) + ":" + data.get("encrypted_data")
    signature = hmac_sha256(content, app_key)
    if signature != data.get("signature"):
        return ;

    cipher = hashlib.md5(app_key.encode('utf-8')).hexdigest()
    decrypyed_data = decrypt(data.get("encrypted_data"), cipher, data.get("nonce"))

    print("decrypyed_data: " + decrypyed_data)


def hmac_sha256(message, secret):
    message = message.encode('utf-8')
    secret = secret.encode('utf-8')

    signature = base64.urlsafe_b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
    return signature.decode("utf-8").rstrip('=')


def decrypt(encryted_data, cipher, nonce):
    aes = AES.new(cipher.encode("utf-8"), AES.MODE_CBC, nonce.encode("utf-8"))
    decrypted_text = aes.decrypt(base64.decodebytes(bytes(encryted_data, encoding='utf8'))).decode("utf8")
    decrypted_text = decrypted_text[:-ord(decrypted_text[-1])]

    return decrypted_text

解密算法(Java)

java
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import sun.misc.BASE64Decoder;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class DecodeDemo {
    static String appId = "AK******";
    static String appKey = "d1202a4b******7254bd3404c";

    public static void main(String[] args) {
        /**
         * data = {
         *  "topic": "wps.open.plus.company",
         *  "operation": "update",
         *  "time": 1592275396,
         *  "nonce": "aae******3bb2a6",
         *  "signature": "zbwP0rFm7T******CMbNQIHuX-UU",
         *  "encrypted_data": "79Nsnsdq******fK2lZZFMQ==",
         * }
         */
        String str = "{\"topic\":\"wps.open.app.ticket\",\"operation\":\"create\",\"time\":1650349794,\"nonce\":\"aae******3bb2a6\",\"signature\":\"zbwP0rFm7T******CMbNQIHuX-UU\",\"encrypted_data\":\"79Nsnsdq******fK2lZZFMQ==\"}";
        Data data = new Gson().fromJson(str, Data.class);

        String content = appId + ":" + data.topic + ":" + data.nonce + ":" + data.time + ":" + data.encryptedData;
        String signature = null;
        try {
            signature = macSha256(content.getBytes(StandardCharsets.UTF_8), appKey.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
        }

        if (!data.signature.equals(signature)) {
            throw new RuntimeException("signature invalidate");
        }

        String cipher = DigestUtils.md5Hex(appKey.getBytes(StandardCharsets.UTF_8));
        try {
            String businessData = decrypt(data.encryptedData, cipher, data.nonce);
            //todo handle business data
        } catch (NoSuchPaddingException | NoSuchAlgorithmException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IOException e) {
            e.printStackTrace();
        }
    }

    public static String macSha256(byte[] data, byte[] secret) throws NoSuchAlgorithmException, InvalidKeyException {
        SecretKeySpec secret_key = new SecretKeySpec(secret, "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(secret_key);
        byte[] bytes = mac.doFinal(data);
        String str = Base64.getUrlEncoder().encodeToString(bytes);
        return StringUtils.strip(str, "=");
    }

    public static String decrypt(String encrytedData, String sk, String nonce) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IOException {
        byte[] raw = sk.getBytes(StandardCharsets.UTF_8);
        SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
        byte[] ivBytes = Arrays.copyOfRange(nonce.getBytes(StandardCharsets.UTF_8), 0, 16);
        IvParameterSpec iv = new IvParameterSpec(ivBytes);

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
        byte[] base64Byte = new BASE64Decoder().decodeBuffer(encrytedData);
        byte[] bytes = cipher.doFinal(base64Byte);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    static class Data {
        private String topic;
        private String operation;
        private int time;
        private String nonce;
        private String signature;
        @SerializedName(value = "encrypted_data")
        String encryptedData;
    }
}

消息解密示例(以 Golang 为例)

接收消息事件为例:

go
import (
    "encoding/json"
    "errors"
    "strconv"

    "github.com/gin-gonic/gin"
)

const (
    appId     = "xxxxxx"
    secretKey = "xxxxxxxxxx"
)

//收到的消息体
type Event struct {
    Topic         string `web:"topic"`
    Operation     string `web:"operation"`
    Time          int64  `web:"time"`
    Signature     string `web:"signature"`
    Nonce         string `web:"nonce"`
    EncryptedData string `web:"encrypted_data"`
}

//解密后的消息体
type MsgEventData struct {
    ChatId      int64       `json:"chat_id"`
    ChatType    uint8       `json:"chat_type"`
    CompanyId   int64       `json:"company_id"`
    Sender      *User       `json:"sender"`
    SendTime    int64       `json:"send_time"`
    MessageId   int64       `json:"message_id"`
    MessageType int64       `json:"message_type"`
    Content     interface{} `json:"content"`
    Mentions    []*User     `json:"mentions"`
}

type User struct {
    CompanyUid string `json:"company_uid"`
    CompanyId  string `json:"company_id"`
}

func openReceive(c *gin.Context) interface{} {
    event := &Event{}
    if err := c.ShouldBindJSON(event); err != nil {
        return err
    }

    content := appId + ":" + event.Topic + ":" + event.Nonce + ":" + strconv.FormatInt(event.Time, 10) + ":" + event.EncryptedData
    signature := HmacSha256(content, secretKey)
    if signature != event.Signature {
        return errors.New("Signature check faild")
    }

    cipher := Md5(secretKey)
    decryptData, err := Decrypt(event.EncryptedData, cipher, event.Nonce)
    if err != nil {
        return err
    }

    data := &MsgEventData{}
    err = json.Unmarshal([]byte(decryptData), data)
    if err != nil {
        return err
    }

    return nil
}