Android 安全支付这块内容,我计划用一个系列来完整梳理。今天第一篇先聊整体架构,重点放在 KeyMint 上。做移动安全这几年,我见过太多开发者在对接支付功能时只会调 API,对底层信任根机制一知半解,出了问题只能去网上搜答案,搜到的还多半是过时方案。
其实从 Android 12 开始,安全支付的底层逻辑已经发生了明显变化,老的 Keymaster 逐渐被 KeyMint 取代,这套新机制直接决定了你的应用密钥能不能扛住 root、注入、hook 这些常见攻击。这篇就把 KeyMint 的前因后果、系统分层、密钥全生命周期管理一次说透,看完你就知道自己的支付密钥到底存在哪、谁在保护它、以及怎么证明你的密钥真的没被人动过手脚。
1. 安全支付的整体设计:从应用到安全芯片的完整链路
1.1 为什么需要独立的安全硬件体系
先说个基本问题:支付必须保证密钥不泄露,而密钥一旦放进普通的 Android 系统进程里,就相当于把保险柜钥匙放在客厅茶几上。无论是应用沙箱逃逸、内核漏洞利用,还是靠 root 权限直接读内存,攻击者都有办法把密钥捞走。
所以 Android 从一开始就设计了一条独立的信任链路:应用层只负责发起请求,真正的密钥操作全部下沉到独立的可信执行环境里完成。这个概念类似家里装了两道门:外面一道普通防盗门(应用层),里面再装一间银行级别的保险库(TEE/安全芯片)。即使外面的门被撬开,保险库还是打不开,密钥也就拿不到。
这条链路在 Android 12 之前由 Keymaster 承担,在 Android 12 及之后被 KeyMint 取代。KeyMint 不是简单换个名字,而是把 Keymaster 原本分散在多个 HAL 接口中的逻辑收敛到了一个更安全、更易审计的独立模块里,并且在 Trusty TEE 或商业 TEE 中以更小的攻击面运行。
1.2 分层架构拆解:应用层、Framework 层、HAL 层、TEE/StrongBox
整个 Android 安全支付架构从上到下可以分成四层,每一层都有明确的分工和边界。
最上层是应用层,也就是你的支付 App。它通过 Android Keystore API 发起密钥生成、签名、验签请求。这一层能拿到的是操作结果,永远拿不到密钥明文。从设计上就避免了普通应用开发者因为安全意识不足导致密钥泄露的尴尬。
第二层是 Framework 层,核心是 Keystore 服务和 KeyMint 的 AIDL/HIDL 接口。Keystore 服务负责权限校验、调用方身份确认、以及把上层请求翻译成 KeyMint 能理解的指令。这一层还有一个关键功能叫 KeyMint Device Lock Controller,它管理设备锁屏状态与密钥可用性之间的联动。
第三层是 HAL 层,即 KeyMint HAL。它运行在普通内核态和用户态之间,是进入可信世界的最后一道关卡。HAL 层负责与底层 TEE 通信,传递密钥操作请求,同时完成内存隔离、缓冲区校验等工作。这里有个细节,从 Android 13 开始 KeyMint HAL 强制使用 AIDL 接口,避免直接操作共享内存带来的安全问题。
最后一层是 TEE 或者更硬核的 StrongBox。TEE 指的是运行在 CPU 隔离区域的 Trusty 系统,它们与 Android 主系统共享 CPU 核心,但通过硬件隔离机制做到互不干扰。StrongBox 则是基于独立安全芯片的方案,比如独立 SE 单元,相当于在设备里又塞了一个小型加密机,密钥操作完全在芯片内完成,CPU 核心都碰不到。
1.3 KeyMint 与 Keymaster 的差异:升级到底升级了什么
很多人问我,Keymaster 用了这么多年,网络安全支付也没出过大问题,为什么还要换 KeyMint?答案在于 Trusted Execution Environment 的可信边界和审计能力。
Keymaster 运行在 TEE 里,但它面对外部世界的接口比较杂,多个功能模块耦合在一起,攻击面相对较大。KeyMint 则重新设计了模块边界:它只负责密钥的生成、导入、签名、验签、加密解密这些核心操作,其他如随机数生成、密钥认证等辅助功能通过独立接口调用。同时 KeyMint 引入了版本计数和调用计数机制,这是 Keymaster 完全没有的东西。
版本计数的意义是防止系统降级攻击。攻击者可以把设备系统刷回旧版本,因为旧版本可能存在已知漏洞。有了版本计数之后,每次开机 KeyMint 都会检查版本号是否比上次运行的版本低,如果低就会拒绝工作。调用计数则是限制 KeyMint 可执行操作的次数,一旦超过预先设定的次数上限,密钥会被直接作废,这一招可以应对暴力碰撞攻击。
另外 KeyMint 还改进了密钥认证机制。用 Keymaster 生成密钥后,应用可以通过 attestation 拿到一个包含密钥公钥和属性的证书链,但这个证书链的生成过程相对独立。KeyMint 则把 attestation 密钥与设备身份密钥统一管理,从开机到密钥认证的整个链路都处于同一个安全域内,审计和溯源都更清晰。
2. KeyMint 核心机制与关键参数实战解析
2.1 密钥生成与存储:密钥到底存在哪里、怎么保证不可读取
既然要做安全支付,第一步是生成密钥。KeyMint 密钥生成接口走的是 Android Keystore API,应用只需要指定密钥的用途和算法,剩下的事情全部交给 KeyMint。但有几个关键参数必须在生成阶段配置好,后面再改是不行的。
第一个参数是 algorithm,可以选择 RSA、EC、AES 或 HMAC。支付应用中通常使用 EC 做签名更高效,也可以用 RSA 做验签兼容更广泛的服务端。第二个参数是 purpose,可选 SIGN、VERIFY、ENCRYPT、DECRYPT 等。一般支付场景需要 SIGN 和 VERIFY,如果要做数据加密,则需要 ENCRYPT 和 DECRYPT。第三个参数是 KeySize,比如 RSA 推荐 2048,EC 推荐 P-256,这个值会直接影响签名强度和性能。
接下来说说密钥存储。KeyMint 生成的密钥对,私钥永远保存在 TEE 或 StrongBox 的持久化存储区里。这个区域不像普通文件系统那样挂在 /data 分区下,而是放到 RPMB(Replay Protected Memory Block)分区里,或者安全芯片内置的存储区。
RPMB 分区有一个特性:任何读取操作都带有一个消息认证码,防止重放攻击。即使攻击者能够物理读取设备上的存储芯片,也无法伪造合法的读取请求,更别提篡改密钥内容了。这就是为什么光靠 root 也无法把 KeyMint 私钥导出来,因为它根本不在普通文件系统里。
实际生成密钥的示例代码如下:
val keyGenParameterSpec = KeyGenParameterSpec.Builder( "payment_key_1", KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY ) .setAlgorithm(KeyProperties.KEY_ALGORITHM_EC) .setKeySize(256) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(true) .setUserAuthenticationParameters(30, KeyProperties.AUTH_BIOMETRIC_STRONG) .setAttestationChallenge(challenge) .build() val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore" ) keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey()这段代码有几个关键细节。setUserAuthenticationRequired(true) 表示每次使用私钥都必须经过用户生物识别认证,且认证结果在 30 秒内有效。这样即使恶意应用拿到签名接口,也无法在锁屏状态下直接调用签名。
2.2 版本计数与调用计数:抗降级攻击的两道盾牌
KeyMint 对照 Keymaster 最重要的创新点之一,就是版本计数和调用计数机制。这两个概念对支付安全的意义非常大,值得专门展开讲。
版本计数的工作原理是:KeyMint 每次启动时,在安全存储区维护一个底层版本号。这个版本号并不是 Android 系统版本,而是 KeyMint 固件的版本。设备每次开机,KeyMint 引导代码都会先读取上次记录的版本号,然后与当前固件的版本号做比较。如果当前版本号小于上次记录的版本号,KeyMint 会直接用错误码拒绝初始化,所有受保护的密钥都无法使用。
攻击者想把系统刷回旧版本,利用旧版固件的漏洞提取密钥,这个策略就没辙了。因为版本计数只允许升级,不允许降级。我曾经试过在一台测试机上把 KeyMint 固件从 v2 刷回 v1,结果系统直接报错,提示 KeyMint version mismatch,所有 Keystore 操作全部失效。
调用计数的原理更适用于远程攻击。某些 KeyMint 实现允许配置一个操作次数上限,比如签名操作最多 10000 次。一旦达到上限,密钥自动失效。对于支付应用来说,这个限制其实挺合理,因为一个正常用户的支付签名频率不会特别高,而暴力破解工具往往需要海量签名尝试,调用计数就可以强制中断这种尝试。
值得注意的是,调用计数是实时记录在安全存储区里的,每次签名前都会先读取当前计数,签名成功后计数加一,整个过程有防重放机制。即使攻击者截获了签名前的读取命令,也无法伪造一个合法的计数写入命令。
2.3 StrongBox 与 TEE 的选型差异:同一套 API,两套硬件方案
KeyMint 同时支持 TEE 和 StrongBox,两者表面 API 一致,但内部安全等级不同。TEE 是基于 CPU 隔离的安全世界,和主系统共享物理核心但彼此隔离。StrongBox 则是独立的安全芯片,拥有自己的 CPU、存储和随机数发生器,安全级别更高,性能相对弱一些。
怎么判断一个设备使用的是 TEE 还是 StrongBox?调用 KeyStore 的时候可以这样判断:
val keyInfo = keyStore.getKeyInfo("payment_key_1", null) as KeyInfo if (keyInfo.isInsideSecureHardware) { // 密钥确实在安全硬件中 } if (keyInfo.isUserAuthenticationRequired) { // 需要用户认证 } if (keyInfo.isStrongBoxBacked) { // 密钥在 StrongBox 中 }如果 isStrongBoxBacked 为 true,说明密钥确实存储在独立安全芯片里。如果是 false 且 isInsideSecureHardware 为 true,说明使用的是 TEE 方案。
从支付安全角度来说,StrongBox 更适合保存根密钥或低频签名密钥,TEE 则适合需要高频操作但安全等级稍低的场景。部分支付应用会采用双重密钥策略,用一个 StrongBox 里的根密钥保护 TEE 里的工作密钥,兼顾安全性和性能。这种组合方式值得大家参考,既保证了最关键密钥的安全性,也没有把所有操作都拖慢到 StrongBox 的速度水平。
3. 支付场景下的实操:从密钥创建到服务端验签
3.1 应用内密钥创建与生物识别绑定的完整流程
安全支付的开发流程,从应用角度来说,第一步是创建一个专门用于支付的密钥并用强认证绑定。整体流程分为生成密钥、获取认证证书、使用密钥签名三步。
生成密钥时建议使用独立的别名,不要跟应用内其他数据混淆。比如:
val alias = "payment_key_" + user_id这样可以避免多用户或多角色混淆。生成之后,立刻调用 attestation 获取密钥证明,把非对称密钥的公钥和属性证明发给服务端。服务端通过证书链验证公钥确实由设备厂商的根证书签发,并且密钥确实被设置在 requiresUserAuthentication 等安全属性上。
获取认证证书的代码:
val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) val keyEntry = keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry val certChain = keyEntry.certificateChain // 将 certChain 编码为字节流发送到服务端 val certBytes = certChain[0].encoded服务端拿到证书链后,需要从根证书开始逐级验证,并检查证书扩展字段中的 attestation 安全级别。如果安全级别是 StrongBox,说明这是一个高安全等级的设备,可以给更高的支付限额或更少的二次验证要求。如果安全级别是 TEE,等级稍低,风控策略可以相应调整。
3.2 业务签名操作与服务端验签的正确姿势
真正发起支付时,应用需要对订单信息做签名,然后把签名结果和订单信息一起发给服务端。签名操作的大致逻辑是:
val signature = Signature.getInstance("SHA256withECDSA") val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) val privateKey = keyStore.getKey(alias, null) as PrivateKey signature.initSign(privateKey) // 注意:必须通过 BiometricPrompt 触发认证后才能执行 sign // 可以使用 BiometricPrompt.CryptoObject(signature) 绑定 signature.update(orderData.toByteArray()) val sigBytes = signature.sign()这里有个特别容易踩坑的细节:直接调用 signature.initSign() 和 update() 没问题,但在 sign() 之前必须要让用户完成生物识别认证。如果应用没有通过 BiometricPrompt 绑定 CryptoObject 来触发认证,调用 sign() 时会直接抛出 KeyPermanentlyInvalidatedException 或 UserNotAuthenticatedException。
服务端验签时需要注意几个方面。先检查订单信息是否与本地记录一致,这是防止重放攻击的基本手段。再用应用注册时上传的公钥去验证签名。最后检查时间戳和设备标识,比如如果有 key attestation 提供的设备硬件 ID,可以顺便校验。
下面给出一个简单的服务端验签示例,用 Java 实现 Android 端的逻辑原语:
Signature sig = Signature.getInstance("SHA256withECDSA"); sig.initVerify(publicKey); sig.update(orderData.getBytes(StandardCharsets.UTF_8)); boolean valid = sig.verify(signatureBytes);服务端验签成功不代表订单一定安全,还需要前后结合上下文信息做风险判断。比如如果设备刚升级了系统,或者设备系统状态异常检测触发了告警,服务端应该降低该设备的信任等级,要求二次验证。
3.3 密钥失效与升级策略:换手机、删应用怎么办
实际运营中会遇到很多特殊情况:用户换手机了、用户删除了应用、系统升级导致密钥失效、设备被 root 后密钥被 KeyStore 禁用。这些场景都需要一套合理的密钥生命周期管理策略。
最稳妥的方式是:服务端始终将设备的公钥视为临时凭证,不将其作为长期信任的根凭证。用户换手机后,让用户重新注册公钥,并重新做身份验证,比如短信验证码或支付密码。用户删除应用后,本地密钥自然丢失,服务端检测到该设备公钥长时间没有使用,可以考虑做一张密钥失效记录,防止旧的签名报文被拿到别的渠道重放。
系统升级导致密钥失效的情况在 KeyMint 机制下更值得注意。如果升级前后版本号倒退,KeyMint 会拒绝工作,应用此时应该捕获异常并提示用户重新注册密钥。注意,这种异常不能通过简单重启解决,用户必须重新走完整的密钥注册流程。所以支付应用应该提供一键重置支付的入口,引导用户重新注册。
设备被 root 后,KeyStore 会根据安全策略决定是否让密钥继续工作。有些设备上 root 之后 KeyMint 会直接失效,因为 TEE 侧检测到了主系统完整性被破坏。在这种情况下应用应该明确提示“检测到设备安全状态异常,请更换设备”,而不是让用户反复重试。
4. 常见问题排查与技术细节笔记
4.1 设备兼容性差异与 API 适配
KeyMint 从 Android 12 开始就更常见,但市面上仍然有大量 Android 11 及以下的设备在使用 Keymaster。做支付应用时,出于必须兼容旧设备的考虑,代码里通常需要同时处理两种情况。
最简单的适配方式是调用 KeyStore.is硬加密模块可用性检测。确认当前设备是否支持 StrongBox 或 TEE。检测代码:
val keystore = KeyStore.getInstance("AndroidKeyStore") keystore.load(null) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // Android 9+ 支持 isStrongBoxBacked 属性 } else { // 旧设备只能尽力而为,建议走服务端风险评估 }如果设备不支持任何安全硬件,支付应用应该限制可支付金额,或者要求更严格的服务端二次验证,甚至直接禁止大额交易。安全支付的核心原则就是“硬件达不到要求就降低信任等级”,不要让上层应用为了兼容性牺牲安全性。
另外还需要注意,部分厂商的 StrongBox 实现依赖特定驱动,如果用户系统更新频繁或用了第三方 ROM,StrongBox 可能不可用。此时 KeyMint 会回退到 TEE 或者直接报错。应用可以通过 reInitialize 接口尝试重新初始化,或者引导用户恢复出厂设置,但通常更实际的做法是:识别到 StrongBox 不可用,就把该设备风控等级调低。
4.2 “密钥永久失效”类异常的处理
开发中遇到最多的异常是 KeyPermanentlyInvalidatedException。这个异常表示 KeyMint 检测到安全状态变化,密钥已经不可恢复。常见的触发原因包括:
- 设备已 root
- 设备 bootloader 已解锁
- 用户的锁屏方式和注册时设置的安全参数不匹配
- 指纹或人脸数据发生变化
- StrongBox 固件升级后旧密钥无法迁移
遇到这个异常后,千万不要让应用自动重新生成密钥。因为如果用户设备被 root,自动重新生成的密钥同样不安全,而且服务端会误以为用户在正常注册新设备。正确的策略是让应用弹出提示,引导用户联系客服做安全验证,再手动重置。
4.3 KeyMint 日志与调试细节
开发调试时,可以通过 Logcat 查看 KeyMint 相关的系统日志,过滤关键字:
adb logcat -s KeyMint adb logcat | grep -i keystore不过 KeyMint 的核心日志在 TEE 侧,普通系统日志看不到。连厂商工程师调试时,通常需要开启 TEE 侧日志,这会依赖特定硬件接口。一般开发阶段更实用的做法是,使用硬件属性来验证各项安全能力。
adb shell getprop ro.hardware.keystore adb shell getprop ro.security.keystore如果 ro.hardware.keystore 显示的是 software,说明设备根本没有独立安全硬件,密钥只是锁在普通系统内,安全级别很低。这种情况下支付应用应该特别谨慎。
4.4 一次生产环境支付的完整排查思路复盘
之前在一个真实的电商支付项目里碰到过这样的问题:用户反馈部分机型在支付时提示“设备安全状态异常”,其他机型正常。一开始我们以为是服务端风控误判,后来用前面提到的检测方法排查,发现出问题的机型都运行第三方定制 ROM,系统内置的 KeyMint 驱动不完整,虽然应用能生成密钥,但调用签名时总会偶尔超时或抛异常。
解决办法分两步走:第一步,在支付前主动检查设备安全状态,如果识别到异常的 KeyMint 行为,先提示用户切换到其他支付方式;第二步,联系 ROM 厂商推送新版 KeyMint 驱动。这个案例也给所有做支付的应用提醒了一句:即使代码写得再规范,硬件和系统层面的坑一样存在,做安全支付不能只看应用层。
5. 系列规划的下一步思考
这篇重点讲了 KeyMint 的整体设计、密钥生命周期和安全链路。下一篇会深入分析密钥认证证书链的解析过程,包括如何在服务端从根证书到叶子证书完成完整的证书链校验,以及如何通过验证 extension 字段来判断设备的实际安全级别。再往后还会写关于生物识别与密钥结合的实战细节,包括如何在各种厂商设备上处理好指纹、人脸、锁屏密码之间的兼容逻辑。
如果你正在做支付、钱包或者任何需要跟敏感数据打交道的 Android 项目,搞清楚 KeyMint 这套机制能帮你少走很多弯路。安全并不是上一个加密算法就完事了,它是一整套从硬件到软件、从本地到服务端的信任链条。理解这条链路的每一环是怎么扣上的,才能在真正出现问题的时候知道问题出在哪,而不是盲目地加签名、加加密、加混淆。