16、Security-Crypto:EncryptedSharedPreferences、文件加密、密钥管理
说到 Android 上的数据安全,很多人第一反应就是「我用数据库存,别人总看不到吧?」。嗯,太天真了。我见过太多项目,SharedPreferences 里直接存明文 token,数据库文件被 root 过的手机一读就全暴露了。今天我们来聊聊 Jetpack 里的 Security-Crypto 库,说白了就是帮你把数据「锁起来」——而且是官方帮你锁。
16.1 为什么需要 Security-Crypto?
你想想看,Android 设备上存储的数据,尤其是用户隐私信息,比如登录 token、支付密码、身份证号,一旦被恶意应用或者通过 adb backup 拿到,后果很严重。SharedPreferences 默认是明文存储的,文件路径在 /data/data/包名/shared_prefs/ 下,root 过的手机直接 cat 就能看到。
Security-Crypto 库提供了两样核心武器:
- EncryptedSharedPreferences:自动加密的键值对存储
- 文件加密:对任意文件进行加解密操作
背后依赖的是 Android Keystore 系统,密钥由硬件安全模块(TEE)保管,应用层拿不到原始密钥材料。这才是真正的「锁在保险柜里」。
16.2 EncryptedSharedPreferences 实战
先加依赖。在 build.gradle 里引入:
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}
然后创建实例。我个人习惯把初始化封装成一个单例,避免到处传 Context:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 使用方式跟普通 SharedPreferences 一模一样
sharedPreferences.edit().putString("token", "eyJhbGciOiJIUzI1NiIs...").apply()
val token = sharedPreferences.getString("token", null)
注意看,MasterKey 的构建用了 AES256_GCM 方案,这是 Android 推荐的对称加密算法。密钥会自动生成并存入 Keystore,你不需要关心它长什么样。
16.3 文件加密:不只是存个字符串
有时候我们需要加密的不只是键值对,而是一整个文件——比如用户下载的 PDF 合同、拍照的身份证图片。Security-Crypto 提供了 EncryptedFile 类。
我在项目中遇到过这样一个场景:用户上传身份证照片前,需要先加密存储在本地,等网络恢复后再上传。直接用 EncryptedFile 搞定:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedFile = EncryptedFile.Builder(
context,
File(context.filesDir, "id_card.jpg"),
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
// 写入加密数据
encryptedFile.openFileOutput().use { outputStream ->
// 假设 bitmap 是用户拍摄的身份证图片
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
}
// 读取解密数据
encryptedFile.openFileInput().use { inputStream ->
val decryptedBitmap = BitmapFactory.decodeStream(inputStream)
}
这里有个细节:AES256_GCM_HKDF_4KB 中的 4KB 指的是文件分块大小。加密时文件会被切成 4KB 的块,每块独立加密。这样做的好处是——你不需要一次性把整个文件加载到内存里,对大文件非常友好。
use 块确保关闭。我早期有个版本忘了关流,导致文件句柄泄漏,应用运行几天后突然无法写入新文件。排查了半天才发现是流没关。血的教训。
16.4 密钥管理:最容易被忽视的环节
加密算法再强,密钥泄露就全完了。Android Keystore 系统提供了硬件级保护,但开发者还是有一些事情要注意。
我总结了几条关键原则:
| 原则 | 说明 | 我的建议 |
|---|---|---|
| 不要硬编码密钥 | 密钥字符串写在代码里,反编译就暴露 | 用 MasterKey.Builder 自动生成 |
| 区分设备绑定 | 密钥是否绑定到设备?绑定后无法迁移 | 敏感数据用绑定密钥,非敏感用可迁移 |
| 密钥轮换 | 定期更换密钥,降低泄露风险 | 每次应用更新时重新生成 MasterKey |
| 生物识别保护 | 密钥使用前需要指纹/人脸验证 | 支付类场景强烈建议开启 |
如果你需要密钥绑定到生物识别,可以这样配置:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setUserAuthenticationRequired(true) // 需要用户验证
.setUserAuthenticationValidityDurationSeconds(30) // 30秒内有效
.build()
这样每次读取加密数据时,系统会弹出指纹/人脸验证对话框。嗯,用户体验上会多一步操作,但安全性提升了一个量级。
16.5 知识体系总览
下面这张图把 Security-Crypto 的核心脉络梳理清楚了。你可以看到,所有加密操作都依赖 Keystore 生成的 MasterKey,然后通过 EncryptedSharedPreferences 或 EncryptedFile 对外提供服务。
16.6 避坑指南
最后分享几个我实际踩过的坑,希望能帮你省点时间:
- 曾经遇到过:在 Android 6.0 以下设备上,Keystore 不支持 AES256_GCM,会直接崩溃。解决方案是运行时检测 SDK 版本,低版本回退到
AES128_GCM。 - 曾经遇到过:EncryptedSharedPreferences 的
apply()是异步的,如果你紧接着杀掉进程,数据可能没写进去。重要数据建议用commit()确保同步写入。 - 曾经遇到过:文件加密时,如果文件超过 2GB,EncryptedFile 的分块机制会导致性能急剧下降。大文件建议自己实现流式加密,或者用 NDK 层处理。
- 曾经遇到过:用户清除应用数据后,Keystore 里的密钥也会被清除。如果你有云端同步需求,记得把密钥用用户密码派生后上传,不要直接存 Keystore 里的原始密钥。
好了,Security-Crypto 的核心内容就这些。说白了就是三件事:用 EncryptedSharedPreferences 加密键值对、用 EncryptedFile 加密文件、用 Keystore 管好密钥。记住一点——不要自己写加密算法,用官方库永远是最安全的。