Retrofit+OkHttp 参数使用AES加密Demo

Posted microhex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Retrofit+OkHttp 参数使用AES加密Demo相关的知识,希望对你有一定的参考价值。

最近在做App代码安全方面的优化,特此记录一下。
我们现在App大多数都是基于Retrofit+OkHttp的网络请求框架,现在的需求是需要将请求的参数进行加密传输,下面图片中我们进行一个对比,一个是明文传输,一个是密文传输:

明文传输密文传输

由于考虑到加密和解密的效率,我们现在选用的是AES对称加密。至于对称和非对称加密,可以参考以往的文章

1. 客户端修改

在未加密前,我们客户端与后端协商,是采用Json的形式传输字段请求的,比如一个登录接口,还有userNamepassword,那么就直接传输为:

"password":"12345","userName":"tom"

Http请求体可以看到如下内容:

POST /user/login/ HTTP/1.1
Content-Type: application/json; charset=UTF-8
Content-Length: 37
Host: 192.168.2.105:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.9.3
Pragma: no-cache
Cache-Control: no-cache

"password":"12345","userName":"tom"

我们现在的目标,是需要将

"password":"12345","userName":"tom"

加密,加密成密文之后,再传输给后端服务器。

由于采用的是OKhttp网络请求框架,我们很容易会想到使用拦截器Interceptor来实现我们的需求。首先画图说明一下Interceptor的逻辑:


我们将要对需要出去的数据进行加密,然后对返回的数据进行解密。那么因此我们可以定义一下DataEncryptInterceptor逻辑,代码如下:

public class DataEncryptInterceptor implements Interceptor 

    @NonNull
    @Override
    public Response intercept(@NonNull Chain chain) throws IOException 
        Request request = chain.request();

        RequestBody oldBodyRequest = request.body();

        Buffer requestBuffer = new Buffer();
        oldBodyRequest.writeTo(requestBuffer);
        String oldBodyStr = requestBuffer.readUtf8();
        requestBuffer.close();

        Log.d("TAG", "the old body str is :" + oldBodyStr);

        //String randomKeyValue = "hello_" + System.currentTimeMillis() + "_world";
        String randomKeyValue = "zhangsanlisiwangwu";
        String newBodyStr = AESUtils.encrypt(oldBodyStr,randomKeyValue);
        if (TextUtils.isEmpty(newBodyStr)) newBodyStr = "";

        MediaType mediaType = MediaType.parse("text/plain;charset=utf-8");
        RequestBody newRequestBody = RequestBody.create(mediaType, newBodyStr);

        //构建新的request
        Request newRequest = request.newBuilder().header("Content-type", newRequestBody.contentType().toString())
                .header("Content-Length", String.valueOf(newRequestBody.contentLength()))
                .method(request.method(), newRequestBody)
                .header("key", randomKeyValue)
                .build();

        Response response = chain.proceed(newRequest);
        if (response.code() / 200 == 1) 
            ResponseBody oldResponseBody = response.body();

            String oldResponseBodyStr = oldResponseBody.string();

            String newResponseBodyStr = AESUtils.decrypt(oldResponseBodyStr,randomKeyValue);
            if (TextUtils.isEmpty(newResponseBodyStr)) newResponseBodyStr = "data decrypy error";
            
            ResponseBody responseBody = ResponseBody.create(mediaType, newResponseBodyStr);

            response = response.newBuilder().body(responseBody).build();
        
		return response;
    


整体的请求大概分为这8步,逻辑已经写的很清楚了。这里讲一下第二步,对旧的请求体进行AES加密。我们知道对AES加密时,需要提供密钥的,就像是开一把锁,需要有钥匙一样。这里我写的比较简单,使用的是固定的值,在正式的商业开发中,我们需要与后端进行协商,尽量每次传输使用动态的,破解难度比较的密钥。

写完之后,直接在OkHttpClient中添加即可:

OkHttpClient.Builder().
                    connectTimeout(8_000, TimeUnit.MILLISECONDS).
                    readTimeout(8_000,TimeUnit.MILLISECONDS).
                    addInterceptor(logger).
                    addInterceptor(DataEncryptInterceptor()).
                    build()

然后其它的地方几乎不需要动任何代码,直接就能实现数据的加密和解密了。

对于几个比较重要的类,现在直接把代码贴出来,到时候可以直接烤着拿去用:

AESUtils:

import java.io.Closeable;
import java.nio.charset.StandardCharsets;

import javax.crypto.Cipher;

import java.nio.charset.Charset;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;


public class AESUtils 


    /**
     * 加密算法
     */
    private static final String KEY_ALGORITHM = "AES";

    /**
     * AES 的 密钥长度,32 字节,范围:16 - 32 字节
     */
    public static final int SECRET_KEY_LENGTH = 32;

    /**
     * 字符编码
     */
    private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;

    /**
     * 秘钥长度不足 16 个字节时,默认填充位数
     */
    private static final String DEFAULT_VALUE = "0";
    /**
     * 加解密算法/工作模式/填充方式
     */
    private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
    /**
     * 加密密码,长度:16 或 32 个字符(随便定义)
     */
    private static final String secretKey = "zhangsanlisiwangwu";

    public static String getSecretKey() 
        return secretKey;
    

    public static String encrypt(String data, String secretKey) 
        try 
            //创建密码器
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            //初始化为加密密码器
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(secretKey));
            byte[] encryptByte = cipher.doFinal(data.getBytes(CHARSET_UTF8));
            // 将加密以后的数据进行 Base64 编码
            return base64Encode(encryptByte);
         catch (Exception e) 
            handleException(e);
        
        return null;
    



    /**
     * AES 加密
     *
     * @param data 待加密内容
     * @return 返回Base64转码后的加密数据
     */
    public static String encrypt(String data) 
        return encrypt(data,secretKey);
    


    public static String decrypt(String base64Data, String secretKey) 
        try 
            byte[] data = base64Decode(base64Data);
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            //设置为解密模式
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(secretKey));
            //执行解密操作
            byte[] result = cipher.doFinal(data);
            return new String(result, CHARSET_UTF8);
         catch (Exception e) 
            handleException(e);
        
        return null;
    


    /**
     * AES 解密
     *
     * @param base64Data 加密的密文 Base64 字符串
     */
    public static String decrypt(String base64Data) 
        return decrypt(base64Data,secretKey);
    

    /**
     * 使用密码获取 AES 秘钥
     */
    public static SecretKeySpec getSecretKey(String secretKey) 
        secretKey = toMakeKey(secretKey, SECRET_KEY_LENGTH, DEFAULT_VALUE);
        return new SecretKeySpec(secretKey.getBytes(CHARSET_UTF8), KEY_ALGORITHM);
    

    /**
     * 如果 AES 的密钥小于 @code length 的长度,就对秘钥进行补位,保证秘钥安全。
     *
     * @param secretKey 密钥 key
     * @param length    密钥应有的长度
     * @param text      默认补的文本
     * @return 密钥
     */
    private static String toMakeKey(String secretKey, int length, String text) 
        // 获取密钥长度
        int strLen = secretKey.length();
        // 判断长度是否小于应有的长度
        if (strLen < length) 
            // 补全位数
            StringBuilder builder = new StringBuilder();
            // 将key添加至builder中
            builder.append(secretKey);
            // 遍历添加默认文本
            for (int i = 0; i < length - strLen; i++) 
                builder.append(text);
            
            // 赋值
            secretKey = builder.toString();
        
        return secretKey;
    

    /**
     * 将 Base64 字符串 解码成 字节数组
     */
    public static byte[] base64Decode(String data) 
        return Base64.decode(data, Base64.NO_WRAP);
    

    /**
     * 将 字节数组 转换成 Base64 编码
     */
    public static String base64Encode(byte[] data) 
        return Base64.encodeToString(data, Base64.NO_WRAP);
    

    /**
     * 处理异常
     */
    private static void handleException(Exception e) 
        e.printStackTrace();
        System.out.println(e.getLocalizedMessage());
    

    /**
     * 初始化 AES Cipher
     *
     * @param secretKey  密钥
     * @param cipherMode 加密模式
     * @return 密钥
     */
    private static Cipher initFileAESCipher(String secretKey, int cipherMode) 
        try 
            // 创建密钥规格
            SecretKeySpec secretKeySpec = getSecretKey(secretKey);
            // 获取密钥
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            // 初始化
            cipher.init(cipherMode, secretKeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
            return cipher;
         catch (Exception e) 
            handleException(e);
        
        return null;
    

    /**
     * 关闭流
     *
     * @param closeable 实现Closeable接口
     */
    private static void closeStream(Closeable closeable) 
        try 
            if (closeable != null) closeable.close();
         catch (Exception e) 
            handleException(e);
        
    



其中还有一个Base64类逻辑,也算是比较简单:
Base64:


import java.io.UnsupportedEncodingException;
import java.io.UnsupportedEncodingException;

public class Base64 
    /**
     * Default values for encoder/decoder flags.
     */
    public static final int DEFAULT = 0;

    /**
     * Encoder flag bit to omit the padding '=' characters at the end
     * of the output (if any).
     */
    public static final int NO_PADDING = 1;

    /**
     * Encoder flag bit to omit all line terminators (i.e., the output
     * will be on one long line).
     */
    public static final int NO_WRAP = 2;

    /**
     * Encoder flag bit to indicate lines should be terminated with a
     * CRLF pair instead of just an LF.  Has no effect if @code
     * NO_WRAP is specified as well.
     */
    public static final int CRLF = 4;

    /**
     * Encoder/decoder flag bit to indicate using the "URL and
     * filename safe" variant of Base64 (see RFC 3548 section 4) where
     * @code - and @code _ are used in place of @code + and
     * @code /.
     */
    public static final int URL_SAFE = 8;


    public static final int NO_CLOSE = 16;

    //  --------------------------------------------------------
    //  shared code
    //  --------------------------------------------------------

    /* package */ static abstract class Coder 
        public byte[] output;
        public int op;

        /**
         * Encode/decode another block of input data.  this.output is
         * provided by the caller, and must be big enough to hold all
         * the coded data.  On exit, this.opwill be set to the length
         * of the coded data.
         *
         * @param finish true if this is the final call to process for
         *        this object.  Will finalize the coder state and
         *        include any final bytes in the output.
         *
         * @return true if the input so far is good; false if some
         *         error has been detected in the input stream..
         */
        public abstract boolean process(byte[] input, int offset, int len, boolean finish);

        /**
         * @return the maximum number of bytes a call to process()
         * could produce for the given number of input bytes.  This may
         * be an overestimate.
         */
        public abstract int maxOutputSize(int len);
    

    //  --------------------------------------------------------
    //  decoding
    //  --------------------------------------------------------

    /**
     * Decode the Base64-encoded data in input and return the data in
     * a new byte array.
     *
     * <p>The padding '=' characters at the end are considered optional, but
     * if any are present, there must be the correct number of them.
     *
     * @param str    the input String to decode, which is converted to
     *               bytes using the default charset
     * @param flags  controls certain features of the decoded output.
     *               Pass @code DEFAULT to decode standard Base64.
     *
     * @throws IllegalArgumentException if the input contains
     * incorrect padding
     */
    public static byte[] decode(String str, int flags) 
        return decode(str.getBytes(), flags);
    

    /**
     * Decode the Base64-encoded data in input and return the data in
     * a new byte array.
     *
     * <p>The padding '=' characters at the end are considered optional, but
     * if any are present, there must be the correct number of them.
     *
     * @param input the input array to decode
     * @param flags  controls certain features of the decoded output.
     *               Pass @code DEFAULT to decode standard Base64.
     *
     * @throws IllegalArgumentException if the input contains
     * incorrect padding
     */
    public static byte[] decode(byte[] input, int flags) 
        return decode(input, 0, input.length, flags);
    

    /**
     * Decode the Base64-encoded data in input and return the data in
     * a new byte array.
     *
     * <p>The padding '=' characters at the end are considered optional, but
     * if any are present, there must be the correct number of them.
     *
     * @param input  the data to decode
     * @param offset the position within the input array at which to start
     * @param len    the number of bytes of input to decode
     * @param flags  controls certain features of the decoded output.
     *               Pass @code DEFAULT to decode standard Base64.
     *
     * @throws IllegalArgumentException if the input contains
     * incorrect padding
     */
    public static byte[] decode(byte[] input, int offset, int len, int flags) 
        // Allocate space for the most data the input could represent.
        // (It could contain less if it contains whitespace, etc.)
        Decoder decoder = new Decoder(flags, new byte[len*3/4]);

        if (!decoder.process(input, offset, len, true)) 
            throw new IllegalArgumentException("bad base-64");
        

        // Maybe we got lucky and allocated exactly enough output space.
        if (decoder.op == decoder.output.length) 
            return decoder.output;
        

        // Need to shorten the array, so allocate a new one of the
        // right size and copy.
        byte[] temp = new byte[decoder.op];
        System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
        return temp;
    

    /* package */ static class Decoder extends Coder 
        /**
         * Lookup table for turning bytes into their position in the
         * Base64 alphabet.
         */
        private static final int DECODE[] = 
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
            52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
            -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25Retrofit+OkHttp 参数使用AES加密Demo

Retrofit+OkHttp 参数使用AES加密Demo

Android Okhttp/Retrofit网络请求加解密实现方案

如何使用 Retrofit 2 + OkHttp 3 加密/隐藏 HTTPS 调用的主体?

Retrofit/OkHttp API接口加固技术实践(下)

retrofit和okhttp请求url的参数拼接