DEV Community

Cover image for Testing Biometrics in Android Apps
Pan Chasinga
Pan Chasinga

Posted on

3 2

Testing Biometrics in Android Apps

Disclaimer: I’m working at HeadSpin developing SDKs and developer tools to make app-testing awesome.

Biometrics have been increasingly vital to the digital economy. In China, some grocery stores offer face detection at checking out instead of cash or credit card. Apps are using biometric authentication as a more secure and smoother experience for users to access information.

If you have been writing automated tests for Android apps, chances are you are not new to Appium and the use of XPath API to query app components and simulate users’ interactions.

However, if your app incorporates a biometric authentication, which has become more common even for non-financial apps, it is not possible to automate your way in. Unless you can programmatically simulate a fingerprint impression on the device (you cannot), there is no way to herald your test parade through the biometric gate without manually pressing your finger on the device.

One way you can think of is to write a dedicated mock activity that fakes the whole biometric charades. But you’re just eating your own dog food because what you fake in the mock is what you test, not the actual behavior.

Enter HeadSpin Biometric SDK

At HeadSpin, we make testing mobile apps simple. We think it should be easy to test your apps because nobody wants to spend the same amount of development time fidgeting with the tests. HeadSpin wants developers to focus on building apps and delighting their customers all over the world.

We came up with a developer-friendly solution to testing biometric apps on Android — an Android library! All you have to do is import a component from the library, swap it with whatever you’re using in your app code, and run on a real device and start remotely control the biometric authentication on your app through our provided HTTP endpoints. Yes, the good old REST API everyone knows and loves.

Check out the demo video below.

I was able to remotely log into my test app without having to use my fingerprint.

Using HeadSpin SDK’s version of FingerprintManager, I was able to remotely send HTTP POST request to one of the REST endpoints provided by HeadSpin’s platform to authenticate my app, as shown above.

Here is a snippet of a demo activity using HeadSpin’s HSFingerprintManager to enable remote biometric authentication instead of Android’s FingerprintManager. It took me only 2–3 lines of code to swap in the HS component, and the app can be authenticated normally as well as remotely.

/*-
* #%L
* Demo Fingerprint Authentication Activity
* %%
* Copyright (C) 2019 Headspin Inc.
* %%
* #L%
*/
package com.nextunicorn.app;
import android.annotation.TargetApi;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.content.Context;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintManager.*;
import android.support.v4.app.ActivityCompat;
import android.widget.Toast;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.security.keystore.KeyProperties;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import javax.crypto.KeyGenerator;
import android.security.keystore.KeyGenParameterSpec;
import java.security.cert.CertificateException;
import java.security.InvalidAlgorithmParameterException;
import java.io.IOException;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.support.annotation.RequiresApi;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.UnrecoverableKeyException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.Cipher;
// Import from HeadSpin biometrics library
import io.headspin.instruments.fingerprint.HSFingerprintManager;
import io.headspin.instruments.fingerprint.HSFingerprintAuthCallback;
@TargetApi(28)
@RequiresApi(23)
class DemoFingerprintActivity extends AppCompatActivity {
private static String TAG = "DemoFingerprintActivity";
private static String KEY_NAME = "hot_key";
private HSFingerprintManager hsFingerprintManager;
private KeyguardManager keyguardManager;
private KeyStore keyStore;
private KeyGenerator keyGenerator;
private Cipher cipher;
private CryptoObject cryptoObject;
private CancellationSignal cancellationSignal;
/**
* Inherit [HSFingerprintAuthCallback] to get default toast messages or
* or [android.hardware.fingerprint.FingerprintManager.AuthenticationCallback]
* to implement your own callbacks.
*/
private HSFingerprintCallback authCallback = new HSFingerprintAuthCallback() {
@Override
void onAuthenticationSucceeded(AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 2000);
}
};
@Override
void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "creating DemoFingerprintActivity")
super.onCreate(savedInstanceState);
hsfingerprintManager = new HSFingerprintManager(this);
setContentView(R.layout.activity_fingerprint);
if (managersReady()) {
generateKey();
if (initCipher()) {
cryptoObject = new CryptoObject(cipher);
cancellationSignal = new CancellationSignal();
if (hsFingerprintManager.ready()) {
/*
* authenticate() sends authCallback to the underlying HSfingerprintService
* so it can be called by Headspin biometrics API.
*/
hsFingerprintManager.authenticate(cryptoObject, cancellationSignal,
0, authCallback, null);
}
}
}
}
@Override
void onStop() {
super.onStop();
// IMPORTANT: call close() to stop the HSFingerprintService.
hsFingerprintManager.close();
}
/*
* This is a sample and thus using CBC Block mode should be good enough.
* However, please take care to implement a more secure key for your app.
*/
private boolean initCipher() {
try {
cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException e) {
throw RuntimeException("Failed to get Cipher", e);
} catch (NoSuchPaddingException e) {
throw RuntimeException("Failed to get Cipher", e);
}
try {
if (keyStore != null) {
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, null);
cipher.init(Cipher.ENCRYPT_MODE, key);
return true;
}
} catch (KeyPermanentlyInvalidatedException e) {
return false;
} catch (KeyStoreException e) {
throw RuntimeException("Failed to init Cipher", e);
} catch (CertificateException e) {
throw RuntimeException("Failed to init Cipher", e);
} catch (UnrecoverableKeyException e) {
throw RuntimeException("Failed to init Cipher", e);
} catch (IOException e) {
throw RuntimeException("Failed to init Cipher", e);
} catch (NoSuchAlgorithmException e) {
throw RuntimeException("Failed to init Cipher", e)
} catch (InvalidKeyException e) {
throw RuntimeException("Failed to init Cipher", e)
}
}
private void generateKey() {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (Exception e) {
e.printStackTrace();
}
try {
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore");
} catch (NoSuchAlgorithmException e) {
throw RuntimeException("Failed to get KeyGenerator instance", e);
} catch (NoSuchProviderException e) {
throw RuntimeException("Failed to get KeyGenerator instance", e);
}
try {
if (keyStore != null) {
keyStore.load(null);
keyGenerator.init(KeyGenParameterSpec.Builder(
KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
keyGenerator.generateKey();
}
} catch (NoSuchAlgorithmException e ) {
throw RuntimeException(e);
} catch (InvalidAlgorithmParameterException e) {
throw RuntimeException(e);
} catch (CertificateException e) {
throw RuntimeException(e);
} catch (IOException e) {
throw RuntimeException(e);
}
}
private Boolean managersReady() {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
/*
* HSFingerprintManager fashions a ready() method that takes care of
* inquiring the wrapped FingerprintManager's readiness.
*/
if ((keyguardManager == null) || (!hsfingerprintManager.ready())) {
return false;
}
if (keyguardManager.isKeyguardSecure == false) {
Toast.makeText(this,
"Lock screen security is not enabled in Settings",
Toast.LENGTH_LONG).show();
return false;
}
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.USE_FINGERPRINT) !=
PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this,
"Fingerprint authentication permission not enabled",
Toast.LENGTH_LONG).show();
return false;
}
if (hsfingerprintManager?.hasEnrolledFingerprints() == false) {
Toast.makeText(this,
"Register at least one fingerprint in Settings",
Toast.LENGTH_LONG).show();
return false;
}
return true;
}
}
DemoFingerprintActivity.java

Again it’s worth noting this is accomplished without human interventions.🤯

If you are looking into automating tests for biometric apps, look no further than HeadSpin.

p.s. I’m also working on support for the new BiometricPrompt and AndroidX supports for apps targeting Android P. and above in the next release of the SDK, so it’s looking exciting!

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More