New self-serve pricingLearn more
back arrow
Back to blog

An engineer's guide to mobile biometrics: event- vs result-based

Engineering
May 3, 2023
Author: Spencer Lichtenberg
hero-image

At Stytch, we uncovered a variety of interesting challenges and oft-overlooked choices when building our mobile biometrics product. Each problem required us to think deeply about security implications, architecture, and mobile development patterns.

We discovered, unsurprisingly, that when it comes to mobile biometrics, the devil really is in the details.

This is the second post of a three-part series on mobile biometric authentication. We’ll share what we learned and experienced throughout the development process. This content is intended for engineers or folks interested in diving deeper into mobile biometrics implementation.

In this post, we’ll cover different types of mobile biometric architectures and the significant security implications that come with them.

Part one | An engineer's guide to mobile biometrics: step-by-step
Part two | An engineer's guide to mobile biometrics: event- vs result-based
Part three | An engineer's guide to mobile biometrics: Android Keystore pitfalls and best practices

Table of contents

What is the application sandbox
Event-based vs. result-based architecture
Why event-based architecture is less secure
Security implications and tradeoffs
Security without the hassle

What is the application sandbox

For local storage, most mobile applications today rely on the application sandbox. The application sandbox is a per-app, isolated system environment that separates and protects each app's filesystem, manages access to system resources, etc. It allows mobile apps to store information between app lifecycles (aka between killing an app and starting it again, or between phone reboots).

This data, in an ideal world, is only accessible via that application, and allows the app to maintain state across lifecycles. Because access to the data is scoped exclusively to an application, apps should be able to store sensitive data in the application sandbox in order to persist user sessions, and perform other sensitive actions that improve user experience.

A diagram of the app sandbox, and which kinds of data and resources it can access

Read about the app sandbox for iOS and Android

Why does the application sandbox matter for biometrics? It matters because nearly every implementation of native biometrics in a mobile application is going to rely on local storage to some degree in order to store and access credentials to authenticate. The requirement for local storage plays a crucial part in assessing the security of mobile biometric architectures.

Event-based vs. result-based architecture

While there are many nuances to how a developer can implement a biometric authentication system, there is one important decision to be made from a binary set of options: will the architecture be event-based or result-based?

Event-based biometrics

Event-based biometric authentication is an if-else gate that depends on whether or not the user can pass an agnostic biometric prompt. The application raises a biometric challenge, and after a “success” response, authenticates the user using some data (such as a long lived session token or even username/password) stored locally in the application.

suspend fun showBiometricPrompt(
        context: FragmentActivity,
        promptData: Biometrics.PromptData?,
        allowedAuthenticators: Int,
    ): Cipher? =
        suspendCoroutine { continuation ->
            val executor = Executors.newSingleThreadExecutor()
            val callback =
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationError(
                        errorCode: Int,
                        errString: CharSequence,
                    ) {
                        super.onAuthenticationError(errorCode, errString)
                        continuation.resumeWithException(StytchExceptions.Input(errString.toString()))
                    }

                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                        super.onAuthenticationSucceeded(result) // the prompt passed, so we _assume_ they have authenticated
                        continuation.resume(result.cryptoObject?.cipher)
                    }
                }
            val prompt =
                BiometricPrompt.PromptInfo.Builder().apply {
                    setTitle(promptData?.title ?: context.getString(R.string.stytch_biometric_prompt_title))
                    setSubtitle(promptData?.subTitle ?: context.getString(R.string.stytch_biometric_prompt_subtitle))
                    setAllowedAuthenticators(allowedAuthenticators)
                    if (!allowedAuthenticatorsIncludeDeviceCredentials(allowedAuthenticators)) { // can only show negative button if device credentials are not allowed
                        setNegativeButtonText(
                            promptData?.negativeButtonText ?: context.getString(
                                R.string.stytch_biometric_prompt_negative,
                            ),
                        )
                    }
                }.build()
            BiometricPrompt(context, executor, callback).authenticate(prompt)
        }

Result-based biometrics

Result-based biometric authentication relies on hardware-backed biometric APIs to generate cryptographic keys that are only accessible via biometric authentication. These keys are not only stored in the application sandbox, but also gated behind a biometric challenge the operating system enforces. The application must pass a biometric challenge in order to gain access to the cryptographic keys. The application can then authenticate the user in a number of ways:

  1. Use the unlocked key to decrypt and send locally stored credentials (such as a long lived session token or username/password) to the application’s backend.
  2. Use the key to sign a cryptographic challenge generated by a pre-registered public key, which is derived from the biometrically-protected secret key on the device (similar to a WebAuthn flow).

Either way, user authentication within the application depends on access to the cryptographic key stored on-device that is only accessible via biometric authentication.

 suspend fun showBiometricPrompt(
        context: FragmentActivity,
        promptData: Biometrics.PromptData?,
        cipher: Cipher,
        allowedAuthenticators: Int,
    ): Cipher = suspendCoroutine { continuation ->
        val executor = Executors.newSingleThreadExecutor()
        val callback = object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                continuation.resumeWithException(StytchExceptions.Input(errString.toString()))
            }
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                // we rely on the presence of a CryptoObject, which is only provided after successful authentication, to perform cryptographic operations
                result.cryptoObject?.cipher?.let {
                    continuation.resume(it)
                } ?: continuation.resumeWithException(StytchExceptions.Input(AUTHENTICATION_FAILED))
            }
        }
        val prompt = BiometricPrompt.PromptInfo.Builder().apply {
            setTitle(promptData?.title ?: context.getString(R.string.stytch_biometric_prompt_title))
            setSubtitle(promptData?.subTitle ?: context.getString(R.string.stytch_biometric_prompt_subtitle))
            setAllowedAuthenticators(allowedAuthenticators)
            if (!allowedAuthenticatorsIncludeDeviceCredentials(allowedAuthenticators)) {
                // can only show negative button if device credentials are not allowed
                setNegativeButtonText(promptData?.negativeButtonText ?: context.getString(R.string.stytch_biometric_prompt_negative))
            }
        }.build()
        BiometricPrompt(context, executor, callback).authenticate(prompt, CryptoObject(cipher))
    }

Result-based biometrics open source code in Stytch’s Android SDK

Why event-based architecture is less secure

Event-based biometric authentication is less secure primarily due to its returning a boolean value. By acting as a simple pass or fail gate, the implementation logic creates two vulnerabilities:

  1. It can be spoofed and bypassed by injecting the “success” code or by changing the return value.
  2. It stores sensitive information somewhere in local storage insecurely without proper encryption: whether that is a long-lived session token, user credentials, or even a secret key used for cryptographic data signing.

Result-based biometric authentication, on the other hand, is inherently encrypted. In order to breach it, a much more sophisticated attack is required – for example, injecting into the app right after the cryptographic key has been decrypted into application memory – as opposed to simply reading from the application’s local storage like with event-based architecture.

A graph re-capping the difference between event and result-based biometric authentication

Security implications and tradeoffs

Are the security vulnerabilities exposed by event-based biometric flows critical vulnerabilities for any app? Only you can decide what security requirements and risks are acceptable for your application. Let’s further dissect and weigh the tradeoffs and pros and cons between result-based and event-based architecture.

cta image

Want mobile biometrics for your app? Switch to Stytch.

cta image

MASVS L1 and L2 security levels

If a biometrics implementation is event-based, it means that some kind of sensitive data is being stored insecurely on the device. However, that data is theoretically still exclusively scoped to a single application because of the application sandbox. So, in what scenarios does event-based architecture truly result in a security vulnerability? This includes but is not limited to attacks such as:

  • Data extraction via forensics software
  • Data extraction via iTunes or adb backup files
  • Code instrumentation tools like Frida that are used to manually trigger the “success” flow
  • A bad actor gains root access to the device

The Open Worldwide Application Security Project (OWASP) defines multiple layers of security in the Mobile Application Security Verification Standard (MASVS).

  1. L1 (Standard Security) is recommended for all mobile applications.
  2. L2 (Defense in depth) is recommended for industries like healthcare, finance, and other applications that have access to user’s sensitive data.
  3. R (Resiliency Against Reverse Engineering and Tampering) is for institutions that need cutting-edge state-of-the-art security like banking and government agencies.
A graph overviewing the The Open Worldwide Application Security Project (OWASP)'s  three layers of security in the Mobile Application Security Verification Standard (MASVS): L1 (Standard Security) ; L2 (Defense in depth) ; R (Resiliency Against Reverse Engineering and Tampering)

OWASP MASVS verification levels (source)

Event-based biometric architecture does meet L1 standards, as it only exposes sensitive data in the event of a second layer vulnerability – so event-based architecture itself does not pose a security vulnerability. But, only a result-based biometrics flow would be considered secure enough for mobile applications aiming to meet MASVS L2 security requirements – which is non-negotiable if your application handles sensitive user information.

Adding new biometric factors

Do you want to allow biometric factors added to the device after the initial biometric factor (either face or thumbprint) was enabled in-app to have access? With event-based architecture you can’t control this – you must allow all biometric factors registered with the device to have access, or none. With result-based architecture, you can decide whether or not future biometric factors added to the device can be used to authenticate in your application.

Implementation

Another element, ever-present in all engineering decisions, is ease of implementation. This is where event-based has the edge. There are a plethora of event-based biometric authentication libraries publicly available, and fewer complexities to consider when implementing. Result-based architecture on the other hand requires in-depth knowledge of iOS and Android mobile architecture, data encryption/decryption, and native biometric APIs to implement, and the solution is inherently more complex.

Security without the hassle

At Stytch, our customers trust us with their authentication and expect intuitive, frictionless application interfaces.

Our internal mobile biometric solution is result-based, so developers can be sure that their application and users are receiving a higher degree of security. On top of that, implementing biometric authentication for your application is as simple as accessing a handful of methods exposed by Stytch’s mobile SDKs. Our customers get the sophisticated security of result-based architecture, but with the ease of implementation afforded by our dev kits.

Next up in the series, we’ll walk you through building biometric authentication within the Android operating system. With such a large and diverse ecosystem, you need to consider all the pitfalls and gotchas in order to support a secure and stable mobile biometric implementation.

Not ready to build mobile biometrcs from the ground up? Try Stytch’s mobile SDKs for iOS, Android, or ReactNative, and get started with mobile biometrics with just a few lines of code.

Check out our career page if solving problems and reading topics like this interest you!

Share

LinkedIn share
Twitter share
Facebook share