|
@@ -4,9 +4,8 @@ namespace App\Service;
|
|
|
|
|
|
class DingTalkCrypto
|
|
|
{
|
|
|
- protected $token;
|
|
|
- protected $encodingAesKey; // URL safe base64 字符串,长度 43
|
|
|
- // protected $corpid; // 如果需要
|
|
|
+ protected string $token;
|
|
|
+ protected string $encodingAesKey; // URL safe base64 字符串,长度 43
|
|
|
|
|
|
public function __construct(string $token, string $encodingAesKey)
|
|
|
{
|
|
@@ -15,118 +14,98 @@ class DingTalkCrypto
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 验签 + 解密消息体
|
|
|
+ * 解密钉钉事件回调消息
|
|
|
*
|
|
|
- * @param string $signature 从请求 query 参数 signature
|
|
|
- * @param string $timestamp 从请求参数 timestamp
|
|
|
- * @param string $nonce 从请求参数 nonce
|
|
|
- * @param string $encrypt 消息体中的 encrypt 字段
|
|
|
- * @return array|false 解密后的数组,失败返回 false
|
|
|
+ * @param string $signature URL参数 signature
|
|
|
+ * @param string $timestamp URL参数 timestamp
|
|
|
+ * @param string $nonce URL参数 nonce
|
|
|
+ * @param string $encrypt 消息体 encrypt
|
|
|
+ * @return array|false
|
|
|
*/
|
|
|
public function decryptMsg(string $signature, string $timestamp, string $nonce, string $encrypt)
|
|
|
{
|
|
|
- // 1. 验签
|
|
|
- $tmpArr = [$this->token, $timestamp, $nonce, $encrypt];
|
|
|
- sort($tmpArr, SORT_STRING);
|
|
|
- $tmpStr = implode($tmpArr);
|
|
|
+ // 1. 验签(官方:token + timestamp + nonce + encrypt)
|
|
|
+ $tmpStr = $this->token . $timestamp . $nonce . $encrypt;
|
|
|
$hash = sha1($tmpStr);
|
|
|
if ($hash !== $signature) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- // 2. 解密
|
|
|
- $aesKey = base64_decode($this->encodingAesKey . '='); // 注意要加一个 '=' 补足 padding
|
|
|
+ // 2. 解密 AES-256-CBC
|
|
|
+ $aesKey = base64_decode($this->encodingAesKey); // 官方不加 "="
|
|
|
$iv = substr($aesKey, 0, 16);
|
|
|
-
|
|
|
- // 解密,使用 AES-256-CBC
|
|
|
$cipherText = base64_decode($encrypt);
|
|
|
+
|
|
|
$decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);
|
|
|
- if ($decrypted === false) {
|
|
|
- return false;
|
|
|
- }
|
|
|
+ if ($decrypted === false) return false;
|
|
|
|
|
|
- // 去掉 padding, PKCS#7
|
|
|
+ // 3. 去 padding
|
|
|
$decrypted = $this->pkcs7Unpad($decrypted);
|
|
|
|
|
|
- // 解出: 16 位随机字符串 + 4 字节内容长度 network-order + 内容 + corpId(如果有的话)
|
|
|
- // 跳过前 16 位
|
|
|
+ // 4. 去掉前 16 位随机字符串
|
|
|
$content = substr($decrypted, 16);
|
|
|
- // 读长度
|
|
|
+
|
|
|
+ // 5. 读取消息长度
|
|
|
$lenList = unpack("N", substr($content, 0, 4));
|
|
|
- $xmlLen = $lenList[1];
|
|
|
- $xmlContent = substr($content, 4, $xmlLen);
|
|
|
- // 如果有 corpId 在 xmlContent 后还有
|
|
|
- // $fromCorpId = substr($content, 4 + $xmlLen);
|
|
|
-
|
|
|
- $json = $xmlContent; // 钉钉回调 body 是 JSON 字符串
|
|
|
- // 解析
|
|
|
- $arr = json_decode($json, true);
|
|
|
- return $arr;
|
|
|
+ $jsonLen = $lenList[1];
|
|
|
+
|
|
|
+ // 6. 获取消息 JSON
|
|
|
+ $json = substr($content, 4, $jsonLen);
|
|
|
+ return json_decode($json, true);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 加密回复消息,比如返回 “success”
|
|
|
+ * 加密返回给钉钉的消息
|
|
|
*
|
|
|
- * @param string $replyMsg 明文消息,比如 "success"
|
|
|
+ * @param string $replyMsg 明文消息
|
|
|
* @param string $timestamp
|
|
|
* @param string $nonce
|
|
|
- * @return array 包含加密后json结构
|
|
|
+ * @return array
|
|
|
*/
|
|
|
- public function encryptMsg(string $replyMsg, string $timestamp, string $nonce)
|
|
|
+ public function encryptMsg(string $replyMsg, string $timestamp, string $nonce): array
|
|
|
{
|
|
|
- // 1. 构造消息内容
|
|
|
$random16 = $this->getRandomStr(16);
|
|
|
- $replyMsgXml = $replyMsg; // 钉钉回调用 JSON 格式内容
|
|
|
- $msgLen = strlen($replyMsgXml);
|
|
|
- $msgLenBin = pack("N", $msgLen);
|
|
|
- $corpId = ''; // 如果不需要 corpId,可以空
|
|
|
+ $msgLenBin = pack("N", strlen($replyMsg));
|
|
|
+ $corpId = ''; // 不需要corpId
|
|
|
|
|
|
- $plain = $random16 . $msgLenBin . $replyMsgXml . $corpId;
|
|
|
+ $plain = $random16 . $msgLenBin . $replyMsg . $corpId;
|
|
|
|
|
|
- // 2. PKCS#7 padding
|
|
|
+ // PKCS#7 padding
|
|
|
$blockSize = 32;
|
|
|
$pad = $blockSize - (strlen($plain) % $blockSize);
|
|
|
- if ($pad == 0) {
|
|
|
- $pad = $blockSize;
|
|
|
- }
|
|
|
+ $pad = $pad === 0 ? $blockSize : $pad;
|
|
|
$plainPadded = $plain . str_repeat(chr($pad), $pad);
|
|
|
|
|
|
- // 3. AES 加密
|
|
|
- $aesKey = base64_decode($this->encodingAesKey . '=');
|
|
|
+ // AES 加密
|
|
|
+ $aesKey = base64_decode($this->encodingAesKey);
|
|
|
$iv = substr($aesKey, 0, 16);
|
|
|
$encrypted = openssl_encrypt($plainPadded, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);
|
|
|
$encryptBase64 = base64_encode($encrypted);
|
|
|
|
|
|
- // 4. 签名
|
|
|
- $signatureArr = [$this->token, $timestamp, $nonce, $encryptBase64];
|
|
|
- sort($signatureArr, SORT_STRING);
|
|
|
- $signature = sha1(implode($signatureArr));
|
|
|
+ // 计算 msg_signature
|
|
|
+ $signatureStr = $this->token . $timestamp . $nonce . $encryptBase64;
|
|
|
+ $msgSignature = sha1($signatureStr);
|
|
|
|
|
|
- // 5. 返回结构
|
|
|
return [
|
|
|
- 'msg_signature' => $signature,
|
|
|
+ 'msg_signature' => $msgSignature,
|
|
|
'encrypt' => $encryptBase64
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- private function getRandomStr($length = 16)
|
|
|
+ private function getRandomStr(int $length = 16): string
|
|
|
{
|
|
|
$chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
$str = '';
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
|
- $str .= $chars[ord(bin2hex(random_bytes(1))) % strlen($chars)];
|
|
|
+ $str .= $chars[random_int(0, strlen($chars) - 1)];
|
|
|
}
|
|
|
return $str;
|
|
|
}
|
|
|
|
|
|
- private function pkcs7Unpad($text)
|
|
|
+ private function pkcs7Unpad(string $text)
|
|
|
{
|
|
|
- // 去掉结尾的 padding
|
|
|
$pad = ord(substr($text, -1));
|
|
|
- if ($pad < 1 || $pad > 32) {
|
|
|
- // padding 有问题
|
|
|
- return false;
|
|
|
- }
|
|
|
+ if ($pad < 1 || $pad > 32) return false;
|
|
|
return substr($text, 0, -$pad);
|
|
|
}
|
|
|
}
|