RSS Security

🔒
❌ About FreshRSS
There are new articles available, click to refresh the page.
Before yesterdayNVISO Labs

How malicious applications abuse Android permissions

1 September 2021 at 15:07

Introduction

Many Android applications on the Google Play Store request a plethora of permissions to the user. In most cases, those permissions are actually required by the application to work properly, even if it is not always clear why, while other times they are plainly unnecessary for the application or are used for malicious purposes.

In a world where the user’s privacy is becoming one of the primary concerns on the internet, it is important for the users to understand the permissions each application is requesting and to determine whether or not the application really needs it.

In this blog post, we will go over what exactly these permissions are, and we will illustrate legitimate permission usages as well as several illegitimate permission usages. We hope that this blog post will help the reader understand that blindly granting permissions to an application can have a severe impact on their privacy or even wallet.

What are application permissions

If developers want their application to perform a sensitive action, such as accessing private user data, they have to request a specific permission to the Android system. The system will then automatically grant the permission to the application if the permission was already granted before, or the user will be shown a dialog asking for the user to grant the permission. Each granted permission allows the application to perform a specific action. For example, the permission android.permission.READ_EXTERNAL_STORAGE grants read access to the shared storage space of the device.

By default, Android applications do not have any permissions that allow them to perform sensitive actions that would have an impact on the user, the system or other applications. This includes accessing the local storage outside of the application container, accessing the user’s messages or accessing sensitive device information.

The Android operating system differentiates three types of permissions: normal permissions, signature permissions and dangerous permissions.

Normal permissions grant access to data and resources outside of the application sandbox which have very little risk to compromise user’s data or other applications on the system. Normal permissions declared in the application’s manifest are automatically granted upon installing the application on the device. Users do not need to grant these permissions and cannot revoke them. The android.permission.INTERNET permission, allowing the application to access the internet, is an example of normal permission.

Signature permissions grant access to custom permissions declared by an application signed with the same certificate as the requesting application. These permissions are granted automatically by the system upon installation. Users do not need to grant those permissions and cannot revoke them.

Dangerous permissions grant access to data and resources outside of the application sandbox which could have an impact on the user’s data, the system or other applications. If an application requests a dangerous permission in its manifest, the user will have to explicitly grant the permission to the application. On devices running Android 6.0 (API level 23) or above, the application has to request the permission to the user at runtime through a prompt. The user can then choose to allow or deny the permission. The user can later revoke any granted permissions in the settings of the application, in the device settings. On devices running Android 5.1.1 (API level 22) and older, the system will ask the user to grant all the dangerous permissions during installation. If the user accepts, all the permissions will be given to the application. Otherwise, the installation of the application will be cancelled. The android.permission.ACCESS_FINE_LOCATION permission, allowing the application to access the precise location of the device, is an example of a dangerous permission.

Permissions to be granted at install time in Android 5.1.1 and older
Permissions to be granted at runtime in Android 6.0 and above

Android maintains a list of all the permissions and their protection level.

Legitimate permissions usage

While permissions can be dangerous as it allows the applications to access sensitive data or resources, permissions are also essential for many features of a regular application. For example, the Google Map application would not be able to work as intended if it was not granted permission to access the location of the device.

In this section, we will quickly go over a few permissions that are needed for an application to work properly. You will see that while most of the time the reason of the permission is obvious, it may sometimes not be clear why at all.

Let’s start with a basic example: KMI Weather, a Belgian weather application.

The KMI Weather application (version 2.8.8) declares the following standard permissions in its manifest:

  • android.permission.ACCESS_COARSE_LOCATION
  • android.permission.ACCESS_FINE_LOCATION
  • android.permission.ACCESS_NETWORK_STATE
  • android.permission.INTERNET
  • android.permission.WAKE_LOCK
  • android.permission.VIBRATE

The ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions are dangerous permissions used by the application to automatically fetch the weather data for the current city the device is in. The other permissions (ACCESS_NETWORK_STATE, Internet and WAKE_LOCK) are normal permissions and are therefore granted automatically by the system.

We can see in this first basic example that each requested permission has an obvious purpose for the application. However, this does not mean that the application will not misuse these permissions, as we will see in the next section.

In some cases, it is not clear why an application would request a permission. Let’s take for example the well-known Spotify application.

The Spotify application (version 8.5.41.531) requests the following permission in its manifest: android.permission.READ_PHONE_STATE. The permission allows the application to access the phone number, the device IDs, whether a call is active, and the remote number calling the device. Since Spotify is an application to listen to music, why on earth would it need such a permission?

As previously stated, the READ_PHONE_STATE permission allows the application to know whether a call is active or not. This is used by Spotify to pause the music when a call is received by the application, since users would not be happy if the music kept playing while they answer a phone call. In addition, the permission allows the retrieval of the device IDs, which are used by many applications for device binding and analytics purposes.

As was shown in the previous example, applications will sometimes request permissions which don’t appear to make sense in their context at first glance, but which are actually very useful for the application and the user’s experience.

The main problem with many permissions is that they cover many different functionalities. From the user’s perspective, it’s often very difficult to know what exactly a specific permission is used for. The Android team recognized this shortcoming and introduced a new API that allows the developer to explain why a specific permission is requested. This dialog can be shown before the user is asked for the permission:

Dialog explaining why the location permissions is needed by the Twitter application

In addition to the explanation, Android regularly fine-tunes their permissions to prevent confusion. For example, the permission READ_CONTACTS originally also allowed the application to access the call and message logs. This was changed in Android 4.1, where new permissions were added and the READ_CONTACT permission no longer gave access to the call and message logs.

Unfortunately, applications sometimes also request permissions which plainly do not make sense for the application or which are used ill-intentionally, as we will highlight in the next section.

Malicious permissions usage

In the previous section, we saw that applications sometimes request permissions which you would not think they would need at first glance while they have, in fact, a perfectly good reason to request them.

In this section, we will explore the other side of requesting permissions: malicious usage of permissions. We will explore four different scenarios, using real-world examples.

Data collections

The first scenario we will explore is probably the one most people have in mind when an application requests many permissions which don’t appear to make sense for the application: An application harvesting data about its users.

We will illustrate this scenario by using a well-known social media application, Facebook.

It is common knowledge that Facebook harvests data about its users for advertising purposes. The data collected by Facebook ranges from personal information you provided to the platform to call logs history and details about SMS. Here, we will focus on how Facebook gathered the latter.

Just like any other Android application, the Facebook application has to request permissions in order to access the data and resources outside of the application sandbox. If we take a close look at the permissions requested by an older version of the Facebook Lite application, we will see, among others, the following permissions: READ_SMS and READ_CALL_LOG. The first permission allows the application to get details about the SMS messages that were sent and received, and the second permission allows the application to retrieve the call history of the device. While the READ_SMS permission could potentially be used to get multi-factor authentication codes, or to allow the application to act as an SMS application, these permissions also allows a lot of personal data to be collected by the application.

You might think that this is easily resolved as you can just revoke the permission or not grant it in the first place, but before Android 6.0 this was not possible. If you wanted to have an application, you needed to grant all the requested permissions. As for later versions of Android, while it is indeed possible to not grant such permissions, many users will blindly grant them due to ignorance of the consequences or simply out of convenience. Some applications may even refuse to work without the specific permission.

In addition to the above, Facebook uses an alternative way of collecting user data: other applications. There are many instances of applications sharing data with Facebook. Should you grant a sensitive permission to such an application, your data might be shared with Facebook even if you carefully denied the related permissions to Facebook in the first place. As shown by a report from Privacy International, many applications share data they have on you with Facebook., by using the Facebook SDK.

Collecting data for advertising purposes is arguably a malicious usage of application permissions. Whether you’re against this kind of practice or not, it’s easy to agree that collecting such amount of data even for legitimate purposes poses a threat to the user’s privacy and might even impact entire communities. A great example of this is the Facebook-Cambridge Analytica data scandal, where sensitive user’s data was leaked and supposedly used to influence elections.

Note that in the case of Facebook, their business model entirely relies on this data collection and on sharing it with advertisers, which is now public knowledge. In addition, most permissions requested by Facebook are tied to features of their applications. For example, as already mentioned, the READ_SMS permission allows Messenger to be used as an SMS application, while having access to the location allows Facebook to link your images to specific locations, or to share your location with your friends if you want to.

There are many other applications, such as adware, which operate in a similar way, but arguably with more malicious intentions. Such applications will typically request a lot of permissions which will be used to gather data about the user, that will be shared with advertising networks in order to show highly targeted advertisements. More often than not, the requested permissions will have no other purpose than to gather data about the user.

Malware-like applications

The second scenario we will take a look at is applications requesting many permissions and using them to exploit the user or the device.

A good example of this is the Joker malware. Joker will subscribe the user to paid services without the user’s consent by leveraging dangerous permissions granted to the application. This is obviously not something you would want as you will get charged for a service to which you subscribed unknowingly.

The Joker malware will request the READ_PHONE_STATE permission to obtain the user’s phone number and then uses it to initiate subscriptions to paid services. Usually, the paid services will require a confirmation code sent to the provided phone number. The malware will therefore also request the READ_SMS permission to retrieve the confirmation code from the received SMS and ultimately confirm the subscription. The user will then be charged monthly for the service. You can take a look at the in-depth analysis of Joker for more information

This is far from the only example of how permissions could be used maliciously. Another example would be an application that requests the SEND_SMS permission and send SMS to premium numbers (i.e. FakeInst), or an application accessing the SD Card and exfiltrating documents of the user. Other malware-like applications will ask for very limited permissions and rely on those to exploit other legitimate applications with more extensive permissions to perform their malicious actions.

Luckily, Google usually reacts quickly to such applications and removes them from the Google Play Store, preventing further victims of such applications. However, despite their fast reactions, many users will have already downloaded the malicious applications and fall victim to them.

Abuse in legitimate permissions

In this third scenario, we will take a closer look at applications which do require specific permissions to work properly, but which will also abuse said permissions in ways that would not be expected from the users.

An example of such application would be an SMS application, requiring the permissions to read and send SMS, which abuses its permissions to send SMS to premium numbers or to intercept multi-factor authentication tokens, as discussed in this article. The read and send SMS permissions are legitimate permissions for an SMS application, the users will therefore naturally grant such permissions. However, the users do not expect the application to misuse these permissions the way it does, making the user pay for services they never subscribed to, or retrieve data allowing the malicious actor to access accounts of the user.

In such a scenario, the only thing the user can really do to protect their privacy is to stop using the application. This is however not something you would want every time as the application in question may be extremely convenient for the users. The choice then boils down to whether or not the user is willing to sacrifice their privacy to enjoy the convenience of the application.

In practice, such applications are not so common. Most of the time, malicious applications will rather request many permissions, even if they are not legitimate permissions for the application. In addition, malware rarely put a significant effort in having an application that appears to be legitimate. Instead, they will typically invest in hiding the application to the eyes of the user, or provide very limited feature and attempt to hide their malicious behavior in some other ways.

Abusing permissions of other applications

The last example of malicious usage of permissions is that of a malicious application which will abuse the permissions of a legitimate application on the device by exploiting its exposed features.

If a legitimate application requests dangerous permissions and then exposes a feature that uses that dangerous permission to the system, it allows any other application installed on the device to enjoy the permission without the need of requesting it. Let’s take for example a file explorer application with the permission to read files on the external storage. If it also exposes a provider that lets another application request the content of a given folder or file, it essentially allows the other applications to read files on the local storage, even if they do not have the READ_EXTERNAL_STORAGE permission. This is known as the Confused deputy problem.

A good example of this issue would be the Google and Samsung Camera applications which were identified vulnerable to such an attack in 2019. The applications exposed an unprotected feature that allowed another application to take pictures or videos through the Camera application. These pictures were written to the SD card, which is typical for Camera applications. A malicious application could request access to the user’s SD card, something that’s not suspicious by itself, send an Intent to the vulnerable app and then extract those images. Even worse, if Geolocation was enabled while taking the pictures, the application could extract that information from the image and essentially track the user without needing the LOCATION permission.

Google Camera application

Unfortunately for the users, there’s nothing that can be done to prevent these kinds of issues, apart from hoping that the developers correctly protected any exposed features of their application.

Conclusion

Application permissions are essential for almost every application to work properly. However, as we saw in this blog post, the permissions can also be abused to collect data or to create malware applications. It is therefore important for the users to be able to tell when a permission makes sense for an application and when it does not.

Developers should provide the users with a clear reason on why the application requests the permissions it does in order to help the decision of the user. However, even when this is the case, legitimate permissions on legitimate applications can be misused and the decision of the user comes down to whether or not the user is willing to risk his privacy for the convenience of using the application as it is intended.

About the authors

Simon Lardinois
Simon Lardinois

Simon Lardinois is a Security Consultant in the Software and Security assessment team at NVISO. His main area of focus is mobile application security, but is also interested in web and reverse engineering. In addition to mobile applications security, he also enjoys developing mobile applications.

Jeroen Beckers
Jeroen Beckers

Jeroen Beckers is a mobile security expert working in the NVISO Software and Security assessment team. He is a SANS instructor and SANS lead author of the SEC575 course. Jeroen is also a co-author of OWASP Mobile Security Testing Guide (MSTG) and the OWASP Mobile Application Security Verification Standard (MASVS). He loves to both program and reverse engineer stuff.

A closer look at the security of React Native biometric libraries

6 April 2021 at 09:43

Many applications require the user to authenticate inside the application before they can access any content. Depending on the sensitivity of the information contained within, applications usually have two approaches:

  • The user authenticates once, then stays authenticated until they manually log out;
  • The user does not stay logged in for too long and has to re-authenticate after a period of inactivity.

The first strategy, while very convenient for the user, is obviously not very secure. The second approach is pretty secure but is a burden for the users as they have to enter their credentials every time. Implementing biometric authentication reduces this burden as the authentication method becomes quite easy and fast for the user.

Developers typically don’t write these integrations with the OS from scratch, and will typically use libraries either provided by the framework or by a third-party. This is especially true when working with cross-platform mobile application framework such as Flutter, Xamarin or React Native, where such integration needs to be implemented in the platform specific code. As authentication is a security-critical feature, it is important to verify if those third-party libraries have securely implemented the required functionality.

In this blog post, we will first take a look at the basic concept of biometric authentication, so that we can then investigate the security of several React Native libraries that provide support for biometric authentication.

TLDR;

We analyzed five React Native libraries that provide biometric authentication. For each of these libraries, we analyzed how the biometric authentication is implemented and whether it correctly uses the cryptographic primitives provided by the OS to secure sensitive data.

Our analysis showed that only one of the five analyzed libraries provides a secure result-based biometric authentication. The other libraries only offer event-based authentication, which is insecure as the biometric authentication is only validated without actually protecting any data in a cryptographic fashion.

The table below provides a summary of the type of biometric authentication offered by each analyzed library:

Library Event-based* Result-based*
react-native-touch-id
expo-local-authentication
react-native-fingerprint-scanner
react-native-fingerprint-android
react-native-biometrics
* See below for definitions

Biometric authentication

Biometric authentication allows the user to authenticate to an application using their biometric data (fingerprint or face recognition). In general, biometric authentication can be implemented in two different ways:

  • Event-based: the biometric API simply returns the result of the authentication attempt to the application (“Success” or “Failure”). This method is considered insecure;
  • Result-based: upon a successful authentication, the biometric API retrieves some cryptographic object (such as a decryption key) and returns it to the application. Upon failure, no cryptographic object is returned.

Event-based authentication is insecure as it only consists of boolean value (or similar) being returned. It can therefore be bypassed using code instrumentation (e.g. Frida) by modifying the return value or by manually triggering the success flow. If an implementation is event-based, it also means that sensitive information is stored somewhere in an insecure fashion: After the application has received “success” from the biometric API, it will still need to authenticate the user to the back-end using some kind of credentials, which will be retrieved from local storage. This will be done without the need of a decryption key (otherwise the implementation wouldn’t be event-based) which means the credentials are stored somewhere on local storage without proper encryption.

A well-implemented result-based biometric authentication, on the other hand, will not be bypassable with tools such as Frida. To implement a secure result-based biometric authentication, the application must use hardware-backed biometric APIs.

A small note about storing credentials

While we use the term “credentials” in this blog post, we are not advocating for the storage of the user’s credentials (i.e. username and password). Storing the user’s credentials on the device is never a good idea for high-security applications, regardless of the way they are stored. Instead, the “credentials” mentioned above should be credentials dedicated to the biometric authentication (such as a high entropy string), which are generated during the activation of the biometric authentication.

To implement a secure result-based biometric authentication on Android, a cryptographic key requiring user authentication must be generated. This can be achieved by using the setUserAuthenticationRequired method when generating the key. Whenever the application will try to access the key, Android will ensure that valid biometrics are provided. The key must then be used to perform a cryptographic operation that unlocks credentials that can then be sent to the back-end. This is done by supplying a CryptoObject, initiated with the previous key, to the biometric API. For example, the BiometricPrompt class provides an authenticate method which takes a CryptoObject as an argument. A reference to the key can then be obtained in the success callback method, through the result argument. More information on implementing secure biometric authentication on Android can be found in this very nice blogpost by f-secure.

On iOS, a cryptographic key must be generated and stored in the Keychain. The entry in the Keychain must be set with the access control flag biometryAny. The key must then be used to perform cryptographic operation that unlocks credentials that can be sent to the back-end. By querying the Keychain for a key protected by biometryAny, iOS will make sure that the user unlocks the required key using their biometric data. Alternatively, instead of storing the cryptographic key in the Keychain, we could directly store the credentials themselves with the biometryAny protection.

Being even more secure with fingerprints

Android and iOS allow you to either trust ‘all fingerprints enrolled on the device’, or ‘all fingerprints currently enrolled on the device’. In the latter case, the cryptographic object becomes unusable in case a fingerprint is added or removed.
For Android, the default is ‘all fingerprints’, while you can use setInvalidatedByBiometricEnrollment to delete a CryptoObject in case a fingerprint is added to the device.
For iOS, the choice is between biometryAny and biometryCurrentSet.
While the ‘currently enrolled‘ option is the most secure, we will not put any weight on this distinction in this blogpost.

Is event-based authentication really insecure?

Yes and no. This fully depends on the threat model of your mobile application. The requirement for applications to provide result-based authentication is a Level 2 requirement in the OWASP MASVS (MSTG-AUTH-8). Level 2 means that your application is handling sensitive information and is typically used for applications in the financial, medical or government sector.

OWASP MASVS Verification Levels (source)

If your application uses event-based biometric authentication, there are specific attacks that will make the user’s credentials available to the attacker:

  • Physical extraction using forensics software
  • Extraction of data from backup files (e.g. iTunes backups or adb backups)
  • Malware with root access to the device

This last example would also be able to attack an application that uses result-based biometric authentication, as it would be possible to inject into the application right after the credentials have been decrypted in memory, but the bar for such an attack is much higher than simply copying the application’s local storage.

React Native

React Native is an open-source mobile application framework created by Facebook. The framework, built on top of ReactJS, allows for cross-platform mobile application development in JavaScript. This allows developer to develop mobile applications on different platforms at once, using HTML, CSS and JavaScript. Over the past few years it has gained quite some traction and is now used by many developers.

While being a cross-platform framework, some feature still require developing in native Android (Java or Kotlin) or iOS (Objective-C or Swift). To get rid of that need, many libraries have seen the light of the day to take care of the platform specific code and provide a JavaScript API that can be used directly in React Native.

Biometric authentication is one such feature that still requires platform specific code to be implemented. It is therefore no surprise that many libraries have been created in an attempt to spare developers the burden of having to implement them separately on the different platforms.

A closer look at several React Native biometric authentication libraries

In this section, we will take a look at five libraries that provide biometric authentication for React Native applications. Rather than only focusing on the documentation, we will examine the source code to verify if the implementation is secure. Based on the top results on Google for ‘biometric API react native’ we have chosen the following libraries:

For each library, we have linked to the specific commit that was the latest while writing this blogpost. Please use the latest versions of the libraries in case you want to use them.

React-native-touch-id

GitHub: https://github.com/naoufal/react-native-touch-id/ (Reviewed version)

Library no longer maintained

The library is no longer maintained by its developers and should therefore not be used anymore regardless of the conclusions of our analysis.

In the Readme file, we can already find some hints that the library does not support result-based biometric authentication. The example code given in the documentation contains the following lines of code:

TouchID.authenticate('to demo this react-native component', optionalConfigObject)
    .then(success => {
        AlertIOS.alert('Authenticated Successfully');
    })
    .catch(error => {
        AlertIOS.alert('Authentication Failed');
    });

In the above example, it is clear that it is an event-based biometric authentication as the success method does not verify the state of the authentication, nor does it provide a way for the developers to verify it.

The more astute among you will notice the optionalConfigObject parameter, which could very well contain data that would be used in a result-based authentication, right? Unfortunately, that’s not the case. If we look a bit further in the documentation, we will find the following:

authenticate(reason, config)
Attempts to authenticate with Face ID/Touch ID. Returns a Promise object.
Arguments
    - reason - An optional String that provides a clear reason for requesting authentication.
    - config - optional - Android only (does nothing on iOS) - an object that specifies the title and color to present in the confirmation dialog.

As we can see, the authenticate method only takes the two parameters that were used in the example. In addition, the optional parameter config (optionalConfigObject in the example code), which does nothing on iOS, is used for UI information.

Ok, enough with the documentation, let’s now dive into the source code to see if the library provides a way to perform a result-based biometric authentication.

Android

Let’s first take a look at the Android implementation. We can find the React Native authenticate method in the TouchID.android.js file, which is used to perform the biometric authentication. This method is the only method to perform biometric authentication provided by the library. The following code can be found in the method:

authenticate(reason, config) {
  //...
  return new Promise((resolve, reject) => {
    NativeTouchID.authenticate(
      authReason,
      authConfig,
      error => {
        return reject(typeof error == 'String' ? createError(error, error) : createError(error));
      },
      success => {
        return resolve(true);
      }
    );
  });
}

We can already see in the above code snippet that the success callback does not verify the result of the authentication and only returns a boolean value. The Android implementation is therefore event-based.

iOS

Let’s now take look at the iOS implementation. Once again, the TouchID.ios.js file only contains one method for biometric authentication, authenticate, which contains the following code:

authenticate(reason, config) {
  //...
  return new Promise((resolve, reject) => {
    NativeTouchID.authenticate(authReason, authConfig, error => {
      // Return error if rejected
      if (error) {
        return reject(createError(authConfig, error.message));
      }

      resolve(true);
    });
  });
}

As we can see, authentication will fail if the error object is set, and will return a boolean value if not. The library does not provide a way for the application to verify the state of the authentication. The iOS implementation is therefore event-based.

As we saw, react-native-touch-id only supports event-based biometric authentication. Applications using this library will therefore not be able to implement a secure biometric authentication.

Result: Insecure event-based authentication

Expo-local-authentication

GitHub: https://github.com/expo/expo (Reviewed version)

The library only provides one JavaScript method for biometric authentication, authenticateAsync, which can be found in the LocalAuthentication.ts file. The following code is responsible for the biometric authentication:

export async function authenticateAsync(
    options: LocalAuthenticationOptions = {}
): Promise<LocalAuthenticationResult> {
    //...
    const promptMessage = options.promptMessage || 'Authenticate';
    const result = await ExpoLocalAuthentication.authenticateAsync({ ...options, promptMessage });

    if (result.warning) {
        console.warn(result.warning);
    }
    return result;
}

The method performs a call to the native ExpoLocalAuthentication.authenticateAsync method and returns the resulting object. To see which data is included in the result object, we will have to dive into the platform specific part of the library.

Android

The authenticateAsync method called from JavaScript can be found in the LocalAuthenticationModule.java file. The following code snippet is the part that we are interested in:

public void authenticateAsync(final Map<String, Object> options, final Promise promise) {
  
      //...
      Executor executor = Executors.newSingleThreadExecutor();
      mBiometricPrompt = new BiometricPrompt(fragmentActivity, executor, mAuthenticationCallback);

      BiometricPrompt.PromptInfo.Builder promptInfoBuilder = new BiometricPrompt.PromptInfo.Builder()
              .setDeviceCredentialAllowed(!disableDeviceFallback)
              .setTitle(promptMessage);
      if (cancelLabel != null && disableDeviceFallback) {
        promptInfoBuilder.setNegativeButtonText(cancelLabel);
      }
      BiometricPrompt.PromptInfo promptInfo = promptInfoBuilder.build();
      mBiometricPrompt.authenticate(promptInfo);
    }
  });
}

Right away, we can see that the call to BiometricPrompt.authenticate is performed without supplying a BiometricPrompt.CryptoObject. The biometric authentication can therefore only be event-based rather than result-based. For the sake of completeness, let’s verify this assertion by looking at the success callback method:

new BiometricPrompt.AuthenticationCallback () {
  @Override
  public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
    mIsAuthenticating = false;
    mBiometricPrompt = null;
    Bundle successResult = new Bundle();
    successResult.putBoolean("success", true);
    safeResolve(successResult);
  }
};

As expected, the onAuthenticationSucceeded callback method does not verify the value of result and returns a boolean value, which shows that the Android implementation is event-based.

iOS

Let’s now look at the iOS implementation.

The authenticateAsync method called from JavaScript can be found in the EXLocalAuthentication.m file. The following code snippet is the part that we are interested in:

UM_EXPORT_METHOD_AS(authenticateAsync,
                    authenticateWithOptions:(NSDictionary *)options
                    resolve:(UMPromiseResolveBlock)resolve
                    reject:(UMPromiseRejectBlock)reject)
{
    //...
    [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
      localizedReason:reason
        reply:^(BOOL success, NSError *error) {
          resolve(@{
            @"success": @(success),
            @"error": error == nil ? [NSNull null] : [self convertErrorCode:error],
            @"warning": UMNullIfNil(warningMessage),
          });
        }];
}

Just like the Android implementation, the library returns a boolean value indicating whether the authentication succeeded or not. The iOS implementation is therefore event-based.

It is worth noting that the library allows for other authentication methods to be used on iOS (device PIN code, Apple Watch, …). Unfortunately, the implementation of the authentication for the other methods suffers from the same issue as the biometric authentication as can be seen in the following code snippet:

UM_EXPORT_METHOD_AS(authenticateAsync,
                    authenticateWithOptions:(NSDictionary *)options
                    resolve:(UMPromiseResolveBlock)resolve
                    reject:(UMPromiseRejectBlock)reject)
{
  NSString *disableDeviceFallback = options[@"disableDeviceFallback"];
  //...
  if ([disableDeviceFallback boolValue]) {
    // biometric authentication
  } else {
    [context evaluatePolicy:LAPolicyDeviceOwnerAuthentication
      localizedReason:reason
        reply:^(BOOL success, NSError *error) {
          resolve(@{
            @"success": @(success),
            @"error": error == nil ? [NSNull null] : [self convertErrorCode:error],
            @"warning": UMNullIfNil(warningMessage),
          });
        }];
  }
}

As we just saw, the expo-local-authentication library only supports event-based biometric authentication. Developer using this library will therefore not be able to implement a secure biometric authentication.

Result: Insecure event-based authentication

React-native-fingerprint-scanner

Source: https://github.com/hieuvp/react-native-fingerprint-scanner (Reviewed version)

The library provides two different implementations for the two platforms. Let’s start with Android.

Android

The library provides one JavaScript method to authenticate using biometric, authenticate, that can be found in the authenticate.android.js file. On Android 6.0 and above, the authenticate method will be the following:

const authCurrent = (title, subTitle, description, cancelButton, resolve, reject) => {
  ReactNativeFingerprintScanner.authenticate(title, subTitle, description, cancelButton)
    .then(() => {
      resolve(true);
    })
    .catch((error) => {
      reject(createError(error.code, error.message));
    });
}

On Android versions before Android 6.0, the authenticate method will be the following:

const authLegacy = (onAttempt, resolve, reject) => {
  //...
  ReactNativeFingerprintScanner.authenticate()
    .then(() => {
      DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION');
      resolve(true);
    })
    .catch((error) => {
      DeviceEventEmitter.removeAllListeners('FINGERPRINT_SCANNER_AUTHENTICATION');
      reject(createError(error.code, error.message));
    });
}

In both cases, the method will return a boolean value if the call to ReactNativeFingerprintScanner.authenticate did not throw an error, and will raise an exception otherwise. The Android implementation is therefore event-based.

iOS

Just like Android, the library provides one JavaScript method to authenticate using biometric: authenticate. The implementation of the method can be found in the authenticate.ios.js file and can also be found in the following code snippet:

export default ({ description = ' ', fallbackEnabled = true }) => {
  return new Promise((resolve, reject) => {
    ReactNativeFingerprintScanner.authenticate(description, fallbackEnabled, error => {
      if (error) {
        return reject(createError(error.code, error.message))
      }

      return resolve(true);
    });
  });
}

Once again, the method will return a boolean value if the call to ReactNativeFingerprintScanner.authenticate did not return an error. The iOS implementation is therefore event-based.

Similarly to expo-local-authentication, react-native-fingerprint-scanner also supports other authentication methods on iOS. These can be used as fallback methods if the fallbackEnabled parameter is set to true when calling the authenticate method, which is the case by default. As the authenticate method is used for these fallback methods as well, they also suffer from the same issue as the biometric authentication provided by the library.

As we just saw, the react-native-fingerprint-scanner library only supports event-based biometric authentication. Developer using this library will therefore not be able to implement a secure biometric authentication.

Result: Insecure event-based authentication

React-native-fingerprint-android

GitHub: https://github.com/jariz/react-native-fingerprint-android (Reviewed version)

As the name of the library suggests, the library only implements biometric authentication on the Android platform.

The library provides one method for biometric authentication, authenticate, which can be found in the index.android.js file. The part that we are interested in is the following:

static async authenticate(warningCallback:?(response:FingerprintError) => {}):Promise<null> {
  //..
  let err;
  try {
    await FingerprintAndroidNative.authenticate();
  } catch(ex) {
    err = ex
  }
  finally {
    //remove the subscriptions and crash if needed
    DeviceEventEmitter.removeAllListeners("fingerPrintAuthenticationHelp");
    if(err) {
      throw err
    }
  }
}

Right away, we can see in the method prototype that the method returns a Promise<null>. This is similar to returning a boolean value, indicating therefore that the biometric authentication provided by the library is event-based.

However, let’s still dive into the Java implementation of FingerprintAndroidNative.authenticate just to be sure.

The implementation of the method can be found in the FingerprintModule.java file. The relevant lines of the method can be found below:

public void authenticate(Promise promise) {
    //...
    fingerprintManager.authenticate(null, 0, cancellationSignal, new AuthenticationCallback(promise), null); 
    //..
}

As we can see, the method performs a call to the FingerprintManager.authenticate method without providing a FingerprintManager.CryptoObject. The biometric authentication can therefore only be event-based rather than result-based. We could convince ourselves even further by inspecting the OnAuthenticationSucceeded callback method, but this should be enough already.

As we just saw, the react-native-fingerprint-android library only supports event-based biometric authentication. Developer using this library will therefore not be able to implement a secure biometric authentication.

Result: Insecure event-based authentication

React-native-biometrics

GitHub: https://github.com/SelfLender/react-native-biometrics (Reviewed version)

Last, but certainly not least! The library provides two methods to authenticate using biometrics. This looks promising already!

The first method to perform biometric authentication is the simplePrompt method, available in the index.ts file. However, it is clearly mentioned in the documentation that this method only validates the user’s biometrics and that it should not be used for security sensitive features:

simplePrompt(options)
Prompts the user for their fingerprint or face id. Returns a Promise that resolves if the user provides a valid biometrics or cancel the prompt, otherwise the promise rejects.

**NOTE: This only validates a user's biometrics. This should not be used to log a user in or authenticate with a server, instead use createSignature. It should only be used to gate certain user actions within an app.

We will therefore not investigate this method as it should already be clear to the reader that it is an event-based biometric authentication.

The second method to perform biometric authentication in the library is the createSignature method, available in the index.ts file. According to the documentation, to use this method, a key pair must first be created, using the createKeys method, and the public key must be sent to the server. The authentication process consists in a cryptographic signature sent and verified on the server. The diagram below, taken from the Readme file, illustrates this process.

https://camo.githubusercontent.com/8558a1a8617482d43d4ea3fc6d872adcc6c2c42644cdfef994b4ac3b2790907b/68747470733a2f2f322e62702e626c6f6773706f742e636f6d2f2d4c70327a61415a696574772f566935396862366b3653492f4141414141414141424c6b2f48735858425969497771552f73313630302f696d61676530312e706e67
Authentication flow (source)

Alright! On paper, this looks pretty secure: a cryptographic signature being verified on the server is a proper way to perform biometric authentication. However, we still need to verify if the cryptographic operations are done properly in the library.

Let’s analyze the platform specific implementations.

Android

To verify that the library uses a secure implementation, we have to verify that:

  • The private key used to perform the signature requires user authentication;
  • The success callback uses the result of the biometric authentication to perform cryptographic operations;
  • The library returns the result of the above cryptographic operations to the application.

So first, let’s analyze the createSignature method from the ReactNativeBiometrics class:

public void createSignature(final ReadableMap params, final Promise promise) {
    //...
    Signature signature = Signature.getInstance("SHA256withRSA");
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);

    PrivateKey privateKey = (PrivateKey) keyStore.getKey(biometricKeyAlias, null);
    signature.initSign(privateKey);

    BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature);

    AuthenticationCallback authCallback = new CreateSignatureCallback(promise, payload);
    //...
    BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor, authCallback);

    PromptInfo promptInfo = new PromptInfo.Builder()
            .setDeviceCredentialAllowed(false)
            .setNegativeButtonText(cancelButtomText)
            .setTitle(promptMessage)
            .build();
    biometricPrompt.authenticate(promptInfo, cryptoObject);
}

In the above code, we can see that a Signature object is initiated with the private key biometricKeyAlias. A CryptoObject is then initiated with the signature. Finally, we can see that the CryptoObject is correctly given to the BiometricPrompt.authenticate method. Ok, so far so good.

Let’s now take a look at how the used key pair is created:

public void createKeys(Promise promise) {
    //...  
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
    KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(biometricKeyAlias, KeyProperties.PURPOSE_SIGN)
            .setDigests(KeyProperties.DIGEST_SHA256)
            .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
            .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
            .setUserAuthenticationRequired(true)
            .build();
    keyPairGenerator.initialize(keyGenParameterSpec);
    //...
}

We can see in the code snippet above that the AndroidKeystore is used and that the key pair is configured to require user authentication using the setUserAuthenticationRequired method.

We now only need to verify that the success callback properly handles and returns the result of the authentication. Let’s take a look at the onAuthenticationSucceeded method of the CreateSignatureCallback class:

public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
    //...
    BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
    Signature cryptoSignature = cryptoObject.getSignature();
    cryptoSignature.update(this.payload.getBytes());
    byte[] signed = cryptoSignature.sign();
    String signedString = Base64.encodeToString(signed, Base64.DEFAULT);
    signedString = signedString.replaceAll("\r", "").replaceAll("\n", "");

    WritableMap resultMap = new WritableNativeMap();
    resultMap.putBoolean("success", true);
    resultMap.putString("signature", signedString);
    promise.resolve(resultMap);
    //... 
}

The success callback uses the authentication result to get the Signature object and to sign the provided payload. The signature is then encoded in base64 and returned in the promise.

The application can therefore provide a payload to the library, which will be signed after the user successfully provided their biometric data. The signature is then returned to the application, which can finally be sent to the server for verification and complete the authentication.

The Android implementation therefore allows for a secure result-based biometric authentication.

iOS

Like for Android, to verify that the library uses a secure implementation, we have to verify that:

  • The private key requires user authentication;
  • The private key is used to perform cryptographic operations;
  • The library returns the result of the above cryptographic operations to the application.

So, let’s dive right in. The following code snippet shows the relevant part of the createSignature method, available in the ReactNativeBiometrics.m file:

RCT_EXPORT_METHOD(createSignature: (NSDictionary *)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    //...
    NSData *biometricKeyTag = [self getBiometricKeyTag];
    NSDictionary *query = @{
                            (id)kSecClass: (id)kSecClassKey,
                            (id)kSecAttrApplicationTag: biometricKeyTag,
                            (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
                            (id)kSecReturnRef: @YES,
                            (id)kSecUseOperationPrompt: promptMessage
                            };
    SecKeyRef privateKey;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);

    if (status == errSecSuccess) {
      NSError *error;
      NSData *dataToSign = [payload dataUsingEncoding:NSUTF8StringEncoding];
      NSData *signature = CFBridgingRelease(SecKeyCreateSignature(privateKey, kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256, (CFDataRef)dataToSign, (void *)&error));

      if (signature != nil) {
        NSString *signatureString = [signature base64EncodedStringWithOptions:0];
        NSDictionary *result = @{
          @"success": @(YES),
          @"signature": signatureString
        };
        resolve(result);
      }
      //...
    }
}

The library attempts to retrieve the private key, identified by biometricKeyTag, from the Keychain and then uses it to sign a provided payload. When the signature succeeds, the library returns the encrypted data to the application. This looks very good already!

Let’s now take a look at how the private key is generated, to ensure that proper user authentication is needed to access it. The key pair is created in the createKeys method, in the same file. The following code snippet show the relevant part of the method:

RCT_EXPORT_METHOD(createKeys: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    //...
    SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                                    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
                                                                    kSecAccessControlBiometryAny, &error);
    //...
    NSDictionary *keyAttributes = @{
        (id)kSecClass: (id)kSecClassKey,
        (id)kSecAttrKeyType: (id)kSecAttrKeyTypeRSA,
        (id)kSecAttrKeySizeInBits: @2048,
        (id)kSecPrivateKeyAttrs: @{
        (id)kSecAttrIsPermanent: @YES,
        (id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIAllow,
        (id)kSecAttrApplicationTag: biometricKeyTag,
        (id)kSecAttrAccessControl: (__bridge_transfer id)sacObject
        }
    };
    //...
    id privateKey = CFBridgingRelease(SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyAttributes, (void *)&gen_error));
    //...
}

In the above code snippet, we can see that the key pair is generated and added to the Keychain using the kSecAccessControlBiometryAny access control flag. Retrieving the key from the Keychain will therefore require a successful biometric authentication.

The application can therefore provide a payload to the library, which will be signed after the user successfully authenticated. The signature is then returned to the application, which can then be submitted to the server for verification.

The iOS implementation therefore allows for a secure result-based biometric authentication.

As we saw, the react-native-biometrics library provides two biometric authentication methods, one of which, createSignature, offers a secure result-based biometric authentication.

It should be noted that the way the library perform biometric authentication requires the server to implement the signature verification, which is harder and requires more changes on the server than just decrypting a token on the local device and sending it to the server for verification. However, while it is a bit harder to integrate into an application, it has the advantage of preventing replay attacks as the authentication payload sent to the server will be different for every authentication.

Result: Secure result-based authentication

Conclusion

Out of the five libraries we analyzed, only one of them, react-native-biometrics, provides a secure result-based biometric authentication which allows for a non-bypassable authentication implementation. The other four libraries only provide event-based biometric authentication, which only allows for a client-side authentication implementation which would therefore be bypassable.

The table below provides a summary of the type of biometric authentication offered by each analyzed library:

Library Event-based Result-based
react-native-touch-id
expo-local-authentication
react-native-fingerprint-scanner
react-native-fingerprint-android
react-native-biometrics

Usage of third party libraries and mobile development frameworks can certainly decrease the needed development effort, and for applications that don’t require a high level of security, there’s not too much that can go wrong. However, if your application does contain sensitive data or functionality, such as applications from the financial, government or healthcare sector, security should be included in each step of the SDLC. In that case, choosing the correct mobile development framework to use (if any) and which external libraries to trust (if any) is a very important step.

About the authors

Simon Lardinois
Simon Lardinois

Simon Lardinois is a Security Consultant in the Software and Security assessment team at NVISO. His main area of focus is mobile application security, but is also interested in web and reverse engineering. In addition to mobile applications security, he also enjoys developing mobile applications.

Jeroen Beckers
Jeroen Beckers

Jeroen Beckers is a mobile security expert working in the NVISO Software and Security assessment team. He is a SANS instructor and SANS lead author of the SEC575 course. Jeroen is also a co-author of OWASP Mobile Security Testing Guide (MSTG) and the OWASP Mobile Application Security Verification Standard (MASVS). He loves to both program and reverse engineer stuff.

❌