9. Keystore密钥删除:deleteKey与deleteAllKeys、引用计数管理、安全擦除策略
密钥删除这件事,说实话,在Android安全里经常被忽视。很多人觉得「删个key嘛,调个API不就完了?」。嗯,理论上确实是这样。但我在项目中遇到过好几次,删完密钥之后,应用还能通过某些路径把密钥读出来——这就很尴尬了。
所以今天咱们好好聊聊,Keystore里的密钥到底怎么删,才算真正删干净了。
9.1 deleteKey:单密钥删除的底层逻辑
先看最常用的API:deleteKey(String alias)。这个方法是KeyStore类提供的,签名很简单:
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
keyStore.deleteKey("my_key_alias");
调用之后,密钥就消失了吗?
其实没那么简单。AndroidKeyStore的实现里,deleteKey做了三件事:
- 标记删除:在KeyStore的元数据表中,把该别名的记录标记为已删除
- 通知底层:通过KeyMint(或旧版的Keymaster)通知TEE/SE,释放该密钥占用的硬件安全资源
- 清理缓存:移除应用进程内缓存的密钥引用
但注意,这里有个坑:标记删除并不等于物理擦除。在闪存层面,数据可能还残留着。我见过一个案例,某应用删除了密钥,但攻击者通过文件系统dump,愣是把密钥材料从未使用的存储块里挖了出来。
9.2 deleteAllKeys:批量删除的风险与陷阱
deleteAllKeys()这个API,我个人建议你慎用。它会把当前应用在AndroidKeyStore里的所有密钥全部删掉。调用方式:
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
keyStore.deleteKey(alias);
}
// 或者直接遍历删除,但注意:没有原子性的deleteAllKeys方法
为什么说它危险?
- 误删风险:如果你在同一个KeyStore实例里存了多个用途的密钥(比如登录密钥、支付密钥、加密密钥),一个
deleteAllKeys下去,全没了。我曾经有个同事,在测试环境写了个脚本,不小心在线上调了deleteAllKeys——嗯,那天晚上他加班到凌晨三点恢复数据。 - 无事务支持:AndroidKeyStore的删除操作不是原子性的。如果你遍历删除,中途崩溃了,可能删了一半,剩下一半还活着。这会导致系统处于不一致状态。
- 硬件资源泄漏:如果某个密钥正在被硬件绑定(比如被TEE里的安全应用引用着),直接删除可能导致硬件资源无法正常释放。
9.3 引用计数管理:为什么删不掉?
你有没有遇到过这种情况:调了deleteKey,返回成功,但下次用containsAlias检查,密钥还在?
这通常是因为引用计数在作怪。
AndroidKeyStore底层,每个密钥都有一个引用计数器。当多个组件同时持有同一个密钥的句柄时,删除操作只会减少引用计数,不会真正释放资源。只有引用计数降到0,才会触发真正的删除流程。
哪些情况会导致引用计数增加?
| 场景 | 引用计数变化 | 说明 |
|---|---|---|
| 应用正常获取密钥 | +1 | 每次getEntry或getKey都会增加 |
| 跨进程绑定(如绑定到NFC服务) | +1 | 其他进程通过Binder引用该密钥 |
| 硬件绑定(如TEE中的安全应用) | +1 | 安全世界持有该密钥的引用 |
| 调用deleteKey | -1 | 只减一次,不保证归零 |
所以,如果你发现密钥删不掉,先检查一下是不是有其他地方还在引用它。我排查过的一个线上问题,就是某个后台服务持有了密钥的句柄,但忘记释放了,导致前端怎么删都删不掉。
9.4 安全擦除策略:真正「物理删除」
好,前面说了那么多,核心问题来了:怎么才能让密钥真正消失?
安全擦除,说白了就是让密钥材料在物理存储层面变得不可恢复。AndroidKeyStore本身不提供「安全擦除」的API,但我们可以通过一些策略来达到类似效果。
策略一:覆盖写入
在删除密钥之前,先用随机数据覆盖密钥的存储区域。但这需要你有root权限或者直接操作底层存储的能力——对普通应用来说不现实。
策略二:密钥轮换 + 立即删除
这是我在项目中实际用过的方案:
- 生成一个新密钥,替换旧密钥的功能
- 用新密钥加密所有需要保护的数据
- 删除旧密钥
- 立即触发一次GC(虽然不保证立即擦除,但能加速回收)
策略三:利用KeyMint的销毁功能
从Android 12开始,KeyMint(Keymaster 4.0+)支持destroyAttestationId等底层销毁操作。如果你的设备支持,可以通过KeyStore.getInstance().deleteKey()触发硬件层面的安全擦除。
9.5 实战:一个安全的密钥删除流程
最后,我分享一下我自己在项目中用的删除流程。不一定适合所有场景,但至少踩过的坑都填上了:
public boolean safeDeleteKey(String alias) {
try {
// 1. 先检查密钥是否存在
if (!keyStore.containsAlias(alias)) {
return false;
}
// 2. 获取密钥的引用计数(如果有的话)
// 注意:Android没有公开的API获取引用计数,这里只是示意
int refCount = getKeyReferenceCount(alias);
if (refCount > 1) {
// 有外部引用,先释放
releaseExternalReferences(alias);
}
// 3. 执行删除
keyStore.deleteKey(alias);
// 4. 验证删除结果
if (keyStore.containsAlias(alias)) {
// 删除失败,记录日志并重试
Log.w("KeyStore", "Delete failed for: " + alias);
return false;
}
// 5. 触发一次系统GC(虽然不是必须,但有助于加速回收)
System.gc();
return true;
} catch (KeyStoreException e) {
Log.e("KeyStore", "Error deleting key: " + alias, e);
return false;
}
}
这个流程里,第2步的引用计数检查是关键。虽然Android没有直接暴露这个值,但你可以通过KeyStore.getEntry()是否抛出异常来判断密钥是否还被其他地方占用。
getKeyReferenceCount和releaseExternalReferences是伪代码,实际实现需要你自己维护引用关系。不要指望系统帮你做这件事。
9.6 知识体系图
下面这张图,帮你理清密钥删除的完整脉络:
9.7 小结
密钥删除,看起来是个小功能,但背后牵扯的东西不少。总结几个要点:
- deleteKey只保证密钥不可用,不保证物理擦除
- deleteAllKeys有误删和事务风险,慎用
- 引用计数是导致删除失败的常见原因,记得检查外部引用
- 安全擦除需要硬件支持,软件层面只能做到「尽力而为」
我个人习惯是,每次删除密钥后,都做一次containsAlias验证。虽然多了一步操作,但能避免很多线上问题。你想想看,如果密钥没删干净,后续的安全审计出了问题,那可不是闹着玩的。
好了,这一章就到这里。记住:删密钥不是终点,安全擦除才是。
公众号:蓝海资料掘金营,微信deep3321