最近在做区块链 以太坊钱包 项目,为了保证私钥的安全性,需要提供几种保存验证方式,常用的保存方式有如:助记词,KeyStore文件,私钥文件,二维码。验证方式可以用助记词,手势识别,指纹识别,脸部识别等。
接下来我们来讲解以下指纹识别这个功能
首先介绍一下 Android 程序硬件访问机制:
在Android 系统中,应用程序位于最上层,界面是用Java或者Kotlin编写的,Java 编写的应用程序都运行在Android 特有的虚拟机中。Android 系统是基于 Linux 内核构建,Linux 设备驱动程序是使用C语言编写,且运行在Linux 内核空间。用户访问硬件的方法是通过基于C库的系统调用,调用内核空间的设备驱动程序,从而访问到硬件,所以使用 Java语言开发的应用程序是无法访问硬件的。为了解决这个问题,在Android系统中提供了硬件抽象层(HAL)来解决这个问题,硬件抽象层运行在用户空间,并且使用C/C++编写,它向下屏蔽了硬件驱动模块的实现细节,向上踢动了硬件访问服务。
指纹识别这个词听起来应该都很熟悉了,现在新出的手机这个功能基本是必备的。指纹识别是Google从Android 6.0开始新增的,并提供了官方文档以及对外调用的接口。
因为是从6.0开始支持,所以在使用的时候需要先判断手机的系统版本是否支持指纹识别。另外,在实际的开发场景中,使用指纹的主要场景有两种。
由于使用指纹识别功能需要一个加密对象(CrytoObject),该对象一般是由 对称加密 或者 非对称加密 获得。
以上两种场景的实现大同小异,主要区别在于加密过程中秘钥的创建和使用,一般来说:
纯本地使用,只需要对称加密即可;
与后台交互,需要使用对称加密,将私钥用于本地指纹识别,识别成功后将加密信息传给后台,后台开发人员用公钥解密,以获得用户信息。
在使用指纹识别之前,先了解一下什么是对称加密和非对称加密。
对称加密:采用一把秘钥,双方使用同样的秘钥进行加密和解密。秘钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密。因此加密的安全性不仅取决于加密算法本身,秘钥管理的安全性更是重要。因为加密和解密都使用同一把秘钥,如何把秘钥安全地传递到解密这手上就成了必须要解决的问题。
特点:速度快,适合于本地数据和本地数据的加密,安全性不如非对称加密。常见的对称加密算法有DES、3DES、AES、Blowfish、IDEA、RC5、RC6。
非对称加密:采用两把秘钥,公钥和私钥。公钥和私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。如果用私钥对数据进行加密,只有用对应的公钥进行解密。这种加密和解密使用的是不同的秘钥,就叫非对称加密。非对称加密实现机密数据交换的基本过程:A 随机生成一对秘钥,将其中的公钥告诉B,B得到公钥后使用公钥对机密数据后再发送给A,A收到机密数据后用另一把私钥解密。
特点:安全性比较高,适合对需要网络传输的数据进行加密,速度不如对称加密。非对称加密应用于SSH、HTTPS、TLS、电子证书、电子签名、电子身份证等。
签名:在数据的后面再加一段内容,可以证明信息没有被篡改过。一般是对数据做一个hash计算得到一个hash值,注意,这个过程是不可逆的,也就是说无法通过hash值得到原来的数据内容。在把数据发出去时,把这个hash值加密后作为一个签名和数据一起发出去。
Generator 发电机
Authentication 认证
使用指纹识别的对称加密功能的主要流程如下:
1.使用KeyGenerator 创建一个对称秘钥,存放在KeyStore里。
2.设置KeyGenParameterSpec.Builder.setUserAuthenticationRequired()为true。
3.使用创建好的对称秘钥初始化一个Cipher对象,并用该对象调用 FingerprintManager.authenticate() 方法启动指纹传感器并开始监听。
4.重写FingerprintManager.AuthenticationCallback的几个回调方法,以处理指纹识别成功、失败等情况。
创建秘钥 :
创建秘钥要涉及到三个类:KeyGenerator(秘钥发电机)、KeyStore(俗称秘钥商店)和 Cipher(加密规则)
KeyGenerator:产生秘钥
KeyStore:存放获取秘钥 (使用 FingerprintManager+Android KeyStore 方式存储密码)
Cipher:是一个按照一定的加密规则,将数据进行加密后的一个对象
产生秘钥:
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); // 创建KeyGenerator对象
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder("key_name", 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.generateKey(keyGenerator.init(builder.build());); // 生成key
KeyStore存放获取秘钥:
KeyStore mKeyStore = KeyStore.getInstance("AndroidKeyStore");
创建并初始化 Cipher 对象
SecretKey key = (SecretKey) mKeyStore.getKey("key", null);
if (key == null) return;
Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
cipher.init(Cipher.ENCRYPT_MODE, key);
上面几个参数对象就已经创建完成,使用指纹识别时,只需要调用FingerprintManager.authenticate(),系统就会启动指纹传感器,将结果反馈给回调方法。
在完成指纹验证之后,关闭指纹验证,调用 cancel()然后设置为null 就好。
非对称机密的步骤和对称加密算法基本是一样的:
KeyPairGenerator 产生非对称秘钥
Cipher 是通过创建好的私钥进行签名来产生的
创建KeyPairGenerator 对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "'AndroidKeyStore'");
产生秘钥对
keyPairGenerator.initialize(new KeyGenParameterSpec.Builder("key", KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
.setUserAuthenticationRequired(true)
.build());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
使用私钥签名产生Cipher
mKeyStore.load(null);
Signature signature = Signature.getInstance("algorithm");
PrivateKey key = (PrivateKey) mKeyStore.getKey("key_name",null);
signature.initSign(key);
其他流程和对称加密后是一样的。
只需要使用 FingerprintManager.authenticate() 方法启动指纹传感器并开始监听,完成后关闭就可以。
开始:
1.声明权限:
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
2.获得FingerprintManager对象引用:
两种方式:
方式一:直接获取FingerprintManager,这是必须在API 23才有的,需要判断API >= 23。
FingerprintManager manager = getSystemService(Context.FINGERPRINT_SERVICE);
方式二:采用V4兼容包,获取FingerprintManagerCompat,这是Google推荐使用的。
FingerprintManagerCompat compat = FingerprintManagerCompat.from(this);
3.检查运行条件:
Android 6.0才开始支持指纹功能,现在很多5.0的手机也支持指纹功能,但是开发者获取不到FingerprintManager,而是使用Google官方v4兼容包,FingerprintManager改为了FingerprintManagerCompat,官方也推荐使用v兼容包。
if(finerprintManager.isHardwareDetected){
}
if(finerprintManager.isKeyguardSecure){
}
if(finerprintManager.hasEnrolledFingerprints()){
}
指纹识别Google官方文档,官方标准库:
在authenticate方法中,介绍一下这些参数
1.cryto:这是一个FingerprintManager.CrytoObject加密类的对象,指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是null,但是这样的话,就意味着app无条件的信任认证的结果,虽然理论上这个过程可能被攻击,数据可以被篡改,但这是app在这种情况下必须承担的风险。因此,建议这个参数不要置为null。这个类的实例化有点麻烦,主要使用javax的security接口实现。
2.cancel:这个是CancellatioinSignal类的一个对象,这个对象用来在指纹识别器扫描用户指纹,这个对象用来在指纹识别器扫描用户指纹的时候取消扫描操作,如果不取消的话,指纹扫描器会一直扫描到超时(一般为30s,取决于具体的厂商实现),这样的话就会比较耗电,建议这个参数不要置为null。
3.flags:标志位,暂时为0,将来备用。
4.callback:这个是FingerprintManager.AuthenticatioinCallback类的对象,这个是这个接口中除第一个参数之外的最重要的参数了。当系统完成了指纹认证过程(成功或失败都会)后,会回调这个对象的接口,通知App认证的结果,这个参数不能为null。
5.hander:这个是Handler类的对象,如果这个参数不为null的话,那FingerprintManager将会使用这个handler中的looper来处理来自指纹识别硬件的消息。通常来讲,开发不提供这个参数,可以直接为null,因为FingerprintManager会默认使用App的Main Looper来处理。
上面我们分析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。
CryptoObjectHelper cryptoObjectHelper = new CryptoObjectHelper();
fingerprintManager.authenticate(cryptoObjectHelper.buildCryptoObject(), 0,
cancellationSignal, myAuthCallback, null);
使用是比较简单的,首先new一个CryptoObjectHelper对象,然后调用buildCryptoObject方法就能得到CryptoObject对象了。
前面我们分析authenticate接口的时候说道,调用这个接口的时候必须提供FingerprintManager.AuthenticationCallback类的对象,这个对象会在指纹认证结束之后系统回调以通知App认证的结果的。在Android 6.0中,指纹的扫描和认证都是在另外一个进程中完成(指纹系统服务)的,因此底层什么时候能够完成认证我们app是不能假设的。因此,我们只能采取异步的操作方式,也就是当系统底层完成的时候主动通知我们,通知的方式就是通过回调我们自己实现的FingerprintManager.AuthenticationCallback类,这个类中定义了一些回调方法以供我们进行必要的处理:
下面我们简要介绍一下这些接口的含义:
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点需要补充一下:
1. 上面我们说道OnAuthenticationError 和 OnAuthenticationHelp方法中会有错误或者帮助码以提示为什么认证不成功。Android系统定义了几个错误和帮助码在FingerprintManager类中,我们的callback类实现的时候最好需要处理这些错误和帮助码。
2. 当指纹扫描器正在工作的时候,如果我们取消本次操作的话,系统也会回调OnAuthenticationError方法的,只是这个时候的错误码是FingerprintManager.FINGERPRINT_ERROR_CANCELED(值为5),因此app需要区别对待。
Android 6.0上的指纹识别开发的几个要点:
1. 建议使用Android Support Library v4 Compatibility API,不要使用直接framework中的api。
2. 在使用指纹硬件之前一定要检查上面提到的几个检查条件 。
3. 根据google的建议最好使用google提供的指纹是被icon来标示你的指纹识别界面,这个做的目的就是为了很明确地提示用户这是一个指纹识别操作,就像人们看到蓝牙的那个小标识就知道这是蓝牙操作一样。当然,这只是google的一个实践性的建议,并非强制。
4. App需要及时通知用户当前的操作以及操作的结果,比如需要明确告诉用户当前正在扫描指纹,请把你的指纹放在传感器上等。
5. 最后需要注意的就是Android Support Library v4中的FingerprintManager类名字是FingerprintManagerCompat,并且他们的authenticate方法参数顺序不一样,flags和cancel的位置在两个类中是不一样的,这一点需要注意。
参考文章
Android开发学习—指纹识别系统的原理与使用
Android中的指纹识别
Android 6.0指纹识别App开发demo
Android指纹识别深入浅出分析到实战(6.0以下系统适配方案)
java安全之加密技术
Android KeyStore + FingerprintManager 存储密码