13. Keystore密钥来源:TEE生成、SE生成、导入、安全导入,不同来源的安全等级对比
聊到Android Keystore,大家最关心的其实就一个问题:我的密钥到底安不安全?
这个问题的答案,很大程度上取决于密钥是从哪来的。我见过不少开发者,觉得只要用了Keystore就万事大吉了。其实不然。密钥的生成方式、导入方式,直接决定了它的安全等级。
今天我们就来掰扯清楚,Android Keystore里密钥的四种主要来源:TEE生成、SE生成、普通导入、安全导入。它们各自有什么特点?安全等级怎么排?
13.1 密钥来源全景图
先看一张图,帮你快速建立整体认知:
这张图基本把四种来源的定位说清楚了。接下来我们逐个深入。
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。
它的工作流程是这样的:
- Keystore在TEE/SE中生成一个临时的「导入密钥对」
- 把公钥导出到REE侧
- 外部用这个公钥加密真正的密钥材料
- 加密后的密钥材料传入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