Creación de una aplicación Safety Net Checker en Android

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:

  1. Estudio Android 4.xx
  2. Cuenta de la nube de Google
  3. 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í

Publicación traducida automáticamente

Artículo escrito por shanuu12e y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *