简体   繁体   English

指纹上的 Android 生物识别 UserNotAuthenticatedException

[英]Android biometrics UserNotAuthenticatedException on fingerprint

I'm using a SecretKey in my sample app for Mac-signing.我在我的示例应用程序中使用 SecretKey 进行 Mac 签名。 The key is generated with builder parameter密钥是用 builder 参数生成的

setUserAuthenticationValidityDurationSeconds(10)

to allow the usage of the fingerprint and the (un)lock device PIN to secure my key.允许使用指纹(解锁)设备 PIN 来保护我的密钥。

The UI does only contain a SIGN button to start the signature. UI 仅包含用于开始签名的 SIGN 按钮。

If I'm using the biometric prompt via "Use PIN", after input of the PIN I receive the signature (in my example "2BjEuSxl/bOTTUExE4vTX2rnRZEC1Zfa21FooKkBfnc=") [note: expected behaviour].如果我通过“使用 PIN”使用生物识别提示,在输入 PIN 后我会收到签名(在我的示例中为“2BjEuSxl/bOTTUExE4vTX2rnRZEC1Zfa21FooKkBfnc=”)[注意:预期行为]。

Pressing the SIGN-button again within the given 10 seconds "ValidityDuration" period I can use the fingerprint for authorization successfully [note: expected behaviour].在给定的 10 秒“ValidityDuration”期间内再次按下 SIGN 按钮,我可以成功使用指纹进行授权 [注意:预期行为]。

Pressing the SIGN button after 10 seconds and using the fingerprint for authorization [or use the fingerprint without prior usage of the PIN] gives an exception [note: not expected behaviour]: 10 秒后按下 SIGN 按钮并使用指纹进行授权 [或在未事先使用 PIN 的情况下使用指纹] 会出现异常 [注意:不是预期的行为]:

android.security.keystore.UserNotAuthenticatedException: User not authenticated

So my question is: how can I use the same SecretKey to authorize the sign process (or better the release of the key from AndroidKeystore) with the PIN and fingerprint option ?所以我的问题是:如何使用相同的SecretKey 通过 PIN 和指纹选项来授权签名过程(或者更好地从 AndroidKeystore 发布密钥)?

I'm testing on Android SDK 30 (target) and (minimum) 23, the biometric functions are available with implementation 'androidx.biometric:biometric:1.1.0'.我正在 Android SDK 30(目标)和(最低)23 上进行测试,生物识别功能可用于实现“androidx.biometric:biometric:1.1.0”。

Below is the Logcat debug-output with some comments from my side:以下是 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$1.onAuthenticationSucceeded(MainActivity.java:86)
2021-07-04 14:24:25.381 6980-6980/de.biometrics W/System.err:     at androidx.biometric.BiometricFragment$9.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)

This is the complete source code (MainActivity.java):这是完整的源代码(MainActivity.java):

Edit: I slightly changed the createKey-function to be more compliant with the documentation on https://developer.android.com/training/sign-in/biometric-auth#java编辑:我稍微改变了 createKey-function 以更符合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活动_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 (Module): 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'
}

Note: this is just a very simplified program, just for testing the biometric prompt functions.注意:这只是一个非常简化的程序,仅用于测试生物识别提示功能。

I'm answering my own question, as my findings maybe helpful to others.我正在回答我自己的问题,因为我的发现可能对其他人有帮助。

Up to now (July 6th 2021) I do have no idea why a SecretKey that is generated with the option到目前为止(2021 年 7 月 6 日)我不知道为什么使用该选项生成的 SecretKey

.setUserAuthenticationValidityDurationSeconds(10);

cannot get released by fingerprint first.不能先通过指纹释放 As written in my question, when it's released first by PIN (device credential) it could get used ("authenticated") by fingerprint as long the duration period is not expired.正如我的问题中所写,当它首先通过 PIN(设备凭据)发布时,只要持续时间未过期,它就可以通过指纹使用(“验证”)。

Anyway, there is another option available in the docs Authenticate using auth-per-use keys ( https://developer.android.com/training/sign-in/biometric-auth#auth-per-use-keys ) and this option gives the expected result.无论如何,文档中还有另一个选项可以使用 auth-per-use 密钥进行身份验证https://developer.android.com/training/sign-in/biometric-auth#auth-per-use-keys )和这个选项给出了预期的结果。

Don't forget to generate a fresh key when updating your code with the new code!使用新代码更新代码时,不要忘记生成新密钥

When using the new option使用新选项时

.setUserAuthenticationParameters(0 /* duration */,
   KeyProperties.AUTH_BIOMETRIC_STRONG |
   KeyProperties.AUTH_DEVICE_CREDENTIAL)

you will notice that the code requires SDK 30+ to run.您会注意到代码需要 SDK 30+ 才能运行。 For code that runs on SDK > 23 you can use对于在 SDK > 23 上运行的代码,您可以使用

.setUserAuthenticationValidityDurationSeconds(0)

It's important to use "0" seconds as this (internally) defaults to "BIOMETRIC_STRONG | DEVICE_CREDENTIAL", when using "-1" it defaults to "DEVICE_CREDENTIAL" (without the fingerprint option).使用“0”秒很重要,因为这(内部)默认为“BIOMETRIC_STRONG | DEVICE_CREDENTIAL”,当使用“-1”时,它默认为“DEVICE_CREDENTIAL”(没有指纹选项)。

Below I'm providing code that checks for the SDK in use and chooses the correct generateKey-function:下面我提供的代码用于检查正在使用的 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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM