Biometric Authentication On Android

Must Needed

Gaurav Kumar Mathur
Analytics Vidhya
Published in
11 min readOct 18, 2020

--

To protect private and sensitive information, many apps require the user to log in. If your app supports the traditional login experience, it probably works similarly to the process that’s depicted in figure 1. The user enters a username and password, the app sends the credentials to a remote server, and finally the remote server returns a userToken that the app can then use later to query the remote server for restricted data. Whether you require your user to login each time they open the app or just once per installation, figure 1 works fine.

Figure 1: Authentication without biometrics

However, there are several drawbacks to using the process depicted in figure 1:

  • If it’s used for per-session authentication, which banking apps use, then the process becomes cumbersome very quickly, as the user is required to enter a password every time they open the app.
  • If it’s used for per-install authentication, which email apps use, then the device owner’s private content is visible to anyone who just happens to be holding the device since it does not verify the owner’s presence.

To help overcome these drawbacks, biometric authentication offers a number of conveniences that makes the authentication process easier for end-users and more attractive for developers — even developers who might otherwise not require frequent login in their apps. Key among these benefits is that using biometric authentication is as easy as tapping a sensor or looking at your device. And importantly, as a developer, you get to decide how often a user must re-authenticate — once a day, once a week, every time they open the app, etc. All in all, the API surface has a number of features that makes login easier for developers and their users.

Today many apps that handle personal data, such as email or social networking apps, tend to only require a one-time authentication upon installation. That practice was popularized at a time when entering a username and password every time a user opens an app adversely affected the user experience. But with biometric authentication, security doesn’t have to be so taxing on the user. Even if your app would normally require one-time authentication, you may consider requiring biometric authentication periodically to verify user presence. The length of the period is entirely up to you, the developer.

If an app requires authentication per session (or some such frequency as once every 2 hours or once per day, etc.), then looking at the device or tapping on a sensor is only marginally noticeable compared to having to type in a password each time. If an app only requires a one-time authentication, as many emailing apps do, then biometrics would add an additional layer of security at the small cost of the user simply picking up and looking at their device. If a user wants to continue keeping their emails open without re-authenticating, then they should have that choice. But for users who want a bit more privacy, then biometric authentication should provide that additional peace of mind. Either way, the cost on the end-user is minuscule especially compared to the added benefit.

Implementing biometric authentication using BiometricPrompt

The BiometricPrompt API allows you to implement authentication both with and without encryption. If you are working on an app that requires a stronger security system, like a healthcare app or a banking app, then you may want to tie your encryption keys to biometric authentication so as to verify user presence. Otherwise, you may want to implement biometric authentication as a convenience for your users. The code snippets for both cases are very similar, except that for the encryption implementation you would pass in a CryptoObject, whereas for the convenience implementation you would leave out the CryptoObject parameter.

Encryption version:

biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))

While in the code snippet above we pass a Cipher to the CryptoObject, you are free to pass a number of alternatives, such as a Mac or Signature.

No CryptoObject version:

biometricPrompt.authenticate(promptInfo)

To implement biometric authentication in your Android app, use theAndroidX Biometric library. Although the API handles different modalities (fingerprint, face, iris, etc.) automatically under the hood, as a developer you still get to choose the desired security level of biometrics that your app will accept by setting setAllowedAuthenticators()as shown in the code snippet below. Class 3 (formerly Strong) means you want biometrics that unlock credentials stored in the Keystore (i.e. cryptography); Class 2 (formerly Weak) means you just want to unlock your app without relying on credentials that are further protected by cryptography. There is a Class 1, but it doesn’t work with apps. See the Android Compatibility Definition Document for more details.

fun createPromptInfo(activity: AppCompatActivity): BiometricPrompt.PromptInfo =   BiometricPrompt.PromptInfo.Builder().apply {      setAllowedAuthenticators(BIOMETRIC_STRONG)      // Continue setting other PromptInfo attributes such as title,  subtitle, description   }.build()

Encryption and auth-per-use keys vs time-bound keys

An auth-per-use key is a secret key that can be used to perform one cryptographic operation. So, for instance, if you want to perform ten cryptographic operations then you have to unlock the key ten times. Hence the designation auth-per-use: you must authenticate (i.e. unlock the key) each time you use it.

A time-bound key, on the other hand, is a secret key that is valid for a certain time period — which you establish beforehand by passing a number of seconds to setUserAuthenticationValidityDurationSeconds. If the number of seconds you pass to the time-bound function is -1, the default value, then the system assumes you want auth-per-use. For all other numbers, we recommend three seconds or more, the system honors the duration you set. To easily create time-bound keys, see the MasterKeys class in Jetpack Security.

Normally — in conjunction with the aforementioned -1 — you would pass a CryptoObject to BiometricPrompt.authenticate() to request auth-per-use. However, instead of using a CryptoObject, you could set a very short duration, such as 5 seconds, to use a time-bound key as if it were an auth-per-use key. The two approaches are practically equivalent for showing user presence, so the choice is up to you as to how you want to design your app’s UX.

Regarding what’s happening under the hood: When you use CryptoObject, the secret key is unlocked only for the specified operation. This is because the Keymint(or Keymaster) gets a HardwareAuthToken (HAT) with a specific operationId. The secret key gets unlocked and you can only use it for the operation represented by the Cipher/Mac/Signature operation you wrap around the CryptoObject, and you can only perform the specified operation once before it locks again — it’s an auth-per-use key. When you don’t use a CryptoObject, the HAT that gets sent to the Keymint doesn’t have an operationId; therefore, the Keymint simply looks for a HAT with a valid timestamp (timestamp + time-based-key-duration > now), and you can use that key until its time expires — it’s a time-bound key.

At first glance, it may sound like a time-bound key can be accessed by any app as long as the time-window is valid. But the reality is that, short of a compromised user-space, there is no concern about some app X using some app Y’s keys or operations. The Android framework will not allow other apps to find or initialize another app’s operation.

Implementation Of Biometric Authentication

To augment your login process to include biometric authentication, prompt the user to enable biometric authentication right after they successfully sign in. Figure 1A shows a typical sign-in flow, which you are probably already familiar with. After the user presses the sign-in button and a userToken is returned from the server, then prompt them to enable biometrics, as shown in figure 1B. Once the user has enabled biometric authentication, from then on the app should automatically present a biometric prompt each time the user needs to sign in, as shown in figure 2.

Figure 1A: Typical sign-in UI
Figure 1B: Enable biometric authentication
Figure 2: Confirm biometric authentication

While figure 2 shows a confirmation button in the UI, that button is actually optional. For instance, if you are building a restaurant app, it’s recommended that you show this confirmation button because the biometric authentication allows patrons to pay for their meals. For sensitive transactions and payments, we recommend that you require confirmation. To include a confirmation button in your app’s UI, call setConfirmationRequired(true) while building your BiometricPrompt.PromptInfo. Note that if you don’t call setConfirmationRequired() the system will set it to true by default.

Biometrics Design Flow

By way of example, the code snippets use the encrypted version of BiometricPrompt, with CryptoObject.

If your app requires authentication, you should create a dedicated LoginActivity component that serves as your app’s landing page. This is important no matter how frequently you require your users to authenticate, so long as authentication is required. If the user is already authenticated, then that LoginActivity will call finish(), and the user can just move along. If the user is not yet authenticated, then you should check whether biometrics has been enabled.

There are a few ways to check whether biometrics has been enabled. However, instead of wading through the diverse alternatives, let’s dive into one in particular: a null check on the custom property ciphertextWrapper. The CiphertextWrapper is a data class that you create so that you can conveniently store the encrypted userToken, a.k.a ciphertext, in persistent storage such as SharedPreferences or Room when the user successfully enables biometric authentication for your app. Therefore, if ciphertextWrapper is not null, then you have the encrypted version of the userToken that’s needed to access the remote server — which is to say biometrics is enabled.

If biometrics is not enabled, then the user may click to enable it (figure 1B), at which point you would show the user the actual biometric prompt, as shown in figure 3.

In the following snippet, showBiometricPromptForEncryption() shows how to set up a cryptographic key that you associate with the biometric prompt. Essentially, you initialize a Cipher from a String, and then pass the Cipher to CryptoObject. Finally you pass the CryptoObject to biometricPrompt.authenticate(promptInfo, cryptoObject).

Figure 3: Prompt to enable biometrics

At this point, i.e. figure 2 and figure 3, the app only has the userToken. But unless the user will keep using their password every time they open the app, that userToken needs to be stored in the app’s filesystem for later sessions. However, if you store the userToken without first encrypting it, then an attacker with unauthorized access to the device can potentially read the userToken and use it to grab data from the remote server. Hence, it’s good to encrypt the userToken before saving it locally. This is where the biometric prompt in figure 3 comes into play. As the user authenticates with their biometrics, your objective is to use BiometricPrompt to unlock a secret key (either auth-per-use or time-bound) and then use that key to encrypt the server generated userToken before storing it locally. From now on, when the user needs to sign in, they can use their biometrics to authenticate (i.e. biometric authentication -> unlock secret key -> decrypt userToken for server access).

Note the distinction between when the user first enabled biometrics and all the subsequent times when the user is actually signing-in using biometrics. To enable biometric authentication, the app calls showBiometricPromptForEncryption(), which initializes a Cipher for encrypting the userToken. To actually sign-in with biometrics, on the other hand, the app calls showBiometricPromptForDecryption(), which initializes a Cipher for decryption and then uses the Cipher to decrypt the userToken.

Having enabled biometric authentication, the user should see the biometric prompt to authenticate next time they return to the app, as shown in figure 4. Notice that because figure 4 is for signing into the app — as opposed to figure 2 which is for transaction confirmation — no confirmation is necessary since app login is a passive, easily-reversible action.

Figure 4

To implement this flow for your user, when your LoginActivity verifies that the user has already authenticated, you would use the cryptographic object, unlocked by successful BiometricPrompt authentication, to decrypt the userToken and then call finish() on the LoginActivity component.

The Complete Picture

Figure 5 shows a complete flow diagram of the recommended engineering design. We are fully aware that your code may deviate from this recommendation in a number of places. For instance, your own cryptographic solution may require unlocking the key for encryption only and not for decryption. Still we provide a complete sample solution for those who may need it.

Wherever the diagram mentions secret key, you are free to use an auth-per-use key or a time-bound key. Also wherever the diagram mentions “the app’s memory,” you are free to use your favorite solution for structured data storage: SharedPreferences, Room, or anything else. Finally a userToken, as it’s typically called, is any sort of server payload that would give a user access to restricted data or services. The server would normally check for the presence of such payload as evidence that the caller is authorized.

In the diagram, the arrow from “encrypt userToken” could very well go to “login completed” instead of back to “LoginActivity”. Nonetheless, we choose “LoginActivity” to call attention to the fact that it’s okay to use an additional Activity, e.g. EnableBiometricAuthActivity, after the user clicks to “enable biometrics”. Using a separate activity may make your code more modular and therefore more readable. Alternatively, you can create a LoginActivity with two Fragments (backed by a navigation component): one Fragment for the actual authentication flow and one Fragment that responds to the user clicking “enable biometric”.

In addition to this engineering flow diagram, here is design guideline that you can follow when implementing your app. Also,this sample on Github should provide you with further insight.

Figure 5: Complete diagram of biometric login with remote server

Summary

In this post, you learned the following:

  • Why username+password only authentication is problematic.
  • Why it’s a good idea to include biometric authentication in your app.
  • Design considerations for different types of apps.
  • How to invoke BiometricPrompt with or without encryption.
  • The difference between auth-per-use vs time-bound encryption keys.
  • Which UI assets to use to augment your login process with biometrics.
  • The critical user journeys your app should address for biometric authentication.
  • How to design your code to handle the different aspects of biometric authentication.
  • A complete engineering picture of how your login system should flow.

Happy Coding!!

--

--

Gaurav Kumar Mathur
Analytics Vidhya

Software Engineer |Google Developer |Problem Solver| Cloud Enthusiast