指纹识别相关api
FingerprintManagaerCompat
指纹识别的核心包装类
初始化方法
FingerprintManager fingerprintManager = FingerprintManagerCompat.from(context);
用于判断指纹录入情况的方法
//判断指纹识别的硬件是否存在
fingerprintManager.isHardwareDetected();
//判断用户是否录入了指纹
fingerprintManager.hasEnrolledFingerprints();
发起指纹验证
fingerprintManager.authenticate(FingerprintManagerCompat.CryptoObject crypto,
int flags,
CancellationSignal cancel,
FingerprintManagerCompat.AuthenticationCallback callback, Handler handler)
# 参数详解
FingerprintManagerCompat.CryptoObject
//这是一个加密类的包装类,在java中加密所用的类通是Cipher,Signature,Mac
flags
//官方文档指明这是一个optinal,并且这里should be 0 也就是必须是0
CancellationSignal
//这是一个信号类,直接new CancellationSignal()创建一个新对象,然后通过这个对象可以取消指纹扫描等操作
FingerprintManagerCompat.AuthenticationCallback callback
//这是指纹识别结果的监听类,所有的结果处理都将在这个类中
Handler handler
//处理结果用的handler,通常只是用来告知在哪个线程中返回结果,一般设置为null,因为默认会在主线程中回调扫描结果。
FingerprintManagerCompat.CryptoObject
构造方法
FingerprintManagerCompat.CryptoObject(Signature signature)
FingerprintManagerCompat.CryptoObject(Cipher cipher)
FingerprintManagerCompat.CryptoObject(Mac mac)
//三个构造方法分别传入mac,signature,Cipher
FingerprintManagerCompat.AuthenticationCallback
void onAuthenticationError(int errMsgId, CharSequence errString)
//指纹扫描出错,这个错误是指打开指纹扫描失败或者扫描中断等系统错误,无法补救
void onAuthenticationFailed()
//这个错误是指扫描过程完成,但是指纹不匹配
void onAuthenticationHelp(int helpMsgId, CharSequence helpString)
//这是当开启指纹识别时遇到的可补救的错误时提示用户做相应操作的方法,例如你的指纹识别传感器有灰尘等赃物,helpString将会提示你进行清理
void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result)
//指纹识别成功的回调,result是一个包装了CryptoObject对象的结果对象
指纹识别编码示例
- 首先我们需要创建一个
Cipher
(也可以是Signature
或者是Mac
)
创建一个工具类用于获取
cipher
,由于这里指定的加密方式是AES
+CBC
模式,CBC
加密方式的Cipher会创建一个初始向量byte[]数组
,因此在获取相应的解密Cipher时,要把这个byte[]
数据传递进去。
@RequiresApi(api = Build.VERSION_CODES.M)
public class KeyGenTool {
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
private static final String KEY_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
private static final String KEY_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;
private static final String TRANSFORMATION = KEY_ALGORITHM + "/" + KEY_BLOCK_MODE + "/" + KEY_PADDING;
private final String KEY_ALIAS;
public KeyGenTool(Context context) {
KEY_ALIAS = context.getPackageName()+"fingerprint";
}
public Cipher getEncryptCipher() {
Cipher cipher = null;
try {
cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, getKey());
} catch (Exception e) {
e.printStackTrace();
}
return cipher;
}
/**
* 获取解密的cipher
*
* @param initializeVector 加密cipher的一些参数
* 包括initialize vector(AES加密中 以CBC模式加密需要一个初始的数据块,解密时同样需要这个初始块)
* @return
*/
public Cipher getDecryptCipher(byte[] initializeVector) {
Cipher cipher = null;
try {
cipher = Cipher.getInstance(TRANSFORMATION);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializeVector);
cipher.init(Cipher.DECRYPT_MODE, getKey(), ivParameterSpec);
} catch (Exception e) {
e.printStackTrace();
}
return cipher;
}
/**
* 获取key,首先从秘钥库中根据别名获取key,如果秘钥库中不存在,则创建一个key,并存入秘钥库中
* @return
* @throws Exception
*/
private SecretKey getKey() throws Exception {
SecretKey secretKey = null;
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
if (keyStore.isKeyEntry(KEY_ALIAS)) {
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
secretKey = secretKeyEntry.getSecretKey();
} else {
secretKey = createKey();
}
return secretKey;
}
/**
* 在Android中,key的创建之后必须存储在秘钥库才能使用
* @return
* @throws Exception
*/
private SecretKey createKey() throws Exception {
//在创建KeyGenerator的时候,第二个参数指定provider为AndroidKeyStore,这样创建的key就会被存放在这个秘钥库中
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, ANDROID_KEY_STORE);
KeyGenParameterSpec spec = new KeyGenParameterSpec
.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KEY_BLOCK_MODE)
.setEncryptionPaddings(KEY_PADDING)
//这个设置为true,表示这个key必须是通过了用户认证才可以使用
.setUserAuthenticationRequired(true)
.build();
keyGenerator.init(spec);
return keyGenerator.generateKey();
}
}
- 权限
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
- 创建FingerprintManager并开启加密验证
这里加密的字符串为简单的
helloworld
,并且将加密以后的字节数组通过base64
转换成字符串保存在SharePreference
中
这里不单保存了加密过的字符串,还保存了
@RequiresApi(api = Build.VERSION_CODES.M)
public void encrypt(View v) {
mCancelSignal = new CancellationSignal();
FingerprintManagerCompat.CryptoObject object = new FingerprintManagerCompat.CryptoObject(mKeyGenTool.getEncryptCipher());
mFingerprintManagerCompat.authenticate(object, 0, mCancelSignal
, new FingerprintManagerCompat.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
super.onAuthenticationError(errMsgId, errString);
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
super.onAuthenticationHelp(helpMsgId, helpString);
}
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
try {
Cipher cipher = result.getCryptoObject().getCipher();
byte[] bytes = cipher.doFinal("helloworld".getBytes());
//保存加密过后的字符串
SharePreferenceTool.getInstance().saveStr("hello", Base64.encodeToString(bytes,Base64.URL_SAFE));
//保存用于做AES-CBC加密变换的初始向量数组
byte[] iv = cipher.getIV();
SharePreferenceTool.getInstance().saveStr("iv", Base64.encodeToString(iv,Base64.URL_SAFE));
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
},null);
}
- 解密
@RequiresApi(api = Build.VERSION_CODES.M)
public void decrypt(View v) {
//保存用于做AES-CBC
String ivStr = SharePreferenceTool.getInstance().getStr("iv");
byte[] iv = Base64.decode(ivStr, Base64.URL_SAFE);
mCancelSignal = new CancellationSignal();
FingerprintManagerCompat.CryptoObject object = new FingerprintManagerCompat.CryptoObject(mKeyGenTool.getDecryptCipher(iv));
mFingerprintManagerCompat.authenticate(object, 0, mCancelSignal
, new FingerprintManagerCompat.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
super.onAuthenticationError(errMsgId, errString);
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
super.onAuthenticationHelp(helpMsgId, helpString);
}
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
try {
Cipher cipher = result.getCryptoObject().getCipher();
String text = SharePreferenceTool.getInstance().getStr("hello");
byte[] input = Base64.decode(text, Base64.URL_SAFE);
byte[] bytes = cipher.doFinal(input);
//保存加密过后的字符串
Log.d("TestFingerprintActivity", "解密的字符串为: " + new String(bytes));
SharePreferenceTool.getInstance().saveStr("hello", text);
byte[] iv = cipher.getIV();
Log.d("TestFingerprintActivity", "iv:" + Base64.encodeToString(iv, Base64.URL_SAFE));
//保存用于做AES-CBC
Toast.makeText(TestFingerprintActivity.this, "解密成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
}, null);
/* try {
String text = SharePreferenceTool.getInstance().getStr("hello");
byte[] input = Base64.decode(text, Base64.URL_SAFE);
//保存用于做AES-CBC
String ivStr = SharePreferenceTool.getInstance().getStr("iv");
byte[] iv = Base64.decode(ivStr, Base64.URL_SAFE);
Cipher cipher = mKeyGenTool.getDecryptCipher(iv);
byte[] output = cipher.doFinal(input);
Log.d("TestFingerprintActivity", "加密后的字符串为: " + new String(output));
Toast.makeText(this, "解密成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}*/
}
解密操作将保存的
初始化向量
提供给keyGenTool
创建出一个解密用的Cipher
,然后发起指纹验证,当指纹验证成功之后取出Cipher
进行解密,注意到底下注释掉的一行代码。
指纹验证如何保证安全性
- 为什么Cipher需要包装传递给
authenticate()
方法
Cipher
传递给指纹验证方法,再取出来做加密解密,和直接用Cipher
加密解密有什么区别呢?问题的关键还是在创建的Key
上,创建keyGenerator
时,有一个方法setUserAuthenticationRequired(true)
,也就是说这个key秘钥必须用户验证了才可以使用的,所以使用这种key初始化的的Cipher
如果直接用于加密或者解密,会报出错误W/System.err: javax.crypto.IllegalBlockSizeException
,仔细查看错误栈信息会发现它是由于android.security.KeyStoreException: Key user not authenticated
这个错误引起的。而当我们将Cipher
包装传递给指纹验证方法时,其内部验证了用户的身份,也就解除了Cipher
中的key的使用限制,因此在回调方法中就可以使用该Cipher
来加解密了。
- 如何保存相关信息以配合指纹验证身份
指纹验证只是一个认证用户身份的方式,由于用于加密解密的实际操作其实是委托给
CryptoObject
内部的Cipher
,因此主要的工作还是在Cipher
的处理上,以指纹支付为例,加密方式是AES-CBC的对称加密:
- 首先是是加密,当用户选择开通指纹验证登录时,首先会要求用户输入支付密码,接下来代码中初始化一个
Cipher
用于加密操作,接着要求用户验证指纹,指纹验证成功后将支付密码通过Cipher
加密并将做Base64
转换成字符串,同时将Cipher
的初始向量byte[] iv
一起转换成字符串,将这两个字符串存储到服务器。同时,用于初始化Cipher
的key
保存在Android内部秘钥库AndroidKeyStore
中,外部应用程序无法获取。 - 当用户发起支付时,首先从服务器获取
之前加密过的密码字符串和初始向量字符串
,同时从本地秘钥库AndroidKeyStore
中通过alias别名
取出之前存储的key
,用初始向量
和key
初始化一个Cipher
,接下来发起指纹支付,指纹验证通过后便可用这个Cipher
解密字符串获取真正的支付密码
,然后传递给服务器验证。