This is the second article in the Android biometric authentication series,articleBy comparing the traditional authentication methods of user name and password with the different authentication methods of biometric identity, and introducing the different encryption methods of biometric encryption, to show why developers need to use biometric authentication technology in applications.
To extend the traditional login authorization process to support biometric authentication, you can prompt the user to enable biometric authentication after successful login. Figure 1A shows a typical login process that you may already be familiar with. When the user clicks the login button and the application obtains the userToken returned by the server, the user is prompted whether to enable it, as shown in Figure 1B. Once enabled, the application should automatically pop up the biometric authentication dialog box every time the user needs to log in, as shown in Figure 2.
△ Figure 1A: Typical login interface
△ Figure 1B: Enabling biometric authentication
△ Figure 2: Confirm login using biometric authentication
The interface in Figure 2 has a OK button, which is actually optional. For example, if you are developing a restaurant app, it is recommended to display this button because you can use biometric authentication to make the customer pay for the meal. For sensitive transactions and payments, we recommend that you ask the user for confirmation. To contain the confirm button in the interface, you can build BiometricPrompt. PromptInfo calls when setConfirmationRequired (true). Note here that if you do not call setConfirmationRequired(true), it will be set to true by default.
Access to the biometric design process
The code in the example uses an encrypted version of BiometricPrompt with an instance of CryptoObject.
If your application requires authentication, you should create a dedicated LoginActivity component as the application’s login interface. Whenever authentication is required, it should be done no matter how often your application requires it. If the user has been authenticated before, LoginActivity calls the Finish () method to let the user continue. If the user has not authenticated, you should check whether biometric authentication is enabled.
There are many ways to check if biometrics are enabled. Instead of juggling various alternatives, let’s dive right into one particular method: check the custom property ciphertextWrapper for NULL. When the user has enabled biometric authentication in your application, you can create a CiphertextWrapper data class, To store the encrypted userToken (also known as cipherText) in a persistent store like SharedPreferences or Room. Therefore, if ciphertextWrapper is not NULL, you have the encrypted userToken required to access the remote service, which also means that biometrics are currently enabled.
if (ciphertextWrapper ! = null) {// user biometric enabled} else {// Biometric not enabled}Copy the code
If biometrics is not enabled, the user can click (as shown in Figure 1B) to enable it, and you will present the user with a biometrics authentication prompt, as shown in Figure 3.
The following code example, showBiometricPromptForEncryption () shows how to set up the associated with BiometricPrompt encryption key. Essentially, you initialize a Cipher from a String, and then pass that Cipher to CryptoObject. And then pass CryptoObject biometricPrompt. Authenticate (promptInfo CryptoObject) method.
binding.useBiometrics.setOnClickListener {
showBiometricPromptForEncryption()
}
....
private fun showBiometricPromptForEncryption(a) {
val canAuthenticate = BiometricManager.from(applicationContext).canAuthenticate()
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
val secretKeyName = SECRET_KEY_NAME
cryptographyManager = CryptographyManager()
val cipher = cryptographyManager.getInitializedCipherForEncryption(secretKeyName)
val biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(this, ::encryptAndStoreServerToken)
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
Copy the code
△ Figure 3: Prompt for biometric activation
In the scenario shown in Figures 2 and 3, only userToken data is applied. But unless the user enters a password every time the application is opened, the userToken needs to be persisted for subsequent sessions. However, if you store an unencrypted userToken directly, an attacker could hack into the device to read the plaintext userToken and then use it to retrieve data from a remote server. Therefore, it is best to encrypt the userToken before saving it locally, which is what the BiometricPrompt in Figure 3 does. After the user authenticates using biometrics, your goal is to unlock the key with a BiometricPrompt (either auth-per-use or time-bound) and then use that key to encrypt the userToken generated by the server, Then save it to a local directory. Since then, when users need to log in, they can use biometric authentication (i.e., biometric authentication -> Unlock key -> decryption userToken for data access).
It is important to distinguish between the first time a user has enabled biometrics and the login using biometrics. Biometrics is enabled, the application calls showBiometricPromptForEncryption () method, this method will initialize a Cipher for encryption userToken. On the other hand, if the user is in the use of biometrics to log in, it should call showBiometricPromptForDecryption () method, it can initialize a used to decrypt the Cipher, use the Cipher to decrypt userToken again.
After biometrics is enabled, the next time the user returns to the application, it will authenticate through the biometrics authentication dialog box, as shown in Figure 4. Note that since Figure 4 is for the login application and Figure 2 is for determining transactions, there is no confirm button in Figure 4 because the login behavior is a passive, reverse-easy behavior.
Delta figure 4.
To implement this process for your users, when your LoginActivity completes the authentication process, decrypt the userToken using the encrypted object successfully unlocked by BiometricPrompt authentication, and then call the Finish () method in the LoginActivity.
override fun onResume(a) {
super.onResume()
if(ciphertextWrapper ! =null) {
if (SampleAppUser.fakeToken == null) {
showBiometricPromptForDecryption()
} else {
// The user has logged in successfully, so proceed directly to the next application process
// The rest is up to you
updateApp(getString(R.string.already_signedin))
}
}
}
....
private fun showBiometricPromptForDecryption(a){ ciphertextWrapper? .let { textWrapper ->val canAuthenticate = BiometricManager.from(applicationContext).canAuthenticate()
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
val secretKeyName = getString(R.string.secret_key_name)
val cipher = cryptographyManager.getInitializedCipherForDecryption(
secretKeyName, textWrapper.initializationVector
)
biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(
this,
::decryptServerTokenFromStorage
)
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
}
private fun decryptServerTokenFromStorage(authResult: BiometricPrompt.AuthenticationResult){ ciphertextWrapper? .let { textWrapper -> authResult.cryptoObject? .cipher? .let {val plaintext =
cryptographyManager.decryptData(textWrapper.ciphertext, it)
SampleAppUser.fakeToken = plaintext
// Now that you have the token, you can query other data on the server
// We call this fakeToken because it is not actually fetched from the server
// In a real scenario, you would get token data from the server
// Then it can be considered a real token
updateApp(getString(R.string.already_signedin))
}
}
}
Copy the code
Complete blueprint
Figure 5 shows a complete engineering design flow chart, which we recommend. Since you can deviate from this process in many ways during actual coding, for example, the unlock key in your encryption solution may only be used for encryption and not for decryption, we hope to help developers who may need such a complete example here.
Wherever keys are mentioned in the figure, you can use auth-per-use or time-bound keys as required. In addition, wherever the “storage system in application” is mentioned in the figure, you can also interpret it as your preferred structured storage: SharedPreferences, Room, or any other storage scheme. Finally, you can think of a userToken as a token that allows you to access protected user data on the server. The server typically takes this token as proof that the caller is authorized.
The “encrypt userToken” arrow in the figure will most likely point to “Login completed “instead of returning to “LoginActivity”. Still, we let it points to the “LoginActivity” in the picture, just to remind you note that after the user clicks on “activate biometric”, you can use an additional Activity (for example EnableBiometricAuthActivity), Make your code more modular and readable. Alternatively, you can create a LoginActivity with two Fragments: one Fragment for the actual authentication process and one in response to the user clicking “Enable biometrics.”
In addition to the flow chart below, we have also published a design guide that you can refer to when designing your application. In addition, our sample code on Github will hopefully also help you better understand how to use biometric authentication.
△ Figure 5: Complete blueprint for obtaining authorization using biometrics with the server
conclusion
In this article, we introduced:
- How to extend the UI to support biometric authentication;
- What are the key points your application should address for biometric authentication processes;
- How to design your code to handle different scenarios of biometric authentication;
- Complete engineering design drawing for login process.
Have fun coding!