On Using Keys and Biometrics

Below are some findings on using a couple of security features in Android:

  • AndroidKeyStore
  • Biometrics
  • Device locks

Using AndroidKeyStore

The AndroidKeyStore is an implementation of the Java Cryptography Architecture (JCA)'s KeyStore service to manage application-specific cryptographic keys. Such keys can be created or imported with an associated label, then an opaque Key class obtained from that label to use for cryptographic operations. However, they cannot be exported. The remainder here focuses on creating rather than import.

Obtaining this Keystore is done using the static method Keystore.getIstance() and specifying the "AndroidKeyStore" Service Provider Interface (SPI) provider.

New keys are created using an instance of KeyGenerator (or KeyPairGenerator), again specifying "AndroidKeyStore" as the SPI provider. When creating a new key, several properties can be applied via KeyGenParameterSpec, including:

  • key size (in bits)
  • encryption block modes (e.g., GCM)
  • encryption padding (e.g., NONE)
  • require authentication
  • duration until next authentication (in seconds)

Once set, these properties cannot be changed without first deleting then re-importing/-creating the key. If the key creation requests some user authentication is required, it can only be done if the device has a security lock set (e.g., Pattern/PIN/Password); if user authentication is required for every use, it can only be done if the user has at least one fingerprint enrolled.

Keys in the AndroidKeyStore are stored on the device until one of the following happens:

  • The app deletes its entry from the KeyStore
  • The app's data storage (not data cache) is cleared
  • The app is uninstalled

Keys with user authentication required are invalided and cannot be used if any of the following happen:

  • The device is hard reset
  • The security lock is disabled (e.g., changed from Pattern/PIN/Password to Swipe or None)

Further, "authenticate every use" keys are invalided and cannot be used if any of the following happen:

  • A new fingerprint is enrolled
  • All fingerprints are unenrolled

Existing Key objects (secret, private, public, and even certificates) are obtained using the Keystore instance's typical methods (e.g., .getSecretKey()). Note that all of the methods on the Key object that would export the value (e.g., getEncoded()) either throw exception or return null; this is true even for PublicKeys.

the "Java Standard" Cipher/Mac/Signature classes are used in Android as they are in any other Java/Kotlin environment. If the key requires authentication, a UserNotAuthenticatedException is thrown; if the key no longer valid (as above), a KeyPermanentlyInvalidatedException is thrown.

Using KeyguardManager

The KeyguardManager is a system service that originally used to lock/unlock the keyboard, but has since expanded to lock/unlock the user's device. It can only be obtained from a Context (e.g., Activity).

The most interesting methods here are those that determine if a "strong" security lock (Pattern/PIN/Password) is configured and if the device is currently locked. Both are boolean values; it is not possible to determine which method of lock is configured.

In addition, the KeyguardManager can be used to prompt the user to enter their Pattern/PIN/Password by way of createConfirmDeviceCredentialIntent(); if no "strong" security lock is configured, this method returns null.

The Intent is created with optional title and description, then dispatched via startActivityForResult() to trigger the device prompts. Applications receive either RESULT_OK (if successfully unlocked) or RESULT_CANCELED (device prompt is dismissed) via the overridden method onActivityResult(). It is important to note that dispatching and monitoring is best done from a long-running Activity, such as the MainActivity, or the result is never received.

Using FingerprintManager

NOTE: This API is marked as deprecated as of API 28 (Android Pie) and replaced with BiometricPrompt.

The FingerprintManager is a system service used to interact with a device's fingerprint hardware. This was added in API 23, and is now deprecated as of API 28. As with KeyguardManager it can only be obtained from a Context (e.g., Activity). It also requires the USE_FINGERPRINT or USE_BIOMETRIC (added in API 28) permission in the app's manifest.

There are methods to determine if fingerprint authentication is possible; detecting if hardware exists and there is at least one fingerprint enrolled.

Engaging the fingerprint reader is done by calling authenticate(). Before doing so, the app must provide a FingerprintManager.CryptoObject and a FingerprintManager.AuthenticationCallback. This method returns immediately; further interaction happens via the passed-in AuthenticationCallback. An optional CancellationSignal can be provided to disengage the fingerprint hardware out-of-band (e.g., from the user clicking a "Cancel" button).

This object only engages the hardware; it does not display anything to the user itself. The app is responsible for managing a view regarding the fingerprint reading operations.

Once the reader has succeeded or errored, it is no longer valid; a new instance must be obtained.

About the CryptoObject

The required CryptoObject wraps a Cipher, Mac or Signature object, ready and initialized with the desired key. the scanner is engaged regardless of the key's authentication requirements, so even keys without any requirements can be used.

About the AuthenticationCallback

The required AuthenticationCallback is where events from fingerprint reader attempts are dispatched. Subclasses need only override the event methods they are interested in.

On success onAuthenticationSuccessful() is called with the original CryptoObject wrapped in a AuthenticationResult. If the key required authentication, it is now useable within this method's bounds (and only now if authentication is required on every use).

A fingerprint read failure is notified via onAuthenticaitonFailed(), such as a unrecognized print. If there is some other temporary failure (e.g., dirty reader), onAuthenticationHelp() is called with the relevant status code and a (device locale appropriate) user-directed help message.

Permanent errors are notified via onAuthenticationError(), with the relevant status code and (device locale appropriate) user-directed error message.

Using BiometricPrompt

The BiometricPrompt is a class used to engage a device's biometrics hardware using a system-provided dialog. This class is introduced in API 28 to replace FingerprintManager. The intent is to support not only fingerprint readers, but also facial recognition; it also handles hardware variations, such as in-screen fingerprint readers (e.g., display a user prompt that indicates the on-screen boundaries of the reader).

To create a BiometricPrmpt, a BiometricPrompt.Builder is created (with an appropriate Context) and configured. The app can set a title, subtitle, description, and "cancel" button behavior. Once created, the app calls authenticate() (just like with FingerprintManager).

The hardware-engagement behavior is nearly identical to FingerprintManager; the app is notified of events via an instance of BiometricPrompt.AuthenticationCallback (which has the exact same methods as FingerprintManager.AuthenticationCallback), can be canceled via a CancellationSignal, and operates on a CryptoObject. The biggest differences are:

  • The CryptoObject is optional
  • The app must explicitly provide an Executor where events are dispatched (this can be Context.mainExecutor)
  • The app no longer manages any view to interact with the user.