9、安全存储用户凭证:使用指纹加密SharedPreferences、加密存储敏感数据、解密读取数据、数据完整性校验
说实话,指纹认证成功只是第一步。你想想看,用户把手指放上去,系统告诉你“验证通过”,然后呢?你得把用户的登录令牌、支付密码、或者某个私密信息存下来吧?
我见过不少项目,指纹认证做得花里胡哨,结果存数据的时候直接往 SharedPreferences 里一扔,连个 Base64 都没做。嗯,这跟把银行卡密码写在便利贴上贴脑门上没啥区别。
这一章,我们就来聊聊怎么用指纹加密来保护这些敏感数据。说白了,就是让数据跟用户的指纹绑定——指纹不在,数据就读不出来。
9.1 为什么不能直接存 SharedPreferences?
SharedPreferences 本身是明文存储的。文件在 /data/data/包名/shared_prefs/ 目录下,只要手机 root 了,或者有备份漏洞,数据就等于裸奔。
我早期做过一个内部工具,偷懒把 API Token 直接存了 SP。结果测试同事用 adb backup 一把全导出来了……从那以后,凡是跟“凭证”沾边的数据,我绝不用明文 SP。
9.2 核心思路:用指纹派生密钥
Android 的指纹加密,本质上不是用指纹图像去加密。而是利用 Android Keystore 生成一把密钥,这把密钥只有在用户指纹验证通过后才能使用。
流程是这样的:
- 在 Keystore 中生成一个
KeyGenParameterSpec,绑定到指纹。 - 用这个密钥去加密数据(比如用户的 Token)。
- 把加密后的密文存到 SharedPreferences 里。
- 读取时,先触发指纹验证,验证通过后拿到密钥,解密数据。
说白了,SharedPreferences 里存的只是“密文”,没有指纹授权,谁也解不开。
9.3 实战:加密存储敏感数据
我们先看生成密钥的代码。我个人习惯用 AES/GCM/NoPadding,因为它自带完整性校验(GCM 模式)。
private fun generateSecretKey(keyName: String) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // 必须指纹验证
.setInvalidatedByBiometricEnrollment(true) // 新增指纹时失效
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
}
注意 setUserAuthenticationRequired(true) 这一行。没有它,密钥谁都能用。还有 setInvalidatedByBiometricEnrollment(true),意思是用户如果新增了一个指纹,这把密钥就自动废掉——安全级别很高。
加密数据时,我们需要从 Keystore 取出密钥,然后做 AES/GCM 加密:
fun encryptData(keyName: String, plaintext: String): Pair<ByteArray, ByteArray>? {
return try {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKey = keyStore.getKey(keyName, null) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv // 初始化向量,需要保存
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
Pair(iv, ciphertext)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
这里返回了 iv 和 ciphertext。iv 是初始化向量,解密时必须用到。我一般把 iv 和密文拼在一起,或者分别存到 SP 里。
9.4 解密读取数据
解密时,同样需要指纹验证。不过注意,解密操作必须在 BiometricPrompt 的回调里执行,因为只有在那时候 Keystore 才授权使用密钥。
fun decryptData(keyName: String, iv: ByteArray, ciphertext: ByteArray): String? {
return try {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKey = keyStore.getKey(keyName, null) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
val plaintext = cipher.doFinal(ciphertext)
String(plaintext, Charsets.UTF_8)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
我在项目中遇到过一个问题:如果用户锁屏方式从指纹改成了密码,或者删除了所有指纹,这把密钥就永久失效了。这时候解密会抛出 KeyStoreException,需要捕获并引导用户重新录入指纹。
9.5 数据完整性校验
GCM 模式本身带有认证标签(Authentication Tag),它会在解密时自动校验数据是否被篡改。如果密文被改过,doFinal 会抛出 AEADBadTagException。
这其实就是内置的完整性校验。你不需要额外算 MD5 或 SHA256,GCM 已经帮你做了。
但如果你用的是 CBC 模式(不推荐),那就得自己加 HMAC 做校验。我个人建议直接上 GCM,省心。
- GCM 模式:自动校验,无需额外代码。
- CBC 模式:需要手动计算 HMAC-SHA256,并比对。
- 任何时候都不要只用 ECB 模式,那是灾难。
9.6 整体架构图
下面这张图展示了从指纹验证到数据存取的完整流程。我画的时候特意把“Keystore”和“SP”分成了两个区域,方便你理解各自职责。
9.7 避坑指南
我踩过不少坑,挑几个典型的说说:
- 密钥失效问题: 用户删除所有指纹或更改锁屏方式,密钥会立即失效。我曾经没处理这个异常,导致用户升级后所有数据都读不出来。解决方案是在解密失败时,清除旧数据,引导用户重新录入指纹并重新加密。
- 后台加密问题: 不要在
onCreate里直接加密。因为那时候指纹还没验证,Keystore 会抛异常。加密操作必须放在指纹验证成功的回调里。 - 多指纹场景: 用户注册了多个指纹,任何一个都能解密。但新增指纹时,如果
setInvalidatedByBiometricEnrollment设为 true,旧密钥会失效。这个要跟产品经理确认好策略。 - GCM 的 IV 长度: AES/GCM 的 IV 固定为 12 字节(96 位)。如果你自己拼数据,注意别截取错了。
Dispatchers.IO)执行。
9.8 总结
指纹加密存储,说白了就是“用指纹锁住密钥,用密钥锁住数据”。SharedPreferences 只负责存密文,真正的安全由 Android Keystore 和 AES/GCM 保证。
我个人觉得,这套方案最大的价值在于:即使手机丢了,只要指纹没被破解,数据就是安全的。而且 GCM 模式自带的完整性校验,省去了我们手动算哈希的麻烦。
嗯,这一章的内容就到这里。记住一个原则:敏感数据永远不要明文落地,指纹是你的最后一道防线。