1. 程式人生 > >【支付寶小程式】PHP 獲取使用者敏感資訊手機號 驗籤解密 RSA解密 AES解密

【支付寶小程式】PHP 獲取使用者敏感資訊手機號 驗籤解密 RSA解密 AES解密

需求

支付寶小程式端,獲取到加密的使用者手機號資料,需要經過服務端對資料進行解密,得到使用者的手機號

問題

使用者資訊為敏感資訊,需要用到敏感資訊加密解密方法中的方式進行解密

服務端為PHP,由於官方沒有對應的演示demo,經過摸索測試,還是出現了驗籤不通過,並且解密不成功的情況

解決過程

1.分析官方的java例項程式碼

String response = "小程式前端提交的";

//1. 獲取驗籤和解密所需要的引數
Map<String, String> openapiResult = JSON.parseObject(response,
            new
TypeReference<Map<String, String>>() { }, Feature.OrderedField); String signType = StringUtil.defaultIfBlank(openapiResult.get("signType"), "RSA2"); String charset = StringUtil.defaultIfBlank(openapiResult.get("charset"), "UTF-8"); String encryptType = StringUtil.defaultIfBlank
(openapiResult.get("encryptType"), "AES"); String sign = openapiResult.get("sign"); String content = openapiResult.get("response"); //如果密文的 boolean isDataEncrypted = !content.startsWith("{"); boolean signCheckPass = false; //2. 驗籤 String signContent = content; String signVeriKey = "你的小程式對應的支付寶公鑰(為擴充套件考慮建議用appId+signType做金鑰儲存隔離)"
; String encryptType = "你的小程式對應的加解密金鑰(為擴充套件考慮建議用appId+encryptType做金鑰儲存隔離)" //如果是加密的報文則需要在密文的前後新增雙引號 if (isDataEncrypted) { signContent = "\"" + signContent + "\""; } try { signCheckPass = AlipaySignature.rsaCheck(signContent, sign, signVeriKey, charset, signType); } catch (AlipayApiException e) { //驗籤異常, 日誌 } if(!signCheckPass) { //驗籤不通過(異常或者報文被篡改),終止流程(不需要做解密) throw new Exception("驗籤失敗"); } //3. 解密 String plainData = null; if (isDataEncrypted) { try { AlipayEncrypt.decryptContent(content, encryptType, decryptKey, charset); } catch (AlipayApiException e) { //解密異常, 記錄日誌 throw new Exception("解密異常"); } } else { plainData = content; }

直接翻譯這段程式碼為PHP的,並採用阿里雲的SDK呼叫核心的兩個方法。

經過不斷的嘗試,還是以失敗告終。

2.分析定位阿里雲SDK原始碼

阿里雲的SDK中,首先通用的初始化了AopClient,將小程式的引數在裡面初始化,其他操作並沒有。

經過檢查,AopClient中解密和校驗的程式碼如下

/** rsaCheckV1 & rsaCheckV2
	 *  驗證簽名
	 *  在使用本方法前,必須初始化AopClient且傳入公鑰引數。
	 *  公鑰是否是讀取字串還是讀取檔案,是根據初始化傳入的值判斷的。
	 **/
	public function rsaCheckV1($params, $rsaPublicKeyFilePath,$signType='RSA') {
		$sign = $params['sign'];
		$params['sign_type'] = null;
		$params['sign'] = null;
		return $this->verify($this->getSignContent($params), $sign, $rsaPublicKeyFilePath,$signType);
	}
	public function rsaCheckV2($params, $rsaPublicKeyFilePath, $signType='RSA') {
		$sign = $params['sign'];
		$params['sign'] = null;
		return $this->verify($this->getSignContent($params), $sign, $rsaPublicKeyFilePath, $signType);
	}

	function verify($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA') {

		if($this->checkEmpty($this->alipayPublicKey)){

			$pubKey= $this->alipayrsaPublicKey;
			$res = "-----BEGIN PUBLIC KEY-----\n" .
				wordwrap($pubKey, 64, "\n", true) .
				"\n-----END PUBLIC KEY-----";
		}else {
			//讀取公鑰檔案
			$pubKey = file_get_contents($rsaPublicKeyFilePath);
			//轉換為openssl格式金鑰
			$res = openssl_get_publickey($pubKey);
		}



		($res) or die('支付寶RSA公鑰錯誤。請檢查公鑰檔案格式是否正確');  

		//呼叫openssl內建方法驗籤,返回bool值

		$result = FALSE;
		if ("RSA2" == $signType) {
			$result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1);;
		} else {
			$result = (openssl_verify($data, base64_decode($sign), $res)===1);
		}

		if(!$this->checkEmpty($this->alipayPublicKey)) {
			//釋放資源
			openssl_free_key($res);
		}

		return $result;
	}

/** 
	 *  在使用本方法前,必須初始化AopClient且傳入公私鑰引數。
	 *  公鑰是否是讀取字串還是讀取檔案,是根據初始化傳入的值判斷的。
	 **/
	public function rsaDecrypt($data, $rsaPrivateKeyPem = null, $charset = null) {
		
		if($this->checkEmpty($this->rsaPrivateKeyFilePath)){
			//讀字串
			$priKey=$this->rsaPrivateKey;
			$res = "-----BEGIN RSA PRIVATE KEY-----\n" .
				wordwrap($priKey, 64, "\n", true) .
				"\n-----END RSA PRIVATE KEY-----";
		}else {
			$priKey = file_get_contents($this->rsaPrivateKeyFilePath);
			$res = openssl_get_privatekey($priKey);
		}
		($res) or die('您使用的私鑰格式錯誤,請檢查RSA私鑰配置'); 
		//轉換為openssl格式金鑰
		$decodes = explode(',', $data);
		$strnull = "";
		$dcyCont = "";
		foreach ($decodes as $n => $decode) {
			if (!openssl_private_decrypt($decode, $dcyCont, $res)) {
				echo "<br/>" . openssl_error_string() . "<br/>";
			}
			$strnull .= $dcyCont;
		}
		return $strnull;
	}

function verify($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA') {

		if($this->checkEmpty($this->alipayPublicKey)){

			$pubKey= $this->alipayrsaPublicKey;
			$res = "-----BEGIN PUBLIC KEY-----\n" .
				wordwrap($pubKey, 64, "\n", true) .
				"\n-----END PUBLIC KEY-----";
		}else {
			//讀取公鑰檔案
			$pubKey = file_get_contents($rsaPublicKeyFilePath);
			//轉換為openssl格式金鑰
			$res = openssl_get_publickey($pubKey);
		}



		($res) or die('支付寶RSA公鑰錯誤。請檢查公鑰檔案格式是否正確');  

		//呼叫openssl內建方法驗籤,返回bool值

		$result = FALSE;
		if ("RSA2" == $signType) {
			$result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1);;
		} else {
			$result = (openssl_verify($data, base64_decode($sign), $res)===1);
		}

		if(!$this->checkEmpty($this->alipayPublicKey)) {
			//釋放資源
			openssl_free_key($res);
		}

		return $result;
	}

上面的程式碼分別對應的流程中的 驗籤、解密、校驗,通過一行一行執行,定位分析問題,最終定位到的為

$result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256)===1);;

if (!openssl_private_decrypt($decode, $dcyCont, $res)) {
				echo "<br/>" . openssl_error_string() . "<br/>";
			}

也就是說openssl遇到的問題,這也和我在除錯過程中的報錯資訊展示一致

3.論壇&搜尋

通過查詢官方論壇和找到支付寶的官方客服,得到的都只有一句話

你好,解密方式:填充方式是AES/CBC/PKCS5Padding;偏移量全是0

那麼怎麼能用PHP實現AES/CBC/PKCS5Padding呢?

需要用到下面的類

class MagicCrypt {
    private $iv = "0102030405060708";//金鑰偏移量IV,可自定義
 
    private $encryptKey = "自定義16位長度key";//AESkey,可自定義
 
    //加密
    public function encrypt($encryptStr) {
        $localIV = $this->iv;
        $encryptKey = $this->encryptKey;
 
        //Open module
        $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, $localIV);
 
        //print "module = $module <br/>" ;
 
        mcrypt_generic_init($module, $encryptKey, $localIV);
 
        //Padding
        $block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
        $pad = $block - (strlen($encryptStr) % $block); //Compute how many characters need to pad
        $encryptStr .= str_repeat(chr($pad), $pad); // After pad, the str length must be equal to block or its integer multiples
 
        //encrypt
        $encrypted = mcrypt_generic($module, $encryptStr);
 
        //Close
        mcrypt_generic_deinit($module);
        mcrypt_module_close($module);
 
        return base64_encode($encrypted);
 
    }
 
    //解密
    public function decrypt($encryptStr) {
        $localIV = $this->iv;
        $encryptKey = $this->encryptKey;
 
        //Open module
        $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, $localIV);
 
        //print "module = $module <br/>" ;
 
        mcrypt_generic_init($module, $encryptKey, $localIV);
 
        $encryptedData = base64_decode($encryptStr);
        $encryptedData = mdecrypt_generic($module, $encryptedData);

        return $encryptedData;
    }
}

但是,這個類中的mcrypt_module_open對應的擴充套件,在PHP7以上的版本就廢棄了,所以這個方法也是不能用的。 : (

解決方案

疑問1:微信小程式是小程式,支付寶小程式也是小程式,他們之間能不能有什麼關聯?
疑問2:我剛做完微信小程式的使用者資訊獲取,也是採用前端獲取加密資料傳給服務端,服務端解密的方式,那這之間有沒有什麼關聯?

帶著這些疑問,我重新看了下微信小程式提供的解析demo


class WXBizDataCrypt
{

    private $appid;
    private $sessionKey;

    /**
     * 建構函式
     * @param $sessionKey string 使用者在小程式登入後獲取的會話金鑰
     * @param $appid string 小程式的appid
     */
    public function __construct( $appid, $sessionKey)
    {
        $this->sessionKey = $sessionKey;
        $this->appid = $appid;
    }


    /**
     * 檢驗資料的真實性,並且獲取解密後的明文.
     * @param $encryptedData string 加密的使用者資料
     * @param $iv string 與使用者資料一同返回的初始向量
     * @param $data string 解密後的原文
     *
     * @return int 成功0,失敗返回對應的錯誤碼
     */
    public function decryptData( $encryptedData, $iv, &$data )
    {
        if (strlen($this->sessionKey) != 24) {
            return ErrorCode::$IllegalAesKey;
        }
        $aesKey=base64_decode($this->sessionKey);


        if (strlen($iv) != 24) {
            return ErrorCode::$IllegalIv;
        }
        $aesIV=base64_decode($iv);

        $aesCipher=base64_decode($encryptedData);

        $result=openssl_decrypt( $aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV);

        $dataObj=json_decode( $result );
        if( $dataObj  == NULL )
        {
            return ErrorCode::$IllegalBuffer;
        }
        if( $dataObj->watermark->appid != $this->appid )
        {
            return ErrorCode::$IllegalBuffer;
        }
        $data = $result;
        return ErrorCode::$OK;
    }
}

相同點:

  • 都採用aes金鑰解密
  • 解決方案中都存在偏移量iv
    不同點:
  • 微信的aes金鑰為從登入code中拿到的sessionKey,支付寶為管理後臺隨機生成
  • 微信小程式校驗解密得到的資料中的watermark水印

由此可以得出一個大膽的想法,並由此得到一個修改之後的方法


public function decryptData( $encryptedData )
    {
        $key = '支付寶生成的aesKey';
        $aesKey=base64_decode($key);
        $iv = 0;

        $aesIV=base64_decode($iv);

        $aesCipher=base64_decode($encryptedData);

        $result=openssl_decrypt( $aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV);
        
        return $result;
    }
    

經過親測有效,得到的資料為

{"code":"10000","msg":"Success","mobile":"使用者支付寶繫結的手機號"}

返回的結果和官方文件中的一致。

到此,支付寶獲取使用者敏感資訊的PHP後臺解密問題,已經解決。

參考資料