android 指纹验证

参考文章

首先是安卓提供的接口,所有关于指纹识别的接口全部在handroid.hardware.fingerprint的这个类中,这个包中包含的内容不是很多,如下图:

image

api官方链接

上面的图中,我们看到这个包中总共有4个类,下面我们简要介绍一下他们:

  1. FingerprintManager:主要用来协调管理和访问指纹识别硬件设备
  2. FingerprintManager.AuthenticationCallback这个一个callback接口,当指纹认证后系统会回调这个接口通知app认证的结果是什么
  3. FingerprintManager.AuthenticationResult这是一个表示认证结果的类,会在回调接口中以参数给出
  4. FingerprintManager.CryptoObject这是一个加密的对象类,用来保证认证的安全性,这是一个重点,下面我们会分析。
    好了,到这里我们简要知道了android 6.0给出的指纹识别的接口不是很多,可以说是简短干练。

开始指纹验证之前,首先要确认指纹是否支持指纹识别

主要是以下的几个方面:

  1. 在AndroidManifest.xml中声明权限
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
  1. API level 23 。

指纹识别API是在api level 23也就是android 6.0中加入的,因此我们的app必须运行在这个系统版本之上。因此google推荐使用 Android Support Library v4包来获得FingerprintManagerCompat对象,因为在获得的时候这个包会检查当前系统平台的版本。

  1. 硬件

硬件识别要求设备(手机)上有指纹识的硬件,因此在运行中需要检查当前系统中是否有识别的硬件。

if (!fingerprintManager.isHardwareDetected()) {
    // no fingerprint sensor is detected, show dialog to tell user.
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle(R.string.no_sensor_dialog_title);
    builder.setMessage(R.string.no_sensor_dialog_message);
    builder.setIcon(android.R.drawable.stat_sys_warning);
    builder.setCancelable(false);
    builder.setNegativeButton(R.string.cancel_btn_dialog, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            finish();
        }
    });
    // show this dialog.
    builder.create().show();
}
  1. 当前设备必须处在安全保护中。

安全保护的意思指的是当前设备必须使用屏幕锁保护,可以是password,可以是pin或者是图形解锁。因为谷歌认为当前指纹解锁还有一定的不足,仍然需要配合传统的解锁方式,使用以下代码检查:

KeyguardManager keyguardManager =(KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager.isKeyguardSecure()) {
    // this device is secure.
}
  1. 系统中是不是有注册的指纹。

普通app想要使用指纹识别功能,必须现在setting中注册过一个指纹,否则不能使用,使用以下代码进行检查:

if (!fingerprintManager.hasEnrolledFingerprints()) {
    // no fingerprint image has been enrolled.
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle(R.string.no_fingerprint_enrolled_dialog_title);
    builder.setMessage(R.string.no_fingerprint_enrolled_dialog_message);
    builder.setIcon(android.R.drawable.stat_sys_warning);
    builder.setCancelable(false);
    builder.setNegativeButton(R.string.cancel_btn_dialog, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            finish();
        }
    });
    // show this dialog
    builder.create().show();
}

目前谷歌对于所有普通app,只允许对指纹进行识别,并没有提供指纹注册的权限。

一个好的app应该对以上的几个条件进行检查


开始使用指纹识别

主要分以下几个步骤:

  1. 申明权限
  2. 获得FingerprintManager的对象引用
  3. 检查指纹识别兼容性
  4. 扫描用户指纹
  5. 处理验证信息
  6. 其他(取消扫描)

1、申请权限

这个比较简单

<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

2、获得FingerprintManager的对象引用

这个过程有两种方式

// Using the Android Support Library v4
fingerprintManager = FingerprintManagerCompat.from(this);
// Using API level 23:
fingerprintManager = (FingerprintManager)getSystemService(Context.FINGERPRINT_SERVICE);

谷歌推行第一种使用方式,通过v4包获得对象引用。
第二种方式是使用api 23 framework中的接口获得对象引用。

注:这里使用的是FingerprintManagerCompat而不是FingerprintManager。前者兼容后者,

3、检查运行条件,主要指之前的一个板块。这里不多赘述

4、扫描用户指纹

这一步也比较简单,只需要调用FingerprintManager的authenticate方法即可。
看一下这个接口


authenticate接口

上图是google的api文档中的描述,现在我们挨个解释一下这些参数都是什么:

  1. crypto这是一个加密类的对象,指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是null,但是这样的话,就意味这app无条件信任认证的结果,虽然从理论上这个过程可能被攻击,数据可以被篡改,这是app在这种情况下必须承担的风险。因此,建议这个参数不要置为null。这个类的实例化有点麻烦,主要使用javax的security接口实现,后面有一个例子可以参考
  2. cancel 这个是CancellationSignal类的一个对象,这个对象用来在指纹识别器扫描用户指纹的是时候取消当前的扫描操作,如果不取消的话,那么指纹扫描器会移植扫描直到超时(一般为30s,取决于具体的厂商实现),这样的话就会比较耗电。建议这个参数不要置为null。
  3. flags 标识位,根据上图的文档描述,这个位暂时应该为0,这个标志位应该是保留将来使用的。
  4. callback 这个是FingerprintManager.AuthenticationCallback类的对象,这个是这个接口中除了第一个参数之外最重要的参数了。当系统完成了指纹认证过程(失败或者成功都会)后,会回调这个对象中的接口,通知app认证的结果。这个参数不能为NULL。
  5. handler 这是Handler类的对象,如果这个参数不为null的话,那么FingerprintManager将会使用这个handler中的looper来处理来自指纹识别硬件的消息。通常来讲,开发这不用提供这个参数,可以直接置为null,因为FingerprintManager会默认使用app的main looper来处理。

5、处理验证信息

在上一步中我们对指纹进行了识别,接下来我们需要对callback结果进行验证。

实际上,这一步应该在验证之间进行,首先是创建一个callback对象,这个对象中,对返回信息进行重写,然后直接进行验证,验证结果就执行我们重写的内容,达到处理验证信息的目的。

代码如下:

FingerprintManagerCompat.AuthenticationCallback callback = new FingerprintManagerCompat.AuthenticationCallback(){

            @Override
            public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                Toast.makeText(context, "succeed", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onAuthenticationError(int errMsgId, CharSequence errString) {
                super.onAuthenticationError(errMsgId, errString);
                Toast.makeText(context, "error", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Toast.makeText(context, "failed", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
                super.onAuthenticationHelp(helpMsgId, helpString);
                Toast.makeText(context, "help", Toast.LENGTH_SHORT).show();
            }
        };

上面的代码也可以直接新建一个类,继承FingerprintManagerCompat.AuthenticationCallback

callback接口

其中最主要的是这4个返回结果

  1. OnAuthenticationError(int errorCode, ICharSequence errString) 这个接口会再系统指纹认证出现不可恢复的错误的时候才会调用,并且参数errorCode就给出了错误码,标识了错误的原因。这个时候app能做的只能是提示用户重新尝试一遍。

  2. OnAuthenticationFailed() 这个接口会在系统指纹认证失败的情况的下才会回调。注意这里的认证失败和上面的认证错误是不一样的,虽然结果都是不能认证。认证失败是指所有的信息都采集完整,并且没有任何异常,但是这个指纹和之前注册的指纹是不相符的;但是认证错误是指在采集或者认证的过程中出现了错误,比如指纹传感器工作异常等。也就是说认证失败是一个可以预期的正常情况,而认证错误是不可预期的异常情况。

  3. OnAuthenticationHelp(int helpMsgId, ICharSequence helpString) 上面的认证失败是认证过程中的一个异常情况,我们说那种情况是因为出现了不可恢复的错误,而我们这里的OnAuthenticationHelp方法是出现了可以回复的异常才会调用的。什么是可以恢复的异常呢?一个常见的例子就是:手指移动太快,当我们把手指放到传感器上的时候,如果我们很快地将手指移走的话,那么指纹传感器可能只采集了部分的信息,因此认证会失败。但是这个错误是可以恢复的,因此只要提示用户再次按下指纹,并且不要太快移走就可以解决。

  4. OnAuthenticationSucceeded(FingerprintManagerCompati.AuthenticationResult result)这个接口会在认证成功之后回调。我们可以在这个方法中提示用户认证成功。这里需要说明一下,如果我们上面在调用authenticate的时候,我们的CryptoObject不是null的话,那么我们在这个方法中可以通过AuthenticationResult来获得Cypher对象然后调用它的doFinal方法。doFinal方法会检查结果是不是会拦截或者篡改过,如果是的话会抛出一个异常。当我们发现这些异常的时候都应该将认证当做是失败来来处理,为了安全建议大家都这么做。
    关于上面的接口还有2点需要补充一下:

  5. 上面我们说道OnAuthenticationError 和 OnAuthenticationHelp方法中会有错误或者帮助码以提示为什么认证不成功。Android系统定义了几个错误和帮助码在FingerprintManager类中,如下:


    errorcodehelpcode

    我们的callback类实现的时候最好需要处理这些错误和帮助码。

  6. 当指纹扫描器正在工作的时候,如果我们取消本次操作的话,系统也会回调OnAuthenticationError方法的,只是这个时候的错误码是FingerprintManager.FINGERPRINT_ERROR_CANCELED(值为5),因此app需要区别对待。

6、其他处理。

上一步中,当errorcode为5的时候,表示取消处理。这个处理是比较常见的操作。直接使用CancellationSignal的cancel方法实现。
具体使用就是,实力化一个CancellationSignal的对象,在认证时,将该对象传入。需要取消的时候,调用该对象的cancel方法就行了。

附:

创建CryptoObject类对象

上面我们分析FingerprintManager的authenticate方法的时候,看到这个方法的第一个参数就是CryptoObject类的对象,现在我们看一下这个对象怎么去实例化。
我们知道,指纹识别的结果可靠性是非常重要的,我们肯定不希望认证的过程被一个第三方以某种形式攻击,因为我们引入指纹认证的目的就是要提高安全性。但是,从理论角度来说,指纹认证的过程是可能被第三方的中间件恶意攻击的,常见的攻击的手段就是拦截和篡改指纹识别器提供的结果。这里我们可以提供CryptoObject对象给authenticate方法来避免这种形式的攻击。
FingerprintManager.CryptoObject是基于Java加密API的一个包装类,并且被FingerprintManager用来保证认证结果的完整性。通常来讲,用来加密指纹扫描结果的机制就是一个Javax.Crypto.Cipher对象。Cipher对象本身会使用由应用调用Android keystore的API产生一个key来实现上面说道的保护功能。
为了理解这些类之间是怎么协同工作的,这里我给出一个用于实例化CryptoObject对象的包装类代码,我们先看下这个代码是怎么实现的,然后再解释一下为什么是这样。

public class CryptoObjectHelper
{
    // This can be key name you want. Should be unique for the app.
    static final String KEY_NAME = "com.createchance.android.sample.fingerprint_authentication_key";

    // We always use this keystore on Android.
    static final String KEYSTORE_NAME = "AndroidKeyStore";

    // Should be no need to change these values.
    static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
    static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
    static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;
    static final String TRANSFORMATION = KEY_ALGORITHM + "/" +
    BLOCK_MODE + "/" +
    ENCRYPTION_PADDING;
    final KeyStore _keystore;

    public CryptoObjectHelper() throws Exception
    {
        _keystore = KeyStore.getInstance(KEYSTORE_NAME);
        _keystore.load(null);
    }

    public FingerprintManagerCompat.CryptoObject buildCryptoObject() throws Exception
    {
        Cipher cipher = createCipher(true);
        return new FingerprintManagerCompat.CryptoObject(cipher);
    }

    Cipher createCipher(boolean retry) throws Exception
    {
        Key key = GetKey();
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        try
        {
            cipher.init(Cipher.ENCRYPT_MODE | Cipher.DECRYPT_MODE, key);
        } catch(KeyPermanentlyInvalidatedException e)
        {
            _keystore.deleteEntry(KEY_NAME);
            if(retry)
            {
                createCipher(false);
            } else
            {
                throw new Exception("Could not create the cipher for fingerprint authentication.", e);
            }
        }
        return cipher;
    }

    Key GetKey() throws Exception
    {
        Key secretKey;
        if(!_keystore.isKeyEntry(KEY_NAME))
        {
            CreateKey();
        }

        secretKey = _keystore.getKey(KEY_NAME, null);
        return secretKey;
    }

    void CreateKey() throws Exception
    {
        KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM, KEYSTORE_NAME);
        KeyGenParameterSpec keyGenSpec =
                new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setBlockModes(BLOCK_MODE)
                        .setEncryptionPaddings(ENCRYPTION_PADDING)
                        .setUserAuthenticationRequired(true)
                        .build();
        keyGen.init(keyGenSpec);
        keyGen.generateKey();
    }
}

上面的类会针对每个CryptoObject对象都会新建一个Cipher对象,并且会使用由应用生成的key。这个key的名字是使用KEY_NAME变量定义的,这个名字应该是保证唯一的,建议使用域名区别。GetKey方法会尝试使用Android Keystore的API来解析一个key(名字就是上面我们定义的),如果key不存在的话,那就调用CreateKey方法新建一个key。
cipher变量的实例化是通过调用Cipher.getInstance方法获得的,这个方法接受一个transformation参数,这个参数制定了数据怎么加密和解密。然后调用Cipher.init方法就会使用应用的key来完成cipher对象的实例化工作。
这里需要强调一点,在以下情况下,android会认为当前key是无效的:

  1. 一个新的指纹image已经注册到系统中
  2. 当前设备中的曾经注册过的指纹现在不存在了,可能是被全部删除了
  3. 用户关闭了屏幕锁功能
  4. 用户改变了屏幕锁的方式
    当上面的情况发生的时候,Cipher.init方法都会抛出KeyPermanentlyInvalidatedException的异常,上面我的代码中捕获了这个异常,并且删除了当前无效的key,然后根据参数尝试再次创建。
    上面的代码中使用了android的KeyGenerator来创建一个key并且把它存储在设备中。KeyGenerator类会创建一个key,但是需要一些原始数据才能创建key,这些原始的信息是通过KeyGenParameterSpec类的对象来提供的。KeyGenerator类对象的实例化是使用它的工厂方法getInstance进行的,从上面的代码中我们可以看到这里使用的AES(Advanced Encryption Standard )加密算法的,AES会将数据分成几个组,然后针对几个组进行加密。
    接下来,KeyGenParameterSpec的实例化是使用它的Builder方法,KeyGenParameterSpec.Builder封装了以下重要的信息:
  1. key的名字
  2. key必须在加密和解密的时候是有效的
  3. 上面代码中BLOCK_MODE被设置为Cipher Block Chaining也就是KeyProperties.BLOCK_MODE_CBC,这意味着每一个被AES切分的数据块都与之前的数据块进行了异或运算了,这样的目的就是为了建立每个数据块之间的依赖关系。
  4. CryptoObjectHelper类使用了PKSC7(Public Key Cryptography Standard #7)的方式去产生用于填充AES数据块的字节,这样就是要保证每个数据块的大小是等同的(因为需要异或计算还有方面算法进行数据处理,详细可以查看AES的算法原理)。
  5. setUserAuthenticationRequired(true)调用意味着在使用key之前用户的身份需要被认证。
    每次KeyGenParameterSpec创建的时候,他都被用来初始化KeyGenerator,这个对象会产生存储在设备上的key。

比较简单点理解就是,指纹也是属于密码的一种,需要我们对指纹的验证过程进行加密,而这个类就是用来对指纹的验证过程加密。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容