Biometric 指纹认证的不同安全等级,怎么选
Biometric 指纹认证的不同安全等级,怎么选
Android 的 Biometric API 从 BiometricPrompt 取代 FingerprintManager 开始,官方就在推一个"统一入口"的概念。但真到了要上线指纹支付、应用锁、自动登录这些功能的时候,你会发现那个 setAllowedAuthenticators 或者旧版 setDeviceCredentialAllowed 里的参数,选错了就是事故。我去年维护一个金融类 App 的指纹模块,踩过 CLASS 3 和 CLASS 2 的坑,也折腾过 CryptoObject 在特定机型上的兼容问题,这篇把实际选型和测试的细节摊开讲。
安全等级的官方定义,和实际落地的差距
Android 10 引入 BiometricManager 和 BiometricPrompt 的现代化 API 时,同时带来了 BIOMETRIC_STRONG、BIOMETRIC_WEAK、DEVICE_CREDENTIAL 这三个常量。到了 Android 11,Google 又改成了更工程化的 BIOMETRIC_CLASS_3、BIOMETRIC_CLASS_2、BIOMETRIC_CLASS_1,对应的就是原来的 STRONG、WEAK,以及一个更弱的等级。但国内开发者最熟悉的可能是 FingerprintManager.COMPATIBILITY_MODE 这种黑历史,或者直接调用厂商 SDK。
官方文档对 CLASS 3(STRONG)的定义是:具备防篡改硬件、生物特征数据加密存储、活体检测,并且能通过 Google 的兼容性测试。CLASS 2(WEAK)降低了活体检测的要求,或者允许软件层面的部分处理。CLASS 1 基本就是"有就行",连加密存储都不强制。
但问题在于,你作为 App 开发者,调用 BiometricPrompt.Builder.setAllowedAuthenticators(BIOMETRIC_STRONG) 时,系统返回的可用性结果,和实际安全程度之间,隔着一个巨大的灰色地带。我遇到的最典型场景是一台某国产旗舰,Android 12,系统设置里明确显示"指纹支付:安全级别高",但 canAuthenticate(BIOMETRIC_STRONG) 返回的是 BIOMETRIC_ERROR_NONE_ENROLLED,而 BIOMETRIC_WEAK 就能通过。追查下去发现这机器的指纹方案是汇顶的某个光学屏下方案,厂商在 HAL 层申报的是 CLASS 2,但上层 UI 自己加了一套"支付级安全"的营销话术。
这种情况怎么处理?金融 App 的合规审计不认营销话术,只认 BiometricManager.canAuthenticate() 的返回值。我们当时被迫在代码里做了分级降级:优先请求 STRONG,失败则提示用户"当前设备指纹安全等级不足,请使用密码",而不是静默降级到 WEAK。这个决策直接导致了约 12% 的指纹支付用户被阻断,产品和风控扯皮了两周。
CryptoObject 不是摆设,但也不是银弹
很多教程把 CryptoObject 当成"高安全指纹"的标配,代码模板大概是:
val keyGen = KeyGenerator.getInstance("AES", "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder("key", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
keyGen.init(builder.build())
keyGen.generateKey()
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
val cryptoObject = BiometricPrompt.CryptoObject(cipher)然后 biometricPrompt.authenticate(cryptoObject, ...),回调里用 cipher 加密一个 token。
这个流程的理论是:密钥绑定生物特征,指纹验证通过时 KeyStore 才释放密钥使用权,验证失败或指纹变化则密钥失效。听起来完美,实际有三个坑。
第一个坑是 setInvalidatedByBiometricEnrollment(true) 的行为在 Android 9 和 10 之间变过。Android 9 上新增指纹确实会失效密钥,但某些厂商 ROM 在 Android 10 上做了"优化",允许在特定条件下保留密钥,导致合规测试失败。我们抓到的具体 case 是某品牌 Android 10 的某个补丁版本,用户新增指纹后旧密钥仍然可用,但 BiometricPrompt 的回调确实走了 onAuthenticationSucceeded。后来查到这个 ROM 的 KeyStore HAL 实现里,对 invalidate_key 的调用做了延迟处理,美其名曰"避免用户频繁重新设置"。
第二个坑更隐蔽:CryptoObject 的 Cipher 在 onAuthenticationSucceeded 里使用时,如果用户指纹验证通过但紧接着屏幕锁定(比如电源键误触),Cipher 可能进入非法状态。这个不是理论推测,是我们线上监控到的 IllegalStateException: Cipher not initialized for encryption,堆栈指向的正是回调里的 cipher.doFinal()。复现路径是快速验证后立即锁屏,概率大概 0.3%,但金融 App 的崩溃率红线是 0.1%。最后 workaround 是在回调里加了一层 try-catch,失败则降级到重新走密码验证。
第三个坑是 CryptoObject 对指纹"变化"的定义。官方说新增、删除指纹会触发 onAuthenticationError 或密钥失效,但实际测试发现,某些设备上"重命名指纹"这个无实质变化的操作,也会触发密钥失效。用户视角就是"我只是改了个指纹名字,怎么指纹支付要重新设置"。这个问题没有根治方案,我们在设置页加了提示文案,但体验确实打了折扣。
活体检测的暗面:STRONG 也可能被绕过
CLASS 3 强制要求活体检测(liveness detection),但检测的强度没有量化标准。Google 的 CTS 测试里确实有 Spoof Acceptance Rate 和 Bona Fide Presentation Rate 的指标,但测试环境是标准化的,和真实攻击场景差距很大。
我们安全团队做过一轮内部测试,用高清打印的指纹照片(从玻璃杯上提取,1200dpi 扫描,导电墨水打印在 AgIC 纸上)攻击测试机。三台申报 CLASS 3 的设备里,两台被绕过,一台光学屏下指纹的机型在特定角度下识别成功。这个测试不严谨,样本量小,但足以说明"STRONG"不是绝对安全。
这个发现直接影响了我们的风控策略。指纹验证通过后,大额支付仍然要叠加短信验证码或人脸比对,而不是单纯依赖生物特征。这个决策和 Biometric API 的选型无关,但会影响你在代码里怎么设计认证流程。如果你把 BIOMETRIC_STRONG 当成"可以免密"的依据,可能需要重新评估。
反过来,也有过度防御的案例。某次版本更新后,我们收到用户投诉说"湿手无法指纹支付",排查发现是某机型 Android 13 更新后,厂商把活体检测的阈值调高了,正常湿手被判定为"可能的假体攻击"。这种厂商侧的调整 App 完全无法感知,只能通过客服渠道收集机型分布,然后在特定机型上临时关闭指纹支付入口。代码里就是维护一个黑名单:
val blockedModels = setOf("XX-XXXX", "YY-YYYY")
if (Build.MODEL in blockedModels) {
// 强制走密码或人脸
}这种代码很丑,但业务连续性优先。
版本碎片:同一个 API,不同行为
BiometricPrompt 的兼容库 androidx.biometric:biometric 试图抹平版本差异,但抹得不够干净。1.2.0-alpha03 之前有个已知问题:在 Android 9 设备上,如果用户没有录入指纹但设置了 PIN,setDeviceCredentialAllowed(true) 会错误地返回 BIOMETRIC_ERROR_NO_HARDWARE。这个 issue 在 GitHub 上挂了很久,我们当时被迫升到 1.2.0-alpha04,但 alpha 版本进金融 App 需要额外的审批流程。
更头疼的是 Android 14 引入的 BIOMETRIC_STRONG 行为变更。以前 canAuthenticate(BIOMETRIC_STRONG) 返回 BIOMETRIC_SUCCESS 只表示"有 STRONG 级别的生物特征已录入",Android 14 开始还会检查"该生物特征是否满足当前应用的安全策略"。这个"安全策略"的具体定义文档里语焉不详,实际测试发现和应用的目标 SDK 有关。targetSdk 34 的应用在 Android 14 上,某些厂商设备(又是那家)的 canAuthenticate 返回值和 targetSdk 33 不一致,导致我们灰度时指纹支付开通率骤降。
追查到底,是这家厂商在 Android 14 的 Biometric HAL 里加了一层"应用安全等级"的映射,targetSdk 34 默认映射到更严格的检查。我们的解法是在发布说明里明确 targetSdk 34 的变更,但根本原因是 Android 的兼容性设计越来越复杂,Biometric 作为安全敏感模块首当其冲。
还有一个冷门碎片:Android 10 以下的设备,BiometricPrompt 兼容库内部会 fallback 到 FingerprintManager。但 FingerprintManager 的 CryptoObject 和 BiometricPrompt 的 CryptoObject 虽然类名相同,包名不同(android.hardware.fingerprint.FingerprintManager.CryptoObject vs androidx.biometric.BiometricPrompt.CryptoObject),兼容库做了桥接。这个桥接在大部分情况下透明,但我们遇到过一台 Android 9 的定制设备,桥接层抛 ClassCastException,因为厂商修改了 FingerprintManager 的返回类型。最后这台设备被加入禁用指纹功能的名单。
性能数据:验证延迟和用户体验
指纹验证的耗时直接影响转化率。我们埋点统计过 authenticate() 调用到 onAuthenticationSucceeded 回调的间隔,分等级看有明显差异。
CLASS 3 的屏下光学指纹,平均 320ms,但长尾拖到 1.2s(暗光环境需要补光)。CLASS 2 的侧边电容指纹,平均 180ms,几乎无长尾。这个数据和硬件类型强相关,不是安全等级本身导致的,但用户在"更快但可能更弱"和"更慢但标称更强"之间,体感上倾向更快。我们 A/B 测试过两种提示文案:一组强调"安全支付级指纹验证",一组只说"请验证指纹",转化率没有显著差异,说明用户对安全等级的感知很弱,对速度的感知很强。
但速度不能牺牲可靠性。某次优化尝试中,我们把 setAllowedAuthenticators 从 BIOMETRIC_STRONG 放宽到 BIOMETRIC_WEAK or DEVICE_CREDENTIAL,意图是让用户可以选择更快的人脸解锁(该机型 2D 人脸归类为 WEAK)。结果支付场景下,2D 人脸被照片攻击的成功率远高于预期,三天后紧急回滚。这个教训是:安全等级的选择不是纯技术决策,是风险决策,需要安全团队、风控团队、法务一起签字。
另一个性能细节是 BiometricPrompt 的 UI 层级。官方 prompt 是一个系统对话框,但某些厂商会替换成自己的全屏页面,这个替换的耗时在低端机上能达到 500ms 以上。我们在启动指纹验证前预加载了一个空白 Fragment 来"骗"系统提前初始化,效果有限,最后是在点击"指纹支付"按钮后立即调 authenticate(),同时按钮做 200ms 的转圈动画,掩盖对话框的拉起延迟。这种 trick 没有文档支持,靠 systrace 抓到的对话框 onCreate 耗时反推。
具体选型建议:从场景倒推
经过这些坑,我们内部形成了相对固定的选型矩阵,但强调这是"我们业务的风险偏好",不是通用标准。
支付确认、大额转账:强制 BIOMETRIC_STRONG + CryptoObject + 后端二次校验。如果 canAuthenticate(STRONG) 失败,不允许降级到 WEAK,直接引导密码或短信验证。这个决策导致部分用户流失,但合规要求优先。
应用解锁、快捷登录:BIOMETRIC_WEAK 可接受,但 CryptoObject 仍然要绑,只是 setInvalidatedByBiometricEnrollment(false),避免用户改指纹后重新设置。这里有个细节:登录态的 token 刷新用 CryptoObject 加密存 KeyStore,但 token 本身有有效期,密钥失效只影响本地存储的读取,不影响服务端会话,所以可以接受指纹变化不立即失效。
敏感操作确认(如修改绑定手机):BIOMETRIC_STRONG or DEVICE_CREDENTIAL,允许用户回退到 PIN/密码。这个场景下 CryptoObject 不是必须的,因为后端会发验证码再做一次确认,本地生物特征只作为"操作人是机主"的弱证据。
后台自动填充密码:这个场景最纠结。AutoFill 服务调 BiometricPrompt 时,应用处于后台,Android 12 以上有 setAllowedAuthenticators 的限制,后台应用不能调 STRONG,只能 WEAK 或 CREDENTIAL。我们最后放弃了后台指纹自动填充,改为前台切换时触发,牺牲了一点便利性。
一个未解的兼容性问题
写到这里,想起一个至今没根治的问题。某 Android 13 设备,BiometricPrompt 调用后系统对话框不弹出,直接回调 onAuthenticationError(BIOMETRIC_ERROR_USER_CANCELED, ...)。但用户根本没碰屏幕。抓 log 发现是系统侧 BiometricService 在检查"当前应用是否允许显示悬浮窗"时,因为该应用的通知权限被用户关闭,导致了一个连锁的权限拒绝。这个逻辑链在 AOSP 里找不到对应代码,是厂商定制。我们的 workaround 是检测到连续两次 USER_CANCELED 且间隔小于 100ms 时,提示用户检查通知权限,但这完全是黑魔法,没有官方依据。
这类问题在 Biometric 模块特别多,因为指纹/人脸的 HAL 和 UI 层深度耦合厂商定制,Android 的兼容性库只能覆盖最常见路径。作为应用开发者,你能做的是:严格分级、埋点监控、维护机型黑名单、准备好随时降级到密码的兜底流程。
最后放一段我们实际在用的封装逻辑,精简掉业务代码后的大概样子:
fun authenticate(
activity: FragmentActivity,
level: BiometricLevel,
crypto: Boolean,
onSuccess: (Cipher?) -> Unit,
onFallback: () -> Unit
) {
val authenticators = when (level) {
STRONG -> BIOMETRIC_STRONG
WEAK -> BIOMETRIC_WEAK
ANY -> BIOMETRIC_WEAK or DEVICE_CREDENTIAL
}
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("验证身份")
.setAllowedAuthenticators(authenticators)
.setConfirmationRequired(false)
.build()
val prompt = BiometricPrompt(activity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
onSuccess(result.cryptoObject?.cipher)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
if (errorCode == BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL
onFallback()
}
}
}
)
val cryptoObject = if (crypto && level == STRONG) createCryptoObject() else null
prompt.authenticate(promptInfo, cryptoObject)
}createCryptoObject() 里包着前面提到的 KeyStore 初始化,额外加了 try-catch 包裹 generateKey(),因为某些设备 KeyStore 满时会抛 KeyStoreException。这个异常在官方示例代码里不会出现,但线上真实存在。
选安全等级的时候,先想清楚你的兜底方案是什么。STRONG 不是越强越好,WEAK 也不是不能用,关键是当生物特征不可用时,你的流程能不能平滑承接。很多 App 的指纹登录做得漂亮,但指纹失败后的密码输入页转化率暴跌,这才是要命的。