token = $token; $this->encodingAesKey = $encodingAesKey; $this->appid = $appid; // 解码EncodingAESKey:官方文档要求尾部补一个"="后做Base64解码 $key = $encodingAesKey . '='; $this->aesKey = base64_decode($key); if (strlen($this->aesKey) != 32) { throw new Exception('EncodingAESKey 解码后长度必须为32字节,当前长度为:' . strlen($this->aesKey)); } // AES CBC模式的IV取密钥的前16字节(官方示例代码标准做法) $this->iv = substr($this->aesKey, 0, 16); } /** * 验证消息签名(用于接收消息时) * @param string $timestamp URL中的timestamp参数 * @param string $nonce URL中的nonce参数 * @param string $encrypt 消息体中的Encrypt字段(密文) * @param string $msgSignature URL中的msg_signature参数 * @return bool 签名验证结果 */ public function verifySignature(string $timestamp, string $nonce, string $encrypt, string $msgSignature): bool { $signature = $this->generateSignature($timestamp, $nonce, $encrypt); return $signature === $msgSignature; } /** * 解密消息(安全模式接收消息时调用) * @param string $encrypt 消息体中的Encrypt字段(Base64密文) * @return string 解密后的明文消息(XML或JSON格式) * @throws Exception 当解密失败或AppId不匹配时抛出异常 */ public function decryptMsg(string $encrypt): string { // 1. Base64解码 $encryptData = base64_decode($encrypt); if ($encryptData === false) { throw new Exception('Base64解码失败'); } // 2. AES解密(CBC模式,PKCS#7填充) $decrypted = openssl_decrypt($encryptData, 'AES-256-CBC', $this->aesKey, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->iv); if ($decrypted === false) { throw new Exception('AES解密失败'); } // 3. 去除PKCS#7填充 $result = $this->pkcs7Unpad($decrypted); // 4. 解析明文结构:16字节随机字符串 + 4字节网络字节序长度 + 消息内容 + AppId if (strlen($result) < 20) { // 至少需要16+4=20字节 throw new Exception('解密后的数据长度异常'); } $random = substr($result, 0, 16); // 16字节随机字符串(可忽略) $msgLen = unpack('N', substr($result, 16, 4))[1]; // 4字节网络字节序长度 $msg = substr($result, 20, $msgLen); // 消息内容 $appid = substr($result, 20 + $msgLen); // 剩余的AppId // 5. 验证AppId是否匹配 if ($appid !== $this->appid) { throw new Exception('AppId不匹配,可能被篡改'); } return $msg; } /** * 加密消息(安全模式回复消息时调用) * @param string $replyMsg 待回复的明文消息(XML或JSON格式) * @param int|null $timestamp 时间戳(可空,为空则自动生成) * @param string|null $nonce 随机字符串(可空,为空则自动生成) * @return array 包含Encrypt, MsgSignature, TimeStamp, Nonce的关联数组 * @throws Exception 当加密失败时抛出异常 */ public function encryptMsg(string $replyMsg, ?int $timestamp = null, ?string $nonce = null): array { // 1. 生成16字节随机字符串 $random = openssl_random_pseudo_bytes(16); // 2. 计算消息长度(网络字节序) $msgLen = pack('N', strlen($replyMsg)); // 3. 构造待加密的字符串:随机字符串(16B) + 消息长度(4B) + 消息内容 + AppId $plainText = $random . $msgLen . $replyMsg . $this->appid; // 4. PKCS#7填充(按16字节块) $padded = $this->pkcs7Pad($plainText); // 5. AES加密 $encrypted = openssl_encrypt($padded, 'AES-256-CBC', $this->aesKey, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->iv); if ($encrypted === false) { throw new Exception('AES加密失败'); } // 6. Base64编码得到Encrypt字段 $encrypt = base64_encode($encrypted); // 7. 生成时间戳和随机数 $timestamp = $timestamp ?: time(); $nonce = $nonce ?: $this->generateNonce(); // 8. 生成消息签名 $msgSignature = $this->generateSignature($timestamp, $nonce, $encrypt); // 9. 返回加密后的数据包 return [ 'Encrypt' => $encrypt, 'MsgSignature' => $msgSignature, 'TimeStamp' => $timestamp, 'Nonce' => $nonce, ]; } /* -------------------- 内部辅助方法 -------------------- */ /** * 生成消息签名(使用token、timestamp、nonce、encrypt) * @param string $timestamp 时间戳 * @param string $nonce 随机数 * @param string $encrypt 密文 * @return string 签名字符串 */ private function generateSignature(string $timestamp, string $nonce, string $encrypt): string { $params = [$this->token, $timestamp, $nonce, $encrypt]; sort($params, SORT_STRING); // 字典序排序 $str = implode('', $params); // 拼接字符串 return sha1($str); // SHA1哈希 } /** * 生成随机字符串(用于nonce) * @param int $length 长度 * @return string */ private function generateNonce(int $length = 8): string { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $nonce = ''; for ($i = 0; $i < $length; $i++) { $nonce .= $chars[mt_rand(0, strlen($chars) - 1)]; } return $nonce; } /** * PKCS#7填充(按16字节块) * @param string $text 待填充的数据 * @return string 填充后的数据 */ private function pkcs7Pad(string $text): string { $blockSize = 16; // AES块大小固定16字节 $padLen = $blockSize - (strlen($text) % $blockSize); $pad = str_repeat(chr($padLen), $padLen); return $text . $pad; } /** * 去除PKCS#7填充 * @param string $text 解密后的数据(已去除OPENSSL_ZERO_PADDING的填充) * @return string 去除填充后的数据 * @throws Exception 当填充格式错误时抛出异常 */ private function pkcs7Unpad(string $text): string { $len = strlen($text); if ($len == 0) { return $text; } $padLen = ord($text[$len - 1]); // 最后一个字节表示填充的长度 if ($padLen < 1 || $padLen > 16) { throw new Exception('无效的PKCS#7填充'); } // 验证填充字节是否一致(简化验证,可根据需要加强) for ($i = 0; $i < $padLen; $i++) { if (ord($text[$len - 1 - $i]) != $padLen) { throw new Exception('PKCS#7填充验证失败'); } } return substr($text, 0, $len - $padLen); } }