文档: 微信消息加解密官方文档
在文档中的SDK所使用的mcrypt 扩展从PHP 7.2起它将被从核心代码中移除并且移到PECL中。PHP手册在7.1迁移页面给出了替代方案,就是用OpenSSL取代MCrypt.
说明:二、抽离代码,直接对微信消息进行解密”中只是抽离了对消息加解密的部分,抽离的代码只是为了研究微信消息的加密算法,实际场景还需加上签名的验证,实际签名验证的代码请自行查看微信提供的SDK
一、快速替换
替换SDK中 pkcs7Encoder.php
的内容如下:
替换完就可以继续使用SDK了,下面的内容只是进一步的扩展,到这一步就完成对SDK的更改了。
<?php
include_once "errorCode.php";
/**
* PKCS7Encoder class
*
* 提供基于PKCS7算法的加解密接口.
*/
class PKCS7Encoder
{
public static $block_size = 32;
/**
* 对需要加密的明文进行填充补位
* @param $text 需要进行填充补位操作的明文
* @return 补齐明文字符串
*/
function encode($text)
{
$block_size = PKCS7Encoder::$block_size;
$text_length = strlen($text);
//计算需要填充的位数
$amount_to_pad = PKCS7Encoder::$block_size - ($text_length % PKCS7Encoder::$block_size);
if ($amount_to_pad == 0) {
$amount_to_pad = PKCS7Encoder::block_size;
}
//获得补位所用的字符
$pad_chr = chr($amount_to_pad);
$tmp = "";
for ($index = 0; $index < $amount_to_pad; $index++) {
$tmp .= $pad_chr;
}
return $text . $tmp;
}
/**
* 对解密后的明文进行补位删除
* @param decrypted 解密后的明文
* @return 删除填充补位后的明文
*/
function decode($text)
{
$pad = ord(substr($text, -1));
if ($pad < 1 || $pad > 32) {
$pad = 0;
}
return substr($text, 0, (strlen($text) - $pad));
}
}
/**
* Prpcrypt class
*
* 提供接收和推送给公众平台消息的加解密接口.
*/
class Prpcrypt
{
public $key;
function __construct($k)
{
$this->key = base64_decode($k . "=");
}
/**
* 微信旧版本: 对明文进行加密
* @param string $text 需要加密的明文
* @return string 加密后的密文
*/
// public function encrypt($text, $appid)
// {
//
// try {
// //获得16位随机字符串,填充到明文之前
// $random = $this->getRandomStr();
// $text = $random . pack("N", strlen($text)) . $text . $appid;
// // 网络字节序
// $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
// $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
// $iv = substr($this->key, 0, 16);
// //使用自定义的填充方式对明文进行补位填充
// $pkc_encoder = new PKCS7Encoder;
// $text = $pkc_encoder->encode($text);
// mcrypt_generic_init($module, $this->key, $iv);
// //加密
// $encrypted = mcrypt_generic($module, $text);
// mcrypt_generic_deinit($module);
// mcrypt_module_close($module);
//
// //print(base64_encode($encrypted));
// //使用BASE64对加密后的字符串进行编码
// return array(ErrorCode::$OK, base64_encode($encrypted));
// } catch (Exception $e) {
// //print $e;
// return array(ErrorCode::$EncryptAESError, null);
// }
//
// }
public function encrypt($text, $appid)
{
try {
$key = $this->key;
$random = $this->getRandomStr();
$text = $random.pack('N', strlen($text)).$text.$appid;
$padAmount = 32 - (strlen($text) % 32);
$padAmount = 0 !== $padAmount ? $padAmount : 32;
$padChr = chr($padAmount);
$tmp = '';
for ($index = 0; $index < $padAmount; ++$index) {
$tmp .= $padChr;
}
$text = $text.$tmp;
$iv = substr($key, 0, 16);
$encrypted = openssl_encrypt($text, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
return array(ErrorCode::$OK, base64_encode($encrypted));
} catch (Exception $e) {
return array(ErrorCode::$EncryptAESError, null);
}
}
/**
* 对密文进行解密
* @param string $encrypted 需要解密的密文
* @return string 解密得到的明文
*/
public function decrypt($encrypted, $appid)
{
//
// try {
// //使用BASE64对需要解密的字符串进行解码
// $ciphertext_dec = base64_decode($encrypted);
// $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
// $iv = substr($this->key, 0, 16);
// mcrypt_generic_init($module, $this->key, $iv);
//
// //解密
// $decrypted = mdecrypt_generic($module, $ciphertext_dec);
// mcrypt_generic_deinit($module);
// mcrypt_module_close($module);
// } catch (Exception $e) {
// return array(ErrorCode::$DecryptAESError, null);
// }
try {
$key = $this->key;
$ciphertext = base64_decode($encrypted, true);
$iv = substr($key, 0, 16);
$decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
} catch (Exception $e) {
return array(ErrorCode::$DecryptAESError, null);
}
try {
//去除补位字符
$pkc_encoder = new PKCS7Encoder;
$result = $pkc_encoder->decode($decrypted);
//去除16位随机字符串,网络字节序和AppId
if (strlen($result) < 16)
return "";
$content = substr($result, 16, strlen($result));
$len_list = unpack("N", substr($content, 0, 4));
$xml_len = $len_list[1];
$xml_content = substr($content, 4, $xml_len);
$from_appid = substr($content, $xml_len + 4);
} catch (Exception $e) {
//print $e;
return array(ErrorCode::$IllegalBuffer, null);
}
if ($from_appid != $appid)
return array(ErrorCode::$ValidateAppidError, null);
return array(0, $xml_content);
}
/**
* 随机生成16位字符串
* @return string 生成的字符串
*/
function getRandomStr()
{
$str = "";
$str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($str_pol) - 1;
for ($i = 0; $i < 16; $i++) {
$str .= $str_pol[mt_rand(0, $max)];
}
return $str;
}
}
?>
二、抽离代码,直接对微信消息进行解密(只需要引入这个类文件即可完成解密)
class WxEncode{
//微信解密的密钥(AesKey)
private $key = 'MzVkc2N2ZmRHeDlzZktHRU11RWdmZEd4OXNmS0dFTXVFZw==';
//一个块有多少位
private $blockSize = 32;
function __construct()
{
$this->key = base64_decode($this->key . "=");
}
function encrypt($text,$appid)
{
try {
$key = $this->key;
$random = $this->getRandomStr();
$text = $random . pack('N', strlen($text)) . $text . $appid;
$text = $this->encode($text);
$iv = substr($key, 0, 16);
$encrypted = openssl_encrypt($text, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
return base64_encode($encrypted);
} catch (Exception $e) {
return array(ErrorCode::$EncryptAESError, null);
}
}
/**
* 随机生成16位字符串
* @return string 生成的字符串
*/
function getRandomStr()
{
$str = "";
$str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($str_pol) - 1;
for ($i = 0; $i < 16; $i++) {
$str .= $str_pol[mt_rand(0, $max)];
}
return $str;
}
function decrypt($encrypted,$appid)
{
try {
$key = $this->key;
$ciphertext = base64_decode($encrypted, true);
$iv = substr($key, 0, 16);
$decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
} catch (Exception $e) {
return array(ErrorCode::$DecryptAESError, null);
}
try {
//去除补位字符
$result = $this->decode($decrypted);
//去除16位随机字符串,网络字节序和AppId
if (strlen($result) < 16)
return "";
$content = substr($result, 16, strlen($result));
$len_list = unpack("N", substr($content, 0, 4));
$xml_len = $len_list[1];
$xml_content = substr($content, 4, $xml_len);
$from_appid = substr($content, $xml_len + 4);
} catch (Exception $e) {
//print $e;
exit( $e->getMessage());
}
if ($from_appid != $appid)
exit( 'appid错误');
return $xml_content;
}
/**
* 对需要加密的明文进行填充补位
* @param $text /需要进行填充补位操作的明文
* @return
*/
function encode($text)
{
$blockSize = $this->blockSize;
$text_length = strlen($text);
//计算需要填充的位数
$amount_to_pad = $blockSize - ($text_length % $blockSize);
if ($amount_to_pad == 0) {
$amount_to_pad = $blockSize;
}
//获得补位所用的字符
$pad_chr = chr($amount_to_pad);
$tmp = "";
for ($index = 0; $index < $amount_to_pad; $index++) {
$tmp .= $pad_chr;
}
return $text . $tmp;
}
/**
* 对解密后的明文进行补位删除
* @param /decrypted 解密后的明文
* @return /删除填充补位后的明文
*/
function decode($text)
{
$pad = ord(substr($text, -1));
if ($pad < 1 || $pad > $this->blockSize;) {
$pad = 0;
}
return substr($text, 0, (strlen($text) - $pad));
}
}
/**
* 抽离成功,进行测试
*/
//appId
$appid = 'wxf1dea3322522114329';
$wxEncode = new WxEncode();
//步骤1:模拟微信的加密数据
//假设这是微信推送到服务器的消息,但此时还是明文模式(我微信小程序后台设置的是json格式)
$baseData = '{"ToUserName":"gh_2fcc195deca8","FromUserName":"oYBz64smtrDVSB0Zc-Ci3cCGoI","CreateTime":"1570781174","MsgType":"text","Content":"我是用户发送的消息","MsgId":"22488181173754573"}';
//模拟微信对要发送数据进行加密
$encodeData = $wxEncode->encrypt($baseData,$appid);
echo '模拟完成,加密数据如下:';
echo $encodeData;
echo '<hr/>';
//步骤2:模拟微信发送的数据
$wxSendMsg = json_encode(['ToUserName'=>'gh_2fcc195deca8','Encrypt'=>$encodeData],true);
//步骤3:解密微信推送的加密数据(需要实现的实际业务模块)
$sendData = json_decode($wxSendMsg,true);
//解密加密的数据
$decodeData = $wxEncode->decrypt($sendData['Encrypt'],$appid);
echo '这是解密后的数据:'.$decodeData;
三、微信消息加解密的实现细节
对明文msg加密的过程如下:
msg_encrypt = Base64_Encode( AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID] )
AES加密的buf由16个字节的随机字符串、4个字节的msg长度、明文msg和CorpID组成。其中msg_len为msg的字节数,网络字节序;CorpID为企业号的CorpID。经AESKey加密后,再进行Base64编码,即获得密文msg_encrypt。
对应于加密方案,解密方案如下:
1.对密文BASE64解码:aes_msg=Base64_Decode(msg_encrypt)
2.使用AESKey做AES解密:rand_msg=AES_Decrypt(aes_msg)
3.验证解密后CorpID、msg_len
4.去掉rand_msg头部的16个随机字节,4个字节的msg_len,和尾部的CorpID即为最终的消息体原文msg
四、加密前的补位要小心
encode补位的代码要格外注意
/**
* 对需要加密的明文进行填充补位
* @param $text /需要进行填充补位操作的明文
* @return
*/
function encode($text)
{
$blockSize = $this->blockSize;
$text_length = strlen($text);
//计算需要填充的位数
$amount_to_pad = $blockSize - ($text_length % $blockSize);
if ($amount_to_pad == 0) {
$amount_to_pad = $blockSize;
}
//获得补位所用的字符
$pad_chr = chr($amount_to_pad);
$tmp = "";
for ($index = 0; $index < $amount_to_pad; $index++) {
$tmp .= $pad_chr;
}
return $text . $tmp;
}
如果字符不能被一开始设置的块(定义的$blockSize属性)分隔完就需要对原文进行补位,补满 $blockSize对应的数量,就是代码中的
$amount_to_pad = $blockSize - ($text_length % $blockSize);
if ($amount_to_pad == 0) {
$amount_to_pad = $blockSize;
}
如何填充呢?这里有个妙用:$pad_chr = chr($amount_to_pad);
chr返回一个ASCII对应的字符,然后使用返回的字符去填充,注意,现在这里chr的参数是传入的需要填充的数量,一个数字,然后删除补位的时候(decode函数),通过ord函数获取解密后的最后一个字符对应的ASCII表中的位置,而这个位置是chr一开始传入的需要填充的数量,最后得到这个数量,去掉末尾对应的填充字符;
这样的填充我们注意这段代码
if ($amount_to_pad == 0) {
$amount_to_pad = $blockSize;
}
如果刚好可以分隔完,我们也是填充了一个完整的$blockSize对应的长度,意味这最后解密出来的原文不管是否能被分隔完都必须取出最后一位字节对应的ASCII表中的位置,然后去除掉相应数量的补位符;问题也是在这里,如果其他开发这不知道这一情况,他看到解密长度是正确的就不会再去删除填充字节,就会导致当加密的数据刚好可以分隔完进行对称加密时,不知情的其他开发者就会忽略最后一段的填充字符,导致原文最后多了一段填充字符。不过如果全是自己开发的就没问题了,我们自己清楚原文结尾是100%拼入了填充字符的。如果是给其他开发者解密记得提醒别人原文结尾一定要去掉填位字节哦。