需求
支付宝小程序端,获取到加密的用户手机号数据,需要经过服务端对数据进行解密,得到用户的手机号
问题
用户信息为敏感信息,需要用到敏感信息加密解密方法中的方式进行解密
服务端为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后台解密问题,已经解决。