Open In App

Creating a Safety Net Checker Application in Android

Last Updated : 04 Oct, 2021
Improve
Improve
Like Article
Like
Save
Share
Report

In this tutorial we will build a SafetyNet checking application which will help us to understand how exactly does Google’s Safetynet Attestation API Functions and also understand JWS resolving in Kotlin to objects, generating a nonce and passing them during API call. Moreover, understanding Safetynet is necessary for every Android App developer because of its security checking mechanism and makes developers rely upon google’s security check implementation which must be taken into reference while building apps that scale.

Pre-requisites:

  1. Android Studio 4.x.x
  2. Google Cloud Account
  3. Android Device or Emulator

Understanding Safetynet

SafetyNet is a simple and scalable solution from Google to verify device compatibility and security. For app developers having concerns about their application’s security, Google trusts its Android SafetyNet will be the right answer. With a strong emphasis on security, SafetyNet essentially protects the sensitive data within an application and helps preserve user trust as well as device integrity. SafetyNet is a part of Google Play Services and is independent of the device manufacturer. Therefore, it requires Google Play Services to be enabled on the device for the API to function smoothly.

Create a Project under Google Cloud Project

Firstly you need to create a project under GCP and activate Android Device Verification API. Then go to the Credentials section on the platform to get the key, it would be required later for sending attestation request to SafetyNetAttestation API.

Now create an empty project in Android Studio

Basically, create an Empty Application in Android Studio and add the dependencies we will be using for this project. In this, we will use Fragment Navigation and also view binding for handling functionalities of views. For enabling View Binding in your project follow View Binding Guide. Below is the code for the build.gradle file. 

Kotlin




def nav_version = "2.3.1"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "com.google.android.gms:play-services-location:18.0.0"
implementation 'com.google.android.gms:play-services-safetynet:17.0.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'com.google.api-client:google-api-client:1.30.11'


Setting up for Safetynet Application

Now we need to Create 2 Fragments under the MainActivity and can call them as RequestFragment and ResultFragment. Request fragment would have a button to tap on and pull out a request to SafetyAttestationApi for fetching data from it to display in Result fragment. First, create navigation in res named as nav_graph.xml and it should look like this. and add the below code to that file. Below is the code for the nav_graph.xml file.

XML




<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@+id/request_fragment">
    <!--This creates nav path to request fragment-->
    <fragment
        android:id="@+id/request_fragment"
        android:name="com.shanu.safetynetchecker.ui.Request"
        android:label="Safetynet Request"
        tools:layout="@layout/fragment_request"
        >
        <action
            android:id="@+id/action_request_fragment_to_result_fragment"
            app:destination="@id/result_fragment" />
    </fragment>
    
    <!--This creates nav path to result fragment-->
    <fragment
        android:id="@+id/result_fragment"
        android:name="com.shanu.safetynetchecker.ui.Result"
        android:label="Safetynet Result"
        tools:layout="@layout/fragment_result"
        >
        <action
            android:id="@+id/action_result_fragment_to_request_fragment"
            app:destination="@id/request_fragment" />
        <!--This is required parcel we need to pass between them -->
        <argument
            android:name="data"
            app:argType="com.shanu.safetynetchecker.model.SafetynetResultModel" />
    </fragment>
  
  
</navigation>


This graph will connect our Request and Result fragment on top of MainActivity and thus the flow of the application can work smoothly.

Implementing API

Now we need to add functions in Request.kt to get data from API and then display it in the Result screen. Before implementing logic in Kotlin, We need to prepare layouts as following. Below is the code for the activity_main.xml file. 

XML




<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <!--We create a container inside main activity to handle fragments-->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>


Below is the code for the fragment_request.xml file. 

XML




<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/request_fragment"
    tools:context=".ui.Request">
  
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <!--Button to handle click to send request-->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnStatus"
            style="@style/Widget.App.Button.OutlinedButton.Icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:width="160dp"
            android:height="160dp"
            android:text="@string/check_status"
            android:textSize="20sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.MyApp.Button.Circle" />
  
    </androidx.constraintlayout.widget.ConstraintLayout>
  
</FrameLayout>


Below is the code for the fragment_result.xml file. 

XML




<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/result_fragment"
    tools:context=".ui.Result">
  
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
  
        <androidx.cardview.widget.CardView
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="20dp"
            android:layout_marginTop="40dp"
            android:layout_marginEnd="20dp"
            android:layout_marginBottom="40dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
            <!--Container to have data which shows result-->
            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                  
                <!--This shows profile match-->
                <TextView
                    android:id="@+id/textView2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="40dp"
                    android:layout_marginTop="40dp"
                    android:text="Profile Match"
                    android:textSize="16sp"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
                  
                <TextView
                    android:id="@+id/profileMatchText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="40dp"
                    android:layout_marginEnd="60dp"
                    android:text="TextView"
                    android:textSize="16sp"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
                
                <!--This shows evaluation type -->
                <TextView
                    android:id="@+id/textView3"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="40dp"
                    android:layout_marginTop="60dp"
                    android:text="Evaluation Type"
                    android:textSize="16sp"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/textView2" />
  
                <TextView
                    android:id="@+id/evaluationText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="60dp"
                    android:layout_marginEnd="60dp"
                    android:text="TextView"
                    android:textSize="16sp"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/profileMatchText" />
                  
                <!--This shows basic integrity result -->
                <TextView
                    android:id="@+id/textView5"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="48dp"
                    android:layout_marginTop="60dp"
                    android:text="Basic Integrity"
                    android:textSize="16sp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/textView3"
                    app:layout_constraintVertical_bias="0.0" />
  
                <TextView
                    android:id="@+id/basicIntegrityText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="60dp"
                    android:layout_marginEnd="60dp"
                    android:text="TextView"
                    android:textSize="16sp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/evaluationText"
                    app:layout_constraintVertical_bias="0.0" />
            </androidx.constraintlayout.widget.ConstraintLayout>
        </androidx.cardview.widget.CardView>
    </androidx.constraintlayout.widget.ConstraintLayout>
  
</FrameLayout>


So as of now, we’re done with basic layouts of the application and ready to implement the logic which the application needs to work on. The request sent on Safetynet API depends on initially the availability of Google Play Services. So the first and foremost thing that needs to be done is setting up the check for the availability of Google Play Services. Then we can send a request to API with a generated nonce which is needed by API to recheck it while data is returned. Data is returned in JsonWebSignature which needs to be parsed into Kotlin object to be displayed. Google suggests verifying returned data by the backend to avoid irregular attacking on the API system. Here we will just test the application and will not implement it by backend which is required to be done while making production-ready applications. Below is the code for the Request.kt file.

Kotlin




package com.shanu.safetynetchecker.ui
  
import android.os.Bundle
import android.util.Base64.DEFAULT
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.URLUtil.decode
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.safetynet.SafetyNet
import com.google.api.client.json.jackson2.JacksonFactory
import com.google.api.client.json.webtoken.JsonWebSignature
import com.shanu.safetynetchecker.R
import com.shanu.safetynetchecker.databinding.FragmentRequestBinding
import com.shanu.safetynetchecker.model.SafetynetResultModel
import com.shanu.safetynetchecker.util.API_KEY
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.security.SecureRandom
import java.util.*
  
class Request : Fragment() {
  
    private var _binding: FragmentRequestBinding? = null
    private val binding get() = _binding!!
    private val mRandom: Random = SecureRandom()
  
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentRequestBinding.inflate(inflater, container, false)
        return binding.root
    }
  
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
  
        binding.btnStatus.setOnClickListener {
            checkGoogleApi()
        }
    }
  
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
      
    // Checking of google play services is necessary to send request
    private fun checkGoogleApi() {
        if (GoogleApiAvailability.getInstance()
                .isGooglePlayServicesAvailable(requireContext(), 13000000) ==
            ConnectionResult.SUCCESS
        ) {
            sendSafetynetRequest()
        } else {
            Toast.makeText(context,"Update your Google Play Services",Toast.LENGTH_SHORT).show()
        }
    }
  
    private fun sendSafetynetRequest() {
        // Generating the nonce
        val noonceData = "Safety Net Data: " + System.currentTimeMillis()
        val nonce = getRequestNonce(noonceData)
  
        // Sending the request
        SafetyNet.getClient(activity).attest(nonce!!, API_KEY)
            .addOnSuccessListener {
                val jws:JsonWebSignature = decodeJws(it.jwsResult!!)
                Log.d("data", jws.payload["apkPackageName"].toString())
                val data = SafetynetResultModel(
                    basicIntegrity = jws.payload["basicIntegrity"].toString(),
                    evaluationType = jws.payload["evaluationType"].toString(),
                    profileMatch = jws.payload["ctsProfileMatch"].toString()
                )
                binding.btnStatus.isClickable = true
                val directions = RequestDirections.actionRequestFragmentToResultFragment(data)
                findNavController().navigate(directions)
            }
            .addOnFailureListener{
                if(it is ApiException) {
                    val apiException = it as ApiException
                    Log.d("data",apiException.message.toString() )
  
                }else {
                    Log.d("data", it.message.toString())
                }
            }
    }
      
    // This is to decode JWS to kotlin object
    private fun decodeJws(jwsResult:String): JsonWebSignature {
        var jws: JsonWebSignature? = null
        try {
            jws = JsonWebSignature.parser(JacksonFactory.getDefaultInstance())
                .parse(jwsResult)
            return jws!!
        } catch (e: IOException) {
            return jws!!
        }
    }
      
    // Nonce generator to get nonce of 24 length
    private fun getRequestNonce(data: String): ByteArray? {
        val byteStream = ByteArrayOutputStream()
        val bytes = ByteArray(24)
        mRandom.nextBytes(bytes)
        try {
            byteStream.write(bytes)
            byteStream.write(data.toByteArray())
        } catch (e: IOException) {
            return null
        }
        return byteStream.toByteArray()
    }
}


With this, we generate nonce of 24 bytes and then send a request to API passing none into it and we get data as JsonWebSignature(jws) which we fetch into a SafetynetResultModel which is a simple data class which we parcel to send it across fragments. Below is the code for the SafetynetResultModel.kt file.

Kotlin




package com.shanu.safetynetchecker.model
  
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
  
  
@Parcelize
data class SafetynetResultModel(
    val basicIntegrity: String,
    val evaluationType: String,
    val profileMatch: String
): Parcelable


We parceled the data and send it to the Result fragment by navController which we implemented into nav_graph during the first steps. This way our Result fragment has access to the arguments and thus we can extract data and display it on a simple page. Below is the code for the Result.kt file. 

Kotlin




package com.shanu.safetynetchecker.ui
  
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import com.shanu.safetynetchecker.databinding.FragmentResultBinding
import com.shanu.safetynetchecker.model.SafetynetResultModel
  
class Result : Fragment() {
    private var _binding: FragmentResultBinding? = null
    private val binding get() = _binding!!
      
      // Declared to get args passed between navgraph
    private val args: ResultArgs by navArgs()
  
    private lateinit var data:SafetynetResultModel
  
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        displayData()
    }
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentResultBinding.inflate(inflater, container, false)
        data = args.data
        return binding.root
    }
  
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
      
    // Function to display data into screen
    private fun displayData() {
        binding.evaluationText.text = data.evaluationType
        binding.basicIntegrityText.text = data.basicIntegrity
        binding.profileMatchText.text = data.profileMatch
    }
  
}


We get data with navArgs which is generated by passing the data into navController while navigating between fragments. Similar to passing data into intents. Then displayData() function can display it into the views we created in the layout earlier. This creates a basic SafetyNet application. For creating a production-ready application for distribution. You must add a backend to verify the data returned and check if API is abused or attacked and to prevent it add checks into it.

Project Link: Click here



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads