转载请注明出处:http://blog.csdn.net/wl9739/article/details/52444671
评论中很多朋友反映,根据我给出的方案,拿不到指纹信息这个问题,在这里统一说明一下。
首先,这篇文章中涉及到的代码,我在一部魅族手机和一部三星手机上进行测试过,能获取到信息。其他手机机型我没有测试,不知道详细情况。
其次,我在博客中也说明了,在不同手机厂商的定制系统里面获取到的指纹信息很可能是不同的,我测试的魅族手机和三星手机返回的信息格式就不一样。按照本文的方法获取到的指纹信息是一个比较鸡肋的功能,作用有限,我目前也没有能力给出一个完美的解决方案,如果有哪位找到了完美的解决方案,欢迎分享。
另外,指纹信息相关 API 是用 @hide 修饰的,这说明 Google 官方是不希望开发者使用这类 API ,使用这类 API 可能会有风险,如方法参数改变、在后续版本中被移除等等。
因此,对于指纹识别这一块,建议读者使用本文中提及的指纹识别功能,尽可能避免使用本文后面提及的“获取指纹信息”这一功能。
最近项目需要使用到指纹识别的功能,查阅了相关资料后,整理成此文。
指纹识别是在Android 6.0之后新增的功能,因此在使用的时候需要先判断用户手机的系统版本是否支持指纹识别。另外,实际开发场景中,使用指纹的主要场景有两种:
由于使用指纹识别功能需要一个加密对象(CryptoObject)该对象一般是由对称加密或者非对称加密获得。上述两种开发场景的实现大同小异,主要区别在于加密过程中密钥的创建和使用,一般来说,纯本地的使用指纹识别功能,只需要对称加密即可;而与后台交互则需要使用非对称加密:将私钥用于本地指纹识别,识别成功后将加密信息传给后台,后台开发人员用公钥解密,以获得用户信息。
下面先简单介绍一下对称加密和非对称加密的相关概念,然后对两种开发方式的实现分别进行讲解。
在正式使用指纹识别功能之前,有必要先了解一下对称加密和非对称加密的相关内容。
对称加密:所谓对称,就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密。因此加密的安全性不仅取决于加密算法本身,密钥管理的安全性更是重要。因为加密和解密都使用同一个密钥,如何把密钥安全地传递到解密者手上就成了必须要解决的问题。
非对称加密:非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。
签名:在信息的后面再加上一段内容,可以证明信息没有被修改过。一般是对信息做一个hash计算得到一个hash值,注意,这个过程是不可逆的,也就是说无法通过hash值得出原来的信息内容。在把信息发送出去时,把这个hash值加密后做为一个签名和信息一起发出去。
由以上内容可以了解到,对称加密和非对称加密的特点如下:
使用指纹识别的对称加密功能的主要流程如下:
KeyGenParameterSpec.Builder.setUserAuthenticationRequired()
为true,FingerprintManager.authenticate()
方法启动指纹传感器并开始监听。FingerprintManager.AuthenticationCallback
的几个回调方法,以处理指纹识别成功(onAuthenticationSucceeded()
)、失败(onAuthenticationFailed()
和 onAuthenticationError()
)等情况。创建密钥要涉及到两个类:KeyStore 和 KeyGenerator。
KeyStore 是用于存储、获取密钥(Key)的容器,获取 KeyStore的方法如下:
try {
mKeyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
throw new RuntimeException("Failed to get an instance of KeyStore", e);
}
而生成 Key,如果是对称加密,就需要 KeyGenerator 类。获取一个 KeyGenerator 对象比较简单,方法如下:
// 对称加密, 创建 KeyGenerator 对象
try {
mKeyGenerator = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
}
获得 KeyGenerator 对象后,就可以生成一个 Key 了:
try {
keyStore.load(null);
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(defaultKeyName,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(true);
}
keyGenerator.init(builder.build());
keyGenerator.generateKey();
} catch (CertificateException | NoSuchAlgorithmException | IOException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
关于 KeyStrore 和 KeyGenerator 的相关介绍,推荐阅读:Android KeyStore + FingerprintManager 存储密码
Cipher 对象是一个按照一定的加密规则,将数据进行加密后的一个对象。调用指纹识别功能需要使用到这个对象。创建 Cipher 对象很简单,如同下面代码那样:
Cipher defaultCipher;
try {
defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException("创建Cipher对象失败", e);
}
然后使用刚才创建好的密钥,初始化 Cipher 对象:
try {
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
cipher.init(Cipher.ENCRYPT_MODE, key);
return true;
} catch (IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyStoreException | InvalidKeyException e) {
throw new RuntimeException("初始化 cipher 失败", e);
}
真正到了使用指纹识别功能的时候,你会发现其实很简单,只是调用 FingerprintManager 类的的方法authenticate()而已,然后系统会有相应的回调反馈给我们,该方法如下:
public void authenticate(CryptoObject crypto, CancellationSignal cancel, int flags, AuthenticationCallback callback, Handler handler)
该方法的几个参数解释如下:
new FingerprintManager.CryptoObject(cipher)
。new CancellationSignal()
就可以了。完成指纹识别后,还要记得将 AuthenticationCallback 关闭掉:
public void stopListening() {
if (cancellationSignal != null) {
selfCancelled = true;
cancellationSignal.cancel();
cancellationSignal = null;
}
}
调用了 authenticate()
方法后,系统就会启动指纹传感器,并开始扫描。这时候根据扫描结果,会通过FingerprintManager.AuthenticationCallback类返回几个回调方法:
// 成功
onAuthenticationSucceeded()
// 失败
onAuthenticationFaile()
// 错误
onAuthenticationError()
一般我们需要重写这几个方法,以实现我们的功能。关于onAuthenticationFaile()
和onAuthenticationError()
的区别,后面会讲到。
其实流程和上面的流程差不多:
FingerprintManager.authenticate()
方法的一个参数,启动指纹传感器并开始监听。FingerprintManager.AuthenticationCallback
类的几个回调方法,以处理指纹识别成功(onAuthenticationSucceeded()
)、失败(onAuthenticationFailed()
和 onAuthenticationError()
)等情况。可以看见,指纹识别的非对称加密方式和对称加密方式的实现流程是差不多的,它们之间最明显的差别是在于密钥的生成与使用。
这里要使用 KeyPairGenerator 来创建一组非对称密钥,首先是获取 KeyPairGenerator 对象:
// 非对称加密,创建 KeyPairGenerator 对象
try {
mKeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e);
}
得到了 KeyPairGenerator 对象后,就可以创建 KeyPair(密钥对)了:
try {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
mKeyPairGenerator.initialize(
new KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
// Require the user to authenticate with a fingerprint to authorize
// every use of the private key
.setUserAuthenticationRequired(true)
.build());
mKeyPairGenerator.generateKeyPair();
} catch (InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
指纹识别的对称加密实现中使用了Cipher对象来创建CryptoObject对象,而在这里,我们将会使用私钥进行签名,用签名对象来创建CryptoObject对象:
// 使用私钥签名
try {
mKeyStore.load(null);
PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
mSignature.initSign(key);
return true;
} catch (KeyPermanentlyInvalidatedException e) {
return false;
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
| NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Failed to init Cipher", e);
}
同样的,调用new FingerprintManager.CryptoObject(mSignature)
方法创建一个CryptoObject对象。
这里的使用方法和前面“指纹识别的对称加密实现”中的调用方法是一样的,都是调用FingerprintManager.authenticate()
方法。这里就不再叙述。
监听回调也和之前的类似,唯一不同的是,我们在识别成功后需要和后台进行交互,也就是onAuthenticationSucceeded()
中处理的逻辑不一样。
一般来说,为了增加安全性,要求用户在手机的“设置”中开启了密码锁屏功能。当然,使用指纹解锁的前提是至少录入了一个指纹。
// 如果没有设置密码锁屏,则不能使用指纹识别
if (!keyguardManager.isKeyguardSecure()) {
Toast.makeText(this, "请在设置界面开启密码锁屏功能",
Toast.LENGTH_LONG).show();
}
// 如果没有录入指纹,则不能使用指纹识别
if (!fingerprintManager.hasEnrolledFingerprints()) {
Toast.makeText(this, "您还没有录入指纹, 请在设置界面录入至少一个指纹",
Toast.LENGTH_LONG).show();
}
这里用到了两个类:KeyguardManager 和 FingerprintManager,前者是屏幕保护的相关类。后者是指纹识别的核心类。
前面说到AuthenticationCallback类里面的几个回调方法,其中有三个是我们开发中需要用到的:
onAuthenticationError()
onAuthenticationSucceeded()
onAuthenticationFailed()
关于这三个回调方法,有几点需要注意的:
当指纹识别失败后,会调用onAuthenticationFailed()
方法,这时候指纹传感器并没有关闭,系统给我们提供了5次重试机会,也就是说,连续调用了5次onAuthenticationFailed()
方法后,会调用onAuthenticationError()
方法。
当系统调用了onAuthenticationError()
和onAuthenticationSucceeded()
后,传感器会关闭,只有我们重新授权,再次调用authenticate()
方法后才能继续使用指纹识别功能。
当系统回调了onAuthenticationError()
方法关闭传感器后,这种情况下再次调用authenticate()
会有一段时间的禁用期,也就是说这段时间里是无法再次使用指纹识别的。当然,具体的禁用时间由手机厂商的系统不同而有略微差别,有的是1分钟,有的是30秒等等。而且,由于手机厂商的系统区别,有些系统上调用了onAuthenticationError()
后,在禁用时间内,其他APP里面的指纹识别功能也无法使用,甚至系统的指纹解锁功能也无法使用。而有的系统上,在禁用时间内调用其他APP的指纹解锁功能,或者系统的指纹解锁功能,就能立即重置指纹识别功能。
最后, Android Sample 里面关于指纹的示例代码地址如下:
对称加密方式:android-FingerprintDialog。
非对称加密方式:android-AsymmetricFingerprintDialog
以下内容更新于 2017.6.5
越来越多的朋友开始关心指纹识别这一功能模块,并且通过各种渠道向我咨询一些关于指纹识别的需求解决方案。这里就统一说明一下,就不一一回复了。
前面已经说到了,监听指纹识别成功之后会有一个 onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result)
回调方法。该方法会给我们一个 AuthenticationResult
类的对象 result
,该类的源码如下:
public static class AuthenticationResult {
private Fingerprint mFingerprint;
private CryptoObject mCryptoObject;
/**
* Authentication result
*
* @param crypto the crypto object
* @param fingerprint the recognized fingerprint data, if allowed.
* @hide
*/
public AuthenticationResult(CryptoObject crypto, Fingerprint fingerprint) {
mCryptoObject = crypto;
mFingerprint = fingerprint;
}
/**
* Obtain the crypto object associated with this transaction
* @return crypto object provided to {@link FingerprintManager#authenticate(CryptoObject,
* CancellationSignal, int, AuthenticationCallback, Handler)}.
*/
public CryptoObject getCryptoObject() { return mCryptoObject; }
/**
* Obtain the Fingerprint associated with this operation. Applications are strongly
* discouraged from associating specific fingers with specific applications or operations.
*
* @hide
*/
public Fingerprint getFingerprint() { return mFingerprint; }
};
这个类里面包含了一个 Fingerprint
对象,如果我们查看 Fingerprint
类的源码,可以得知该类提供了指纹的一些属性,包括指纹的名称、GroupId、FingerId 和 DeviceId 等属性。也就是说,通过 onAuthenticationSucceeded()
回调方法,我们可以得到识别的指纹的一些信息。
那为什么我们之前不使用该返回给我们的 AuthenticationResult
类对象 result
呢?因为我们没办法通过正常途径获取到这些信息。因为 mFingerprint
属性是私有的,getFingerprint()
方法是被 @hide
修饰的,甚至连存储指纹信息的 Fingerprint
类也是被 @hide
修饰的。
在 Android SDK 中,有两种 API 是我们无法直接获取的,一种是位于包 com.android.internal
下面的 API,另一种是被 @hide
修饰的类和方法。
然而,针对第二种被隐藏的 API,我们可以通过反射的方式来调用相关的类和方法。以 onAuthenticationSucceeded()
回调方法为例,看看如何获取识别成功的指纹信息:
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
try {
Field field = result.getClass().getDeclaredField("mFingerprint");
field.setAccessible(true);
Object fingerPrint = field.get(result);
Class> clzz = Class.forName("android.hardware.fingerprint.Fingerprint");
Method getName = clzz.getDeclaredMethod("getName");
Method getFingerId = clzz.getDeclaredMethod("getFingerId");
Method getGroupId = clzz.getDeclaredMethod("getGroupId");
Method getDeviceId = clzz.getDeclaredMethod("getDeviceId");
CharSequence name = (CharSequence) getName.invoke(fingerPrint);
int fingerId = (int) getFingerId.invoke(fingerPrint);
int groupId = (int) getGroupId.invoke(fingerPrint);
long deviceId = (long) getDeviceId.invoke(fingerPrint);
Log.d(TAG, "name: " + name);
Log.d(TAG, "fingerId: " + fingerId);
Log.d(TAG, "groupId: " + groupId);
Log.d(TAG, "deviceId: " + deviceId);
} catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
显示的结果如下:
name:
fingerId: 1765296462
groupId: 0
deviceId: 547946660160
其中,返回的 name 为空字符串。
这里要提醒一点,由于每个手机厂商都会对 Android 系统进行或多或少的定制,因此在不同的手机上调用上面的方法得到的结果可能会不一样。比如在我的手机上,fingerId
是一个十位数的整数,在其他手机上面可能就是个一位数的整数。
得到了指纹信息之后,我们就可以做一些额外的功能了,比如监听用户是否使用同一个指纹来解锁,就可以用上面的方法来判断。
除了在 onAuthenticationSucceeded()
回调方法中获取被识别的指纹信息外,还可以利用 FingerprintManager
类的 getEnrolledFingerprints()
方法来获取手机中存储的指纹列表:
public void getFingerprintInfo() {
try {
FingerprintManager fingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE);
Method method = FingerprintManager.class.getDeclaredMethod("getEnrolledFingerprints");
Object obj = method.invoke(fingerprintManager);
if (obj != null) {
Class> clazz = Class.forName("android.hardware.fingerprint.Fingerprint");
Method getFingerId = clazz.getDeclaredMethod("getFingerId");
for (int i = 0; i < ((List) obj).size(); i++) {
Object item = ((List) obj).get(i);
if (null == item) {
continue;
}
Log.d(TAG, "fingerId: " + getFingerId.invoke(item));
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
e.printStackTrace();
}
}
在一些场景中,需要对用户手机里面录入的指纹进行监听,当用户手机里面的指纹列表发生了变化(比如用户删除了指纹或者新增了指纹),就需要用户重新输入密码,这时就可以用上面的方法,记录用户手机里面的指纹 ID,并进行前后对比。(当然,这种做法的安全系数并不高,也难以兼容众多设备,这里只是举例说明用途)
参考链接:New in Android Samples: Authenticating to remote servers using the Fingerprint API