April 17, 2019

Extended Guide to SafetyNet

by Karan Gandhi

Extended Guide to SafetyNet

SafetyNet is a set of APIs provided by Google to safeguard Android apps against device tampering and harmful apps. Currently, the SafetyNet API provides Attestation, Safe Browsing, reCAPTCHA and Verify Apps API. This set of APIs requires Google Play Services in order to function correctly.

SafetyNet Attestation is the API commonly associated with SafetyNet. The attestation API checks the device integrity and allows us to filter the apps. Additionally, we can use the attestation API to verify client integrity. You can see SafetyNet implemented in apps like Android Pay, Netflix, and Pokemon Go.

In this article, we will cover SafetyNet in detail, including its integrations with Ionic, NativeScript and React Native. Also, we will be checking out the server-side code with NodeJS.

Device and Client Integrity

Android users can root their device. By doing so, they grant apps which use root privileges a deep level of access to the Android OS System. As a result, rooted devices can circumvent the security laid out by the OS developer.

Also, Android users can install custom ROMs on their devices. Custom ROMs are firmware created by the community based on Android's AOSP project or a rebranded manufacturer ROM. Usually, Custom ROMs have root privileges too.

Having root privileges puts the security of a device in the hands of an end user. As app developers, we would like to avoid such a situation.

Android apps are distributed in app stores and via websites. We would like to make sure that interaction between the app and the server is made by the legitimate app that we distributed. Once a file has been tampered with, its SHA hash changes. Using this bit of knowledge, we can easily verify the app’s integrity.

SafetyNet is a part of Google Play Services which are installed in all Google certified devices. When an app requests a SafetyNet attestation, the service gathers device details, compares with the device’s ctsProfile (which we’ll explain below) and provides the calling app with the response. Using the response, we can deduce whether:

  • The calling app has been tampered with.
  • The calling app is running on a rooted device.
  • The calling app is running on a firmware which has not passed the manufacturer’s CTS (Compatibility Test Suite); usually, custom ROMs and beta firmware don't pass the CTS.

Who should use SafetyNet

While SafetyNet should ideally run on any app to ensure its integrity, it’s especially important in applications that handle sensitive user data or perform important operations. This includes:

  • Apps conforming to standards like PCI - Mobile / PSD2 / HIPAA
  • Banking Apps / Crypto Wallets / Payment Wallets
  • Games
  • E-Commerce Storefronts
  • Chat Clients

Limitations

SafetyNet can be bypassed by some tools like Magisk Hide or SU Hide. It's advisable to use SafetyNet in conjunction with other root detecting tools like RootBeer or JailMonkey.

There are also some limits set in place. Specifically, a limit of 10000 requests per day per app (which can be extended upon request) and a hard limit of 5 requests per app per minute.

Also, an alternative course of action should be in place if there is an API outage. This actually happened recently.

Using SafetyNet

The recommended method to implement attestation is:

  • Generate nonce
  • Request attestation response using API key/nonce
  • Transfer the JWS response from the device to the server
  • Validate the response
  • Execute relevant logic based on the response

This method is useful when performing a spot check while executing a critical functionality like placing an order or transferring funds. The nonce can be used as an additional check.

Some helper libraries generate a nonce and validate the response on the app side. The workflow for such libraries can be:

  • Generate nonce
  • Request and verify attestation response
  • Execute relevant logic based on the response

This method is useful if you are performing a check on app boot or while logging the user in.

Obtaining the API key

We can obtain the API key from the Google Developer Console. Enable Android Device Verification in your existing or new project. The API key is available in the Credentials section. We can restrict API usage by specifying package names & SHA-1 certificate fingerprints.

Understanding Attestation Response

A decoded response received from the attestation API is below:

{
   "nonce": "YWc=",
   "timestampMs": 1553088490946,
   "apkPackageName": "app.cordova.safetynettest",
   "apkDigestSha256": "IUhOmEabdvHmLmIk5DD5fdfs6OPHUwAA+uGgYDVrcsU=",
   "ctsProfileMatch": true,
   "apkCertificateDigestSha256": [
       "EmGH9u67SiSyLuvZCoAN+R+NU/yHP29gSmoUgvNtehk="
   ],
   "basicIntegrity": true
}

Let’s go over the meaning of each item:

  • nonce: base64 encoded nonce string we originally pass to the attestation method.

  • apkPackageName: package name of the calling app.

  • apkDigestSha256: base64 encoded SHA256 hash of the APK.

  • apkCertificateDigestSha256: base64 encoded SHA256 hash of the signing certificates used.

  • ctsProfileMatch: Every device firmware which has Google Play has to pass CTS. The CTS profile match is used to identify the profiles which have passed CTS. In general, apps used on manufacturer OS usually have CTS true. Usually, this is false if:

    • The bootloader is unlocked
    • The device is running an official Beta Firmware
    • The device is running custom ROM like LineageOS
    • basicIntegrity has failed
  • basicIntegrity: Basic Integrity is a root/emulator check. It returns false if:

    • The device is rooted
    • THe app is running on an emulator
    • There is no device (protocol emulating script)
    • There are signs of attacks like API hooking.
  • timestampMs: This is a timestamp generated by Google’s servers when the attestation response was generated.

To ensure device integrity, we require ctsProfileMatch and basicIntegrity to be true. Similarly, to ensure application integrity, apkPackageName, apkDigestSha256, and apkCertificateDigestSha256 must match our original app. Additional checks can be introduced using a nonce.

Integrating SafetyNet on Cross-Platform Mobile Apps

Let's check some plugins to integrate SafetyNet in mobile apps.

Cordova & Ionic Apps

You can use SafetyNet in Ionic/Cordova apps using the SafetyNet plugin. The attest function accepts nonce and API_Key as parameters. On callback success, a JWS token can be obtained. Apart from attestation, methods for Verify Apps and list harmful apps are also available.

To install this plugin, you should use the command below:

cordova plugin add cordova-plugin-android-safetynet

Use the plugin as below:

window.safetynet.attest(nonce ,API_Key ,function(success) {
   console.log(success);
} , function(error){
   console.error(error);
});

Protect your Code with Jscrambler

NativeScript Apps

NativeScript's SafetyNet helper plugin uses a SafetyNet library. The plugin handles nonce generation, decodes and validates the response captured from the SafetyNet API. The request method accepts API_Key as a parameter.

To install this plugin, you should use the command below:

tns plugin add nativescript-safetynet-helper

Use the plugin as below:

import { SafetyNetHelper } from 'nativescript-safetynet-helper';
let helper = new SafetyNetHelper(this._apiKey);
helper.requestTest((err, result) => {
   if (err) {
       console.log(err);       
       return;
   }
   console.log(`Basic Integrity - ${result.basicIntegrity}, CTS Profile Match - ${result.ctsProfileMatch}`)
});

N.B.: The library uses an older version of SafetyNet.

Protect your NativeScript App with Jscrambler

React Native Apps

You can use SafetyNet in React Native with a SafetyNet Plugin. The plugin has separate methods for checking Play Services, generating a nonce, calling attestation API, and verifying the attestation response. The attestation method accepts nonce and API_Key as parameters.

To install this plugin, you should use the command below:

npm install react-native-google-safetynet --save
react-native link react-native-google-safetynet

Use the plugin as below:

import RNGoogleSafetyNet from 'react-native-google-safetynet';

RNGoogleSafetyNet.generateNonce(LENGTH).then((res) => {
   /// Generate nonce of length LENGTH. Result is nonce
});
RNGoogleSafetyNet.sendAttestationRequest(nonce, API_KEY).then((res) => {
   //  Decoded JSON response
});
RNGoogleSafetyNet.verifyAttestationResponse(nonce, JSON_RESPONSE).then((ver) => {
      /// Verifies JSON response. Result is a boolean
});

Preparing NodeJS Server for Attestation

This is a walkthrough for preparing a NodeJS instance for SafetyNet Attestation. You can use this for all types of Android apps. On the server, we need to carry out these tasks:

  • Generate a nonce token
  • Verify the JWS response
  • Parse the JWS token

Depending on our use case, we can use a cryptographic random number, or partial device and server nonce.

Using a cryptographic random number is one of the simplest methods to generate a nonce. We will use the crypto module of NodeJS to create a 32-byte nonce:

const crypto = require('crypto');
let nonce = crypto.randomBytes(32).toString('base64');

As for partial device and server nonce, this can vary from case to case. As a general rule:

  • Generate a server hash with timestamp + unique identifier + some random data
  • Generate a device hash with userid + deviceid
  • Combine the hashes to form a nonce

This requires quite a bit of effort to implement. Depending on the usable data of the nonce, you would reduce the risk of replay attacks on your app.

/*Server Side*/
const crypto = require('crypto');
let timestamp = new Date().getTime();
let timeStampHash = crypto.createHash('sha256').update(timestamp).digest('hex');
let order = '1010101010';
let orderHash = crypto.createHash('sha256').update(order).digest('hex');
let nonceServer = timeStampHash + '.' + orderHash

/*Client Side*/
let userHash = 'userHash';
let deviceHash = 'deviceHash';
let serverHash = nonceServer;
let nonce = userHash + '.' + deviceHash + '.' + serverHash;

A random number nonce is useful if you are performing a check at:

  • App boot
  • Login
  • Scheduled check

Such nonce is useful while performing a spot check like transferring currency, placing an order, or executing a critical piece of code.

Now, let's verify the JWS response with Google servers. We will be using axios to send an HTTP request to verify the JWS message.

npm i axios --save
axios.post('https://www.googleapis.com/androidcheck/v1/attestations/verify?key=API_Key', {
   "signedAttestation": JWS_MESSAGE
 })
 .then(function (response) {
   console.log(response.data);
 })
 .catch(function (error) {
   console.log(error);
 });

The signature verification is validated with the isValidSignature key.

To decode the JWS, we will be using the jws module.

npm install jws --save
const jws = require('jws');
let decodedJSON = jws.decode(req.body.jws);

Conclusion

In this article, we covered the benefits and challenges of implementing SafetyNet, its integrations with JavaScript App frameworks, and its server-side integration with NodeJS.

Using SafetyNet to validate application and device integrity is especially important for apps which handle sensitive user data, such as processing payments, private information, or Personally Identifiable Information (PII).

Lastly, if you're developing your mobile app using JavaScript, don't forget that your code will be completely readable by any end-user and someone can redistribute your app or tamper with it. The best way to prevent this is by protecting your code with several layers. Start your free Jscrambler trial and protect your source code today!