App安全性保障方案

App安全性保障方案

之前晋级考核的时候认识到自己在app安全领域存在薄弱环节,所以这段时间研究了Android应用的安全防护,结合公司的项目特点做出记录和总结;主要包扩两个方面:http接口安全和App防逆向破解

一 Http接口安全

Http接口安全是为了保证客户端与服务端进行数据交互时的数据安全,防止数据库被爬取及数据被篡改

动态token

token是一个与用户身份关联的字符串,作为客户端与服务端交互时的令牌,可用于唯一确定客户端用户身份.
基于token的身份验证方法一般是这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

token一般有个有效期,当token失效时,客户端会使用获取token的接口来更新token,这样能够保证客户端的token有效且是动态的,第三方截取到旧的token也无法发起有效请求,并且刷新token这个过程对于用户是无感知的.

签名验证

如果不对请求做签名验证,则可以简单通过抓包工具拿到请求参数,篡改后提交且大规模调用,导致我们的核心业务数据被篡改且系统资源被大量消耗.签名验证也是第三方开放平台确保接口安全的标准做法:

  • 微信支付

    《App安全性保障方案》 微信支付开放平台签名算法

  • 支付宝支付

    《App安全性保障方案》 支付宝支付签名算法

  • 春雨医生开放平台

    《App安全性保障方案》 春雨开放平台签名

签名算法:signStr=signature(params&timetamp&SIGN_KEY)

  1. 客户端和服务端均保存有相同的key
  2. 客户端发起请求前,参数+时间戳+key使用签名算法生成签名
  3. 客户端发起请求,携带 参数+时间戳+signStr
  4. 服务端验证时间戳是否在有效期内(如2分钟)
  5. 根据同样的签名算法计算签名并验证是否一致

签名验证解决的问题:

  • 避免请求数据被篡改;如果第三方修改其中任意参数,则会导致签名不同,后台验证后则返回调用失败
  • 防止重放攻击,避免数据库被爬取;因为有时间戳验证,同一个签名在短时间内就会无效,服务器直接拦截掉了过期的请求

注意点:

  • key的安全性很重要,建议采用服务端动态下发的方式(下发如何确保安全,也需要更多考虑),且事先约定好key的更新机制(用于key泄露时,服务端重新下发)
  • 客户端时间戳的准确性如何确保?建议每次客户端打开时计算与服务端的差值,用于确保时间戳的一致性

对返回结果加密传输

因为我们的请求会被抓包工具轻易的抓取,所以请求返回的数据需要加密处理

  1. 客户端正常发起请求
  2. 服务端进行业务处理,对response加密
  3. 客户端对response解密

解决的问题:

  • 对Response加密之后,即使第三方抓取到了我们的数据,也无法解密,保证了我们的数据安全

注意:

  • 这里需要使用可逆算法,因为需要加解密,如AES、RSA等;
  • 同样我们的秘钥也需要确保安全,且事先约定好更新机制,用于应对秘钥泄露;
  • 可以对整个response加密,也可以只对真正的业务数据部分加密,可以跟业务特点自行选择;
  • 因为存在一些接口是不能加密的(如初始化接口等),可以增加一个是否加密字段,便于客户端兼容所有接口

在Android下,可以这样简单实现

数据结构

public class CodeEntity {
    public int code;
    public String msg;
    //业务数据
    public Object data;
    //当前是否需要解密,默认不需要
    public boolean encrypted = false;
}

CommonGsonResponseBodyConverter对返回结果解密

public class CommonGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    ...

    @Override
    public T convert(ResponseBody value) throws IOException {
        String response = value.string();
        ...

        /**
         * AES解密
         * 如果该接口有加密,则先解密
         */
        if (entity.encrypted) {
            response =  AES.getInstance().decode("key",response);
        }

        ...

    }
}

AES加解密:

public class AES implements IDecode, IEncode {
    private static final String CBC_PKCS5_PADDING = "AES/ECB/PKCS5Padding";//AES是加密方式 CBC是工作模式 PKCS5Padding是填充模式
    private static final String AES = "AES";//AES 加密
    private static final Charset CHARSET = Charset.forName("UTF-8");

    private volatile static AES sAES = null;

    private AES() {
    }

    public static AES getInstance() {
        if (sAES == null)
            synchronized (AES.class) {
                if (sAES == null)
                    sAES = new AES();
            }
        return sAES;
    }

    @Override
    public String encode(String key, String plaintext) {
        try {
            byte[] raw = key.getBytes(CHARSET);
            SecretKeySpec spec = new SecretKeySpec(raw, AES);
            Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, spec);
            //解密
            byte[] input = cipher.doFinal(plaintext.getBytes(CHARSET));
            //Base64转码
            return Base64.encodeToString(input, Base64.DEFAULT);
        } catch (Exception e) {
            Logger.e("加密出错,直接返回明文==>明文为:" + plaintext);
            return plaintext;
        }
    }

    @Override
    public String decode(String key, String ciphetext) {
        try {
            byte[] raw = key.getBytes(CHARSET);
            SecretKeySpec spec = new SecretKeySpec(raw, AES);
            Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, spec);
            //将密文先使用Base64解码
            byte[] decoder = Base64.decode(ciphetext.getBytes(CHARSET), Base64.DEFAULT);
            //解密
            byte[] bytes = cipher.doFinal(decoder);
            return new String(bytes, CHARSET);
        } catch (Exception e) {
            Logger.e("解密出错,认为无需解密==>密文为:" + ciphetext);
            return ciphetext;
        }
    }

}

二 App防逆向破解

逆向工程一直都是App安全的潜在威胁,如果App没有采取防逆向措施,则你的App能够被轻易破解,包括源代码,资源及本地数据;App逆向工程和反逆向一直都是与时俱进的,这里总结App开发中常用的反逆向方案,往往都是多重方案结合起来使用,以确保App安全

代码混淆

这是最基本的防护方法,也是使用最广泛最成熟的,一般App都会使用;开启混淆后,源代码中的类名、方法名、变量名会变成随机字母,使代码难以阅读但却不影响正常运行,这样App被反编译后代码逻辑也不会暴露,另外混淆还有减少包体积的作用

资源混淆

代码混淆只混淆了我们的代码,apk中的资源在解压之后一目了然,这样导致布局、样式、图片资源等完全处于暴露状态,第三方通过简单的操作就可以拿到,所以资源文件也采取混淆操作.

推荐使用腾讯开源的AndResGuard项目进行资源压缩,不仅可以最大程度保护我们的资源安全,且可以减小APK体积

加固加壳

《App安全性保障方案》 加固

我们在加固的过程中需要三个对象:
1、需要加密的Apk(源Apk)
2、壳程序Apk(负责解密Apk工作)
3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)

主要步骤:
我们拿到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密在将壳Apk进行合并得到新的Dex文件,最后替换壳程序中的dex文件即可,得到新的Apk,那么这个新的Apk我们也叫作脱壳程序Apk.他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源Apk.然后加载Apk,让其正常运行起来。

Apk加固之后是比较安全的(相对),现在市场是有很多加固平台,可自行选择使用

本地数据安全

App有一些数据是存在设备中的,包括缓存、配置、数据库等,如果这些数据不加密,则会存在泄漏的风险,所以对于本地数据根据数据的重要性来决定是否需要加密存储

  1. 文件尽量存放在Internal Storage而不是External Storage,该目录下其他用户无法查看(root可查看)
  2. 数据库采用加密方案,例如Reaml数据可以便捷的加解密,基于Sqlite的数据库也有很多开源加密方案,可自行选择
  3. 采用通用加密方案,对sp、file、数据库均支持加解密,例如FaceBook的开源项目concel
  4. 秘钥尽量不要在本地保存(更不要直接写在代码中),如果一定要保存,需要多层加密,拆分保存

才疏学浅,还请大家及时指出博客中的问题,不慎感激

关于作者

    原文作者:uncochen
    原文地址: https://www.jianshu.com/p/0787e838b8dd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞