背景
在业务中,我们可能需要面对在 Android 本地存储用户 token、email 等敏感数据。本文将讲述一种安全系数较高的 Android 本地存储方案。
思路
整个方案的核心思路围绕 KeyStore 展开,如果不太了解 KeyStore 的小伙伴,请先阅读 Android 密钥库系统。
由于 KeyStore 在 Android 6.0 前后差异较大,并且 Android 4.3 以下系统并不支持 KeyStore,方案需要根据不同的 Android 版本做不同的处理,以及需要提供降级方案。加解密算法采用 AES/CBC/PKCS7Padding。
Android 6.0 或更高版本系统
这种情况下方案最简单,随机生成 128 位 AES key 和 iv,key 存储在 KeyStore 中,设置 alias,iv 存储在 SharedPreferences 中。需要加解密的时候通过 alias 从 KeyStore 中取 key,从 SharedPreferences 中取 iv。
首先初始化 KeyStore:
1 | private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; |
然后就是生成 key 了,这里贴出核心代码:
1 | (api = Build.VERSION_CODES.JELLY_BEAN_MR2) |
那加解密的时候怎么取得 key 呢?很简单:
1 | Key key = mKs.getKey(alias, null); |
取出来 key 作为 Cipher 的参数在 init 的时候传入就可以了,剩下的就是常规的加解密,这里就不贴代码了。
Android 4.3 - 5.1
由于 KeyStore 在 Android 4.3 - 5.1 版本不支持 AES 算法,所以需要随机生成 RSA 的 key pair,算法采用 RSA/ECB/PKCS1Padding,设置 alias 并存储在 KeyStore 中。然后随机生成 128 位 AES key 和 iv,使用前面生成的 RSA 公钥加密 key,key 和 iv 一起存储在 SharedPreferences 中。
需要加解密的时候,从 SharedPreferences 中读取加密后的 key,从 KeyStore 中取出 RSA 私钥,使用 RSA 私钥解密得到真正的 AES key,再进行加解密。
这里贴出第二种情况下的代码:
1 | private static final String TYPE_RSA = "RSA"; |
代码一贴,一切就明朗起来了。这里要注意处理代码中 catch 的几种异常,会在某些三星手机或者其他手机上出现。
那这里怎么取 key 呢?
1 | private static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding"; |
取出来 key 作为 Cipher 的参数在 init 的时候传入就可以了,剩下的就是常规的加解密,这里就不贴代码了。这种情况下,加解密会比第一种情况下耗时,因为需要经历一次 RSA 的解密操作。
Android 4.3 以下版本以及降级方案
KeyStore 不支持 Android 4.3 以下的系统。这里提供一种简单思路:可以写一个 so 库,通过种子字符串生成固定的 128 位 AES key 和 iv,将 iv 存储在 SharedPreferences 中。在 so 库中内置应用签名,在 JNI_OnLoad 函数中进行签名校验,检验不通过直接 Crash。生成的 key 不保存在本地,需要加解密的时候调用 so 方法实时获取 key。
注:种子字符串是一串随机的字符串,内置在 so 库中,用于通过计算生成 key。如果签名验证失败,就无法生成 key。它不是绝对安全的,只是尽可能保证安全。
补充
建议提供 root 检测工具,如果检测到设备已经 root,直接提示用户存在安全风险。
总结
围绕 KeyStore 的方案大致思路讲完了,其中一些坑也是线上踩了总结出来的,核心代码其实很少,思路最关键,欢迎交流。