理论落地。Keystore 让密钥永远不进你的进程内存,而你只需要正确地调用它。
前十九章的密码学,在 Android 上有一个共同的头号问题:密钥存哪? 第 1 章就说过,Kerckhoffs 原则下,一切安全都收敛到密钥。可如果密钥就明晃晃地躺在你的代码、SharedPreferences 或文件里,再强的 AES 也是摆设。Android 的答案是 Keystore。
Android Keystore 的核心思想极其巧妙:密钥生成在一个独立的安全环境里(TEE 可信执行环境,或更强的 StrongBox 安全芯片),永远不以明文形式进入你的 App 进程。你的代码拿到的只是一个"句柄",加解密请求被送进安全环境里执行,结果再送回来。
这意味着:即使你的 App 被逆向、进程内存被 dump、甚至设备被 root,攻击者也拿不到密钥本身——它从未离开过硬件。这是软件层面做不到的保护。
TEE(可信执行环境):主处理器上一个与普通操作系统隔离的安全世界,大多数现代 Android 设备都有。
StrongBox:一块独立的安全芯片(类似手机里的"保险柜"),抗物理攻击更强,Pixel 3+ 等设备支持。生成密钥时加 setIsStrongBoxBacked(true) 即可请求。
加了 硬件背书的密钥,还能通过 Key Attestation 向你的服务器证明"这把密钥确实生成于真实设备的安全硬件里"——用于抵御模拟器和克隆。
把第 9 章的 AES-GCM 和 Keystore 结合起来。注意:密钥是生成在 Keystore 里的,你从头到尾没见过它的字节:
val keyGen = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGen.init(
KeyGenParameterSpec.Builder("my_data_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) // 第 9 章:AEAD
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true) // 有安全芯片就用
.build())
keyGen.generateKey() // 密钥诞生在安全硬件里,你的进程永远看不到它的字节
加密时,系统会自动生成随机 IV(第 8 章的纪律:永不硬编码 IV),你把它和密文一起存:
fun encrypt(plain: ByteArray): Pair<ByteArray, ByteArray> {
val key = (KeyStore.getInstance("AndroidKeyStore")
.apply { load(null) }
.getKey("my_data_key", null)) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key) // 系统生成随机 IV
val ct = cipher.doFinal(plain) // 已含 128-bit tag
return cipher.iv to ct // IV 非秘密,和密文一起存
}
如果你只是想安全地存点本地数据(token、配置),不必自己拼上面这些。Jetpack Security 的 EncryptedSharedPreferences 和 EncryptedFile 把 Keystore + AES-GCM 全封装好了:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context, "secure_prefs", masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
securePrefs.edit().putString("auth_token", token).apply()
// 底层:主密钥在 Keystore,值用 AES-GCM 加密。你几乎不可能用错。
Jetpack Security Crypto 库(androidx.security:security-crypto)在近年进入维护/弃用讨论,Google 也在推荐结合更底层的 Keystore 或第三方方案(如 Tink)。写代码时请查当下的官方文档确认推荐路径。但无论用哪个封装,底层原则不变:主密钥进 Keystore,数据用 AES-GCM。选封装库时,认准这条即可。
Keystore 还能做一件软件绝无可能做到的事:让一把密钥只有在用户刚通过生物识别/锁屏认证后才能使用。这不是"先验证指纹,再由 App 决定放不放行"(那样 App 层能被绕过),而是硬件层拒绝执行加密操作,直到认证发生。
KeyGenParameterSpec.Builder("biometric_key", ...)
.setUserAuthenticationRequired(true) // 关键:硬件强制认证
.setUserAuthenticationParameters(0,
KeyProperties.AUTH_BIOMETRIC_STRONG)
.build()
// 使用时,把 Cipher 交给 BiometricPrompt,认证成功后才解锁这把密钥:
val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply {
init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
}
biometricPrompt.authenticate(promptInfo,
BiometricPrompt.CryptoObject(cipher)) // 认证 ✓ 后 cipher 才可用
用 CryptoObject 把 Cipher 绑进 BiometricPrompt 是精髓:它保证了"生物识别成功"和"这把密钥被解锁"在密码学上绑定,而不只是 UI 上的先后。攻击者没法用"假装点了确定"来绕过——因为解锁发生在安全硬件里,由真实的认证事件触发。
面对一个具体需求,该用 Keystore、EncryptedSharedPreferences 还是别的?下面这个小助手帮你对号入座:
Keystore 保护"密钥的机密性",但它不能阻止一个已经拿到设备控制权的攻击者触发加密操作(除非你用了生物识别绑定)。也就是说:如果恶意软件能在你的 App 上下文里运行,它虽然偷不到密钥字节,却可能调用密钥去解密。真正的纵深防御还要靠:生物识别绑定、Root/篡改检测、以及不把过度敏感的逻辑全放在客户端。没有绝对安全,只有把攻击成本抬到足够高。
密钥存好了,但 App 还要和服务器通信。第 19 章说 TLS 底层已经很安全——可"信任整个 CA 体系"这件事本身,对高价值 App 来说仍是风险(第 18 章的 DigiNotar)。如果你想更进一步,只信任你自己的证书呢?这就是证书固定,以及随之而来的一整套攻防。下一章。
AES/GCM/NoPadding + 系统随机 IV;存简单数据用 EncryptedSharedPreferences 等封装。setUserAuthenticationRequired + BiometricPrompt 的 CryptoObject 把密钥用途与生物识别在密码学上绑定。