4. Android Keystore 系统:硬件-backed 与软件-backed 的区别,Key attestation 机制

聊到 Android 上的密钥存储,Keystore 系统是绕不开的核心。说白了,它就是系统提供的一个安全容器,专门用来保管你的私钥,防止被应用进程或者攻击者直接读取。但这里有个关键问题:这个“容器”到底有多安全?

我个人习惯把 Keystore 分成两类:硬件-backed 和 软件-backed。这两者的安全等级天差地别。你想想看,如果你的密钥只是存在一个文件里,跟普通数据没区别,那逆向工程师拿到 root 权限后,基本就等于裸奔了。

4.1 硬件-backed 与软件-backed 的本质区别

先看一张对比图,帮你快速建立直观印象:

Android Keystore 架构:硬件-backed vs 软件-backed 软件-backed Keystore 应用进程 (内存中操作密钥) KeyStore 服务(用户空间) /data/misc/keystore/ 文件系统存储 (加密后存于磁盘) ⚠ 密钥材料在内存中明文存在 硬件-backed Keystore 应用进程 (仅持有密钥句柄) TEE / StrongBox (独立安全环境) 硬件隔离存储 (eFuse / 安全元件) ✅ 密钥永不离开安全硬件

这张图很直观地展示了两种方案的本质差异。左边是软件-backed,密钥虽然会被加密存储,但在使用时会被解密到应用进程的内存中。右边是硬件-backed,密钥从生成到使用,全程都在安全硬件内部完成,应用层拿到的只是一个“句柄”,根本看不到密钥材料本身。

4.2 软件-backed Keystore:方便但脆弱

软件-backed 的实现,说白了就是 Keystore 服务在用户空间维护一个加密数据库。密钥文件通常存放在 /data/misc/keystore/ 目录下,使用设备主密钥(由锁屏密码派生)进行加密。

它的工作流程大致是这样:

  1. 应用调用 KeyStore.getInstance() 获取服务实例
  2. 生成密钥时,Keystore 服务在内存中创建密钥对象
  3. 使用设备主密钥加密后写入文件系统
  4. 使用密钥时,从文件读取密文,解密到内存中供应用使用

这里有个致命问题——密钥在内存中是明文的。我曾经在一个渗透测试项目中遇到过这种情况:攻击者通过 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(密钥证明) 机制要解决的问题。它本质上是一个由设备制造商签名的证书链,用来证明密钥的“出身”。

流程是这样的:

  1. 应用调用 KeyStore.getCertificateChain() 获取密钥的证书链
  2. 这个证书链的根证书由设备制造商(如 Google、高通、三星)的根密钥签名
  3. 服务器验证这个证书链,确认密钥确实是在 TEE/StrongBox 中生成的
  4. 证书中包含了密钥的详细信息:算法、用途、是否支持用户认证等

代码实现其实不复杂,我贴一段核心逻辑:

// 生成带 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 权限,依赖硬件隔离而非应用层防护