php7.1微信公眾平臺解密失敗DecryptAESError = -40007

小虎哥-技術部落格發表於2018-11-06

php7.1釋出後新特性吸引了不少PHPer,大家都在討論新特性帶來的好處與便利。但是從php7.0 升級到 php7.1 廢棄(過時)了一個在過去普遍應用的擴充套件(mcrypt擴充套件)。官方提供了相應的解決提示,卻沒有提供更詳細的解決辦法。於是坑來了….

首頁要確保類的構造方法支援php7版本,PHP 7開始使用和類名相同的方法名作為構造方法會報E_DEPRECATED級別的錯誤,提示在未來版本中會徹底拋棄類同名方法作為建構函式,但程式仍然會正常執行。

Deprecated: Methods with the same name as their class will not be constructor...

這裡涉及兩個檔案的建構函式的修改:

pkcs7Encoder.php,將建構函式改為

/**
 * Prpcrypt class
 *
 * 提供接收和推送給公眾平臺訊息的加解密介面.
 */
class Prpcrypt
{
	public $key;

	function __construct($k)
	{
		$this->key = base64_decode($k . "=");
	}

以下程式碼省略

topClient.php,建構函式改為

/**
 * 1.第三方回覆加密訊息給公眾平臺;
 * 2.第三方收到公眾平臺傳送的訊息,驗證訊息的安全性,並對訊息進行解密。
 */
class wXBizMsgCrypt
{
    private $token;
    private $encodingAesKey;
    private $appId;

    /**
     * 建構函式
     * @param $token string 公眾平臺上,開發者設定的token
     * @param $encodingAesKey string 公眾平臺上,開發者設定的EncodingAESKey
     * @param $appId string 公眾平臺的appId
     */
    function __construct($token, $encodingAesKey, $appId)
    {
     $this->token = $token;
     $this->encodingAesKey = $encodingAesKey;
     $this->appId = $appId;
    }

以下程式碼省略

wxBizMsgCrypt.php,以下程式碼改為

/**
 * 1.第三方回覆加密訊息給公眾平臺;
 * 2.第三方收到公眾平臺傳送的訊息,驗證訊息的安全性,並對訊息進行解密。
 */
class WXBizMsgCrypt
{
    private $token;
    private $encodingAesKey;
    private $appId;

    /**
     * 建構函式
     * @param $token string 公眾平臺上,開發者設定的token
     * @param $encodingAesKey string 公眾平臺上,開發者設定的EncodingAESKey
     * @param $appId string 公眾平臺的appId
     */
    function __construct($token, $encodingAesKey, $appId)
    {
     $this->token = $token;
     $this->encodingAesKey = $encodingAesKey;
     $this->appId = $appId;
    }

以下程式碼省略

下面是微信官方提供的訊息加密解密演算法中的核心部分,來自檔案pkcs7Encoder.php

/**
 * 對明文進行加密
 * @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);
    }
}

/**
 * 對密文進行解密
 * @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 {
        //去除補位字元
        $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);

}

以上程式碼中使用了mcrypt擴充套件

php官方只是輕飄飄的說mcrypt擴充套件被 OpenSSL取代了,卻並未提供相應的轉換辦法。網路提供的演算法大多是mcrypt加密的使用mcrypt解密或者OpenSSL加密的使用OpenSSL解密。並未提供互通替換的方案。

對比研究
作者開始分析這兩種演算法時感覺總是摸不著頭腦,感覺兩種演算法的結果外掛太多,完全不相關聯的樣子。通過一番查閱和反覆測試終於明白這兩種演算法的區別了。

在演算法、data、key、vi 一致的情況下

openssl_encrypt 加密相當於將 mcrypt_encrypt 的加密結果執行一次 base64_encode

openssl_decode 解密相當於 先將加密結果執行一次base64_decode 然後再通過mcrypt_encrypt 解密

調整後的程式碼,相容php7


	/**
	 * 對明文進行加密
	 * @param string $text 需要加密的明文
	 * @return string 加密後的密文
	 */
	public function encrypt($text, $appid)
	{

		try {
			if(version_compare(PHP_VERSION, '7','>=')) {
		 		//獲得16位隨機字串,填充到明文之前
		        $random = $this->getRandomStr();
		        $text = $random . pack("N", strlen($text)) . $text . $appid;
		        $iv = substr($this->key, 0, 16);
		        $pkc_encoder = new PKCS7Encoder;
		        $text = $pkc_encoder->encode($text);
		        $encrypted = openssl_encrypt($text,'AES-256-CBC',substr($this->key, 0, 32),OPENSSL_ZERO_PADDING,$iv);
			} else {
				//獲得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);
				$encrypted = base64_encode($encrypted);
			}

			//print($encrypted);
			//使用BASE64對加密後的字串進行編碼
			return array(ErrorCode::$OK, $encrypted);
		} catch (Exception $e) {
			//print $e;
			return array(ErrorCode::$EncryptAESError, null);
		}
	}

	/**
	 * 對密文進行解密
	 * @param string $encrypted 需要解密的密文
	 * @return string 解密得到的明文
	 */
	public function decrypt($encrypted, $appid)
	{

		try {
			if(version_compare(PHP_VERSION, '7','>=')) {
				$iv = substr($this->key, 0, 16);          
        		$decrypted = openssl_decrypt($encrypted,'AES-256-CBC',substr($this->key, 0, 32),OPENSSL_ZERO_PADDING,$iv);
			} else {
				//使用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 {
			//去除補位字元
			$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);
			if(version_compare(PHP_VERSION, '7','>=')) {
		        if (!$appid) {
		        	//如果傳入的appid是空的,則認為是訂閱號,使用資料中提取出來的appid
		            $appid = $from_appid; 
		        }
		    }
		} catch (Exception $e) {
			//print $e;
			return array(ErrorCode::$IllegalBuffer, null);
		}
		if ($from_appid != $appid)
			return array(ErrorCode::$ValidateAppidError, null);
		//不註釋上邊兩行,避免傳入appid是錯誤的情況
		if(version_compare(PHP_VERSION, '7','>=')) {
			//增加appid,為了解決後面加密回覆訊息的時候沒有appid的訂閱號會無法回覆
			return array(0, $xml_content, $from_appid);
		} else {
			return array(0, $xml_content);
		}

	}

 

相關文章