0x01 加密方案

比较安全的方案应该是AES+RSA的加密方式。具体如下图所示。

1.1 为什么要这样做呢?

  1. RSA是非对称加密,公钥和私钥分开,且公钥可以公开,很适合网络数据传输场景。但RSA加密比较慢,据说比AES慢100倍,且对加密的数据长度也有限制。
  2. AES是对称加密,加密速度快,安全性高,但密钥的保存是个问题,在网络数据传输的场景就很容易由于密钥泄露造成安全隐患
  3. 所以,AES+RSA结合才更好,AES加密数据,且密钥随机生成,RSA用对方(服务器)的公钥加密随机生成的AES密钥。传输时要把密文,加密的AES密钥和自己的公钥传给对方(服务器)。对方(服务器)接到数据后,用自己的私钥解密AES密钥,再拿AES密钥解密数据得到明文。这样就综合了两种加密体系的优点。
  4. 除上面说的外,还可以加签名,即对传输的数据(加密前)先做个哈希,然后用自己的RSA私钥对哈希签名(对方拿到自己的公钥可以验签),这样可以验证传输内容有没有被修改过。

0x02 加密相关的一些坑

2.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.2 加密参数

总而言之,这些不同语言都有实现库,调用即可,关键是参数要一致,具体还需要和后台联调一下。

rsa加解密的内容超长的问题解决

AES算法:
Android端--->"AES/CFB/NOPADDING"
密钥长度一般128,256安全性更高
ECB模式不安全,使用会有黄色警告。
 
RSA算法:
密钥长度=1024已经被认为不安全了(RSA 768已于2009年被破解),推荐>=2048。加密的明文长度和密钥长度是相关的。
Android端-->"RSA/ECB/PKCS1Padding"
 
RSA签名算法:
Android端-->"SHA1withRSA"
试过MD5withRSA,但是和后台无法兼容

0x03 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();
            }
            response.close();
            //返回
            return response;
        }
    }
 
----->然后OkHttp加入该拦截器:
new OkHttpClient.Builder()
                .addInterceptor(new DataEncryptInterceptor())
                ....
                .build();
 
----->这样就搞定了。


主要注意点:

0、和接口无关的新加的数据放在请求头里。
1、该close的要close,不然会内存泄漏。
2、新旧Request和Response要区分好,新的要替换旧的去传递或返回。
3、要对response.code()做处理,只有在和后台约定好的返回码下才走解密的逻辑,具体看自己的需求,不一定都是200。

Last modification:March 10th, 2020 at 07:33 pm
如果觉得我的文章对你有用,请随意赞赏