写代码的经历中,总少不了与外部的程序对接,一旦有这样的事,往往周期会很长,很麻烦,因为你要考虑的事会多了很多,其中安全性的加密解密就是重要的一项。写代码,可以出Bug,但逼格不能弱。什么是逼格?和别人对接一下,连加密解密都没有,连验证签名都没有,别人一眼就望穿你,这就是眼界的问题了。
这次的故事是对接一个大的支付系统,对方也是第一个对接我们,然后定了接口和加解密算法,给了个Java的Demo,问了声,有没有PHP的,没有,歇菜,自己来吧。
代码说多不多,说少不少,为了先说事,代码放在最后面。
第一个坑:加密算法多多,你到底要闹咋样?
码农兄弟们可以先下去看一眼代码,然后说说它用了啥算法?
接口传输的安全性算法,首先要区分是加签名还是加密?区别是,签名+原文可以验证收到的信息是否被篡改,不过别指望有了签名就可以还原出原文来。加密就是不让别人看到原文是啥,然后给把钥匙,要让接收的人解密看出原文来。两者的算法基本上来说,就是完全不同的。
加密还分对称非对称。对称有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK、AES等,非对称有:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)
还有,你以为拿个公钥就够了?当然不是,还要一对。更坑的是,可能有不同的格式。对方给了我一个keystore格式的,发觉php完全不支持,想办法转成了pem格式的。
常见的密钥格式:jks,jce,p12,pfx,bks,ubr等等
常见的证书文件格式:cer,crt,rsa,p7b,p7r,p7c,pem,p10,csr等等
最后还有一点,这次碰到的算法具体的参数。
我这次遇到的是3DES加密,AlGORITHM = "DESede/ECB/PKCS5Padding";,后面的类型和填充方式都不能差一点。
第二个坑:到底什么是密钥?
你会说这个很简单啊
Java里就是像这样:
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray());
|
php里就是像这样:
$privateKey=openssl_pkey_get_private(file_get_contents($this->privatePemFilePath));
|
你以为你以为的就是你以为的吗?前面说了,即使算法一样,密钥格式不一样,开发语言一样,用法也完全不一样。
上面的只是格式不同,下面的还有编码的不同:
看起来我从代码里读出的密码是这个,但其实送入算法中的可不是,还要做一个Base64转换,如果你送入了错误的,会永远困在迷惘中。
$this->dESCORPKey = C('lakala_encrypt_key'); $key = $this->$dESCORPKey; $encryptData = self::encrypt($key, $signedReq); ... public function encrypt($key,$data){ $decode_key = base64_decode($key); $encData = openssl_encrypt($data, 'DES-EDE3', $decode_key, OPENSSL_RAW_DATA); $encData = base64_encode($encData); return $encData; }
|
PS:网上有在线加密解密算法的工具,往往和代码出来的结果不一致,除了各种参数都需要写对以外,特别注意密码(密钥)的输入格式,要不要Base64编码或者解码。
第三个坑:带中文的字符串格式
看一眼下面的代码,你就会知道,php里有很多json_encode,json_decode,java代码里有很多getByte()这样转换成字节的操作,一个经典的问题就来了,你如果传入了中文,中文按什么格式进行编码?编码不同,决定了加密算法操作时二进制的不同,也决定了最终输出的不同。
在写代码的时候查阅了java的getByte()方法,默认值竟然是读取机器的字符格式!!!所以,写代码的时候一定要注意,最好加上具体格式,不要用默认值,要这么写:getByte("UTF-8"),否则换了机器表现不一样,你会死的很难看。
第四个坑:语言的问题
虽然上帝说人人平等,但现实远远复杂的多。
虽然加密解密本质上说就是一种运算变换,什么语言都可以实现的,可你不会从底层开始搞的,那就涉及语言的问题。
最简单的例子:java里具体运算时都会把String转换成byte进行操作。PHP就不行。
加密算法这个玩意虽然复杂,不过前人已经给我们造了很多轮子了,网上随便一搜就可以找到很多资料,问题是没有成系统,特别各个语言间的轮子差异还蛮大的。如何做适配,在网上的资料非常的难找。
PHP里加密方法还有mcrypt和openssl两套,互相之间的写法还差异蛮大的,让人头疼。
举个栗子(这里的Java解析使用了fastjson):
1 java中: 2 CommReq req = JSON.parseObject(jsonStr, CommReq.class, Feature.OrderedField); 3 PHP中: 4 $req = json_decode(trim($jsonStr), true); 5 ksort($req);
|
看起来很像了吧?才不是呢!以下是输入的Json
{ "head": { "serviceSn": "B5D79F38B96040B7B992B6BE329D9975", "serviceId": "MPBF001", "channelId": "05", "inputSource": "I002", "opId": "", "requestTime": "20180628142105", "versionId": "1.0.0", "businessChannel": "LKLZFLLF" }, "request": { "userId": "40012345678", "userName": "AA", "userMobile": "18675529912", "idNo": "110101198609096078" } }
|
问题出在具体的类型定义,以及Json解析的深度
JAVA中有定义具体按哪个类型解析
1 public class CommReq implements Serializable { 2 private static final long serialVersionUID = 1L; 3 private CommReqHead head; 4 private String request; 5 }
|
JAVA这种强类型语言,必须定义类型,将request定义成String型,导致JSON解析出来的结果:
"request":"{\"userId\":\"40012345678\",\"userName\": \"AA\", \"userMobile\": \"18675529912\", \"idNo\": \"110101198609096078\" }";
|
而PHP是弱类型语言,直接嵌套进去也解析出来了
"request": {"userId":"40012345678","userName": "AA", "userMobile": "18675529912", "idNo": "110101198609096078" } }
|
如果PHP要和JAVA真正保持一致(因为一旦不一致,加密结果就不一样了)
1 $req = json_decode(trim($jsonStr), true); 2 ksort($req); 3 req['request']=json_encode(req['request']);
|
前面也提到了密钥类型的问题,其实也和语言有关,PHP只支持PEM格式,所以,还用工具对keystore进行了转换,转换之后发现几个密码都已经不需要了。生成公钥PEM和私钥PEM加上加密解密Key就可以了。
小结
回顾来看,其实只是解决了很小的一个问题,将一段JAVA代码转换成了PHP代码,甚至中间复杂的算法细节都调用原来就有的模块,更不用怀疑这些模块写的算法的正确性,但调试这样一个东西,却的的确确花费了非常大的精力。技术没有任何中间地带,只有行或者不行,容不得半分作假。开发必须要注重细节,细节到位了才不会出Bug,这点在加密解密这件事上,尤其的明显,输入差一个字符,输出完全不同。开发没有很容易的事,只有我做过,我熟悉的事。
代码在这里哦。钥匙就不提供了,这样直接copy代码跑不起来是正常的。哈哈
package com.chuangmi.foundation.lakala.service.impl;
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Security; import java.security.Signature;
import javax.annotation.PostConstruct; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.security.cert.X509Certificate; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.Feature; import com.chuangmi.foundation.lakala.service.SignService; import com.chuangmi.foundation.lakala.service.models.CommReq; import com.chuangmi.foundation.lakala.service.models.CommRequest; import com.chuangmi.foundation.lakala.service.models.CommRes; import com.chuangmi.foundation.lakala.service.models.CommResponse;
@Service public class SignServiceImpl implements SignService {
private static final Logger logger = LoggerFactory.getLogger(SignService.class);
@Value("${cer.filePath}") private String cerFilePath;
@Value("${key.filePath}") private String keyFilePath;
@Value("${key.passWord}") private String keyPassWord;
@Value("${key.alias}") private String alias;
@Value("${encrypt.key}") private String dESCORPKey;
public static final String AlGORITHM = "DESede/ECB/PKCS5Padding";
@PostConstruct public void init(){ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){
System.out.println("security provider BC not found, will add provider");
Security.addProvider(new BouncyCastleProvider());
} }
@Override public String signRequestJsonToSend(String jsonStr) throws JSONException { JSONObject resultObj = null; if(jsonStr.indexOf("\"head\"") >= 0) { CommReq req = JSON.parseObject(jsonStr, CommReq.class, Feature.OrderedField); String signData = signData(req.getRequest());
logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);
req.getHead().setSignData(signData); resultObj = JSONObject.parseObject(JSON.toJSONString(req), Feature.OrderedField); } else if(jsonStr.indexOf("\"comm\"") >= 0) { CommRequest req = JSON.parseObject(jsonStr, CommRequest.class, Feature.OrderedField); String signData = signData(req.getData());
logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);
req.getComm().setSigntx(signData); resultObj = JSONObject.parseObject(JSON.toJSONString(req), Feature.OrderedField); }
String signedReq = String.valueOf(JSONObject.toJSON(resultObj)); logger.info("加签后的报文:" + signedReq);
byte[] key = Base64.decodeBase64(dESCORPKey); byte[] encryptData = encrypt(key, signedReq.getBytes());
String encryptStr = Base64.encodeBase64String(encryptData);
logger.info("加密成功,密文:" + encryptStr);
return encryptStr; }
@Override public String signResponseJsonToSend(String jsonStr) throws JSONException { JSONObject resultObj = null; if(jsonStr.indexOf("\"head\"") >= 0) { CommRes response = JSON.parseObject(jsonStr, CommRes.class, Feature.OrderedField); resultObj = JSONObject.parseObject(JSON.toJSONString(response), Feature.OrderedField); } else if(jsonStr.indexOf("\"comm\"") >= 0) { CommResponse response = JSON.parseObject(jsonStr, CommResponse.class, Feature.OrderedField); String signData = signData(response.getData());
logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);
response.getComm().setSigntx(signData); resultObj = JSONObject.parseObject(JSON.toJSONString(response), Feature.OrderedField); }
String signedReq = String.valueOf(JSONObject.toJSON(resultObj)); logger.info("加签后的响应报文:" + signedReq);
byte[] key = Base64.decodeBase64(dESCORPKey); byte[] encryptData = encrypt(key, signedReq.getBytes());
String encryptStr = Base64.encodeBase64String(encryptData);
logger.info("加密成功的响应报文,密文:" + encryptStr);
return encryptStr; }
@Override public String verifyReceivedRequest(HttpServletRequest request) throws IOException, JSONException { String json = extractJson(request); logger.info("接收报文密文:" + json);
return verifyRequestJson(json); }
@Override public String verifyRequestJson(String json) throws UnsupportedEncodingException, JSONException { byte[] key = Base64.decodeBase64(dESCORPKey); byte[] decryptBytes = decrypt(key, Base64.decodeBase64(json)); String orig = new String(decryptBytes, "UTF-8"); logger.info("【收到的报文请求】接收报文:" + orig);
JSONObject obj = JSONObject.parseObject(orig, Feature.OrderedField); String reqStr = String.valueOf(JSONObject.toJSON(obj)); if(reqStr.indexOf("\"comm\"") >= 0) { CommRequest req = JSON.parseObject(reqStr, CommRequest.class, Feature.OrderedField); String signtx = req.getComm().getSigntx();
logger.info("报文中的签名:" + signtx); boolean nRet = verifyData(req.getData(), signtx); if(nRet) { req.getComm().setSigntx(""); return JSON.toJSONString(req); } else { return null; } } else if(reqStr.indexOf("\"head\"") >= 0) { CommReq req = JSON.parseObject(reqStr, CommReq.class, Feature.OrderedField); String signData = req.getHead().getSignData();
logger.info("报文中的签名:" + signData); boolean nRet = verifyData(req.getRequest(), signData); if(nRet) { req.getHead().setSignData(""); return JSON.toJSONString(req); } else { return null; } } else { return null; } }
@Override public String verifyResponseJson(String json) throws UnsupportedEncodingException, JSONException { byte[] key = Base64.decodeBase64(dESCORPKey); byte[] decryptBytes = decrypt(key, Base64.decodeBase64(json)); String orig = new String(decryptBytes, "UTF-8"); logger.info("【收到的响应报文】报文:" + orig);
JSONObject obj = JSONObject.parseObject(orig, Feature.OrderedField); String reqStr = String.valueOf(JSONObject.toJSON(obj)); if(reqStr.indexOf("\"comm\"") >= 0) { CommResponse response = JSON.parseObject(reqStr, CommResponse.class, Feature.OrderedField); String signtx = response.getComm().getSigntx();
logger.info("报文中的签名:" + signtx); boolean nRet = verifyData(response.getData(), signtx); if(nRet) { response.getComm().setSigntx(""); return JSON.toJSONString(response); } else { return null; } } else if(reqStr.indexOf("\"head\"") >= 0) { CommRes response = JSON.parseObject(reqStr, CommRes.class, Feature.OrderedField); return JSON.toJSONString(response); } else { return null; } }
public String extractJson(HttpServletRequest request) throws IOException { StringBuilder jsonString = new StringBuilder(); BufferedReader reader = request.getReader(); try { String line; while ((line = reader.readLine()) != null) { jsonString.append(line); } } finally { reader.close(); } String data = jsonString.toString(); return data; }
private String signData(String param) { InputStream inputStream = null; try {
String store_password = keyPassWord; String password = keyPassWord; String keyAlias = alias;
inputStream = new FileInputStream(keyFilePath);
KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(inputStream, store_password.toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray());
Signature dsa = Signature.getInstance("SHA1withRSA"); dsa.initSign(privateKey); byte[] p = param.getBytes(); dsa.update(p);
return Base64.encodeBase64String(dsa.sign());
} catch (Exception gse) {
gse.printStackTrace(); return null;
} finally {
try { if (inputStream != null) inputStream.close(); } catch (Exception e) { e.printStackTrace(); } }
}
private boolean verifyData(String urlParam, String signParam) { boolean verifies = false;
InputStream in = null;
try {
InputStream inputStream = new FileInputStream(cerFilePath);
X509Certificate cert = X509Certificate.getInstance(inputStream);
PublicKey pubKey = cert.getPublicKey();
if (pubKey != null) { Signature dsa = Signature.getInstance("SHA1withRSA"); dsa.initVerify(pubKey); dsa.update(urlParam.getBytes());
verifies = dsa.verify(Base64.decodeBase64(signParam)); }
} catch (Exception gse) { gse.printStackTrace(); } finally {
try { if (in != null) in.close(); } catch (Exception e) { e.printStackTrace(); } } return verifies;
}
private byte[] encrypt(byte[] key, byte[] src) { try {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){
System.out.println("security provider BC not found, will add provider");
Security.addProvider(new BouncyCastleProvider());
}
SecretKey deskey = new SecretKeySpec(key, AlGORITHM); Cipher c1 = Cipher.getInstance(AlGORITHM); c1.init(Cipher.ENCRYPT_MODE, deskey); return c1.doFinal(src); } catch (java.security.NoSuchAlgorithmException e1) { e1.printStackTrace(); } catch (javax.crypto.NoSuchPaddingException e2) { e2.printStackTrace(); } catch (java.lang.Exception e3) { e3.printStackTrace(); } return null; }
private byte[] decrypt(byte[] keybyte, byte[] src) { try {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){
System.out.println("security provider BC not found, will add provider");
Security.addProvider(new BouncyCastleProvider());
}
SecretKey deskey = new SecretKeySpec(keybyte, AlGORITHM); Cipher c1 = Cipher.getInstance(AlGORITHM); c1.init(Cipher.DECRYPT_MODE, deskey); return c1.doFinal(src); } catch (java.security.NoSuchAlgorithmException e1) { e1.printStackTrace(); } catch (javax.crypto.NoSuchPaddingException e2) { e2.printStackTrace(); } catch (java.lang.Exception e3) { e3.printStackTrace(); }
return null; }
public static void main(String[] args) throws JSONException {
InputStream inputStream = null; try {
String store_password = "123456"; String password = "123456"; String keyAlias = "www.lakala.com";
inputStream = new FileInputStream("/Users/rinson/eclipse-workspace/lakala/lakala-server/asdc.keystore");
KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(inputStream, store_password.toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray());
Signature dsa = Signature.getInstance("SHA1withRSA"); dsa.initSign(privateKey); String param = "XXXXX"; dsa.update(param .getBytes());
System.out.println(Base64.encodeBase64String(dsa.sign()));
} catch (Exception gse) {
gse.printStackTrace();
} finally {
try { if (inputStream != null) inputStream.close(); } catch (Exception e) { e.printStackTrace(); } } } }
|
PHP
<?php
namespace Common\Lib\Lakala;
class SignService { private $publicPemFilePath=''; private $privatePemFilePath=''; private $dESCORPKey = '';
public function __construct() { $this->publicPemFilePath = C('lakala_cer_filePath'); $this->privatePemFilePath=C('lakala_key_filePath'); $this->dESCORPKey = C('lakala_encrypt_key'); }
public function ToLakala($head,$data,$url){ $ret = [ 'head'=>$head, 'request'=>$data, ]; $ret = json_encode($ret); $params = $this->signRequestJsonToSend($ret);
$ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8', 'Content-Length: ' . strlen($data))); curl_setopt($ch, CURLOPT_POSTFIELDS, $params); $result = curl_exec($ch); curl_close($ch); $result = $params; $response = $this->verifyResponseJson($result); return $response; }
public function FromLakala(){ $lakalaSign = new SignService(); $params = I(''); $params = $this->verifyRequestJson($params); return $params; }
public function FromLakalaResponse($head,$response){ $ret = [ 'head'=>$head, 'response'=>$response, ]; $res = $this->signResponseJsonToSend($ret); return $res; }
public function signRequestJsonToSend($jsonStr) { if(strpos($jsonStr,"\"head\"")!= false) { $req = json_decode(trim($jsonStr), true); ksort($req); $signData = $this->signData($req['request']); $req['head']['signData']= $signData; $req['request']=json_encode($req['request'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); ksort($req['head']); $resultObj = $req;
} else if(strpos($jsonStr,"\"comm\"")!= false) { $req = json_decode(trim($jsonStr), true); ksort($req); $signData = $this->signData($req['data']); $req['comm']['signtx']=$signData; $req['data']=json_encode($req['data'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); ksort($req['head']); $resultObj = $req; }
$signedReq = json_encode($resultObj,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$key = $this->dESCORPKey; $encryptData = self::encrypt($key,$signedReq);
return $encryptData; }
public function signResponseJsonToSend($jsonStr) { if(strpos($jsonStr,"\"head\"")!= false) { $response = json_decode(trim($jsonStr), true); $resultObj = json_decode(json_encode($response)); } else if(strpos($jsonStr,"\"comm\"")!= false) { $response = json_decode(trim($jsonStr), true); ksort($response); $signData = $this->signData($response['data']);
$response['comm']['signTx']=$signData; $response['data']=json_encode($response['data'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); ksort($response['comm']); $resultObj = $response; }
$signedReq = json_encode($resultObj,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$key = $this->$dESCORPKey; $encryptData = self::encrypt($key, $signedReq);
return $encryptData; }
public function verifyResponseJson($json) { $key = $this->dESCORPKey; $decryptBytes = self::decrypt($key, $json);
$obj = json_decode($decryptBytes); $reqStr = json_encode($obj); if(strpos($reqStr,"\"comm\"")!= false) { $response = json_decode($reqStr,true); $signtx = $response['comm']['signtx'];
$nRet = $this->verifyData($response['data'], $signtx); if($nRet) { $response['comm']['signtx']=""; return json_encode($response); } else { return null; } } else if(strpos($reqStr,"\"head\"")!= false) { return $reqStr; } else { return null; } }
public function verifyRequestJson($json) { $key = $this->dESCORPKey; $decryptBytes = self::decrypt($key, $json);
$obj = json_decode($decryptBytes); $reqStr = json_encode($obj); if(strpos($reqStr,"\"comm\"")!= false) { $req = json_decode($reqStr,true); ksort($req); $signtx = $req['comm']['signtx'];
$nRet = $this->verifyData($req['data'], $signtx); if($nRet) { $req['comm']['signtx']=""; return json_encode($req); } else { return null; } } else if(strpos($reqStr,"\"head\"")!= false) { $req = json_decode($reqStr,true); ksort($req); $signData = $req['head']['signData']; $nRet = $this->verifyData($req['request'], $signData); return $nRet; if($nRet) { $req['head']['signData']=""; return json_encode($req); } else { return null; } } else { return null; } }
private function signData($param) { $content = json_encode($param,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); $privateKey=openssl_pkey_get_private(file_get_contents($this->privatePemFilePath)); openssl_sign($content, $signature, $privateKey); openssl_free_key($privateKey); return base64_encode($signature);
}
private function verifyData($urlParam,$signParam) { $signature = base64_decode($signParam); $pubkeyid = openssl_pkey_get_public(file_get_contents($publicPemFilePath)); $verifies = openssl_verify($urlParam, $signature, $pubkeyid); return $verifies; }
public function encrypt($key,$data){ $decode_key = base64_decode($key); $encData = openssl_encrypt($data, 'DES-EDE3', $decode_key, OPENSSL_RAW_DATA); $encData = base64_encode($encData); return $encData; }
public function decrypt($key,$data){ $decode_key = base64_decode($key); $data = base64_decode($data); $decData = openssl_decrypt($data, 'DES-EDE3', $decode_key, OPENSSL_RAW_DATA); return $decData; }
}
|