How you should secure your Android’s app biometric authentication

Thomas Lim
CSG @ GovTech
Published in
7 min readAug 19, 2020
Image source — https://www.progress.com/blogs/how-to-integrate-biometric-authentication-in-ios-and-android

Introduction

Vulnerabilities related to authentication and authorisation have always been consistently ranked within Open Web Application Security Project (OWASP)’s Top 10 Mobile Risk. In this article, I will be sharing my observations on Android’s app biometric authentication.

Android’s app biometric authentication works by authenticating the user against credentials stored on the device, such as Keystore. The KeyStore serves as a storage facility for cryptographic keys and certificates. In this case, the user can authenticate using functionalities like:

· PINs or Passwords, or

· Biometrics, such as facial features, irises, or fingerprints.

Developers can also choose between two types of key material to implement and store in Keystore using biometric authentication:

1. Symmetric keys

These work like passwords. The Android Developer Guide recommends the use of symmetric keys to secure access to databases or offline files.

2. Asymmetric keys

In this case, the app will provide a key pair, comprising of a public key and a private key. The public key can be safely sent across the internet and stored on a remote server. The private key can later be used to sign data, such that the signature can be verified using the public key. Signed data cannot be tampered with, and positively identifies the original author of the data. Asymmetric keys are preferred for network logins or authenticating online transactions.

Biometric Authentication

While biometric authentication offers users a convenient way to prove their identities, it also introduces an additional attack surface. The Android Developer Documentation provides an interesting overview and indicators for measuring biometric security on a mobile device.

The Android platform offers three different classes for biometric authentication:

1. BiometricManager — Used on devices running on Android 10 (API 29) and above, this class queries the framework’s version of BiometricManager. On Android 9.0 (API 28) and prior versions, it queries FingerprintManagerCompact. The BiometricManager that can be used to verify biometric hardware is available on the device and is configured by the user.

2. BiometricPrompt — Introduced in Android 9.0 (API 28), it serves as a class that manages a system-provided biometric dialog. The BiometricPrompt class is a significant improvement to FingerprintManager as it provides the user interface (UI) for biometric authentication on Android and supports more sensors than just the fingerprint sensor.

3. FingerprintManager -deprecated in Android 9.0 (API 28).

BiometricPrompt architecture from https://source.android.com/security/biometric

Note: This article will focus on BiometricManager and BiometricPrompt. FingerprintManager has been deprecated and will not be covered in this article.

An example of a biometric authentication bypass

Research conducted by F-Secure Labs revealed that 70% of the sampled mobile applications on Google Play store that have implemented biometric authentication can be easily bypassed. Additionally, 50% of these applications store sensitive data, which can be retrieved by attackers without valid biometric credentials. To demonstrate the potential impact of an insecure biometric authentication, I have written a demo app to lock/unlock the EditText field using one’s biometric credentials.

Figure 2 — Simple app where users can enter text into the EditText field
Figure 3 — App prompts the user to present their biometric credential to lock the text
Figure 4 — EditText field was locked after user authentication

The authentication flow of the application I wrote behaves just like any other mobile application that uses biometrics for authentication. Let’s have a look at its underlying logic flow:

The first function, setupBiometricPrompt, sets up the system dialog requesting for biometric authentication. The second function, showBiometricDialog, displays the dialog and authenticates the user when the credential is presented by passing promptInfo as a parameter. The BiometricPrompt.AuthenticaionCallback() parameter implements the onAuthenticationSucceeded(), onAuthenticationError() and onAuthenticationFailed() listeners when a user authenticates on the device. For this implementation, the callback method is considered insecure because onAuthenticationSucceeded() does not use CrytoObject to unlock cryptographic operations. onAuthenticationSucceeded() simply assumes that the authentication was successful since the method was called. As such, attackers can create malware to hook onto the mobile application process and call onAuthenticationSucceeded(), bypassing the authentication without providing valid biometrics as proof-of-identity.

PS: The Fingerprint sensor at the back of the device

To verify this flaw, we can attempt to perform a biometric bypass using the following Frida Script from F-Secure. The script hooks onto the authentication logic flow by passing a null crypto object to the authenticating method to trigger the onAuthenticationSucceeded() callback instead of onAuthenticationFailed().

To attempt the bypass, we run the script using the following command:

frida -U -f <App Name> -l hook_biometric_bypass.js

Note that the biometric prompt did not appear when the app is hooked using Frida.

Voila! An attacker has successfully bypassed biometric authentication!

How to implement local authentication securely?

Google’s Android Developer Guide outlines instructions on the use of biometric authentication. Notably, it recommends implementing CrytoObject — only if you feel that it is necessary. In the example above, however, I highlighted how rooted devices are susceptible to malware that bypasses local authentication via runtime tampering even if the mobile app does not handle any sensitive information. Thus, I highly recommend all developers to consider implementing CrytoObject in Kotlin (please refer to the Java version in GitHub).

To check if your device supports biometric authentication, developers can invoke BiometricPrompt by using the canAuthenticate() method in the BiometricManager class.

As biometric authentication requires the display of a system prompt for users to authenticate using their biometric credentials, this is how it can be achieved using the biometric library:

First, implement the setup by adding a dependency for the biometric library in your app/build.gradle file.

dependencies {implementation 'androidx.biometric:biometric:1.0.1'}

Now, to ensure a secure implementation, it is important to integrate your BiometricPrompt authentication method using a cryptographic operation. This will further protect sensitive information within your app using the following parameters:

· crypto — contains a reference to the KeyStore entry that should be unlocked. To implement local authentication securely, the KeyStore key that is inside of this crypto object must be used for some application cryptographic operation.

· callback — will be called by the Operating System (OS) when the user places his/her finger on the fingerprint sensor, or when the prompt is cancelled.

To implement the cryptographic objects, please use the following logic code:

Your mobile app should use a secret key that requires biometric credentials to unlock. The user will then be required to present their biometric credentials each time before your app accesses the key. These three steps ensure a secure implementation to generate the secret key:

Step 1: Generate a key that uses the following KeyGenParameterSpec configuration.

Step 2: It is important to invalidate the biometric key if the user has registered a new biometric credential on the mobile device by setting the parameter setInvalidatedByBiometricEnrollment to true.

Step 3: The parameter setUserAuthenticationValidityDurationSeconds should be set to -1. This would strictly allow the key to be unlocked using fingerprint or biometrics. Do note that if it is set to any other value, the key can be unlocked using a device screen lock too.

The code block below details the implementation:

Now, when the user presents his/her biometric credentials, a secure workflow would incorporate a cipher during the authentication. This can be achieved by implementing BiometricPrompt.AuthenticationCallback.onAuthenticationSucceeded callback that retrieves the cipher object from the parameter for encryption or decryption of sensitive information.

An example could be the decryption of existing session key:

We can validate this application authentication flow using the script earlier and this time, an attacker would not be able to perform a biometric bypass as the application now expects a crypto object and does not allow a null value to perform onAuthenticationSucceeded() callback. For my example, attempting to run the bypass will result in the mobile app to crash.

Conclusion

In summary, developers should not simply implement BiometricPrompt API calls to perform authentication as an attacker can easily bypass such security measures using available Frida JavaScript. Instead, developers should use the following authentication flow:

1. The app creates a key in the Android KeyStore with setUserAuthenticationRequired and setInvalidatedByBiometricEnrollment set to true. Additionally, setUserAuthenticationValidityDurationSeconds should be set to -1.

2. Note that this key is used to encrypt information that is authenticating the user (e.g. session information or access token to server resources).

3. A valid set of biometrics must be presented by the user before the key is released from the KeyStore to decrypt the data — validated through the authentication method (face/fingerprint) and the CryptoObject.

4. This solution cannot be bypassed, even on rooted devices as the key from the KeyStore can only be used after successful biometric authentication.

Lastly, it is crucial for developers to use the CryptoObject to encrypt/decrypt data for the application to function properly. This will prevent attackers from using runtime tampering tools to bypass and return a null CryptoObject.

If you would like to learn more about the implementation, the project repository is available on GitHub here.

References

[1] https://owasp.org/www-project-top-ten/

[2]https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05f-testing-local-authentication

[3] https://labs.f-secure.com/blog/how-secure-is-your-android-keystore-authentication

[4] https://android-developers.googleblog.com/2015/10/new-in-android-samples-authenticating.html

[5] https://developer.android.com/jetpack/androidx/releases/biometric

Once again, special thanks to Hui Yi, Si Han, Leon, Zheng Kai, Medha and Serene for making this sharing possible!

--

--

Thomas Lim
CSG @ GovTech

Security Enthusiast | Mobile App Pen Tester | InfoSec Learner