Security pitfalls in authenticating users and protecting secrets with biometry on mobile devices (Apple & Android) feature image

In a previous post we presented Windows Hello which is the solution to protect secrets and authenticate users using biometry (fingerprint, face recognition, iris…) on modern Microsoft Windows.

Biometry in the consumer world was first introduced on mobile devices, and especially Apple and Android platforms. Therefore, we will see here what they offer, and security pitfalls similar to the one highlighted in Windows Hello.

Apple MacOS and iOS: Touch ID / Face ID 🔗

This article is oriented towards mobile devices but Apple offers similar features and APIs on MacOS and iOS.

Apple has documented a solution around their LocalAuthentication framework which seems to be the equivalent of Windows Hello.

Authenticate users biometrically or with a passphrase they already know.

This framework leverages the Secure Enclave to perform biometry operations and offer credentials management.

User authentication 🔗

If an app only wants to authenticate users, it can use this feature. The Secure Enclave, and then the OS, will return a boolean value indicating if the user authentication through biometry (or passcode, if this fallback is accepted by the developer) was successful or not.

Refer to the Apple article “Logging a User into Your App with Face ID or Touch ID”

⚠️ This feature is convenient, but in my understanding, it is the same as the Windows UserConsentVerifier. A client-side check that could be patched, skipped or simply removed in an updated version of the app. The mitigating factor being that tampering with an iOS device, even if it is obtained unlocked, is usually harder than on a Windows computer.

Secrets protection 🔗

However, this framework also allows apps to protect credentials strongly tied to biometry.

The equivalent of the Windows PasswordVault in the Apple world is the keychain that allows apps to securely store user secrets. The keychain allows developers to tweak the security level of each individual item, depending on the desired balance of security and usability. More specifically, a keychain item can be configured to “demand user presence” (through biometry, or passcode, or any of those, depending on the developer’s choice) each time it is accessed.

Refer to the Apple article “Accessing Keychain Items with Face ID or Touch ID”

💡 I have not verified the security of its implementation, but this usage of the Secure Enclave to both hold the credentials and perform the biometry check, possibly in one operation, does not seem to be vulnerable to similar attacks.

It seems that most banking apps on iOS, allowing customers to login with Touch ID or Face ID, use this.

If you know better the Apple environment and have found a mistake above, please do not hesitate to contact me! 😉

Android 🔗

We will see first the available features for dealing with biometry in Android. Then, we will see which usages are secure and which are not!

Available features 🔗

KeyStore and KeyguardManager 🔗

Unfortunately Android is not as straightforward as Apple. Android has a KeyChain class (since API level 14 = Android 4.0 = Ice Cream Sandwich) but it is oriented towards manipulating encryption keys, and not directly credentials, and it does not allow to directly protect an entry with biometry.

In addition to the KeyChain, Android has always had a KeyStore component which comes from Java (java.security package). It lets developers generate and store cryptographic keys in a more robust container.

An application can also require user authentication for key usage. For this, there are two operation modes described as:

  • “User authentication authorizes the use of keys for a duration of time” which is the case when the setUserAuthenticationValidityDurationSeconds(int) method is used when generating the key.

    The key is unlocked if the device has been unlocked recently, otherwise the KeyguardManager class has a createConfirmDeviceCredentialIntent() method to validate credentials.

    ⚠️ This method could also be used standalone, but in this case it is similar to Windows UserConsentVerifier in the way that it is only a client-side check that does not really protect credentials.

    Also, note that this method does not allow to differentiate or select between a “what you know” factor (PIN, pattern, password) and a biometric factor.

  • “User authentication authorizes a specific cryptographic operation associated with one key” which is the case when the setUserAuthenticationRequired(boolean) method is used when generating the key.

    This method seems to support only biometry: “the only means of such authorization is fingerprint authentication”.

FingerprintManager and BiometricPrompt 🔗

The android.hardware.fingerprint package has the FingerprintManager class with its authenticate() method, since API level 23 = Android 6 = Android Marshmallow.

And starting from API level 28 = Android 9 = Android Pie, FingerprintManager is deprecated and replaced by the android.hardware.biometrics package that has the BiometricPrompt class with its authenticate() methods. The main difference is that the UI is managed by the Android OS, instead of the app, so the user experience is consistent across all apps. This also supports other kinds of biometry such as face recognition and iris.

FingerprintManager and BiometricPrompt can be used standalone, just to check if the user can authenticate with biometry. There is one sample on GitHub that implements that (see the accompanying article).

FingerprintManager and BiometricPrompt can also be used to unlock CryptoObjects, which are in turn used for encryption, signature or MAC. The difference is that FingerprintManager only has one authenticate() method, but the CryptoObject can be null, whereas BiometricPrompt has two authenticate() methods, one with a mandatory CryptoObject: authenticate(BiometricPrompt.CryptoObject crypto, CancellationSignal cancel, Executor executor, BiometricPrompt.AuthenticationCallback callback), and one without: authenticate(CancellationSignal cancel, Executor executor, BiometricPrompt.AuthenticationCallback callback). Regarding what we are interested in here, it does not seem to change anything and we will make the same observations for both.

Note that I have found examples where CryptoObjects are created to trigger an authentication, but are not actually used for any crypto operation afterwards.

First example, presented as an alternative for BiometricPrompt on older devices, through FingerprintManager:

  1. A key is generated with user authentication required

    .setUserAuthenticationRequired(true)
    
  2. Transformed to a CryptoObject

    cryptoObject = new FingerprintManagerCompat.CryptoObject(cipher);
    
  3. Used in a call to authenticate

    fingerprintManagerCompat.authenticate(cryptoObject, 0, new CancellationSignal(),
    

This code closely inspired a second example, the react-native-touch-id library which offers a common Touch ID experience for both Android and iOS:

  1. A key is generated with user authentication required
  2. Transformed to a CryptoObject
  3. Used in a call to authenticate

(In)secure usages 🔗

All these features, with different optional arguments, ultimately fall into two categories: insecure and secure 🙂

⚠️ If an app only uses this to authenticate users, then my understanding is that this is similarly flawed as Windows UserConsentVerifier. A client-side check that could be patched, skipped or simply removed in an updated version of the app.

This is the case when:

  • the authenticate methods of FingerprintManager or BiometricPrompt are called with a null crypto object, or without any
  • the createConfirmDeviceCredentialIntent method of KeyguardManager is called simply to validate credentials
  • keys are created without requiring user authentication

💡 Fortunately, there are secure usages too! These are the opposites cases and they can be summarized by the fact that keys (for signing or encryption operations) are created linked to users’ biometry. These keys must then be used for the intended operation.

This is clearly explained in a StackOverflow answer to “Why crypto object is needed for Android fingerprint authentication?”. Its author has released the Fingerprint library which can be used in a secure mode (with CryptoObject), but also in an insecure mode (without CryptoObject).

External references 🔗

The insecure usage we discussed was highlighted in the paper “Broken Fingers: On the Usage of the Fingerprint API in Android” (Antonio Bianchi, Yanick Fratantonio, Aravind Machiry, Christopher Kruegel, Giovanni Vigna, Simon Pak Ho Chung, Wenke Lee). They refer to it as the “weak usage” of the fingerprint API, and they found that half of the analyzed apps using this API fall into the “weak usage” category 😲! I encourage you to read this detailed paper which explains other possible mistakes.

The Firefox Lockbox for Android, which is Mozilla’s experimental password manager, has an interesting article “on using keys and biometrics” which wraps-up all of these. You can also get a look at the code of their implementation.

The fingerprint API is also nicely explained in the amazing MOBISEC course from Yanick Fratantonio.

F-Secure Labs also covered this issue and the more general topic of Android KeyStore in “How Secure is your Android Keystore Authentication ?”.

If you know better the Android environment and have found a mistake above, please do not hesitate to contact me!