Android 指纹生物识别 UserNotAuthenticatedException
Android biometrics UserNotAuthenticatedException on fingerprint
我在示例应用程序中使用 SecretKey 进行 Mac 签名。密钥是使用构建器参数生成的
setUserAuthenticationValidityDurationSeconds(10)
允许使用指纹和(解锁)设备 PIN 来保护我的密钥。
UI 仅包含一个用于启动签名的 SIGN 按钮。
如果我通过“使用 PIN”使用生物识别提示,输入 PIN 后我会收到签名
(在我的示例“2BjEuSxl/bOTTUExE4vTX2rnRZEC1Zfa21FooKkBfnc=”中)[注意:预期行为]。
在给定的 10 秒“ValidityDuration”时间内再次按下 SIGN 按钮,我可以成功使用指纹进行授权[注意:预期行为]。
10秒后按SIGN键,使用指纹授权
[或在没有事先使用 PIN 的情况下使用指纹] 给出了一个例外 [注意: 不是 预期的行为]:
android.security.keystore.UserNotAuthenticatedException: User not authenticated
所以我的问题是:如何使用 same SecretKey 来授权签名过程(或者更好地从 AndroidKeystore) 与 PIN 和指纹选项 ?
我正在 Android SDK 30(目标)和(最低)23 上进行测试,生物识别功能可用于实现 'androidx.biometric:biometric:1.1.0'。
下面是 Logcat 调试输出以及我这边的一些评论:
>>> first start
2021-07-04 14:23:47.178 6980-6980/de.biometrics D/*** Biometric ***: sign started
2021-07-04 14:23:47.181 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:23:47.211 6980-6980/de.biometrics D/*** Biometric ***: generated fresh key, try to load
2021-07-04 14:23:47.223 6980-6980/de.biometrics D/*** Biometric ***: UserNotAuthenticatedException thrown, try to authenticate
>>> biometric prompt, used the PIN [authType = 1]:
2021-07-04 14:24:04.089 6980-6980/de.biometrics D/*** Biometric ***: Authentication succeeded with authType 1
2021-07-04 14:24:04.089 6980-6980/de.biometrics D/*** Biometric ***: (authType: 1=PIN, 2=fingerprint)
2021-07-04 14:24:04.092 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:04.097 6980-6980/de.biometrics D/signed data: a1wbBdybQkP30XWFBj0o8fiVrS8BXlREGmDHQQQhEwg=
>>> pressed "SIGN" again after 5 seconds
2021-07-04 14:24:09.725 6980-6980/de.biometrics D/*** Biometric ***: sign started
2021-07-04 14:24:09.730 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:13.421 6980-6980/de.biometrics D/*** Biometric ***: Authentication succeeded with authType 2
2021-07-04 14:24:13.422 6980-6980/de.biometrics D/*** Biometric ***: (authType: 1=PIN, 2=fingerprint)
2021-07-04 14:24:13.426 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:13.432 6980-6980/de.biometrics D/signed data: a1wbBdybQkP30XWFBj0o8fiVrS8BXlREGmDHQQQhEwg=
>>> pressed "SIGN" again 21 seconds after the PIN authorization
2021-07-04 14:24:23.348 6980-6980/de.biometrics D/*** Biometric ***: sign started
2021-07-04 14:24:23.359 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:23.366 6980-6980/de.biometrics D/*** Biometric ***: UserNotAuthenticatedException thrown, try to authenticate
>>> fingerprint is accepted [authType = 2]
2021-07-04 14:24:25.356 6980-6980/de.biometrics D/*** Biometric ***: Authentication succeeded with authType 2
2021-07-04 14:24:25.356 6980-6980/de.biometrics D/*** Biometric ***: (authType: 1=PIN, 2=fingerprint)
2021-07-04 14:24:25.361 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
>>> the exception is thrown on line 86:
>>> mac.init(getOrCreateSecretKey(KEY_NAME_SIGN));
2021-07-04 14:24:25.377 6980-6980/de.biometrics W/System.err: android.security.keystore.UserNotAuthenticatedException: User not authenticated
2021-07-04 14:24:25.377 6980-6980/de.biometrics W/System.err: at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1346)
2021-07-04 14:24:25.378 6980-6980/de.biometrics W/System.err: at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1388)
2021-07-04 14:24:25.379 6980-6980/de.biometrics W/System.err: at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
2021-07-04 14:24:25.379 6980-6980/de.biometrics W/System.err: at android.security.keystore.AndroidKeyStoreHmacSpi.ensureKeystoreOperationInitialized(AndroidKeyStoreHmacSpi.java:184)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at android.security.keystore.AndroidKeyStoreHmacSpi.engineInit(AndroidKeyStoreHmacSpi.java:101)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at javax.crypto.Mac.chooseProvider(Mac.java:443)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at javax.crypto.Mac.init(Mac.java:513)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at de.biometrics.MainActivity.onAuthenticationSucceeded(MainActivity.java:86)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err: at androidx.biometric.BiometricFragment.run(BiometricFragment.java:907)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err: at android.os.Handler.handleCallback(Handler.java:938)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err: at android.os.Handler.dispatchMessage(Handler.java:99)
2021-07-04 14:24:25.382 6980-6980/de.biometrics W/System.err: at android.os.Looper.loop(Looper.java:223)
2021-07-04 14:24:25.384 6980-6980/de.biometrics W/System.err: at android.app.ActivityThread.main(ActivityThread.java:7656)
2021-07-04 14:24:25.385 6980-6980/de.biometrics W/System.err: at java.lang.reflect.Method.invoke(Native Method)
2021-07-04 14:24:25.386 6980-6980/de.biometrics W/System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
2021-07-04 14:24:25.386 6980-6980/de.biometrics W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
这是完整的源代码(MainActivity.java):
编辑: 我稍微更改了 createKey 函数,使其更符合 https://developer.android.com/training/sign-in/biometric-auth#java
上的文档
package de.biometrics;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.UserNotAuthenticatedException;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.Executor;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class MainActivity extends AppCompatActivity {
// use dependency in build.graddle:
// implementation 'androidx.biometric:biometric:1.1.0'
private static final String KEY_NAME_SIGN = "SignKey";
private static final int VALIDITY_DURATION_SECONDS = 10;
private static final String APP_TAG = "*** Biometric *** ";
private BiometricPrompt biometricPrompt;
private BiometricPrompt.PromptInfo promptInfo;
private Executor executor;
Button btnSign;
Mac mac;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
executor = ContextCompat.getMainExecutor(this);
btnSign = (Button) findViewById(R.id.button);
// Allows user to authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
// Can't call setNegativeButtonText() and
// setAllowedAuthenticators(...|DEVICE_CREDENTIAL) at the same time.
// .setNegativeButtonText("Use account password")
.setAllowedAuthenticators(
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
| androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build();
biometricPrompt = new BiometricPrompt(MainActivity.this,
executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode,
@NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
Log.d(APP_TAG, "Authentication succeeded!");
}
@Override
public void onAuthenticationSucceeded(
@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
int authorizationType = result.getAuthenticationType();
Log.d(APP_TAG, "Authentication succeeded with authType " + authorizationType);
Log.d(APP_TAG, "(authType: 1=PIN, 2=fingerprint)");
try {
// init mac from scratch
mac = Mac.getInstance("HmacSHA256");
mac.init(getOrCreateSecretKey(KEY_NAME_SIGN));
byte[] bytes = "secret-text".getBytes(StandardCharsets.UTF_8);
byte[] macResult = mac.doFinal(bytes);
Log.d("signed data ", Base64.encodeToString(macResult, Base64.NO_WRAP));
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Log.d(APP_TAG, "Authentication failed");
}
});
btnSign.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sign();
}
});
}
private void sign() {
// simple sign function
Log.d(APP_TAG, "sign started");
// setup the mac
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(getOrCreateSecretKey(KEY_NAME_SIGN));
} catch (UserNotAuthenticatedException e) {
Log.d(APP_TAG, "UserNotAuthenticatedException thrown, try to authenticate");
biometricPrompt.authenticate(promptInfo);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
biometricPrompt.authenticate(promptInfo);
}
private SecretKey getOrCreateSecretKey(String keyName) {
SecretKey secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "try to load secretKey from keystore");
if (secretKey == null) {
createSecretKey(keyName);
secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "generated fresh key, try to load");
}
return secretKey;
}
private SecretKey getSecretKey(String keyName) {
KeyStore keyStore = null;
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
// Before the keystore can be accessed, it must be loaded.
keyStore.load(null);
return ((SecretKey) keyStore.getKey(keyName, null));
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
return null;
}
}
private void createSecretKey(String keyName) {
generateSecretKey(new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_SIGN)
.setUserAuthenticationRequired(true)
//.setInvalidatedByBiometricEnrollment(true)
.setUserAuthenticationValidityDurationSeconds(10)
.build());
}// All exceptions unhandled
private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
KeyGenerator keyGenerator = null;
try {
keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) {
e.printStackTrace();
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:text="Sign"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
build.graddle(模块):
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "de.biometrics"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.biometric:biometric:1.1.0'
}
注意:这只是一个非常简化的程序,只是为了测试生物识别提示功能。
我正在回答我自己的问题,因为我的发现可能对其他人有帮助。
到目前为止(2021 年 7 月 6 日)我确实不知道为什么使用选项生成的 SecretKey
.setUserAuthenticationValidityDurationSeconds(10);
无法通过指纹先释放。正如我在问题中所写,当它首先通过 PIN(设备凭据)发布时,只要持续时间未过期,它就可以通过指纹使用(“认证”)。
无论如何,文档中还有另一个选项使用每次使用授权的密钥进行身份验证
(https://developer.android.com/training/sign-in/biometric-auth#auth-per-use-keys) 和这个选项
给出了预期的结果。
使用新代码更新代码时,不要忘记生成一个新密钥!
使用新选项时
.setUserAuthenticationParameters(0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG |
KeyProperties.AUTH_DEVICE_CREDENTIAL)
您会注意到代码需要 SDK 30+ 到 运行。对于 SDK > 23 上 运行 的代码,您可以使用
.setUserAuthenticationValidityDurationSeconds(0)
使用“0”秒很重要,因为这(内部)默认为“BIOMETRIC_STRONG | DEVICE_CREDENTIAL”,
当使用“-1”时,它默认为“DEVICE_CREDENTIAL”(没有指纹选项)。
下面我提供了检查正在使用的 SDK 并选择正确的 generateKey 函数的代码:
private SecretKey getOrCreateSecretKey(String keyName) {
SecretKey secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "try to load secretKey from keystore");
if (secretKey == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &
Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
createSecretKeyApi2329(keyName);
Log.d(APP_TAG, "createSecretKeyApi2329 SDK in use: " + Build.VERSION.SDK_INT);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
createSecretKeyApi30(keyName);
Log.d(APP_TAG, "createSecretKeyApi30 SDK in use: " + Build.VERSION.SDK_INT);
}
// as minimum SDK in build.gradle was set to 23 the version can't be below 23
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.d(APP_TAG, "SDK in use is to old, minimum SDK is 23 = M");
finish();
}
secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "generated fresh key, try to load");
}
return secretKey;
}
@RequiresApi(api = Build.VERSION_CODES.R)
private void createSecretKeyApi30(String keyName) {
generateSecretKey(new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_SIGN)
//.setInvalidatedByBiometricEnrollment(true)
// Accept either a biometric credential or a device credential.
// To accept only one type of credential, include only that type as the
// second argument.
// @RequiresApi(api = Build.VERSION_CODES.R)
.setUserAuthenticationParameters(0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG |
KeyProperties.AUTH_DEVICE_CREDENTIAL)
.build());
}// All exceptions unhandled
//@RequiresApi(api = Build.VERSION_CODES.M)
private void createSecretKeyApi2329(String keyName) {
generateSecretKey(new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_SIGN)
//.setInvalidatedByBiometricEnrollment(true)
// Accept either a biometric credential or a device credential.
// To accept only one type of credential, include only that type as the
// second argument.
// for SDK < 30 use .setUserAuthenticationValidityDurationSeconds(0)
// see https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
// parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
// parameter "-1" default to AUTH_BIOMETRIC_STRONG
.setUserAuthenticationValidityDurationSeconds(0)
.build());
}// All exceptions unhandled
我在示例应用程序中使用 SecretKey 进行 Mac 签名。密钥是使用构建器参数生成的
setUserAuthenticationValidityDurationSeconds(10)
允许使用指纹和(解锁)设备 PIN 来保护我的密钥。
UI 仅包含一个用于启动签名的 SIGN 按钮。
如果我通过“使用 PIN”使用生物识别提示,输入 PIN 后我会收到签名 (在我的示例“2BjEuSxl/bOTTUExE4vTX2rnRZEC1Zfa21FooKkBfnc=”中)[注意:预期行为]。
在给定的 10 秒“ValidityDuration”时间内再次按下 SIGN 按钮,我可以成功使用指纹进行授权[注意:预期行为]。
10秒后按SIGN键,使用指纹授权 [或在没有事先使用 PIN 的情况下使用指纹] 给出了一个例外 [注意: 不是 预期的行为]:
android.security.keystore.UserNotAuthenticatedException: User not authenticated
所以我的问题是:如何使用 same SecretKey 来授权签名过程(或者更好地从 AndroidKeystore) 与 PIN 和指纹选项 ?
我正在 Android SDK 30(目标)和(最低)23 上进行测试,生物识别功能可用于实现 'androidx.biometric:biometric:1.1.0'。
下面是 Logcat 调试输出以及我这边的一些评论:
>>> first start
2021-07-04 14:23:47.178 6980-6980/de.biometrics D/*** Biometric ***: sign started
2021-07-04 14:23:47.181 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:23:47.211 6980-6980/de.biometrics D/*** Biometric ***: generated fresh key, try to load
2021-07-04 14:23:47.223 6980-6980/de.biometrics D/*** Biometric ***: UserNotAuthenticatedException thrown, try to authenticate
>>> biometric prompt, used the PIN [authType = 1]:
2021-07-04 14:24:04.089 6980-6980/de.biometrics D/*** Biometric ***: Authentication succeeded with authType 1
2021-07-04 14:24:04.089 6980-6980/de.biometrics D/*** Biometric ***: (authType: 1=PIN, 2=fingerprint)
2021-07-04 14:24:04.092 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:04.097 6980-6980/de.biometrics D/signed data: a1wbBdybQkP30XWFBj0o8fiVrS8BXlREGmDHQQQhEwg=
>>> pressed "SIGN" again after 5 seconds
2021-07-04 14:24:09.725 6980-6980/de.biometrics D/*** Biometric ***: sign started
2021-07-04 14:24:09.730 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:13.421 6980-6980/de.biometrics D/*** Biometric ***: Authentication succeeded with authType 2
2021-07-04 14:24:13.422 6980-6980/de.biometrics D/*** Biometric ***: (authType: 1=PIN, 2=fingerprint)
2021-07-04 14:24:13.426 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:13.432 6980-6980/de.biometrics D/signed data: a1wbBdybQkP30XWFBj0o8fiVrS8BXlREGmDHQQQhEwg=
>>> pressed "SIGN" again 21 seconds after the PIN authorization
2021-07-04 14:24:23.348 6980-6980/de.biometrics D/*** Biometric ***: sign started
2021-07-04 14:24:23.359 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
2021-07-04 14:24:23.366 6980-6980/de.biometrics D/*** Biometric ***: UserNotAuthenticatedException thrown, try to authenticate
>>> fingerprint is accepted [authType = 2]
2021-07-04 14:24:25.356 6980-6980/de.biometrics D/*** Biometric ***: Authentication succeeded with authType 2
2021-07-04 14:24:25.356 6980-6980/de.biometrics D/*** Biometric ***: (authType: 1=PIN, 2=fingerprint)
2021-07-04 14:24:25.361 6980-6980/de.biometrics D/*** Biometric ***: try to load secretKey from keystore
>>> the exception is thrown on line 86:
>>> mac.init(getOrCreateSecretKey(KEY_NAME_SIGN));
2021-07-04 14:24:25.377 6980-6980/de.biometrics W/System.err: android.security.keystore.UserNotAuthenticatedException: User not authenticated
2021-07-04 14:24:25.377 6980-6980/de.biometrics W/System.err: at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1346)
2021-07-04 14:24:25.378 6980-6980/de.biometrics W/System.err: at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1388)
2021-07-04 14:24:25.379 6980-6980/de.biometrics W/System.err: at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
2021-07-04 14:24:25.379 6980-6980/de.biometrics W/System.err: at android.security.keystore.AndroidKeyStoreHmacSpi.ensureKeystoreOperationInitialized(AndroidKeyStoreHmacSpi.java:184)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at android.security.keystore.AndroidKeyStoreHmacSpi.engineInit(AndroidKeyStoreHmacSpi.java:101)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at javax.crypto.Mac.chooseProvider(Mac.java:443)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at javax.crypto.Mac.init(Mac.java:513)
2021-07-04 14:24:25.380 6980-6980/de.biometrics W/System.err: at de.biometrics.MainActivity.onAuthenticationSucceeded(MainActivity.java:86)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err: at androidx.biometric.BiometricFragment.run(BiometricFragment.java:907)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err: at android.os.Handler.handleCallback(Handler.java:938)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err: at android.os.Handler.dispatchMessage(Handler.java:99)
2021-07-04 14:24:25.382 6980-6980/de.biometrics W/System.err: at android.os.Looper.loop(Looper.java:223)
2021-07-04 14:24:25.384 6980-6980/de.biometrics W/System.err: at android.app.ActivityThread.main(ActivityThread.java:7656)
2021-07-04 14:24:25.385 6980-6980/de.biometrics W/System.err: at java.lang.reflect.Method.invoke(Native Method)
2021-07-04 14:24:25.386 6980-6980/de.biometrics W/System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
2021-07-04 14:24:25.386 6980-6980/de.biometrics W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
这是完整的源代码(MainActivity.java):
编辑: 我稍微更改了 createKey 函数,使其更符合 https://developer.android.com/training/sign-in/biometric-auth#java
上的文档package de.biometrics;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.UserNotAuthenticatedException;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.Executor;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class MainActivity extends AppCompatActivity {
// use dependency in build.graddle:
// implementation 'androidx.biometric:biometric:1.1.0'
private static final String KEY_NAME_SIGN = "SignKey";
private static final int VALIDITY_DURATION_SECONDS = 10;
private static final String APP_TAG = "*** Biometric *** ";
private BiometricPrompt biometricPrompt;
private BiometricPrompt.PromptInfo promptInfo;
private Executor executor;
Button btnSign;
Mac mac;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
executor = ContextCompat.getMainExecutor(this);
btnSign = (Button) findViewById(R.id.button);
// Allows user to authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
// Can't call setNegativeButtonText() and
// setAllowedAuthenticators(...|DEVICE_CREDENTIAL) at the same time.
// .setNegativeButtonText("Use account password")
.setAllowedAuthenticators(
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
| androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build();
biometricPrompt = new BiometricPrompt(MainActivity.this,
executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode,
@NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
Log.d(APP_TAG, "Authentication succeeded!");
}
@Override
public void onAuthenticationSucceeded(
@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
int authorizationType = result.getAuthenticationType();
Log.d(APP_TAG, "Authentication succeeded with authType " + authorizationType);
Log.d(APP_TAG, "(authType: 1=PIN, 2=fingerprint)");
try {
// init mac from scratch
mac = Mac.getInstance("HmacSHA256");
mac.init(getOrCreateSecretKey(KEY_NAME_SIGN));
byte[] bytes = "secret-text".getBytes(StandardCharsets.UTF_8);
byte[] macResult = mac.doFinal(bytes);
Log.d("signed data ", Base64.encodeToString(macResult, Base64.NO_WRAP));
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Log.d(APP_TAG, "Authentication failed");
}
});
btnSign.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sign();
}
});
}
private void sign() {
// simple sign function
Log.d(APP_TAG, "sign started");
// setup the mac
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(getOrCreateSecretKey(KEY_NAME_SIGN));
} catch (UserNotAuthenticatedException e) {
Log.d(APP_TAG, "UserNotAuthenticatedException thrown, try to authenticate");
biometricPrompt.authenticate(promptInfo);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
biometricPrompt.authenticate(promptInfo);
}
private SecretKey getOrCreateSecretKey(String keyName) {
SecretKey secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "try to load secretKey from keystore");
if (secretKey == null) {
createSecretKey(keyName);
secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "generated fresh key, try to load");
}
return secretKey;
}
private SecretKey getSecretKey(String keyName) {
KeyStore keyStore = null;
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
// Before the keystore can be accessed, it must be loaded.
keyStore.load(null);
return ((SecretKey) keyStore.getKey(keyName, null));
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
return null;
}
}
private void createSecretKey(String keyName) {
generateSecretKey(new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_SIGN)
.setUserAuthenticationRequired(true)
//.setInvalidatedByBiometricEnrollment(true)
.setUserAuthenticationValidityDurationSeconds(10)
.build());
}// All exceptions unhandled
private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
KeyGenerator keyGenerator = null;
try {
keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) {
e.printStackTrace();
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:text="Sign"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
build.graddle(模块):
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "de.biometrics"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.biometric:biometric:1.1.0'
}
注意:这只是一个非常简化的程序,只是为了测试生物识别提示功能。
我正在回答我自己的问题,因为我的发现可能对其他人有帮助。
到目前为止(2021 年 7 月 6 日)我确实不知道为什么使用选项生成的 SecretKey
.setUserAuthenticationValidityDurationSeconds(10);
无法通过指纹先释放。正如我在问题中所写,当它首先通过 PIN(设备凭据)发布时,只要持续时间未过期,它就可以通过指纹使用(“认证”)。
无论如何,文档中还有另一个选项使用每次使用授权的密钥进行身份验证 (https://developer.android.com/training/sign-in/biometric-auth#auth-per-use-keys) 和这个选项 给出了预期的结果。
使用新代码更新代码时,不要忘记生成一个新密钥!
使用新选项时
.setUserAuthenticationParameters(0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG |
KeyProperties.AUTH_DEVICE_CREDENTIAL)
您会注意到代码需要 SDK 30+ 到 运行。对于 SDK > 23 上 运行 的代码,您可以使用
.setUserAuthenticationValidityDurationSeconds(0)
使用“0”秒很重要,因为这(内部)默认为“BIOMETRIC_STRONG | DEVICE_CREDENTIAL”, 当使用“-1”时,它默认为“DEVICE_CREDENTIAL”(没有指纹选项)。
下面我提供了检查正在使用的 SDK 并选择正确的 generateKey 函数的代码:
private SecretKey getOrCreateSecretKey(String keyName) {
SecretKey secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "try to load secretKey from keystore");
if (secretKey == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &
Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
createSecretKeyApi2329(keyName);
Log.d(APP_TAG, "createSecretKeyApi2329 SDK in use: " + Build.VERSION.SDK_INT);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
createSecretKeyApi30(keyName);
Log.d(APP_TAG, "createSecretKeyApi30 SDK in use: " + Build.VERSION.SDK_INT);
}
// as minimum SDK in build.gradle was set to 23 the version can't be below 23
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.d(APP_TAG, "SDK in use is to old, minimum SDK is 23 = M");
finish();
}
secretKey = getSecretKey(keyName);
Log.d(APP_TAG, "generated fresh key, try to load");
}
return secretKey;
}
@RequiresApi(api = Build.VERSION_CODES.R)
private void createSecretKeyApi30(String keyName) {
generateSecretKey(new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_SIGN)
//.setInvalidatedByBiometricEnrollment(true)
// Accept either a biometric credential or a device credential.
// To accept only one type of credential, include only that type as the
// second argument.
// @RequiresApi(api = Build.VERSION_CODES.R)
.setUserAuthenticationParameters(0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG |
KeyProperties.AUTH_DEVICE_CREDENTIAL)
.build());
}// All exceptions unhandled
//@RequiresApi(api = Build.VERSION_CODES.M)
private void createSecretKeyApi2329(String keyName) {
generateSecretKey(new KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_SIGN)
//.setInvalidatedByBiometricEnrollment(true)
// Accept either a biometric credential or a device credential.
// To accept only one type of credential, include only that type as the
// second argument.
// for SDK < 30 use .setUserAuthenticationValidityDurationSeconds(0)
// see https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
// parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
// parameter "-1" default to AUTH_BIOMETRIC_STRONG
.setUserAuthenticationValidityDurationSeconds(0)
.build());
}// All exceptions unhandled