事件安全校验
协作开放平台推送的事件消息,均会经过加密。应用接收到的消息体参数如下:
| 参数 | 参数类型 | 是否必带 | 说明 |
|---|---|---|---|
| id | string | true | 消息 id |
| topic | string | true | 消息主题 |
| operation | string | true | 消息变更动作 |
| time | int64 | true | 时间(秒为单位的时间戳) |
| nonce | string | true | iv 向量(解密时使用) |
| signature | string | true | 消息签名 |
| encrypted_data | string | true | 消息变更的加密字段 |
应用需根据协作官方提供的解密算法,对加密消息体进行解密,方可得到消息内容。
解密算法(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
}