8. Keystore密钥导出:exportKey与getKeyCharacteristics、公钥导出限制、特征信息获取
聊到密钥导出,很多同学第一反应就是:「密钥都存到Keystore里了,还能导出来?那安全吗?」
嗯,这个问题问得好。其实Android Keystore的设计哲学是:私钥永远不出安全区。但公钥和一些元信息,是可以拿出来的。今天我们就来掰扯清楚,哪些能导、哪些不能导、怎么导、以及导出来之后能干嘛。
8.1 公钥导出:能拿到的只有「半张牌」
先明确一个核心原则:私钥不可导出。这是Keystore的底线。你想想看,如果私钥能被App轻易读出来,那跟存在SharedPreferences里有什么区别?
但公钥不一样。公钥本来就是公开的,用来加密或验签。所以Android提供了exportKey()方法,让你把公钥拿出来用。
KeyStore.PrivateKeyEntry,调用getCertificate()拿到的也是证书,里面只包含公钥。
我在项目中遇到过一个问题:有个同事想用exportKey()导出私钥,结果发现返回的是null。他跑来问我是不是Keystore坏了。我说:「兄弟,Keystore没坏,是设计就不让你这么干。」
8.1.1 导出公钥的两种方式
实际开发中,导出公钥有两种常见姿势:
- 通过证书拿公钥:
keyStore.getCertificate(alias).getPublicKey() - 通过Key对象拿公钥:
keyStore.getKey(alias, null)然后强转
我个人习惯用第一种,因为证书里还包含了算法信息、有效期等,信息更完整。
// 方式一:从证书中提取公钥
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
Certificate cert = keyStore.getCertificate("my_key_alias");
PublicKey publicKey = cert.getPublicKey();
// 方式二:直接从Key对象获取
Key key = keyStore.getKey("my_key_alias", null);
if (key instanceof PublicKey) {
PublicKey pubKey = (PublicKey) key;
}
8.2 getKeyCharacteristics:密钥的「体检报告」
光拿到公钥还不够,你还得知道这把密钥是什么来头。比如:它是在安全硬件里生成的吗?它支持哪些用途?它有没有过期?
这时候就需要getKeyCharacteristics()了。这个API是Android 9(API 28)引入的,专门用来查询密钥的元信息。
8.2.1 能查到什么信息?
| 特性字段 | 说明 | 典型值 |
|---|---|---|
| 算法 | 密钥使用的算法 | EC、RSA、AES |
| 密钥用途 | 加密/解密/签名/验签 | PURPOSE_ENCRYPT | PURPOSE_DECRYPT |
| 填充模式 | 支持的填充方式 | PKCS7、OAEP、PSS |
| 摘要算法 | 签名时使用的摘要 | SHA256、SHA512 |
| 是否安全硬件 | 密钥是否存储在TEE/SE中 | true/false |
| 用户认证要求 | 是否需要生物识别 | true/false + 超时时间 |
| 密钥有效期 | 创建和过期时间 | 时间戳 |
说白了,这就是密钥的「体检报告」。你拿到这份报告,就能判断这把密钥能不能用在当前场景下。
8.2.2 代码怎么用?
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// 获取密钥特征
KeyStore.Entry entry = keyStore.getEntry("my_key_alias", null);
if (entry instanceof KeyStore.SecretKeyEntry) {
// 对称密钥的特征获取方式略有不同
KeyCharacteristics keyCharacteristics =
((KeyStore.SecretKeyEntry) entry).getKeyCharacteristics();
// 检查是否在安全硬件中
boolean isSecure = keyCharacteristics.isInsideSecurityHardware();
// 获取密钥用途
int purposes = keyCharacteristics.getPurposes();
boolean canSign = (purposes & KeyProperties.PURPOSE_SIGN) != 0;
// 获取算法
String algorithm = keyCharacteristics.getAlgorithm();
Log.d("Keystore", "安全硬件: " + isSecure);
Log.d("Keystore", "可签名: " + canSign);
Log.d("Keystore", "算法: " + algorithm);
}
getKeyCharacteristics() 只在 Android 9+ 上可用。如果你的App还要兼容老版本,建议用Certificate里的信息做降级处理。我曾经在Android 8.1的设备上踩过这个坑,直接Crash了。
8.3 公钥导出的实际限制
虽然公钥可以导出,但也不是完全没限制。我总结了几条实际开发中会遇到的情况:
- 算法限制:某些算法(如AES)没有公钥概念,自然无法导出。只有非对称密钥(RSA、EC)才有公钥。
- 安全等级限制:如果密钥是在StrongBox(独立安全芯片)中生成的,部分设备可能限制公钥导出频率。我遇到过一台Pixel设备,连续导出100次后开始返回null。
- 用户认证绑定:如果密钥设置了
setUserAuthenticationRequired(true),导出公钥时可能要求用户先通过生物识别。这个设计是为了防止恶意App在后台偷偷导出密钥。
8.4 特征信息的实际应用场景
拿到特征信息后,能做什么?我举几个实际例子:
- 密钥轮换判断:通过
getKeyCharacteristics()拿到密钥创建时间,如果超过90天,就生成新密钥替换旧密钥。 - 安全等级校验:检查
isInsideSecurityHardware(),如果返回false,说明密钥是纯软件生成的,安全性不够,可以提示用户升级设备。 - 兼容性适配:不同厂商对Keystore的支持程度不同。通过特征信息,可以判断当前设备是否支持你需要的算法和填充模式。
// 实际项目中的密钥轮换逻辑
private boolean shouldRotateKey(String alias) {
try {
KeyStore.Entry entry = keyStore.getEntry(alias, null);
if (entry instanceof KeyStore.SecretKeyEntry) {
KeyCharacteristics characteristics =
((KeyStore.SecretKeyEntry) entry).getKeyCharacteristics();
long creationTime = characteristics.getCreationDate();
long ageInDays = (System.currentTimeMillis() - creationTime)
/ (24 * 60 * 60 * 1000);
return ageInDays > 90;
}
} catch (Exception e) {
Log.e("Keystore", "检查密钥年龄失败", e);
}
return false;
}
8.5 知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
从图上可以看得很清楚:Keystore是中心,左边是公钥导出路径,右边是特征信息获取路径。两条路径最终都服务于上层业务——密钥轮换、安全校验、兼容性适配。
好了,关于密钥导出和特征信息获取,就聊这么多。记住一句话:私钥是底线,公钥随便拿,特征信息要会查。下一章我们聊聊更进阶的话题——密钥迁移与备份策略,到时候见。