9、安全存储用户凭证:使用指纹加密SharedPreferences、加密存储敏感数据、解密读取数据、数据完整性校验

说实话,指纹认证成功只是第一步。你想想看,用户把手指放上去,系统告诉你“验证通过”,然后呢?你得把用户的登录令牌、支付密码、或者某个私密信息存下来吧?

我见过不少项目,指纹认证做得花里胡哨,结果存数据的时候直接往 SharedPreferences 里一扔,连个 Base64 都没做。嗯,这跟把银行卡密码写在便利贴上贴脑门上没啥区别。

这一章,我们就来聊聊怎么用指纹加密来保护这些敏感数据。说白了,就是让数据跟用户的指纹绑定——指纹不在,数据就读不出来。

9.1 为什么不能直接存 SharedPreferences?

SharedPreferences 本身是明文存储的。文件在 /data/data/包名/shared_prefs/ 目录下,只要手机 root 了,或者有备份漏洞,数据就等于裸奔。

我早期做过一个内部工具,偷懒把 API Token 直接存了 SP。结果测试同事用 adb backup 一把全导出来了……从那以后,凡是跟“凭证”沾边的数据,我绝不用明文 SP。

⚠️ 警告: 不要用 SharedPreferences 存储密码、Token、密钥、身份证号等任何敏感信息。即使你做了简单的 XOR 或 Base64 编码,也挡不住逆向分析。

9.2 核心思路:用指纹派生密钥

Android 的指纹加密,本质上不是用指纹图像去加密。而是利用 Android Keystore 生成一把密钥,这把密钥只有在用户指纹验证通过后才能使用。

流程是这样的:

  1. 在 Keystore 中生成一个 KeyGenParameterSpec,绑定到指纹。
  2. 用这个密钥去加密数据(比如用户的 Token)。
  3. 把加密后的密文存到 SharedPreferences 里。
  4. 读取时,先触发指纹验证,验证通过后拿到密钥,解密数据。

说白了,SharedPreferences 里存的只是“密文”,没有指纹授权,谁也解不开。

💡 核心原则: 密钥不出 Keystore,数据以密文形式存在 SP 中。指纹是钥匙,Keystore 是保险柜。

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
    }
}

这里返回了 ivciphertext。iv 是初始化向量,解密时必须用到。我一般把 iv 和密文拼在一起,或者分别存到 SP 里。

📌 小技巧: 可以把 iv 直接拼在密文前面,解密时截取前 12 字节作为 iv。这样只需要存一个字段,省事。

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”分成了两个区域,方便你理解各自职责。

指纹加密存储与解密流程 👆 用户指纹验证 验证通过 BiometricPrompt 🔐 Android Keystore 密钥生成 / 授权使用 AES/GCM 加密 / 解密 📄 SharedPreferences(存密文) 数据流说明 1. 指纹验证通过 2. Keystore 授权密钥 3. 加密/解密数据 4. 密文存入 SP 5. 读取时反向操作

9.7 避坑指南

我踩过不少坑,挑几个典型的说说:

  • 密钥失效问题: 用户删除所有指纹或更改锁屏方式,密钥会立即失效。我曾经没处理这个异常,导致用户升级后所有数据都读不出来。解决方案是在解密失败时,清除旧数据,引导用户重新录入指纹并重新加密。
  • 后台加密问题: 不要在 onCreate 里直接加密。因为那时候指纹还没验证,Keystore 会抛异常。加密操作必须放在指纹验证成功的回调里。
  • 多指纹场景: 用户注册了多个指纹,任何一个都能解密。但新增指纹时,如果 setInvalidatedByBiometricEnrollment 设为 true,旧密钥会失效。这个要跟产品经理确认好策略。
  • GCM 的 IV 长度: AES/GCM 的 IV 固定为 12 字节(96 位)。如果你自己拼数据,注意别截取错了。
⚠️ 特别注意: 不要在 UI 线程做加解密操作。虽然 AES 很快,但 Keystore 的访问可能涉及 IPC 调用,建议放到后台线程(比如 Dispatchers.IO)执行。

9.8 总结

指纹加密存储,说白了就是“用指纹锁住密钥,用密钥锁住数据”。SharedPreferences 只负责存密文,真正的安全由 Android Keystore 和 AES/GCM 保证。

我个人觉得,这套方案最大的价值在于:即使手机丢了,只要指纹没被破解,数据就是安全的。而且 GCM 模式自带的完整性校验,省去了我们手动算哈希的麻烦。

嗯,这一章的内容就到这里。记住一个原则:敏感数据永远不要明文落地,指纹是你的最后一道防线。


公众号:蓝海资料掘金营,微信deep3321