Okhttp/Retrofit网络请求加解密实现方案
一、加密方案
比较安全的方案应该是AES+RSA的加密方式。具体如下图(此图源于网络)所示。
AES+RSA流程
为什么要这样做呢?
1、RSA是非对称加密,公钥和私钥分开,且公钥可以公开,很适合网络数据传输场景。但RSA加密比较慢,据说比AES慢100倍,且对加密的数据长度也有限制。
2、AES是对称加密,加密速度快,安全性高,但密钥的保存是个问题,在网络数据传输的场景就很容易由于密钥泄露造成安全隐患
3、所以,AES+RSA结合才更好,AES加密数据,且密钥随机生成,RSA用对方(服务器)的公钥加密随机生成的AES密钥。传输时要把密文,加密的AES密钥和自己的公钥传给对方(服务器)。对方(服务器)接到数据后,用自己的私钥解密AES密钥,再拿AES密钥解密数据得到明文。这样就综合了两种加密体系的优点。
4、除上面说的外,还可以加签名,即对传输的数据(加密前)先做个哈希,然后用自己的RSA私钥对哈希签名(对方拿到自己的公钥可以验签),这样可以验证传输内容有没有被修改过。
二、加密相关的一些坑
1、数据类型
就java来说,加密的输入和输出都是字节数组类型的,也就是二进制数据,网络传输或本地保存都需要重新编码为字符串。推荐使用Base64。Android 有自带的Base64实现,flag要选Base64.NO_WRAP,不然末尾会有换行影响服务端解码。
Android中Base64加密
//字节数组转字符串
String str = Base64.encodeToString(byte_data, Base64.NO_WRAP);
//字符串转字节数组
byte[] bytes = Base64.decode(keyStr, Base64.NO_WRAP);
2、加密参数
总而言之,这些不同语言都有实现库,调用即可,关键是参数要一致,具体还需要和后台联调一下。
rsa加解密的内容超长的问题解决
AES算法:
Android端--->"AES/CFB/NOPADDING"
密钥长度一般128,256安全性更高
ECB模式不安全,使用会有黄色警告。
RSA算法:
密钥长度=1024已经被认为不安全了(RSA 768已于2009年被破解),推荐>=2048。加密的明文长度和密钥长度是相关的。
Android端-->"RSA/ECB/PKCS1Padding"
RSA签名算法:
Android端-->"SHA1withRSA"
试过MD5withRSA,但是和后台无法兼容
三、OkHttp/Retrofit的实现
现在说到网络框架,应该毫无疑问是Retrofit了。上面说的加密方案说到底还是要在网络请求框架内加上,怎么做入侵最小,怎么做最方便才是重点。
1、坑定不能直接在接口调用层做加密,加参数,这样每个接口都要修改,这是不可能的。
2、ConverterFactory处理,这也是网上可以搜到的很多文章的写法,但我觉得还是有入侵。而且有点麻烦。
3、OkHttp添加拦截器,这种方法入侵最小(可以说没有),实现呢也非常优雅。
下面的实现,网上也找不到多少可以参考的文章,但不得不说,OkHttp的封装和设计真的很好用,所见即所得。看下源码,就知道该怎么用了,连文档都不用查。
-----------------------------------------------------------------------------------------------------------------------------------------------
----->先定义一个拦截器的实现:
-----------------------------------------------------------------------------------------------------------------------------------------------
public class DataEncryptInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
//请求
Request request = chain.request();
RequestBody oldRequestBody = request.body();
Buffer requestBuffer = new Buffer();
oldRequestBody.writeTo(requestBuffer);
String oldBodyStr = requestBuffer.readUtf8();
requestBuffer.close();
MediaType mediaType = MediaType.parse("text/plain; charset=utf-8");
//生成随机AES密钥并用serverPublicKey进行RSA加密
SecretKeySpec appAESKeySpec = EncryptUtils.generateAESKey(256);
String appAESKeyStr = EncryptUtils.covertAESKey2String(appAESKeySpec);
String appEncryptedKey = RSAUtils.encryptDataString(appAESKeyStr, serverPublicKey);
//计算body 哈希 并使用app私钥RSA签名
String appSignature = RSAUtils.signature(oldBodyStr, appPrivateKey);
//随机AES密钥加密oldBodyStr
String newBodyStr = EncryptUtils.encryptAES(appAESKeySpec, oldBodyStr);
RequestBody newBody = RequestBody.create(mediaType, newBodyStr);
//构造新的request
request = request.newBuilder()
.header("Content-Type", newBody.contentType().toString())
.header("Content-Length", String.valueOf(newBody.contentLength()))
.method(request.method(), newBody)
.header("appEncryptedKey", appEncryptedKey)
.header("appSignature", appSignature)
.header("appPublicKey", appPublicKeyStr)
.build();
//响应
Response response = chain.proceed(request);
if (response.code() == 200) {//只有约定的返回码才经过加密,才需要走解密的逻辑
//获取响应头
String serverEncryptedKey = response.header("serverEncryptedKey");
//用app的RSA私钥解密AES加密密钥
String serverDecryptedKey = RSAUtils.decryptDataString(serverEncryptedKey, appPrivateKey);
SecretKeySpec serverAESKeySpec = EncryptUtils.covertString2AESKey(serverDecryptedKey);
//用AES密钥解密oldResponseBodyStr
ResponseBody oldResponseBody = response.body();
String oldResponseBodyStr = oldResponseBody.string();
String newResponseBodyStr = EncryptUtils.decryptAES(serverAESKeySpec, oldResponseBodyStr);
oldResponseBody.close();
//构造新的response
ResponseBody newResponseBody = ResponseBody.create(mediaType, newResponseBodyStr);
response = response.newBuilder().body(newResponseBody).build();
newResponseBody.close();
}
//返回
return response;
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------
----->然后OkHttp加入该拦截器:
-----------------------------------------------------------------------------------------------------------------------------------------------
new OkHttpClient.Builder()
.addInterceptor(new DataEncryptInterceptor())
....
.build();
-----------------------------------------------------------------------------------------------------------------------------------------------
----->这样就搞定了。
-----------------------------------------------------------------------------------------------------------------------------------------------
主要注意点:
0、和接口无关的新加的数据放在请求头里。
1、该close的要close,不然会内存泄漏。
2、新旧Request和Response要区分好,新的要替换旧的去传递或返回。
3、要对response.code()做处理,只有在和后台约定好的返回码下才走解密的逻辑,具体看自己的需求,不一定都是200。