BiometricManager.java

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.biometric;

import android.app.KeyguardManager;
import android.content.Context;
import android.os.Build;
import android.util.Log;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * A class that provides system information related to biometrics (e.g. fingerprint, face, etc.).
 *
 * <p>On devices running Android 10 (API 29) and above, this will query the framework's version of
 * {@link android.hardware.biometrics.BiometricManager}. On Android 9.0 (API 28) and prior
 * versions, this will query {@link androidx.core.hardware.fingerprint.FingerprintManagerCompat}.
 *
 * @see BiometricPrompt To prompt the user to authenticate with their biometric.
 */
@SuppressWarnings("deprecation")
public class BiometricManager {
    private static final String TAG = "BiometricManager";

    /**
     * The user can successfully authenticate.
     */
    public static final int BIOMETRIC_SUCCESS = 0;

    /**
     * Unable to determine whether the user can authenticate.
     *
     * <p>This status code may be returned on older Android versions due to partial incompatibility
     * with a newer API. Applications that wish to enable biometric authentication on affected
     * devices may still call {@code BiometricPrompt#authenticate()} after receiving this status
     * code but should be prepared to handle possible errors.
     */
    public static final int BIOMETRIC_STATUS_UNKNOWN = -1;

    /**
     * The user can't authenticate because the specified options are incompatible with the current
     * Android version.
     */
    public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2;

    /**
     * The user can't authenticate because the hardware is unavailable. Try again later.
     */
    public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1;

    /**
     * The user can't authenticate because no biometric or device credential is enrolled.
     */
    public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11;

    /**
     * The user can't authenticate because there is no suitable hardware (e.g. no biometric sensor
     * or no keyguard).
     */
    public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12;

    /**
     * The user can't authenticate because a security vulnerability has been discovered with one or
     * more hardware sensors. The affected sensor(s) are unavailable until a security update has
     * addressed the issue.
     */
    public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15;

    /**
     * A status code that may be returned when checking for biometric authentication.
     */
    @IntDef({
        BIOMETRIC_SUCCESS,
        BIOMETRIC_STATUS_UNKNOWN,
        BIOMETRIC_ERROR_UNSUPPORTED,
        BIOMETRIC_ERROR_HW_UNAVAILABLE,
        BIOMETRIC_ERROR_NONE_ENROLLED,
        BIOMETRIC_ERROR_NO_HARDWARE,
        BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface AuthenticationStatus {}

    /**
     * Types of authenticators, defined at a level of granularity supported by
     * {@link BiometricManager} and {@link BiometricPrompt}.
     *
     * <p>Types may combined via bitwise OR into a single integer representing multiple
     * authenticators (e.g. {@code DEVICE_CREDENTIAL | BIOMETRIC_WEAK}).
     *
     * @see #canAuthenticate(int)
     * @see BiometricPrompt.PromptInfo.Builder#setAllowedAuthenticators(int)
     */
    public interface Authenticators {
        /**
         * Any biometric (e.g. fingerprint, iris, or face) on the device that meets or exceeds the
         * requirements for <strong>Class 3</strong> (formerly <strong>Strong</strong>), as defined
         * by the Android CDD.
         */
        int BIOMETRIC_STRONG = 0x000F;

        /**
         * Any biometric (e.g. fingerprint, iris, or face) on the device that meets or exceeds the
         * requirements for <strong>Class 2</strong> (formerly <strong>Weak</strong>), as defined by
         * the Android CDD.
         *
         * <p>Note that this is a superset of {@link #BIOMETRIC_STRONG} and is defined such that
         * {@code BIOMETRIC_STRONG | BIOMETRIC_WEAK == BIOMETRIC_WEAK}.
         */
        int BIOMETRIC_WEAK = 0x00FF;

        /**
         * The non-biometric credential used to secure the device (i.e. PIN, pattern, or password).
         * This should typically only be used in combination with a biometric auth type, such as
         * {@link #BIOMETRIC_WEAK}.
         */
        int DEVICE_CREDENTIAL = 1 << 15;
    }

    /**
     * A bitwise combination of authenticator types defined in {@link Authenticators}.
     */
    @IntDef(flag = true, value = {
        Authenticators.BIOMETRIC_STRONG,
        Authenticators.BIOMETRIC_WEAK,
        Authenticators.DEVICE_CREDENTIAL
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface AuthenticatorTypes {}

    /**
     * An injector for various class and method dependencies. Used for testing.
     */
    @VisibleForTesting
    interface Injector {
        /**
         * Provides the framework biometric manager that may be used on Android 10 (API 29) and
         * above.
         *
         * @return An instance of {@link android.hardware.biometrics.BiometricManager}.
         */
        @RequiresApi(Build.VERSION_CODES.Q)
        @Nullable
        android.hardware.biometrics.BiometricManager getBiometricManager();

        /**
         * Provides the fingerprint manager that may be used on Android 9.0 (API 28) and below.
         *
         * @return An instance of
         * {@link androidx.core.hardware.fingerprint.FingerprintManagerCompat}.
         */
        @Nullable
        androidx.core.hardware.fingerprint.FingerprintManagerCompat getFingerprintManager();

        /**
         * Checks if the current device is capable of being secured with a lock screen credential
         * (i.e. PIN, pattern, or password).
         */
        boolean isDeviceSecurable();

        /**
         * Checks if the current device is secured with a lock screen credential (i.e. PIN, pattern,
         * or password).
         *
         * @return Whether the device is secured with a lock screen credential.
         */
        boolean isDeviceSecuredWithCredential();

        /**
         * Checks if the current device has a hardware sensor that may be used for fingerprint
         * authentication.
         *
         * @return Whether the device has a fingerprint sensor.
         */
        boolean isFingerprintHardwarePresent();

        /**
         * Checks if all biometric sensors on the device are known to meet or exceed the security
         * requirements for <strong>Class 3</strong> (formerly <strong>Strong</strong>).
         *
         * @return Whether all biometrics are known to be <strong>Class 3</strong> or stronger.
         */
        boolean isStrongBiometricGuaranteed();
    }

    /**
     * Provides the default class and method dependencies that will be used in production.
     */
    private static class DefaultInjector implements Injector {
        @NonNull private final Context mContext;

        /**
         * Creates a default injector from the given context.
         *
         * @param context The application or activity context.
         */
        DefaultInjector(@NonNull Context context) {
            mContext = context.getApplicationContext();
        }

        @Override
        @RequiresApi(Build.VERSION_CODES.Q)
        @Nullable
        public android.hardware.biometrics.BiometricManager getBiometricManager() {
            return Api29Impl.create(mContext);
        }

        @Override
        @Nullable
        public androidx.core.hardware.fingerprint.FingerprintManagerCompat getFingerprintManager() {
            return androidx.core.hardware.fingerprint.FingerprintManagerCompat.from(mContext);
        }

        @Override
        public boolean isDeviceSecurable() {
            return KeyguardUtils.getKeyguardManager(mContext) != null;
        }

        @Override
        public boolean isDeviceSecuredWithCredential() {
            return KeyguardUtils.isDeviceSecuredWithCredential(mContext);
        }

        @Override
        public boolean isFingerprintHardwarePresent() {
            return PackageUtils.hasSystemFeatureFingerprint(mContext);
        }

        @Override
        public boolean isStrongBiometricGuaranteed() {
            return DeviceUtils.canAssumeStrongBiometrics(mContext, Build.MODEL);
        }
    }

    /**
     * The injector for class and method dependencies used by this manager.
     */
    @NonNull private final Injector mInjector;

    /**
     * The framework biometric manager. Should be non-null on Android 10 (API 29) and above.
     */
    @Nullable private final android.hardware.biometrics.BiometricManager mBiometricManager;

    /**
     * The framework fingerprint manager. Should be non-null on Android 9.0 (API 28) and below.
     */
    @Nullable private final androidx.core.hardware.fingerprint.FingerprintManagerCompat
            mFingerprintManager;

    /**
     * Creates a {@link BiometricManager} instance from the given context.
     *
     * @param context The application or activity context.
     * @return An instance of {@link BiometricManager}.
     */
    @NonNull
    public static BiometricManager from(@NonNull Context context) {
        return new BiometricManager(new DefaultInjector(context));
    }

    /**
     * Creates a {@link BiometricManager} instance with the given injector.
     *
     * @param injector An injector for class and method dependencies.
     */
    @VisibleForTesting
    BiometricManager(@NonNull Injector injector) {
        mInjector = injector;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            mBiometricManager = injector.getBiometricManager();
            mFingerprintManager = null;
        } else {
            mBiometricManager = null;
            mFingerprintManager = injector.getFingerprintManager();
        }
    }

    /**
     * Checks if the user can authenticate with biometrics. This requires at least one biometric
     * sensor to be present, enrolled, and available on the device.
     *
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with biometrics. Otherwise,
     * returns an error code indicating why the user can't authenticate, or
     * {@link #BIOMETRIC_STATUS_UNKNOWN} if it is unknown whether the user can authenticate.
     *
     * @deprecated Use {@link #canAuthenticate(int)} instead.
     */
    @Deprecated
    @AuthenticationStatus
    public int canAuthenticate() {
        return canAuthenticate(Authenticators.BIOMETRIC_WEAK);
    }

    /**
     * Checks if the user can authenticate with an authenticator that meets the given requirements.
     * This requires at least one of the specified authenticators to be present, enrolled, and
     * available on the device.
     *
     * <p>Note that not all combinations of authenticator types are supported prior to Android 11
     * (API 30). Specifically, {@code DEVICE_CREDENTIAL} alone is unsupported prior to API 30, and
     * {@code BIOMETRIC_STRONG | DEVICE_CREDENTIAL} is unsupported on API 28-29. Developers that
     * wish to check for the presence of a PIN, pattern, or password on these versions should
     * instead use {@link KeyguardManager#isDeviceSecure()}.
     *
     * @param authenticators A bit field representing the types of {@link Authenticators} that may
     *                       be used for authentication.
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with an allowed
     * authenticator. Otherwise, returns {@link #BIOMETRIC_STATUS_UNKNOWN} or an error code
     * indicating why the user can't authenticate.
     */
    @AuthenticationStatus
    public int canAuthenticate(@AuthenticatorTypes int authenticators) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            if (mBiometricManager == null) {
                Log.e(TAG, "Failure in canAuthenticate(). BiometricManager was null.");
                return BIOMETRIC_ERROR_HW_UNAVAILABLE;
            }
            return Api30Impl.canAuthenticate(mBiometricManager, authenticators);
        }
        return canAuthenticateCompat(authenticators);
    }

    /**
     * Checks if the user can authenticate with an authenticator that meets the given requirements.
     *
     * <p>This method attempts to emulate the behavior of {@link #canAuthenticate(int)} on devices
     * running Android 10 (API 29) and below.
     *
     * @param authenticators A bit field representing the types of {@link Authenticators} that may
     *                       be used for authentication.
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with the given set of allowed
     * authenticators. Otherwise, returns an error code indicating why the user can't authenticate,
     * or {@link #BIOMETRIC_STATUS_UNKNOWN} if it is unknown whether the user can authenticate.
     */
    @AuthenticationStatus
    private int canAuthenticateCompat(@AuthenticatorTypes int authenticators) {
        if (!AuthenticatorUtils.isSupportedCombination(authenticators)) {
            return BIOMETRIC_ERROR_UNSUPPORTED;
        }

        // Match the framework's behavior for an empty authenticator set on API 30.
        if (authenticators == 0) {
            return BIOMETRIC_ERROR_NO_HARDWARE;
        }

        // Authentication is impossible if the device can't be secured.
        if (!mInjector.isDeviceSecurable()) {
            return BIOMETRIC_ERROR_NO_HARDWARE;
        }

        // No authenticators are enrolled if the device is not secured.
        if (!mInjector.isDeviceSecuredWithCredential()) {
            return BIOMETRIC_ERROR_NONE_ENROLLED;
        }

        // Credential authentication is always possible if the device is secured.
        if (AuthenticatorUtils.isDeviceCredentialAllowed(authenticators)) {
            return BIOMETRIC_SUCCESS;
        }

        // The class of some non-fingerprint biometrics can be checked on API 29.
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
            return AuthenticatorUtils.isWeakBiometricAllowed(authenticators)
                    ? canAuthenticateWithWeakBiometricOnApi29()
                    : canAuthenticateWithStrongBiometricOnApi29();
        }

        // Non-fingerprint biometrics may be invoked but can't be checked on API 28.
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
            // Having fingerprint hardware is a prerequisite, since BiometricPrompt internally
            // calls FingerprintManager#getErrorString() on API 28 (b/151443237).
            return mInjector.isFingerprintHardwarePresent()
                    ? canAuthenticateWithFingerprintOrUnknown()
                    : BIOMETRIC_ERROR_NO_HARDWARE;
        }

        // No non-fingerprint biometric APIs exist prior to API 28.
        return canAuthenticateWithFingerprint();
    }

    /**
     * Checks if the user can authenticate with a <strong>Class 3</strong> (formerly
     * <strong>Strong</strong>) or better biometric sensor on a device running Android 10 (API 29).
     *
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with a
     * <strong>Class 3</strong> or better biometric sensor. Otherwise, returns an error code
     * indicating why the user can't authenticate, or {@link #BIOMETRIC_STATUS_UNKNOWN} if it is
     * unknown whether the user can authenticate.
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    @AuthenticationStatus
    private int canAuthenticateWithStrongBiometricOnApi29() {
        // Use the hidden canAuthenticate(CryptoObject) method if it exists.
        final Method canAuthenticateWithCrypto = Api29Impl.getCanAuthenticateWithCryptoMethod();
        if (canAuthenticateWithCrypto != null) {
            final android.hardware.biometrics.BiometricPrompt.CryptoObject crypto =
                    CryptoObjectUtils.wrapForBiometricPrompt(
                            CryptoObjectUtils.createFakeCryptoObject());
            if (crypto != null) {
                try {
                    final Object result =
                            canAuthenticateWithCrypto.invoke(mBiometricManager, crypto);
                    if (result instanceof Integer) {
                        return (int) result;
                    }
                    Log.w(TAG, "Invalid return type for canAuthenticate(CryptoObject).");
                } catch (IllegalAccessException | IllegalArgumentException
                        | InvocationTargetException e) {
                    Log.w(TAG, "Failed to invoke canAuthenticate(CryptoObject).", e);
                }
            }
        }

        // Check if we can use canAuthenticate() as a proxy for canAuthenticate(BIOMETRIC_STRONG).
        @AuthenticationStatus final int result = canAuthenticateWithWeakBiometricOnApi29();
        if (mInjector.isStrongBiometricGuaranteed() || result != BIOMETRIC_SUCCESS) {
            return result;
        }

        // If all else fails, check if fingerprint authentication is available.
        return canAuthenticateWithFingerprintOrUnknown();
    }

    /**
     * Checks if the user can authenticate with a <strong>Class 2</strong> (formerly
     * <strong>Weak</strong>) or better biometric sensor on a device running Android 10 (API 29).
     *
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with a
     * <strong>Class 2</strong> or better biometric sensor. Otherwise, returns an error code
     * indicating why the user can't authenticate.
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    @AuthenticationStatus
    private int canAuthenticateWithWeakBiometricOnApi29() {
        if (mBiometricManager == null) {
            Log.e(TAG, "Failure in canAuthenticate(). BiometricManager was null.");
            return BIOMETRIC_ERROR_HW_UNAVAILABLE;
        }
        return Api29Impl.canAuthenticate(mBiometricManager);
    }

    /**
     * Checks if the user can authenticate with fingerprint, falling back to
     * {@link #BIOMETRIC_STATUS_UNKNOWN} for any error condition.
     *
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with fingerprint, or
     * {@link #BIOMETRIC_STATUS_UNKNOWN} otherwise.
     */
    @AuthenticationStatus
    private int canAuthenticateWithFingerprintOrUnknown() {
        return canAuthenticateWithFingerprint() == BIOMETRIC_SUCCESS
                ? BIOMETRIC_SUCCESS
                : BIOMETRIC_STATUS_UNKNOWN;
    }

    /**
     * Checks if the user can authenticate with fingerprint.
     *
     * @return {@link #BIOMETRIC_SUCCESS} if the user can authenticate with fingerprint.
     * Otherwise, returns an error code indicating why the user can't authenticate.
     */
    @AuthenticationStatus
    private int canAuthenticateWithFingerprint() {
        if (mFingerprintManager == null) {
            Log.e(TAG, "Failure in canAuthenticate(). FingerprintManager was null.");
            return BIOMETRIC_ERROR_HW_UNAVAILABLE;
        }
        if (!mFingerprintManager.isHardwareDetected()) {
            return BIOMETRIC_ERROR_NO_HARDWARE;
        }
        if (!mFingerprintManager.hasEnrolledFingerprints()) {
            return BIOMETRIC_ERROR_NONE_ENROLLED;
        }
        return BIOMETRIC_SUCCESS;
    }

    /**
     * Nested class to avoid verification errors for methods introduced in Android 11 (API 30).
     */
    @RequiresApi(Build.VERSION_CODES.R)
    private static class Api30Impl {
        // Prevent instantiation.
        private Api30Impl() {}

        /**
         * Calls {@link android.hardware.biometrics.BiometricManager#canAuthenticate(int)} for the
         * given biometric manager and set of allowed authenticators.
         *
         * @param biometricManager An instance of
         *                         {@link android.hardware.biometrics.BiometricManager}.
         * @param authenticators   A bit field representing the types of {@link Authenticators} that
         *                         may be used for authentication.
         * @return The result of
         * {@link android.hardware.biometrics.BiometricManager#canAuthenticate(int)}.
         */
        @AuthenticationStatus
        static int canAuthenticate(
                @NonNull android.hardware.biometrics.BiometricManager biometricManager,
                @AuthenticatorTypes int authenticators) {
            return biometricManager.canAuthenticate(authenticators);
        }
    }

    /**
     * Nested class to avoid verification errors for methods introduced in Android 10 (API 29).
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    private static class Api29Impl {
        // Prevent instantiation.
        private Api29Impl() {}

        /**
         * Gets an instance of the framework
         * {@link android.hardware.biometrics.BiometricManager} class.
         *
         * @param context The application or activity context.
         * @return An instance of {@link android.hardware.biometrics.BiometricManager}.
         */
        @Nullable
        static android.hardware.biometrics.BiometricManager create(@NonNull Context context) {
            return context.getSystemService(android.hardware.biometrics.BiometricManager.class);
        }

        /**
         * Calls {@link android.hardware.biometrics.BiometricManager#canAuthenticate()} for the
         * given biometric manager.
         *
         * @param biometricManager An instance of
         *                         {@link android.hardware.biometrics.BiometricManager}.
         * @return The result of
         * {@link android.hardware.biometrics.BiometricManager#canAuthenticate()}.
         */
        @AuthenticationStatus
        static int canAuthenticate(
                @NonNull android.hardware.biometrics.BiometricManager biometricManager) {
            return biometricManager.canAuthenticate();
        }

        /**
         * Checks for and returns the hidden {@link android.hardware.biometrics.BiometricManager}
         * method {@code canAuthenticate(CryptoObject)} via reflection.
         *
         * @return The method {@code BiometricManager#canAuthenticate(CryptoObject)}, if present.
         */
        @SuppressWarnings("JavaReflectionMemberAccess")
        @Nullable
        static Method getCanAuthenticateWithCryptoMethod() {
            try {
                return android.hardware.biometrics.BiometricManager.class.getMethod(
                        "canAuthenticate",
                        android.hardware.biometrics.BiometricPrompt.CryptoObject.class);
            } catch (NoSuchMethodException e) {
                return null;
            }
        }
    }
}