目前指纹领域不管从产品角度仍是技术角度都已经趋于成熟,可是当各位开发者准备深刻探究的时候,却发现网上不少文章都是皮毛,很难有较深的启示。本文将着重介绍指纹验证开发整个过程,包括技术选型、产品的设计方案逻辑、代码的架构以及后续测试中遇到的兼容性问题等几个方面。在这里抛砖引玉,但愿能给予你们一些启发。php
产品:我们 Android 端能作指纹验证吗?
开发:不能,一堆兼容问题。
产品:我们 Android 端能作指纹验证吗?
开发:不能,一堆兼容问题。
产品:我们 Android 端能作指纹验证吗?
开发:不能,一堆兼容问题。
产品:我们 Android 端能作指纹验证吗?
开发:我……我试试吧……java
着手调研,开发前确定先拿市面上竞品的功能来瞧瞧。咱们同比了支付宝、微信支付和招商App。android
产品:怎么支付宝和微信就没兼容问题了?git
开发:那是由于支付宝和腾迅有本身的协议!(一听怎么XXX支持,怎么XXX没问题,升起无名火)这个标准直接和设备厂商合做,而应用方只有微信和支付宝本身。支付宝指纹支付标准是 IFAA ,腾讯的指纹支付标准是 SOTER,也就是说没有其余应用方会使用这个标准。因此很看应用方和设备厂商的协商程度。如今 IFAA 没有开源,只有 SOTER 是开源的了,若是接入,咱们能省去兼容性测试的工做量,并且有些 6.0 如下的机型 SOTER 也支持。还有!(星星眼)每一个指纹将会有惟一 ID,也就是说,咱们能把帐号和指纹绑定起来,更加安全。github
产品:不行不行!这 SOTER 压根没支持华为,华为用户是咱们的主要用户群,并且之后机型的扩展受第三方支持的限制。api
开发:以前小米和华为就没有支持 SOTER 标准,如今小米是支持了,华为不见得会支持,由于 SOTER 和厂商合做,出厂的时候就将私钥存储在 TEE 中,华为目前多 TEE 系统开发还没有成熟,只能支持一个 TEE ,显然华为不肯意将惟一的 TEE 交给腾讯掌控。其余手机厂商通常使用高通或第三方的 TEE 系统方案,这些系统目前都支持多 TEE 运行环境,即便将其中一个 TEE 的公共密钥交给腾讯运营,并不影响手机厂商运营本身的 TEE 平台。缓存
产品:不接入了,咱们用 Google API。安全
开发:那好,来制定下条件先:bash
产品:(点头)能够,开干吧!用 Google API 兼容性问题处理和测试量较大,因此咱们支持的机型作成可配置,控制风险。第一期先支持几个机型。微信
- Google官方Sample
- SOTER 介绍
SOTER 支持机型
SOTER SDK地址- 阿里指纹
- IFAA暂无开源
2018.12.10 更新
SOTER 已支持部分华为机型 SOTER 支持机型 wiki
好了,demo 写完了,看下了产品文档。啥?场景这么复杂?!分支繁多,还须要结合到以前存在的手势验证功能(用户有两种安全方式可选:指纹验证和手势验证)。
业务场景有四个:
每一次验证的状态,都会经过 AuthenticationCallback 回调,咱们能够理解为是指纹验证的生命周期。
public class MyAuthCallback extends FingerprintManagerCompat.AuthenticationCallback {
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
//验证过程当中遇到不可恢复的错误
super.onAuthenticationError(errMsgId, errString);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
//验证过程当中遇到可恢复错误
super.onAuthenticationHelp(helpMsgId, helpString);
}
}
复制代码
onAuthenticationSucceeded 和 onAuthenticationError 的回调意味着本次的认证结束,会根据当前所处业务场景给予用户不一样的引导。
而 onAuthenticationFailed 和 onAuthenticationHelp 的状况,四个业务场景都是同样的,都是在界面上提示用户,咱们能够合并一块儿处理。
因此咱们根本不须要一个业务场景就对应一个 AuthenticationCallback 回调类,咱们能够只用一个 AuthenticationCallback 回调类来根据当前所处的业务场景分发行为。可是我又不想在 onAuthenticationSucceeded 和 onAuthenticationError 的回调中有 Switch 逻辑。因此对于四个场景各不相同的 onAuthenticationSucceeded 和 onAuthenticationError 的回调方法,咱们用状态模式来分离,这样把与特定状态相关的行为局部化,而且将不一样场景下的行为分割开来。(须要给用户什么提示,什么操做,包括验证次数超限的处理,取决于当前所处的场景状态)
另一点:须要在运行时刻根据状态来改变行为,好比说用户从一个正常态,转移到验证过程异常或者验证过程被劫持的状态。
验证过程异常状况,也便是说,受用户 root 或自定制状况,经过测试的同一个机型有可能验证过程异常。
验证过程被劫持,由于 Google API 只返回 true 或 false,咱们固然不能无条件相信这个验证结果,因此须要在应用内产生一对非对称的密钥,保证验证过程不会被篡改。若是拿到验证结果解密失败,就进入了被劫持的状态了。
验证过程异常和验证被劫持的状态基本处理一致,都是属于用户没法再继续验证的场景,咱们能够把这两个状态合为一。按照开发的思路,有异常,被劫持,那确定是失败了,是吧? 可是按照产品的思路,其余 3 个业务场景按失败处理,但若是是关闭指纹的场景下(4. 设置页手动关闭指纹登录),就算是失败了,也要让他去关闭成功,否则可能会出现用户手机中途 root 或极端状况下,没法关闭指纹,从而引发客诉。
按照分析咱们能够发现,被劫持和验证过程异常的状况的处理,依赖于当时所处的场景,因此呢,咱们没法把被劫持和验证过程异常当作一个独立的状态了。只能抽出做为一个公共方法。
为了避免和业务逻辑耦合在一块儿,工具类包装了一层,主要封装了验证条件的判断,指纹类的初始化等等,最主要的是封装了加密类 CryptoObjectCreatorHelper ,咱们考虑到安全因素,若是不加密的话,就意味着App 无条件信任认证的结果,这个过程可能被攻击,数据能够被篡改,这是 App 在这种状况下必须承担的风险。可是这个加密过程和业务是无关的,咱们不想让 Activity 层感知到,因此密钥和加密对象的销毁,会统一由工具类来把控。
为了安全,每次验证过程的密钥都不一样,验证过程一结束,也就是回调 onAuthenticationSucceeded 和 onAuthenticationError 时,都须要销毁掉密钥,可是咱们不想让业务层来操做,因此工具类也有本身的一个 AuthenticationCallback ,在 AuthenticationCallback 里作一些和业务无关的操做,再回调 Activity 的 AuthenticationCallbackListener 。
工具类的 CallBack 是 FingerprintManagerCompat.AuthenticationCallback 实现类,业务层的 AuthenticationCallbackListener 是自定义接口,由于不想把和业务无关的往上传递,好比说,验证成功的 AuthenticationResult ,验证错误的 typeId,这些业务并不关心。Activity 的 AuthenticationCallbackListener 会把请求统一转发给控制器 FingerPrintTypeController,在转发给控制器的先后,咱们能够作一些通用的业务操做,好比说中止界面的扫描动画,发一些异步的请求等等,这个就是代理模式的应用了。
那控制器 FingerPrintTypeController 和四个场景的关系又是如何?咱们看看类图。
能够看到,四个场景,对应四个状态类,控制器和状态类实现了同一个接口,在内部根据当前场景转发给对应的类, 那怎么根据场景转发给对应类?咱们创建一个映射表,把场景和类对应起来。每次匹配的话只要 O(1) 复杂度。
private interface FingerPrintType {
void onAuthenticationSucceeded();
void onAuthenticationError(String content);
}
private class LoginAuthType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class ClearType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class LoginSettingType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class SettingType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class FingerPrintTypeController implements FingerPrintType {
private Map<String, FingerPrintType> typeMappingMap = new HashMap<>();
public FingerPrintTypeController() {
typeMappingMap.put(GESTURE_FINGER_SETTING, new SettingType());
typeMappingMap.put(GESTURE_FINGER_LOGIN_SETTING, new LoginSettingType());
typeMappingMap.put(GESTURE_FINGER_CLEAR, new ClearType());
typeMappingMap.put(GESTURE_FINGER_LOGIN, new LoginAuthType());
}
@Override
public void onAuthenticationSucceeded() {
typeMappingMap.get(mType).onAuthenticationSucceeded();
}
@Override
public void onAuthenticationError(String content) {
typeMappingMap.get(mType).onAuthenticationError(content);
}
}
复制代码
这个时候产品又说了,一样是异常状况,可是被劫持和异常过程异常的提示文案要不同,ok,那咱们将提示语和操做分离开来,提示和业务场景的对应关系也预先缓存在 Map 里,直接 get 获取具体提示,做为参数传入就能够了。
//普通异常状况提示
exceptionTipsMappingMap = new HashMap<>();
exceptionTipsMappingMap.put(GESTURE_FINGER_SETTING, getString(R.string.fingerprint_no_support_fingerprint_gesture));
exceptionTipsMappingMap.put(GESTURE_FINGER_LOGIN_SETTING, getString(R.string.fingerprint_no_support_fingerprint_gesture));
exceptionTipsMappingMap.put(GESTURE_FINGER_CLEAR, null);
exceptionTipsMappingMap.put(GESTURE_FINGER_LOGIN, getString(R.string.fingerprint_no_support_fingerprint_account));
复制代码
在同一机型上调用 FingerprintManagerCompat 的 isHardwareDetected() 和 hasEnrolledFingerprints() 时候,返回的都是 false,可是调用 FingerprintManager 的 isHardwareDetected() 和 hasEnrolledFingerprints() 时,倒是返回 true。
解决:是否符合指纹条件能够多加一层判断。
onAuthenticationError 和 onAuthenticationFailed,理论上应该是识别失败的状况,可是该机型点击取消指纹识别也会先回调一次Error,若是遇到这种状况,只能根据具体项目环境中去进行规避适配了。
onAuthenticationHelp 回调不按套路出牌,正常官网文档解释,这个方法的回调时机是在指纹认证期间发生可恢复性的错误时回调。结果在魅族上,启动指纹识别认证的时候就会回调这个方法,里面传递回来的信息提示是“等待按下手指”,也就是说,它的 onAuthenticationHelp 回调跟官网时机不同,并且方法的做用也变了,它在正常的状况回调了 onAuthenticationHelp。
解决:不影响验证流程,无需解决
产品需求:用户锁屏或切到后台时(onStop)自动中止指纹验证,回到界面时(onResume)自动调起验证。
因此我在指纹回调方法中加入了标志位 isInAuth。onStop时保存 isInAuth,onResume时 isInAuth == true 则自动调起验证。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
isInAuth = false;
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
isInAuth = false;
}
@Override
public void onAuthenticationFailed() {
isInAuth = true;
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
isInAuth = true;
}
复制代码
然而小米六、米mix2 锁屏时的生命周期是 onAuthenticationError -> onStop;切到后台是 onStop -> onAuthenticationError。致使不一样流程下拿到 isInAuth 标志位不一致,没法自动调起验证。
解决:界面指纹按钮能够手动调起验证,无需兼容处理。
小米5生命周期同上,可是不管是自动仍是手动调起验证,立刻就回调了 onAuthenticationError,也就是说 MI5 从后台切回来后,指纹验证流程中断。
解决:用一个栈来存储调用方法顺序,若是验证方法调起,立刻就回调 onAuthenticationError 方法,则断定是属于兼容问题,按验证失败来解决。
三星SM-A9100 、Nexus 6P密钥解密失败
解决:暂没法解决
其余兼容解决方案:
- 三星passSdk(不过从2018下半年开始,Pass SDK 将再也不提供 DEVICE_FINGERPRINT_UNIQUE_ID 。也就是再也不为每一个已注册的指纹提供索引了。所以将没法经过 SDK 区分使用哪一个指纹来验证用户。)
- 魅族 flyme开发平台提供了指纹验证官方api
系统中注册了一个新的指纹的状况下,即便指纹在系统指纹列表里,验证也不经过。
解决:删除了当前无效的key,而后根据参数再次生成密钥。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
...
/**
* doFinal方法会检查结果是否是会拦截或者篡改过,
* 若是是的话会抛出一个异常,异常的时候都将认证当作是失败来处理
*/
try {
result.getCryptoObject().getCipher().doFinal();
mCustomCallback.onAuthenticationSucceeded(true);
} catch (IllegalBlockSizeException e) {
//若是是新录入的指纹,会抛出该异常,须要从新生成密钥对从新验证,这里加个次数限制,避免进入验证异常->从新验证->又验证异常的死循环
if (happenCount == 0) {
beginAuthenticate();
happenCount++;
return;
}
mCustomCallback.onAuthenticationSucceeded(false);
} catch (Exception e) {
mCustomCallback.onAuthenticationSucceeded(false);
}
...
}
复制代码
非复现,和设备无关,怀疑是谷歌 API 的坑。
java.lang.IllegalStateException: At least one fingerprint must be enrolled to create keys requiring user authentication for every use
复制代码
解决:暂时只想到针对这个特定异常,直接使用无密钥验证,有必定的安全风险,有更好方案欢迎补充。
本文完整 Demo 地址 Demo 仅供参考架构和兼容处理,若是后续接入魅族和三星 SDK,能够考虑用策略模式替换Goolge API。