Creating a Safety Net Checker Application in Android
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:
- Android Studio 4.x.x
- Google Cloud Account
- 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" ?> 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" 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" ?> 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" ?> 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
Please Login to comment...