24、Security:EncryptedSharedPreferences、EncryptedFile、密钥管理、实战:敏感数据加密
说到 Android 安全,很多人第一反应是网络加密、HTTPS。但说实话,本地存储的敏感数据往往才是最容易翻车的地方。
我见过不少项目,SharedPreferences 里直接存明文 token、身份证号、支付密码。嗯,你想想看,只要手机被 root 或者备份文件被拿到,这些数据就等于裸奔。
这一章,我们就来聊聊 Jetpack Security 库。它提供了两个核心武器:EncryptedSharedPreferences 和 EncryptedFile。再加上合理的密钥管理,基本能搞定 90% 的本地敏感数据加密需求。
核心要点:加密不是目的,密钥安全才是。Jetpack Security 帮你封装了 AES-256 加密 + Android Keystore 密钥存储,你只需要关心「存什么」和「怎么读」。
24.1 为什么需要 EncryptedSharedPreferences?
传统的 SharedPreferences,说白了就是一个 XML 文件,放在 /data/data/包名/shared_prefs/ 下。任何有 root 权限的进程,或者通过 adb backup 都能直接读取。
我曾在一次安全审计中,随手 pull 了一个应用的 SP 文件,里面赫然写着 user_token=abc123、id_card=110101199001011234。当时我就想,这要是被黑产拿到,后果不堪设想。
EncryptedSharedPreferences 做的事情很简单:写入时自动加密,读取时自动解密。你不需要手动处理加解密逻辑,API 用法和普通 SP 几乎一样。
24.2 快速上手:EncryptedSharedPreferences
首先,在 build.gradle 里添加依赖:
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}
然后,创建加密的 SP 实例:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"my_secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 写入
sharedPreferences.edit().putString("token", "eyJhbGciOiJIUzI1NiIs...").apply()
// 读取
val token = sharedPreferences.getString("token", null)
看到了吗?写入和读取的代码和普通 SP 一模一样。区别在于,底层数据已经被 AES-256 加密了。你打开 XML 文件,看到的是一堆乱码。
我的习惯:我会把加密 SP 单独封装成一个类,比如 SecurePrefsManager。这样业务层完全感知不到加密的存在,切换也方便。
24.3 EncryptedFile:加密文件存储
如果你需要加密的不是键值对,而是整个文件(比如图片、日志、数据库文件),那就轮到 EncryptedFile 出场了。
我记得有个项目需要缓存用户的人脸特征数据,明文存肯定不行。用 EncryptedFile 就非常合适。
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedFile = EncryptedFile.Builder(
context,
File(context.filesDir, "face_feature.dat"),
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
// 写入加密数据
encryptedFile.openFileOutput().use { outputStream ->
outputStream.write(faceData.toByteArray())
}
// 读取加密数据
encryptedFile.openFileInput().use { inputStream ->
val data = inputStream.readBytes()
}
这里有个细节:AES256_GCM_HKDF_4KB 表示使用 4KB 的块大小。对于大文件,块越大性能越好,但内存占用也越高。我个人建议,普通文件用 4KB 就够,超大文件(比如视频)可以考虑 16KB。
我曾经踩过的坑:EncryptedFile 不支持随机读写。如果你需要频繁修改文件中间某一段,建议用 EncryptedSharedPreferences 或者加密数据库(比如 SQLCipher)。
24.4 密钥管理:MasterKey 与 Android Keystore
加密算法再强,密钥泄露就全完了。Jetpack Security 的 MasterKey 默认使用 Android Keystore 来存储密钥。Keystore 是系统级的安全硬件(TEE/SE),应用进程无法直接读取密钥材料。
MasterKey 的构建方式有两种:
| 方式 | 说明 | 适用场景 |
|---|---|---|
KeyScheme.AES256_GCM |
生成 AES-256 密钥,存储在 Keystore | 绝大多数场景,推荐 |
KeyScheme.AES256_GCM_WITH_KEYSTORE_BACKED |
强制使用硬件支持的密钥(如果设备支持) | 高安全要求场景 |
你还可以自定义密钥参数:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setKeyStorage(MasterKey.KeyStorage.KEYSTORE) // 强制 Keystore
.setBlockModes(MasterKey.BlockMode.GCM)
.setEncryptionAlgorithms(MasterKey.EncryptionAlgorithm.AES_256_GCM)
.build()
关键认知:MasterKey 本身不直接加密你的数据。它生成一个 AES 密钥,这个密钥被 Keystore 保护。然后 EncryptedSharedPreferences 或 EncryptedFile 用这个密钥去加密你的实际数据。这叫「密钥加密密钥」体系。
24.5 实战:敏感数据加密完整示例
假设我们要做一个「安全记事本」功能,用户输入的笔记需要加密存储。我们同时使用 EncryptedSharedPreferences 存元数据(标题、更新时间),用 EncryptedFile 存正文内容。
class SecureNoteManager(private val context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val prefs = EncryptedSharedPreferences.create(
context,
"note_meta",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveNote(noteId: String, title: String, content: String) {
// 元数据存 SP
prefs.edit().putString("${noteId}_title", title).apply()
prefs.edit().putLong("${noteId}_time", System.currentTimeMillis()).apply()
// 正文存文件
val encryptedFile = EncryptedFile.Builder(
context,
File(context.filesDir, "notes/${noteId}.dat"),
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
encryptedFile.openFileOutput().use { out ->
out.write(content.toByteArray(Charsets.UTF_8))
}
}
fun loadNote(noteId: String): Note? {
val title = prefs.getString("${noteId}_title", null) ?: return null
val time = prefs.getLong("${noteId}_time", 0L)
val encryptedFile = EncryptedFile.Builder(
context,
File(context.filesDir, "notes/${noteId}.dat"),
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
val content = encryptedFile.openFileInput().use { input ->
input.readBytes().toString(Charsets.UTF_8)
}
return Note(noteId, title, content, time)
}
}
data class Note(
val id: String,
val title: String,
val content: String,
val timestamp: Long
)
这个例子中,密钥只创建一次,SP 和文件共用同一个 MasterKey。既保证了安全性,又避免了重复创建密钥的开销。
避坑指南:我曾经在测试时发现,如果设备不支持 Keystore 硬件加密(比如某些模拟器),MasterKey 会降级使用软件加密。这在开发环境没问题,但生产环境一定要做兼容性测试。可以用 MasterKey.isKeyStoreSupported() 提前判断。
24.6 知识体系总览
下面这张图,帮你理清 Jetpack Security 的核心脉络:
从这张图可以看得很清楚:应用层只管读写,加密引擎负责加解密,MasterKey 和 Keystore 负责保护密钥。每一层各司其职,你不需要关心底层细节。
24.7 总结与建议
- 能用 EncryptedSharedPreferences 就别手写加密——它已经经过了 Google 的安全审计,比自己拼凑的 AES 实现靠谱得多。
- 密钥不要硬编码——永远不要写
private static final String KEY = "123456"。用 MasterKey + Keystore,让系统帮你管密钥。 - 注意性能——EncryptedFile 适合大文件,但每次读写都有加解密开销。如果只是存几个字符串,用 EncryptedSharedPreferences 更轻量。
- 备份问题——加密后的数据如果被备份到云端,恢复时只要 Keystore 密钥还在就能解密。但如果用户刷机或换设备,密钥会丢失。这时候需要配合服务器端的密钥恢复方案。
最后提醒一句:加密不是银弹。如果你的应用本身存在漏洞(比如 WebView 远程代码执行),攻击者可以在你的应用进程内读取解密后的数据。安全是一个系统工程,加密只是其中一环。
好了,这一章的内容就到这里。代码不多,但背后的安全理念值得反复琢磨。下次你写 SharedPreferences 的时候,不妨多问自己一句:这个数据如果被别人看到了,会出问题吗?
公众号:蓝海资料掘金营,微信deep3321