我和Android指纹的故事

Google官方从Android6.0(API 23)开始支持指纹功能。
指纹功能常用于用户屏幕解锁、登陆验证、支付验证,很方便。

Google官方也给出了指纹的demo,但是官方demo太复杂了,下图左。
网上关于指纹的资料也不是很完整,所有我决定自己写一篇总结指纹功能的文章,Demo效果如下图右。
我和Android指纹的故事_第1张图片 我和Android指纹的故事_第2张图片


指纹基本使用

使用指纹功能,需要添加指纹权限,指纹权限为普通权限,在AndroidManifest.xml文件中添加后即可使用,不需要使用过程中动态申请:


开始指纹识别

开启指纹识别,我们需要用到FingerprintManager类,FingerprintManager实例的获取,和ActivityManager、WindowManager这些Manager一样,通过Context的getSystemService方法获取。

fingerprintManager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE);

在FingerprintManager类中,调用authenticate方法,就能打开指纹传感器。
在FingerprintManager中,有2个重载authenticate方法:
我和Android指纹的故事_第3张图片
这2个方法中,有一个为hide,能直接调用的方法,如下图所示:
在这里插入图片描述我和Android指纹的故事_第4张图片
此方法有5个参数:

  • CryptoObject——用于加解密,可为null;
  • CancellationSignal——用于取消指纹识别;
  • int——传0即可;
  • AuthenticationCallback——指纹识别结果回调;
  • Handler——用于指明回调所处的线程,传入null时,在主线程处理回调。

CryptoObject:会在接下来的内容详细讲解,这里先传null;
CancellationSignal:new一个对象即可;
int:传入0;
Handler:传入null。
所以,我们现在只需要关注一个参数AuthenticationCallback:
我和Android指纹的故事_第5张图片
这是一个抽象类,有5个方法,其中onAuthenticationAcquired为hide方法,所以需要重写4个方法。

  • onAuthenticationError:会关闭指纹传感器。发生了不可恢复的错误,会调用此方法,比如多次识别失败之后,会调用此方法,并在短时间内(据说30s)不能打开指纹传感器。
  • onAuthenticationHelp:不会关闭指纹传感器。发生了可恢复的错误,会调用此方法,比如手指移动太快。
  • onAuthenticationSucceeded:会关闭指纹传感器。指纹识别成功回调。
  • onAuthenticationFailed:不会关闭指纹传感器。指纹识别失败,可继续识别。

指纹功能封装在FingerSimpleUtil.java类中,并实现了AuthenticationCallback接口。

FingerSimpleUtil类怎么用呢?

  1. new一个FingerUtil实例;
  2. 调用setCallback设置回调;
  3. 调用startAuthenticate开始指纹识别。

通过这3步,就能开始指纹识别了,识别结果会回传到第2步设置的回调中。

注意事项

并不是所有手机都支持指纹功能,也不是所有Android版本都支持指纹功能,所以应用中不能直接使用指纹功能,使用之前要进行一系列的判断:

  1. 应用是否添加指纹权限;
  2. 设备是否支持指纹功能;
  3. 设备是否开启锁屏密码;
  4. 设备是否录入指纹;

使用指纹功能,系统会要求用户添加备用PIN码、图案或密码。因为有时候(例如重启设备或系统无法识别指纹时),需要使用非指纹方式来解锁设备。

使用指纹功能之前的一系列判断,封装代码如下:

public class Util {

    /**
     * 使用指纹之前的一系列检查,有:
* 应用是否添加指纹权限;
* 设备是否支持指纹功能;
* 设备是否开启锁屏密码;
* 设备是否录入指纹;
* @param context * @return 如果任一项检查失败,返回false */ public static boolean isFingerAvailable(Context context) { if(!CommonUtils.hasPermission(context, Manifest.permission.USE_FINGERPRINT)) { CommonUtils.toast(context, "应用未添加指纹权限"); return false; } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { FingerprintManager fingerprintManager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE); KeyguardManager keyguardManager = (KeyguardManager)context.getSystemService(Context.KEYGUARD_SERVICE); if(!fingerprintManager.isHardwareDetected()) { CommonUtils.toast(context, "设备不支持指纹功能"); return false; } if(!keyguardManager.isKeyguardSecure()) { CommonUtils.toast(context, "设备未开启锁屏密码"); return false; } if(!fingerprintManager.hasEnrolledFingerprints()) { CommonUtils.toast(context, "设备未录入指纹"); return false; } return true; } else { CommonUtils.toast(context, "设备不支持指纹功能"); return false; } } }

至此,指纹的基本使用,就讲解完毕了。
这里是完整示例代码。


指纹与加解密

FingerprintManager开启指纹验证的方法authenticate中,第一个参数为CryptoObject类型,这是一个用于加密的包装类,指纹的加解密,就要用到这个类。

怎么构造一个CryptoObject对象呢?
我和Android指纹的故事_第6张图片
CryptoObject有3个构造函数,分别传入Signature、Cipher、Mac对象,其中Signature用于数字签名、Mac用于信息检验码,所以要用CryptoObject(@NonNull Cipher cipher)方法构造CryptoObject对象。

Cipher对象怎么构造呢?
Cipher都构造函数没有public类型,所以只能调用static方法getInstance构造Cipher对象,选用只有一个String参数的方法,参数用于指定加密算法,一般传入“AES/CBC/PKCS7Padding”即可,关于加密CBC加密模式,推荐一篇博客。

有了Cipher对象之后,需要用密钥初始化Cipher。用到了密钥,那密钥的保存就是一个很关键的问题,因为对称加密密钥一但泄露,后果很严重。

申请密钥库

于是,用KeyStore申请一个密钥库,用来存放密钥,申请的代码如下所示:

try {
	keyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
   	throw new RuntimeException("Failed to get an instance of KeyStore", e);
}

KeyStore.getInstance(String)方法中,传入的参数是“AndroidKeyStore”,此参数说明密钥库的类型。“AndroidKeyStore”类型的密钥库,在什么应用中保存的密钥,只能在什么应用中取出。比如:在A应用中,生成了密钥key1,保存在“AndroidKeyStore”类型的密钥库中,那只能在A应用中取出key1,在B应用中是无法取出的。这样密钥独立于应用,更能保证安全性。

有了密钥库,就能用KeyGenerator来生成密钥了。

这里,我就不介绍加解密相关的知识了。加密有2类:对称加密非对称加密
Demo中用对称加密讲解指纹与加解密,因为对称加密效率要比非对称加密高,加密等级也能达到要求,而对称加密使用最广泛的,就是AES加密算法。

先初始化一个KeyGenerator对象:

keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");

生成密钥,封装在一个方法中:

private SecretKey generateKey() {
	try {
	     if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
	         KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, purpose)
                        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                        .setUserAuthenticationRequired(true)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
	
	         keyGenerator.init(builder.build());
	         return keyGenerator.generateKey();     // 生成密钥
	     }
	     else {
	         throw new RuntimeException("android.os.Build.VERSION.SDK_INT < 23(Android 6.0)");
	     }
	 } catch (Exception e) {
	     throw new RuntimeException("Generate key failed.", e);
	 }
}

生成密钥,需要先对KeyGenerator进行初始化。
初始化中有一个设置方法setUserAuthenticationRequired(true),方法描述中有这样一段话:
在这里插入图片描述
此方法用于设置,是否在用户身份验证后,才允许使用密钥,默认是false。所以需要手动设置为true,这个设置和接下来,依靠密钥判断指纹是否发生变化有关。

初始化Cipher

有了密钥,就可以用密钥来初始化Cipher,代码如下:

/**
* 初始化{@link Cipher}
* @param purpose 用于指明是加密,还是解密。
* 加密传入{@link KeyProperties#PURPOSE_ENCRYPT};
* 解密传入{@link KeyProperties#PURPOSE_DECRYPT}。 * @param iv 初始向量,解密才需要传入,如果是加密,传入null即可。 * @return 如果初始化失败,返回null。 */ public Cipher initCipher(int purpose, String iv) { if(purpose != KeyProperties.PURPOSE_ENCRYPT && purpose != KeyProperties.PURPOSE_DECRYPT) { throw new IllegalArgumentException("Unknown purpose: " + purpose); } try { if (purpose == KeyProperties.PURPOSE_ENCRYPT) { SecretKey key = generateKey(); cipher.init(purpose, key); } else { SecretKey key = (SecretKey) keyStore.getKey(KEYSTORE_ALIAS, null); if (key == null || iv == null || iv.equals("")) { return null; } cipher.init(purpose, key, new IvParameterSpec(Base64.decode(iv, Base64.URL_SAFE))); } return cipher; } catch (Exception e) { e.printStackTrace(); return null; } }

需要说明的是:加密和解密,初始化Cipher是不同的。解密Cipher的初始化,需要多传入一个参数iv(初始向量),什么是iv(初始向量),可以参考这篇博客。

完整的封装,放在了FingerSecurity.java类中。

指纹识别成功后的加解密

从前面指纹识别基本使用中得知,使用指纹其实很简单。
调用FingerprintManager类的authenticate方法,就可以开启指纹识别。识别成功,或者失败都会回调AuthenticationCallback中相应的方法。

指纹识别加解密,一般都是指纹识别成功之后,对数据进行加解密。加密和解密,都是调用Cipher类的doFinal方法,Cipher对象,是开启指纹识别时传入的参数,核心代码如下所示:

String iv = purpose == KeyProperties.PURPOSE_ENCRYPT ? null : Preference.getString(Preference.FINGER_KEY_IV, App.app.preferences);
Cipher cipher = fingerSecurity.initCipher(purpose, iv);
if(cipher == null) {
	callback.onError(ERROR_CLOSE, context.getString(R.string.finger_authenticate_failed));
	return ;
}

FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
cancellationSignal = new CancellationSignal();
fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, this, null);

为了得到Cipher对象,调用FingerSecurity的initCipher方法。但Cipher是区分加密对象和解密对象的,所以先判断是解密还是解密,如果是加密,iv(初始向量)为null即可;如果是解密,则要获取相应的iv(初始向量),iv(初始向量)是怎么来的,接下来的内容会说到。

得到Ciph对象后,把Cipher对象封装在CryptoObject对象中,调用authenticate方法打开指纹传感器,开始指纹识别。

识别成功后,会回调AuthenticationCallback的onAuthenticationSucceeded方法,方法代码如下:

/**
* 指纹识别成功,不会关闭指纹传感器。
* @param result
*/
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
   super.onAuthenticationSucceeded(result);

   FingerprintManager.CryptoObject cryptoObject = result.getCryptoObject();
   if(cryptoObject == null) {
       callback.onError(ERROR_CLOSE, context.getString(R.string.finger_authenticate_failed));
       return ;
   }

   Cipher cipher = cryptoObject.getCipher();
   String data;
   if(getPurpose() == KeyProperties.PURPOSE_ENCRYPT) {
       try {
           // 保存加密后的数据
           byte[] encryptData = cipher.doFinal(FingerFragment.SECRET_MESSAGE.getBytes());
           data = Base64.encodeToString(encryptData, Base64.URL_SAFE);
           String keyIV = Base64.encodeToString(cipher.getIV(), Base64.URL_SAFE);
           Preference.putString(Preference.FINGER_DATA, data, App.app.preferences);
           Preference.putString(Preference.FINGER_KEY_IV, keyIV, App.app.preferences);
           callback.onAuthenticated(data);
       }
       catch (Exception e) {
           e.printStackTrace();
           callback.onError(ERROR_CLOSE, context.getString(R.string.finger_authenticate_failed));
       }
   }
   else {
       try {
           // 解密数据
           String encrypt = Preference.getString(Preference.FINGER_DATA, App.app.preferences);
           byte[] encryptByte = cipher.doFinal(Base64.decode(encrypt, Base64.URL_SAFE));
           data = new String(encryptByte);
           callback.onAuthenticated(data);
       }
       catch (Exception e) {
           e.printStackTrace();
           callback.onError(ERROR_CLOSE, context.getString(R.string.finger_change));
       }
   }
}

指纹识别成功后,onAuthenticationSucceeded方法会传入一个AuthenticationResult对象,它的源码如图所示:
我和Android指纹的故事_第7张图片
AuthenticationResult的成员中,有一个CryptoObject对象。这个对象就是调用authenticate方法,开启指纹识别传入的对象。从CryptoObject对象中,能拿到Cipher对象,用于加密和解密。

在onAuthenticationSucceeded方法的if加密代码块中,对数据进行加密,并保存SharedPreferences文件中。
特别指出的是,这里要获取Cipher对象的iv(初始向量)值,也保存在SharedPreferences中,以便初始化解密Cipher时使用。

在onAuthenticationSucceeded方法的else解密代码块中,用获取到的Cipher进行解密即可。

完整的代码封装在FingerAdvanceUtil.java类中。


检测指纹变化

在指纹识别的过程中,如何检测指纹的变化呢?
检测指纹的变化,需要依赖前面提到的密钥。
再来看一下FingerSecurity.java类中生成密钥generateKey方法的代码:

// 指明密钥的目的,这里指定为加密、解密
int purpose = KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, purpose)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setUserAuthenticationRequired(true)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

    keyGenerator.init(builder.build());
    return keyGenerator.generateKey();     // 生成密钥
}

着重看KeyGenParameterSpec.Builder参数的设置。
setUserAuthenticationRequired(true)方法前面已经讲过,标识是否在用户身份验证后,才允许使用密钥,默认是false。
KeyGenParameterSpec.Builder中有一个boolean型成员变量mInvalidatedByBiometricEnrollment,默认值为true,该变量的set方法描述如下图所示:
我和Android指纹的故事_第8张图片描述第一段和第二段内容,大意为:

  • 设置此密钥在新指纹录入后是否无效,但需要setUserAuthenticationRequired(true),setUserAuthenticationValidityDurationSeconds(int)设置的参数为负数(默认为-1),这意味着密钥只适用于指纹验证。
  • mInvalidatedByBiometricEnrollment默认值为true。当录入新指纹,或者删除所有指纹后(如果删除指纹,但没删除所有,密钥还是会有效),密钥会失效。

这就是检测指纹变化的依据,有3个相关值:

  • mUserAuthenticationRequired必须为true,所以调用setUserAuthenticationRequired(true);
  • mInvalidatedByBiometricEnrollment默认为true,不需要设置。
  • mUserAuthenticationValidityDurationSeconds默认为-1(表示密钥只能用于指纹验证),不需要设置。

所以需要对KeyGenParameterSpec.Builder进行如下设置。

KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEYSTORE_ALIAS, purpose)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setUserAuthenticationRequired(true)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

指纹验证的密钥失效了,怎么用呢?
核心代码如下所示,这是FingerAdvanceUtil.java类onAuthenticationSucceeded方法中的一段代码:

try {
     // 解密数据
     String encrypt = Preference.getString(Preference.FINGER_DATA, App.app.preferences);
     byte[] encryptByte = cipher.doFinal(Base64.decode(encrypt, Base64.URL_SAFE));
     data = new String(encryptByte);
     callback.onAuthenticated(data);
 }
 catch (Exception e) {
     e.printStackTrace();
     callback.onError(ERROR_CLOSE, context.getString(R.string.finger_change));
 }

原理很简单,当密钥失效后,用Cipher进行解密,会抛出异常,判定为指纹发生变化。
需要注意的是:这个时候,指纹识别已经成功了,只是解密失败了。


获取指纹详细信息

在指纹识别成功回调onAuthenticationSucceeded(FingerprintManager.AuthenticationResult) 方法中,传入了FingerprintManager.AuthenticationResult对象,它一个Fingerprint成员变量,这个变量中保存了指纹名称、指纹Id、设备Id等指纹详细信息。
所以理论上,获取到了Fingerprint对象,就获取到了指纹的详细信息。

FingerprintManager.AuthenticationResult类中,有一个getFingerprint()方法,但这个方法是一个hide方法,无法调用。

网上一些资料提到通过放射可以获取到FingerprintManager.AuthenticationResult中的Fingerprint变量,已经实验过,也无效。

所以,到目前为止,还没有找到方法获取指纹的详细信息,如果有朋友找到可行方法,欢迎交流。

最后,再次附上项目完整Demo。

你可能感兴趣的:(android,指纹,加解密)