Google官方从Android6.0(API 23)开始支持指纹功能。
指纹功能常用于用户屏幕解锁、登陆验证、支付验证,很方便。
Google官方也给出了指纹的demo,但是官方demo太复杂了,下图左。
网上关于指纹的资料也不是很完整,所有我决定自己写一篇总结指纹功能的文章,Demo效果如下图右。
使用指纹功能,需要添加指纹权限,指纹权限为普通权限,在AndroidManifest.xml文件中添加后即可使用,不需要使用过程中动态申请:
开启指纹识别,我们需要用到FingerprintManager类,FingerprintManager实例的获取,和ActivityManager、WindowManager这些Manager一样,通过Context的getSystemService方法获取。
fingerprintManager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE);
在FingerprintManager类中,调用authenticate方法,就能打开指纹传感器。
在FingerprintManager中,有2个重载authenticate方法:
这2个方法中,有一个为hide,能直接调用的方法,如下图所示:
此方法有5个参数:
CryptoObject:会在接下来的内容详细讲解,这里先传null;
CancellationSignal:new一个对象即可;
int:传入0;
Handler:传入null。
所以,我们现在只需要关注一个参数AuthenticationCallback:
这是一个抽象类,有5个方法,其中onAuthenticationAcquired为hide方法,所以需要重写4个方法。
指纹功能封装在FingerSimpleUtil.java类中,并实现了AuthenticationCallback接口。
FingerSimpleUtil类怎么用呢?
通过这3步,就能开始指纹识别了,识别结果会回传到第2步设置的回调中。
并不是所有手机都支持指纹功能,也不是所有Android版本都支持指纹功能,所以应用中不能直接使用指纹功能,使用之前要进行一系列的判断:
使用指纹功能,系统会要求用户添加备用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对象呢?
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,代码如下:
/**
* 初始化{@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对象,它的源码如图所示:
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方法描述如下图所示:
描述第一段和第二段内容,大意为:
这就是检测指纹变化的依据,有3个相关值:
所以需要对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。