4. Android Keystore 系统:硬件-backed 与软件-backed 的区别,Key attestation 机制
聊到 Android 上的密钥存储,Keystore 系统是绕不开的核心。说白了,它就是系统提供的一个安全容器,专门用来保管你的私钥,防止被应用进程或者攻击者直接读取。但这里有个关键问题:这个“容器”到底有多安全?
我个人习惯把 Keystore 分成两类:硬件-backed 和 软件-backed。这两者的安全等级天差地别。你想想看,如果你的密钥只是存在一个文件里,跟普通数据没区别,那逆向工程师拿到 root 权限后,基本就等于裸奔了。
4.1 硬件-backed 与软件-backed 的本质区别
先看一张对比图,帮你快速建立直观印象:
这张图很直观地展示了两种方案的本质差异。左边是软件-backed,密钥虽然会被加密存储,但在使用时会被解密到应用进程的内存中。右边是硬件-backed,密钥从生成到使用,全程都在安全硬件内部完成,应用层拿到的只是一个“句柄”,根本看不到密钥材料本身。
4.2 软件-backed Keystore:方便但脆弱
软件-backed 的实现,说白了就是 Keystore 服务在用户空间维护一个加密数据库。密钥文件通常存放在 /data/misc/keystore/ 目录下,使用设备主密钥(由锁屏密码派生)进行加密。
它的工作流程大致是这样:
- 应用调用
KeyStore.getInstance()获取服务实例 - 生成密钥时,Keystore 服务在内存中创建密钥对象
- 使用设备主密钥加密后写入文件系统
- 使用密钥时,从文件读取密文,解密到内存中供应用使用
这里有个致命问题——密钥在内存中是明文的。我曾经在一个渗透测试项目中遇到过这种情况:攻击者通过 ptrace 附加到应用进程,直接 dump 了进程的堆内存,很轻松就找到了解密后的 AES 密钥。嗯,这就是软件-backed 的软肋。
⚠ 重要提醒: 软件-backed Keystore 无法防御以下攻击场景:
- 攻击者拥有 root 权限,可以读取
/data/misc/keystore/目录 - 攻击者通过调试器或内存 dump 工具获取进程内存中的明文密钥
- 设备锁屏密码被暴力破解后,主密钥直接泄露
4.3 硬件-backed Keystore:真正的安全屏障
硬件-backed 方案,才是我们做安全存储时真正应该依赖的。它利用设备上的专用安全硬件——TEE(Trusted Execution Environment,可信执行环境)或 StrongBox(独立安全元件)——来隔离密钥操作。
关键特性如下:
| 特性 | TEE(如 TrustZone) | StrongBox(如独立 SE) |
|---|---|---|
| 硬件隔离级别 | 与主系统共享物理内存,但通过硬件强制隔离 | 完全独立的芯片,物理隔离 |
| 防侧信道攻击 | 中等 | 高(专用硬件防护) |
| 密钥生成 | 在 TEE 内部生成,主系统不可见 | 在 SE 内部生成,主系统不可见 |
| 性能 | 较高(与主系统通信延迟低) | 较低(独立芯片通信延迟高) |
| 典型场景 | 指纹支付、屏幕解锁 | 银行 U 盾级、企业证书 |
我个人建议,如果你的应用处理的是金融交易、用户隐私数据这类高价值信息,一定要优先使用 StrongBox。虽然性能慢一点,但安全收益是质的飞跃。
4.4 Key Attestation:验证你的密钥真的在硬件里
好,现在你用了硬件-backed Keystore,但你怎么向你的服务器证明——这个密钥确实是在安全硬件里生成的,而不是应用层伪造的?
这就是 Key Attestation(密钥证明) 机制要解决的问题。它本质上是一个由设备制造商签名的证书链,用来证明密钥的“出身”。
流程是这样的:
- 应用调用
KeyStore.getCertificateChain()获取密钥的证书链 - 这个证书链的根证书由设备制造商(如 Google、高通、三星)的根密钥签名
- 服务器验证这个证书链,确认密钥确实是在 TEE/StrongBox 中生成的
- 证书中包含了密钥的详细信息:算法、用途、是否支持用户认证等
代码实现其实不复杂,我贴一段核心逻辑:
// 生成带 attestation 的密钥
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"my_hardware_key",
KeyProperties.PURPOSE_SIGN)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
.setKeySize(256)
.setAttestationChallenge(challenge) // 服务器下发的随机数
.build();
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
kpg.initialize(spec);
KeyPair kp = kpg.generateKeyPair();
// 获取 attestation 证书链
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Certificate[] certs = ks.getCertificateChain("my_hardware_key");
// 将 certs 发送到服务器验证
// 服务器需要验证:
// 1. 证书链的根证书是否来自可信的制造商
// 2. attestation 证书中的 challenge 是否匹配
// 3. 密钥用途、算法等属性是否符合预期
💡 实战经验: 我在做金融 SDK 时,曾经遇到过一个坑——某些国产手机的 Key Attestation 证书链不完整,导致服务器验证失败。解决方案是:在服务器端维护一个“已知问题设备列表”,对这些设备降级使用软件-backed 方案,但配合额外的风控策略(如设备指纹、行为分析)来弥补安全缺口。
4.5 如何判断设备是否支持硬件-backed
你不可能在所有设备上都依赖硬件-backed。有些低端设备根本没有 TEE,或者 TEE 实现有漏洞。我建议你在运行时做检测:
// 检查密钥是否在安全硬件中生成
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry("my_key", null);
if (entry instanceof KeyStore.SecretKeyEntry) {
SecretKeyFactory factory = SecretKeyFactory.getInstance(
entry.getKey().getAlgorithm(), "AndroidKeyStore");
KeyInfo keyInfo = (KeyInfo) factory.getKeySpec(entry.getKey(), KeyInfo.class);
if (keyInfo.isInsideSecureHardware()) {
// 密钥在安全硬件中,可以信任
} else {
// 密钥是软件-backed,需要额外防护
}
}
这里有个细节要注意:isInsideSecureHardware() 返回 true 并不代表一定安全。我记得有一次,某款设备虽然返回了 true,但实际上密钥是通过软件模拟的 TEE 生成的。嗯,这就是为什么我们需要 Key Attestation 来做远程验证。
4.6 避坑指南:我踩过的几个坑
做安全存储这么多年,有些坑真的是血泪教训:
- 不要假设所有设备都支持 StrongBox——Android 9 才开始引入 StrongBox API,而且很多设备根本没有独立安全元件。我建议你在初始化时做能力检测,动态选择方案。
- Key Attestation 的 challenge 必须由服务器生成——如果你在客户端生成 challenge,攻击者可以重放旧的 attestation 证书。我曾经见过一个 App 用当前时间戳做 challenge,结果被中间人攻击绕过了验证。
- 注意密钥的用途限制——硬件-backed 密钥一旦设置了用途(如仅用于签名),就不能用于加密。这个限制是硬件强制的,无法绕过。设计密钥策略时一定要想清楚。
- 备份和迁移问题——硬件-backed 密钥无法导出,这意味着用户换手机后,旧设备上的密钥就永远丢失了。你需要设计合理的密钥备份方案,比如使用服务器端的密钥托管服务。
📌 核心要点总结:
- 软件-backed:密钥在内存中明文存在,适合低安全场景
- 硬件-backed:密钥在 TEE/StrongBox 中,应用层不可见
- Key Attestation:通过证书链向服务器证明密钥的硬件来源
- 运行时检测:使用
KeyInfo.isInsideSecureHardware()判断密钥类型 - 始终假设攻击者拥有 root 权限,依赖硬件隔离而非应用层防护