PIN authentication fades into the background because fingerprint authentication is more quickly and comfortable for application users. According to Androidauthority, over 70 percent of smartphones shipped in 2018 will have fingerprint sensors
You can see global smartphone fingerprint sensor penetration on the graphic below:
Android provides different ways of using fingerprint authenticate. The most popular of them is FingerprintManager added in Android Marshmallow (API level 23). But with the release of the Android Pie (API level 28) it was deprecated. FingerprintManager was replaced by BiometricPrompt. It’s a class that manages a system-provided biometric dialogue. For support device with versions less than API 28 android support library has BiometricPromtCompat, but sometimes it works incorrect (with bugs and application crash). For temporarily fix we can create your own realization and provide FingerprintManager (for 23 =< API version < 28) and BiometricPrompt (API version >= 28) with our own custom AuthManager.
So, let’s start to provide both of them into your project.
It’ll be a simple manager with a provider which will allow using BiometricPrompt for devices with API version >= 28 and FingerprintManager for devices with API version < 28 (but not less than 23).
But previously let’s create and talk about fingerprint device states.
So, according to states, we should create an enum class with context extension function for check Android fingerprint device state.
enum class FingerprintState(val stateMessage: String) { FINGERPRINT_ALLOW("Everything will be nice :)"), VERSION_DOES_NOT_ALLOW("Your device version doesn't allow for fingerprint"), DEVICE_HARDWARE_DOES_NOT_ALLOW("Your device hasn't hardware for fingerprint"), NO_ENROLLED_FINGERPRINTS("Please, add fingerprint for device in security settings"), UNKNOWN_STATE("Ooooops, wtf"); }
fun Context.checkDeviceFingerprintState() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val fingerprintManager = this.getSystemService(Context.FINGERPRINT_SERVICE) as? FingerprintManager if (fingerprintManager == null) { FingerprintState.UNKNOWN_STATE } else if (!fingerprintManager.isHardwareDetected) { FingerprintState.DEVICE_HARDWARE_DOES_NOT_ALLOW } else if (!fingerprintManager.hasEnrolledFingerprints()) { FingerprintState.NO_ENROLLED_FINGERPRINTS } else { FingerprintState.FINGERPRINT_ALLOW } } else { FingerprintState.VERSION_DOES_NOT_ALLOW }
Next step is creating AuthManager. It’ll be base cascade for biometric and fingerprint managers (look at the pseudo scheme below).
AuthManager it’s a simple Kotlin interface and Biometric, Fingerprint managers are realization according to API version.
For listen, user auth state AuthenticationListener should be created. There are three states when a user using a fingerprint feature:
interface AuthManager { fun authenticate() fun attachAuthListener(authenticationListener: AuthenticationListener) }
interface AuthenticationListener { fun onAuthSuccess() fun onAuthFailed() fun onAuthCancel() }
Now let’s create realizations that implement AuthManager interface, but previously we must add permissions in the Android manifest file:
<uses-permission android:name="android.permission.USE_BIOMETRIC"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
For creating Biometric manager, we should use BiometricPrompt. Builder which builds BiometricPrompt instance. As you remember, BiometricPrompt is a simple dialogue for authenticating with user fingerprint. So, you can build a biometric dialogue with your own title, subtitle, description, etc.
For call system touch sensor listen you should call authenticate() method from BiometricPrompt instance. You must set AuthenticationCallback and CancellationSignal. It’ll look like in the code below:
override fun authenticate() { val appName = context.getString(R.string.app_name) val biometricPrompt = BiometricPrompt.Builder(context) .setTitle(context.getString(R.string.fingerprint_dialog_title_text)) .setDescription(context.getString(R.string.biometric_dialog_description_text, appName)) .setNegativeButton( context.getString(R.string.cancel_button_text), context.mainExecutor, DialogInterface.OnClickListener { _, _ -> mAuthenticationListener?.onAuthCancel() }) .build() biometricPrompt.authenticate( getCancellationSignal(), context.mainExecutor, mBiometricAuthCallback ) } private fun getCancellationSignal(): CancellationSignal { // With this cancel signal, we can cancel biometric prompt operation val cancellationSignal = CancellationSignal() cancellationSignal.setOnCancelListener { mAuthenticationListener?.onAuthCancel() } return cancellationSignal }
You can also set CryproObject for BiometricPrompt authenticate() method.
Using AuthenticationCallback for BiometricPrompt authenticate() method we can send events with our AuthenticationListener like in the code below:
private val mBiometricAuthCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { mAuthenticationListener?.onAuthFailed() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { mAuthenticationListener?.onAuthSuccess() } override fun onAuthenticationFailed() { mAuthenticationListener?.onAuthFailed() } }
For realization this manager we can create your own dialogue. We should start and stop listening for touch sensor (authenticate() method in FingerprintManager), and initialize FingerprintManager.CryptoObject for this (like in the code below).
private fun isCipherInitSuccess(): Boolean { return try { initKeyStoreAndKeyGenerator() mKeyStore = KeyStore.getInstance(ANDROID_KEY_STORE) mKeyStore.load(null) val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, mKeyStore.getKey(BIP_KEY_ALIAS, null)) mCryptoObject = FingerprintManager.CryptoObject(cipher) true } catch (e: Exception) { Log.d(TAG, "isCipherInitSuccess: ${e.message}") false } } private fun initKeyStoreAndKeyGenerator() { fun logException(e: Exception) { Log.d(TAG, "initKeyStoreAndKeyGenerator: ${e.message}") e.printStackTrace() } try { mKeyStore = KeyStore.getInstance(ANDROID_KEY_STORE) mKeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) mKeyStore.load(null) val keyProperties = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT val builder = KeyGenParameterSpec.Builder(BIP_KEY_ALIAS, keyProperties) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setUserAuthenticationRequired(true) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) mKeyGenerator.init(builder.build()) mKeyGenerator.generateKey() } catch (e: KeyStoreException) { logException(e) } catch (e: IOException) { logException(e) } catch (e: NoSuchAlgorithmException) { logException(e) } catch (e: CertificateException) { logException(e) } catch (e: InvalidAlgorithmParameterException) { logException(e) } }
For creating separation between our own fingerprint dialogue logic and UI logic we can create Fingerprint UiHelper with Lottie animation realization. Also, we should listen to FingerprintManager.AuthenticationCallback() for change Lottie animation and send our events using our AuthenticationCallback.
P.S. Creating fingerprint authenticate dialogue is very useful for creating and customizing dialogue design that should match the design of your application.
For providing auth manager you are can use DI libraries. Or you can create a provider which throw our own exception with FingerprintState instance like this:
@RequiresApi(api = Build.VERSION_CODES.M) class AuthManagerProvider { companion object { @Throws(AuthManagerProvideException::class) fun provideAuthManager(context: Context, authenticationListener: AuthenticationListener): AuthManager { val fingerprintState = context.checkDeviceFingerprintState() return if (fingerprintState == FingerprintState.FINGERPRINT_ALLOW) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && context.isBiometricSupported()) { BiometricAuthManager(context).apply { attachAuthListener(authenticationListener) } } else { FingerprintAuthManager(context).apply { attachAuthListener(authenticationListener) } } } else { throw AuthManagerProvideException(fingerprintState) } } } }
After AuthManager providing, you can use authenticate() method for authenticating user, listen AuthenticationListener and create your own realization for listener methods.
You can check the full code in GitHub.
Martin Smith says:
Good to know about this info, this helps a lot to keep our data secure. Thanks for sharing the informative article…
sowmyasri says:
Great share, good to know about this …
marlene mcquillen says:
I don’t ordinarily comment but I gotta say regards for the post on this amazing one :D.
shaynahale says:
Hi, just wanted to tell you, I liked this article. It was practical.
Keep on posting!
pearline.degillern says:
J’épluche tous des articles du site toujours super écrit ne changez
rien