密语 · CIPHER | 目录 CH.20 / 26
绝密
Phase 5 · 工程实战

Android 密码学实践

理论落地。Keystore 让密钥永远不进你的进程内存,而你只需要正确地调用它。

阅读 ~16 分钟 前置 第 9、18 章 Demo 选对工具决策助手

前十九章的密码学,在 Android 上有一个共同的头号问题:密钥存哪? 第 1 章就说过,Kerckhoffs 原则下,一切安全都收敛到密钥。可如果密钥就明晃晃地躺在你的代码、SharedPreferences 或文件里,再强的 AES 也是摆设。Android 的答案是 Keystore。

一、Android Keystore:密钥不进你的内存

Android Keystore 的核心思想极其巧妙:密钥生成在一个独立的安全环境里(TEE 可信执行环境,或更强的 StrongBox 安全芯片),永远不以明文形式进入你的 App 进程。你的代码拿到的只是一个"句柄",加解密请求被送进安全环境里执行,结果再送回来。

这意味着:即使你的 App 被逆向、进程内存被 dump、甚至设备被 root,攻击者也拿不到密钥本身——它从未离开过硬件。这是软件层面做不到的保护。

TEE 与 StrongBox

TEE(可信执行环境):主处理器上一个与普通操作系统隔离的安全世界,大多数现代 Android 设备都有。
StrongBox:一块独立的安全芯片(类似手机里的"保险柜"),抗物理攻击更强,Pixel 3+ 等设备支持。生成密钥时加 setIsStrongBoxBacked(true) 即可请求。
加了 硬件背书的密钥,还能通过 Key Attestation 向你的服务器证明"这把密钥确实生成于真实设备的安全硬件里"——用于抵御模拟器和克隆。

二、在 Keystore 里生成一把 AES-GCM 密钥

把第 9 章的 AES-GCM 和 Keystore 结合起来。注意:密钥是生成在 Keystore 里的,你从头到尾没见过它的字节:

Kotlin · 在 Keystore 生成硬件背书的 AES 密钥
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),你把它和密文一起存:

Kotlin · 用 Keystore 密钥加密
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 非秘密,和密文一起存
}

三、Jetpack Security:一行搞定加密存储

如果你只是想安全地存点本地数据(token、配置),不必自己拼上面这些。Jetpack Security 的 EncryptedSharedPreferencesEncryptedFile 把 Keystore + AES-GCM 全封装好了:

Kotlin · EncryptedSharedPreferences
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 层能被绕过),而是硬件层拒绝执行加密操作,直到认证发生。

Kotlin · 需要生物识别才能使用的密钥
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 还是别的?下面这个小助手帮你对号入座:

DEMO 选对工具决策助手
经验法则:能用高层封装就别碰底层,能交给平台就别自己扛。密码学工程里,"少写代码"往往等于"少犯错"。
Keystore 的边界要知道

Keystore 保护"密钥的机密性",但它不能阻止一个已经拿到设备控制权的攻击者触发加密操作(除非你用了生物识别绑定)。也就是说:如果恶意软件能在你的 App 上下文里运行,它虽然偷不到密钥字节,却可能调用密钥去解密。真正的纵深防御还要靠:生物识别绑定、Root/篡改检测、以及不把过度敏感的逻辑全放在客户端。没有绝对安全,只有把攻击成本抬到足够高。

密钥存好了,但 App 还要和服务器通信。第 19 章说 TLS 底层已经很安全——可"信任整个 CA 体系"这件事本身,对高价值 App 来说仍是风险(第 18 章的 DigiNotar)。如果你想更进一步,只信任你自己的证书呢?这就是证书固定,以及随之而来的一整套攻防。下一章。

本章要点

  • Android Keystore 让密钥生成并驻留在 TEE/StrongBox,永不进入 App 进程——逆向/dump/root 也偷不到。
  • 本地加密用 AES/GCM/NoPadding + 系统随机 IV;存简单数据用 EncryptedSharedPreferences 等封装。
  • setUserAuthenticationRequired + BiometricPrompt 的 CryptoObject 把密钥用途与生物识别在密码学上绑定。
  • 工程原则:能用高层封装就别碰底层,能交给平台就别自研;少写密码学代码 = 少犯错。