13. Keystore密钥来源:TEE生成、SE生成、导入、安全导入,不同来源的安全等级对比

聊到Android Keystore,大家最关心的其实就一个问题:我的密钥到底安不安全?

这个问题的答案,很大程度上取决于密钥是从哪来的。我见过不少开发者,觉得只要用了Keystore就万事大吉了。其实不然。密钥的生成方式、导入方式,直接决定了它的安全等级。

今天我们就来掰扯清楚,Android Keystore里密钥的四种主要来源:TEE生成、SE生成、普通导入、安全导入。它们各自有什么特点?安全等级怎么排?

13.1 密钥来源全景图

先看一张图,帮你快速建立整体认知:

Android Keystore 密钥来源与安全等级 TEE 生成 Trusted Execution Environment SE 生成 Secure Element 普通导入 直接导入密钥材料 安全导入 加密通道导入 ★★★★★ 极高 ★★★★☆ 很高 ★★☆☆☆ 较低 ★★★★☆ 很高 关键特性对比 硬件隔离 密钥不出TEE 支持密钥用途绑定 独立安全芯片 防物理攻击 eSE/SIM/SD卡 密钥材料暴露 依赖调用方安全 易被截获 加密传输 认证通道 防篡改 安全等级排序:SE生成 ≈ 安全导入 ≥ TEE生成 >> 普通导入

这张图基本把四种来源的定位说清楚了。接下来我们逐个深入。

13.2 TEE生成:最常用的安全方案

TEE,全称Trusted Execution Environment,可信执行环境。说白了,就是在主处理器上划出一块安全区域,和普通的Android系统(REE)隔离开。

我习惯把TEE理解成「大楼里的保险库」。普通应用在开放区域活动,而密钥则锁在保险库里。就算整栋楼被入侵了,保险库的门还是焊死的。

核心特点:

  • 密钥在TEE内部生成,私钥永远不会离开TEE
  • 支持密钥用途绑定(比如只能签名,不能解密)
  • 支持用户认证绑定(需要指纹/人脸才能用密钥)
  • 几乎所有现代Android设备都支持

代码示例:在TEE中生成一个签名密钥

KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
        "my_signing_key",
        KeyProperties.PURPOSE_SIGN)
    .setDigests(KeyProperties.DIGEST_SHA256)
    .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
    .setUserAuthenticationRequired(true)  // 需要用户认证
    .setInvalidatedByBiometricEnrollment(true)
    .build();

KeyPairGenerator generator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
generator.initialize(spec);
KeyPair keyPair = generator.generateKeyPair();

注意看,这里没有指定任何「在TEE中生成」的参数。因为Android KeyStore默认就会优先使用TEE。如果设备支持的话。

我的经验: 有一次我在项目中排查一个诡异的问题——某款低端机上,生成的密钥居然没有硬件支持。后来发现是厂商没有实现TEE。所以,永远不要假设所有设备都有TEE。建议在运行时通过 KeyStore.getInstance().containsAlias() 配合 KeyInfo.isInsideSecurityHardware() 做检测。

13.3 SE生成:物理级的安全保障

SE,Secure Element,安全元件。它和TEE最大的区别是:SE是一块独立的物理芯片

你想想看,TEE虽然安全,但它和主系统共享同一块CPU。如果攻击者能攻破CPU的硬件漏洞,TEE也不是绝对安全的。但SE是独立的芯片,有自己的处理器、存储、甚至操作系统。攻击者想拿到SE里的密钥,基本只能靠物理拆解。

常见的SE形态有:

  • eSE:嵌入式安全元件,焊在主板上
  • SIM卡:运营商提供的SIM卡也包含SE
  • SD卡:部分安全SD卡也带SE功能

SE生成密钥的代码和TEE几乎一样,区别在于底层实现。Android KeyStore会根据设备能力自动选择最安全的硬件。

安全等级: SE > TEE

原因很简单:SE是物理隔离,TEE是逻辑隔离。物理隔离永远比逻辑隔离更难攻破。

注意: 并不是所有设备都有SE。目前只有高端机型(如Google Pixel系列、三星Galaxy S系列、iPhone)才配备eSE。如果你的应用面向中低端市场,不要依赖SE。

13.4 普通导入:最方便但最危险

普通导入,就是直接把密钥材料(比如PEM文件、DER编码的私钥)塞进Keystore。

代码看起来很简单:

PrivateKey externalPrivateKey = loadPrivateKeyFromFile("mykey.pem");

KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
keyStore.setKeyEntry(
    "imported_key",
    externalPrivateKey,
    null,  // 不需要密码
    null   // 不需要证书链
);

但这里有个巨大的坑:密钥在导入过程中是明文暴露的

你想想看,从文件读取私钥,到调用setKeyEntry,这中间私钥在Java堆里、在native层、在内存里,都是明文存在的。如果这时候有恶意应用能读取你的内存,或者你的应用被调试了,私钥就完全暴露了。

我曾经踩过的坑: 有一个金融类App,他们把服务器下发的RSA私钥通过普通导入方式存到Keystore。结果被逆向工程师用Frida hook了 setKeyEntry 方法,直接拿到了私钥明文。嗯,后来整个密钥体系都重构了。

所以我的建议是:除非万不得已,不要用普通导入。如果一定要导入,确保密钥材料在传输和加载过程中是加密的,并且只在安全环境中解密。

13.5 安全导入:兼顾灵活与安全

安全导入(Secure Import)是Android 9引入的特性。它解决了普通导入的痛点——让密钥在加密通道中传输,直接进入TEE/SE

它的工作流程是这样的:

  1. Keystore在TEE/SE中生成一个临时的「导入密钥对」
  2. 把公钥导出到REE侧
  3. 外部用这个公钥加密真正的密钥材料
  4. 加密后的密钥材料传入Keystore,在TEE/SE内部解密并存储

整个过程,私钥明文只在TEE/SE内部出现一次。REE侧永远看不到。

代码示例:

// 1. 生成导入密钥对
KeyGenParameterSpec importSpec = new KeyGenParameterSpec.Builder(
        "import_wrapping_key",
        KeyProperties.PURPOSE_WRAP_KEY)
    .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
    .build();
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
kpg.initialize(importSpec);
KeyPair wrappingKeyPair = kpg.generateKeyPair();

// 2. 导出公钥给外部
byte[] wrappingPublicKey = wrappingKeyPair.getPublic().getEncoded();

// 3. 外部用公钥加密密钥材料(这里假设已经加密好了)
byte[] encryptedKeyMaterial = ...; // 从外部获取

// 4. 安全导入
WrappedKeyEntry wrappedKeyEntry = new WrappedKeyEntry(
        encryptedKeyMaterial,
        "import_wrapping_key",
        null,
        new AlgorithmParameterSpec[]{new IvParameterSpec(iv)},
        KeyProperties.KEY_ALGORITHM_AES,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);

KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
keyStore.setEntry("secure_imported_key", wrappedKeyEntry, null);

我的建议: 如果你的应用需要从服务器获取密钥,优先使用安全导入。虽然实现起来比普通导入复杂一些,但安全性的提升是质的飞跃。我在一个支付SDK中就用到了安全导入,审计的时候安全团队直接给了绿灯。

13.6 安全等级对比总结

最后,我们用一个表格来直观对比四种方式:

特性 TEE生成 SE生成 普通导入 安全导入
密钥生成位置 TEE内部 SE内部 REE侧 TEE/SE内部
私钥是否离开安全区 是(明文暴露) 否(加密传输)
防软件攻击 很强
防物理攻击
设备兼容性 高(Android 6+) 低(仅高端机) 极高(所有设备) 中(Android 9+)
实现复杂度 极低
推荐场景 通用密钥管理 高安全需求 不推荐使用 密钥分发

看到这个表格,你应该能理解为什么我反复强调「不要用普通导入」了。它唯一的优势就是实现简单,但代价是安全性的全面崩塌。

在实际项目中,我的选择优先级是这样的:

  • 如果设备支持SE → 优先用SE生成
  • 如果设备不支持SE但支持TEE → 用TEE生成
  • 如果需要从外部导入密钥 → 用安全导入
  • 普通导入?嗯,我只有在写Demo的时候才会用

好了,关于密钥来源和安全等级,今天就聊到这里。记住一句话:密钥的安全,从它诞生的那一刻就决定了。选对了来源,后面的事情就简单多了。


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