Sometimes it is necessary to validate a user’s identity in order to provide access to sensitive information within an app. The traditional mechanism of a username and password has always been rather clunky on mobile because of small screens and soft keyboards. However with the arrival of hardware such as fingerprint scanners arriving on devices, there are some new much more user-friendly mechanisms that we can use for authentication. In this series we’ll explore how to implement these within our apps.
Before we begin, I should mention that I believe that biometric data such as fingerprints should be considered analogous with a username and not a password. The reasoning behind this is that a username is generally not considered secure, and is potentially already publicly available – for example using an email address as a username. On the other hand a password should be secure and the user should be able to change it in the event of a security breach in order to keep their account secure.
A fingerprint, for example, fits much for within the constraints for a username – you leave a copy of it every time you touch something and you certainly cannot change it in response to a security breach. Moreover, if someone is able to falsify your fingerprint (which is certainly not impossible) it should be the equivalent of someone knowing your email address – it should not grant them access to sensitive data on its own. The following example should be seen simply as a mechanism to establish the user’s identity, and not provide secure access without some additional security.
While there is an official sample project which demonstrates this, I found the sample code somewhat badly organised and difficult to follow because it didn’t group the relevant parts together. Hopefully the sample code supporting this series will be a little easier to understand.
All that out of the way, let’s take a look at how we can use the fingerprint scanner within an application. The app consists of two Activities. The first performs a fingerprint scan to determine whether the user is the device owner, who has previously registered their fingerprint on the device. If this check is successful then the second Activity is displayed. In the event of a failure, either because fingerprint hardware is not available on the device, or because the scanned fingerprint does not match one of those registered on the device, then an appropriate error will be displayed and the user will remain in the first Activity.
I’ve created a BaseActivity class which provides some basic, common functionality (we’ll be adding a different mechanism and Activity later in the series). This functionality is for displaying error messages, and providing access to the KeyTools (which we’ll get to shortly). I won’t explain it here because it’s pretty basic stuff, but it’s available in the source.
Let’s take a look at the Activity which will perform the fingerprint scan:
public class FingerprintActivity extends BaseActivity { private FingerprintManagerCompat fingerprintManager; private AuthenticationCallback authenticationCallback = new AuthenticationCallback(); private CancellationSignal cancellationSignal = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fingerprintManager = FingerprintManagerCompat.from(this); } @Override protected void onResume() { super.onResume(); if (!fingerprintManager.isHardwareDetected()) { showError(R.string.no_fingerprint_hardware); } else if (!fingerprintManager.hasEnrolledFingerprints()) { showError(R.string.no_fingerprints_enrolled); } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { showError("Permission not granted"); } else { authenticate(); } } @Override protected void onPause() { if (cancellationSignal != null) { cancellationSignal.cancel(); } super.onPause(); } . . . }
The basic Activity lifecycle methods are pretty straightforward. In onCreate()
we get a FingerprintManagerCompat reference. We could use the native one in Marshmallow, but it’s always good to use Compat implementations wherever possible, and this one is provided by the support-v4
library.
In onResume()
we perform a few checks. First we check for the necessary hardware and display an appropriate message if it’s not found. In a real app it would be sensible to have a fallback identification mechanism for devices which don’t have a fingerprint scanner. The next check is that the user has actually registered one or more fingerprints on the device. In a real app it is may be nice to provide the user with some help on how to register a fingerprint. The final check is that we have the necessary permission to use the fingerprint. The USE_FINGERPRINT
permission is a normal level permission so we get it granted automatically by declaring it in the Manifest. However it is always best practice to check that any permissions we require have been granted just for protection against changes in permission level in future versions of Android. For a real app, once it would be best to include the code to request the permission if we don’t have it – but I’ve omitted that in order to keep this code lean and focused. If all these checks pass then be begin the authentication.
In onPause()
we send a cancellation signal if we have one. We’ll come back to this, but it is basically a mechanism for cancelling the the fingerprint scan when the Activity is paused.
The only other thing worthy of note here is we define a callback which will handle the fingerprint auth results.
Let’s take a look at the authenticate()
method which kicks off the fingerprint scan:
private void authenticate() { Cipher cipher; try { cipher = getUserAuthCipher(); } catch (KeyToolsException e) { showError(e.getMessage()); return; } CryptoObject crypto = new CryptoObject(cipher); cancellationSignal = new CancellationSignal(); fingerprintManager.authenticate(crypto, 0, cancellationSignal, authenticationCallback, null); }
But what’s this weird Cipher
thing? For those unfamiliar with cryptography, a cipher is, very simply, a method of encrypting data. The cipher itself is clearly and publicly defined, but the encryption is performed using a specific key which is secret. At this point in time, the cipher will fail if we try to use it because it has not met the necessary criteria for it to be useable. We’ll come back to this in greater detail in the next article but, hopefully, that should be enough information for now to understand how the basic process works.
We wrap this cipher in a CryptoObject and pass this in when we start the scan.
We also pass in the cancellationSignal
object we saw earlier. The cancellation signal allows us to cancel the scan (which we do in onPause()
). The authenticationCallback
will have its appropriate method called depending on whether the scan was successful, failed, or there was an error. For the latter two cases we display an appropriate error, and on success we’ll validate things further.
private class AuthenticationCallback extends FingerprintManagerCompat.AuthenticationCallback { @Override public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { validate(result.getCryptoObject().getCipher()); } @Override public void onAuthenticationError(int errMsgId, CharSequence errString) { showError(errString); } @Override public void onAuthenticationFailed() { showError(R.string.identification_failed); } }
When we receive the success callback we’re given an AuthenticationResult object and we can extract the cipher from this and we pass this to our validate()
method:
private void validate(Cipher cipher) { if (tryEncrypt(cipher)) { setContentView(R.layout.activity_user_identified); } else { showError(R.string.validation_error); } }
So we try and encrypt using the cipher and if it success we display a new layout (which is the secure data that we’re protecting), and if it fails we show an error instead. The encryption code itself is actually pretty straightforward too:
private boolean tryEncrypt(Cipher cipher) { try { cipher.doFinal(SECRET_BYTES); } catch (Exception e) { e.printStackTrace(); return false; } return true; }
SECRET_BYTES
is just an arbitrary byte array which is defined in the BaseActivity, and passing this to Cipher#doFinal will perform an encryption of that data but throw an exception if the cipher has not met the necessary criteria.
In essence that’s how this works – we create an incomplete cipher, pass it to the FingerprintManager and the cipher will only work once certain criteria have been met – in this case it is a successful fingerprint scan. So simply by managing to successfully encrypt using the cipher we know that the fingerprint scan was successful.
We can see the process working with first a failed scan (because I used a finger whose print hasn’t been registered), followed by a successful scan:
That is the basic operation of how the process works, but there’s a lot of unanswered questions such as: How do we create the cipher, and how do we specify that a fingerprint scan is required before the cipher will work. All of this behaviour is handled by the KeyTools.java code which is in the source, but we’ll do a deep dive in to how this all works in the next article.
The source is available here.
© 2015, Mark Allison. All rights reserved.
Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
We’re working on an Android M touch id plugin (Cordova) for our mobile project (Ionic). Currently on iOS, we are using the cordovaTouchID and cordovaKeychain together to authenticate the user by sending the username/password that was stored in the cordovaKeychain to the API. If I had a requirement to do the same thing on Android, would you say that it is fairly secure to follow this workflow and then store the encrypted credentials into internal storage?