This is a follow-up to The Mess that is Android Biometrics, as we found even more wonderful surprises.

So the androidx biometric compat lib is out and stable. And it (mostly) blacklists Samsung devices so they are forced to use fingerprint. We can plop it in and all our woes are gone right? Sigh, I guess you can see where this is going. I wouldn’t be writing another blog post otherwise.

When the user enables biometrics in our app, we show the biometrics prompt. While is not strictly necessary, it is a nice assurance to the user that it’s working correctly. Well it turns out that while the blacklist solved the Samsung issue on login, it did not solve it for this prompt. This lead to an interesting concept about android’s biometrics implementation that’s not really called out anywhere in the documentation.

Wait, not all biometrics are supposed to be secure?

Android actually supports two forms of biometrics: secure and insecure. Secure biometrics pass a certain level of security requirements which allows them to unlock encryption keys. Insecure biometrics… do not. What’s the point in prompting for insecure biometrics that can’t be used for encryption? Who knows? I guess security theater could be important to somebody. Well when we were showing the prompt to enable biometrics we were not passing in a cipher as we didn’t need one. Unfortunately, this means that insecure biometrics are allowed and so it doesn’t use the blacklist. So lesson learned: always pass in the cipher when showing the prompt to get a consistent experience.

Did anyone know this?

It seems like even the people who created the biometircs api didn’t realize this. We got BiometricManager.canAuthenticate() in api 29, but this method does not distinguish between secure and insecure biometrics. So if you are using biometrics for it’s intended purpose, it’s useless. Luckily, there is another way. If you try to set up a key when the user has no secure biometrics enrolled it’ll throw an exception.

fun canSecurelyAuthenticate(): Boolean {
    try {
        val keystore = KeyStore.getInstance("AndroidKeyStore")
        KeyGenerator.getInstance("AES", keystore.provider)
    	    .init(
    		    KeyGenParameterSpec.Builder("DUMMY_KEY_ALIAS", KeyProperties.PURPOSE_DECRYPT)
    			    .setUserAuthenticationRequired(true)
    			    .build()
    	    )
        return true
    } catch (e: InvalidAlgorithmParameterException) {
        // expected error if user isn't enrolled in secure biometrics
        return false
    } catch (e: Exception) {
        // Log unexpected errors, though if there's an issue with the keystore we probably can't use
        // biometrics anyway.
        Log.w("keystore error", e)
        return false
    }
}

What now?

A feature request has been filed for having a secure form of BiometricMnager.canAuthenticate(). It appears that the necessary api will be available in Android 11 and hopefully there will be some androidx workaround. In the meantime, I’ve been maintaining a sample repo that collects all the workarounds necessary for getting biometrics to work correctly.