En este tutorial, crearemos una aplicación de verificación de SafetyNet que nos ayudará a comprender cómo funciona exactamente la API de atestación de Safetynet de Google y también comprenderemos la resolución de JWS en Kotlin en objetos, generando un nonce y pasándolos durante la llamada API. Además, comprender Safetynet es necesario para todos los desarrolladores de aplicaciones de Android debido a su mecanismo de verificación de seguridad y hace que los desarrolladores confíen en la implementación de verificación de seguridad de Google, que debe tenerse en cuenta al crear aplicaciones que escalan.
Requisitos previos:
- Estudio Android 4.xx
- Cuenta de la nube de Google
- Dispositivo Android o Emulador
Comprender la red de seguridad
SafetyNet es una solución simple y escalable de Google para verificar la compatibilidad y la seguridad de los dispositivos. Para los desarrolladores de aplicaciones que tienen dudas sobre la seguridad de sus aplicaciones, Google confía en que su Android SafetyNet será la respuesta correcta. Con un fuerte énfasis en la seguridad, SafetyNet esencialmente protege los datos confidenciales dentro de una aplicación y ayuda a preservar la confianza del usuario y la integridad del dispositivo. SafetyNet es parte de Google Play Services y es independiente del fabricante del dispositivo. Por lo tanto, requiere que Google Play Services esté habilitado en el dispositivo para que la API funcione sin problemas.
Crear un proyecto en Google Cloud Project
En primer lugar, debe crear un proyecto en GCP y activar la API de verificación de dispositivos Android. Luego vaya a la sección Credenciales en la plataforma para obtener la clave, se requerirá más tarde para enviar la solicitud de certificación a la API de SafetyNetAttestation.
Ahora crea un proyecto vacío en Android Studio
Básicamente, cree una aplicación vacía en Android Studio y agregue las dependencias que usaremos para este proyecto. En esto, usaremos Fragment Navigation y también veremos el enlace para manejar las funcionalidades de las vistas. Para habilitar Ver enlace en su proyecto, siga la Guía de enlace de vista. A continuación se muestra el código para el archivo build.gradle .
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'
Configuración de la aplicación Safetynet
Ahora necesitamos crear 2 fragmentos en MainActivity y podemos llamarlos como RequestFragment y ResultFragment. El fragmento de solicitud tendría un botón para tocar y extraer una solicitud a SafetyAttestationApi para obtener datos de él y mostrarlos en el fragmento de resultado. Primero, cree una navegación en resolución llamada nav_graph.xml y debería verse así. y agregue el siguiente código a ese archivo. A continuación se muestra el código para el archivo nav_graph.xml .
XML
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" 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>
Este gráfico conectará nuestro fragmento de solicitud y resultado en la parte superior de MainActivity y, por lo tanto, el flujo de la aplicación puede funcionar sin problemas.
API de implementación
Ahora necesitamos agregar funciones en Request.kt para obtener datos de la API y luego mostrarlos en la pantalla de resultados. Antes de implementar la lógica en Kotlin, debemos preparar los diseños de la siguiente manera. A continuación se muestra el código para el archivo activity_main.xml .
XML
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" 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>
A continuación se muestra el código para el archivo fragment_request.xml .
XML
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" 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>
A continuación se muestra el código para el archivo fragment_result.xml .
XML
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" 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>
Entonces, a partir de ahora, hemos terminado con los diseños básicos de la aplicación y estamos listos para implementar la lógica en la que la aplicación necesita funcionar. La solicitud enviada a la API de Safetynet depende inicialmente de la disponibilidad de Google Play Services. Entonces, lo primero y más importante que debe hacerse es configurar la verificación de la disponibilidad de Google Play Services. Luego, podemos enviar una solicitud a la API con un nonce generado que la API necesita para volver a verificarlo mientras se devuelven los datos. Los datos se devuelven en JsonWebSignature, que debe analizarse en el objeto Kotlin para que se muestre. Google sugiere verificar los datos devueltos por el backend para evitar ataques irregulares en el sistema API. Aquí solo probaremos la aplicación y no la implementaremos por backend, lo que se requiere hacer al crear aplicaciones listas para producción. A continuación se muestra el código para elArchivo Request.kt .
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() } }
Con esto, generamos nonce de 24 bytes y luego enviamos una solicitud a la API que no le pasa ninguno y obtenemos datos como JsonWebSignature (jws) que buscamos en un SafetynetResultModel, que es una clase de datos simple que empaquetamos para enviarlo a través de fragmentos. A continuación se muestra el código del archivo SafetynetResultModel.kt .
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
Empaquetamos los datos y los enviamos al fragmento Result por navController que implementamos en nav_graph durante los primeros pasos. De esta manera, nuestro fragmento de resultado tiene acceso a los argumentos y, por lo tanto, podemos extraer datos y mostrarlos en una página simple. A continuación se muestra el código para el archivo Result.kt .
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 } }
Obtenemos datos con navArgs que se generan al pasar los datos a navController mientras navegamos entre fragmentos. Similar a pasar datos a intenciones. Luego , la función displayData() puede mostrarlo en las vistas que creamos en el diseño anterior. Esto crea una aplicación básica de SafetyNet. Para crear una aplicación lista para producción para su distribución. Debe agregar un backend para verificar los datos devueltos y verificar si se abusa o ataca la API y para evitar que agregue verificaciones en ella.
Enlace del proyecto: Haga clic aquí