4. Android KeyStore 系统:密钥到底该放哪?
聊到密码SDK,逃不开的一个话题就是密钥存储。Android从4.0(API 14)就引入了KeyStore,但说实话,早期版本就是个软件实现,跟随便写个文件存一下差别不大。真正让KeyStore变“硬”的是Android 6.0以后引入的硬件支持。我自己做第一个金融SDK时,踩过不少KeyStore的坑,今天把核心内容一次讲透。
4.1 KeyStore 提供程序:Android自己的密钥保险柜
Android KeyStore不是一个新的KeyStore类型,它其实是一个Provider——AndroidKeyStoreProvider。你平时用Java标准库的KeyStore.getInstance("AndroidKeyStore"),拿到的就是这个Provider的实例。
它的特点很直接:
- 密钥不可导出:一旦存入,应用层拿不到密钥的明文。
- 绑定应用身份:每个应用的KeyStore空间隔离,别的应用读不了。
- 支持加密算法:AES, RSA, EC等,且硬件支持时使用硬件加密引擎。
KeyStore.getInstance("JKS")存密钥,结果被逆向工具直接拖走。用AndroidKeyStore,至少密钥不会明文出现在APK或data分区里。
4.2 密钥生成与存储:不是new一个对象那么简单
密钥生成要用KeyPairGenerator或KeyGenerator,但需要指定KeyStore作为数据存储目标。这里我习惯用KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"),第二个参数告诉系统用KeyStore的Provider。
看一个实际的RSA密钥对生成例子:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
"my_rsa_key_pair",
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setKeyValidityForOriginationEnd(
Calendar.getInstance().getTimeInMillis() + 1000L * 60 * 60 * 24 * 365);
kpg.initialize(builder.build());
KeyPair keyPair = kpg.generateKeyPair();
这里有个关键点:KeyGenParameterSpec配置了用途、摘要、有效期等。为什么要设有效期?我在一个项目中见过,密钥永不过期,结果用户换了SIM卡、设备重刷后,旧密钥还能解密,这不安全。
setKeyValidityForOriginationEnd,甚至配合生物识别使用setUserAuthenticationRequired(true)。否则,密钥被恢复出厂设置后还能用,就等于白给了攻击者一个后门。
存储之后,读取也用KeyStore实例:
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
PrivateKey privateKey = (PrivateKey) ks.getKey("my_rsa_key_pair", null);
PublicKey publicKey = ks.getCertificate("my_rsa_key_pair").getPublicKey();
你看,根本不需要密码或KeyStore口令——因为KeyStore绑定的是应用UID,系统自动管理。我当初写第一个版本时,还多传了个null的password进去,其实没用。
4.3 硬件支持:TEE、StrongBox,以及芯片级的保障
说到硬件支持,这是Android KeyStore真正的王牌。从Android 6.0起,系统可以利用TEE(可信执行环境)来做密钥操作;到了Android 9,又引入了StrongBox KeyStore——一个独立的、有自己CPU和RAM的安全芯片。
| 安全级别 | 说明 | 性能 | 典型平台 |
|---|---|---|---|
| 软件实现(无硬件) | 密钥在Android系统内存中操作,容易被HOOK或读取 | 快 | 模拟器、低端老设备 |
| TEE(如ARM TrustZone) | 密钥操作在独立安全OS中,主系统没法直接访问 | 中 | 大多数中高端Android手机 |
| StrongBox(独立安全芯片) | 有独立的CPU、RAM、RNG,抗物理攻击更强 | 慢(约软件方案的1/10) | Pixel系列、部分高端旗舰 |
你可能会问:“那我怎么知道设备支持什么级别?” 可以用KeyStore.getInstance().isHardwareBacked(),但这个API已废弃。更靠谱的做法是通过Context.getSystemService(KeyStoreManager.class)的isHardwareBacked()方法,或者检查设备是否声明了android.hardware.strongbox_keystore功能。
我曾经在一款国产机上发现,明明SOC是支持TEE的,但厂商没给KeyStore驱动打补丁,结果密钥操作回退到软件实现。这事很难从代码层面判断,建议SDK在初始化时做一个sign + verify循环基准测试,如果耗时异常低(比如低于10ms),可能没走硬件。
4.4 从生成到使用的完整流程
这张图把密钥从生成到销毁走了一遍。重点看“检查硬件支持级别”这一步——很多SDK开发者忽略了,直接假设设备一定支持StrongBox。结果在千元机上密钥操作奇慢(因为StrongBox响应太慢),或者根本不工作。我建议你先尝试创建带setIsStrongBoxBacked(true)的密钥,如果抛出StrongBoxUnavailableException,再降级为TEE,最后才是纯软件实现。
4.5 开发者必须避开的三个坑
- 密钥别名冲突:同一个KeyStore不许重复生成同名密钥,否则抛异常。我习惯在生成前先
ks.containsAlias("key_name")检查一下,如果已存在就直接使用。 - KeyGenParameterSpec的setEncryptionPaddings:RSA加密如果不设
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP),默认可能是PKCS1Padding,这在某些硬件(尤其是Smartcard)上不支持。曾经有一次因为这个导致合作银行的测试机验签失败,查了两天才发现是填充模式不匹配。 - 用户认证绑定:如果你用了
setUserAuthenticationRequired(true),密钥在用户锁定屏幕后会被清空,不过只有下次解锁才能再次使用。注意:如果你同时设置了setUserAuthenticationValidityDurationSeconds(-1),那就只验证一次,直到设备重启。这在密码SDK中用于“记住登录状态”很常见,但要配合KeyguardManager的API使用。
4.6 硬件支持判定代码片段
最后给你一段实用的判断代码,直接Copy到你的初始化逻辑里:
private boolean isStrongBoxSupported(Context context) {
PackageManager pm = context.getPackageManager();
if (pm.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
return true;
}
// 回退检测:尝试创建StrongBox密钥
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES", "AndroidKeyStore");
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"_test_strongbox_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setIsStrongBoxBacked(true)
.build();
keyGen.init(spec);
keyGen.generateKey();
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
ks.deleteEntry("_test_strongbox_key");
return true;
} catch (StrongBoxUnavailableException | ProviderException | NoSuchAlgorithmException |
NoSuchProviderException | InvalidAlgorithmParameterException |
KeyStoreException | CertificateException | IOException e) {
return false;
}
}
这段代码我放进了SDK的onCreate里,只跑一次,同时把结果缓存到一个SharedPreference中。注意:setIsStrongBoxBacked只在API 28+有效,低版本直接跳过StrongBox检测。
嗯,Android KeyStore的内容就这么多。其实记住两点就行:密钥别明文存、硬件降级要有日志。你想想看,如果某天用户反馈“我的密钥被黑了”,至少你能从日志里知道当时是用StrongBox还是软件实现,定位问题就快得多。