在 Android 中创建 Safety Net Checker 应用程序
在本教程中,我们将构建一个 SafetyNet 检查应用程序,这将帮助我们了解 Google 的 Safetynet Attestation API 函数究竟是如何工作的,并了解 JWS 在 Kotlin 中解析为对象、生成随机数并在 API 调用期间传递它们。此外,了解 Safetynet 对每个 Android 应用程序开发人员都是必要的,因为它的安全检查机制使开发人员在构建可扩展的应用程序时必须参考谷歌的安全检查实现。
先决条件:
- 安卓工作室 4.xx
- 谷歌云账户
- Android 设备或模拟器
了解安全网
SafetyNet 是 Google 提供的一种简单且可扩展的解决方案,用于验证设备兼容性和安全性。对于担心应用程序安全性的应用程序开发人员,谷歌相信其 Android SafetyNet 将是正确的答案。 SafetyNet 非常重视安全性,从本质上保护应用程序中的敏感数据,并有助于维护用户信任和设备完整性。 SafetyNet 是 Google Play 服务的一部分,独立于设备制造商。因此,需要在设备上启用 Google Play 服务,API函数顺利运行。
在 Google Cloud Project 下创建一个项目
首先你需要在 GCP 下创建一个项目并激活 Android Device Verification API。然后去平台上的Credentials部分获取密钥,稍后将需要它向SafetyNetAttestation API发送证明请求。
现在在 Android Studio 中创建一个空项目
基本上,在 Android Studio 中创建一个空应用程序并添加我们将用于该项目的依赖项。在这里,我们将使用 Fragment Navigation 和视图绑定来处理视图的功能。要在您的项目中启用视图绑定,请遵循视图绑定指南。下面是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'
XML
XML
XML
XML
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()
}
}
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
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
}
}
设置 Safetynet 应用程序
现在我们需要在 MainActivity 下创建 2 个 Fragment 并且可以将它们称为 RequestFragment 和 ResultFragment。请求片段将有一个按钮可以点击并向 SafetyAttestationApi 拉出请求,以从中获取数据以显示在结果片段中。首先,在名为 nav_graph.xml 的 res 中创建导航,它应该如下所示。并将以下代码添加到该文件中。下面是nav_graph.xml文件的代码。
XML
该图将在 MainActivity 之上连接我们的 Request 和 Result 片段,因此应用程序的流程可以顺利运行。
实现API
现在我们需要在 Request.kt 中添加函数以从 API 获取数据,然后将其显示在 Result 屏幕中。在 Kotlin 中实现逻辑之前,我们需要准备如下布局。下面是activity_main.xml文件的代码。
XML
下面是fragment_request.xml文件的代码。
XML
下面是fragment_result.xml文件的代码。
XML
所以到目前为止,我们已经完成了应用程序的基本布局,并准备好实现应用程序需要处理的逻辑。 Safetynet API 上发送的请求最初取决于 Google Play 服务的可用性。因此,需要做的第一件事也是最重要的事情是设置对 Google Play 服务可用性的检查。然后我们可以使用生成的随机数向 API 发送请求,API 需要在返回数据时重新检查它。数据在 JsonWebSignature 中返回,需要解析成 Kotlin 对象才能显示。谷歌建议由后端验证返回的数据,以避免对API系统的不规则攻击。在这里,我们将只测试应用程序,不会通过后端实现它,而后端需要在制作生产就绪的应用程序时完成。下面是Request.kt文件的代码。
科特林
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()
}
}
有了这个,我们生成了 24 字节的随机数,然后向 API 发送一个请求,不传递任何内容,我们将数据作为 JsonWebSignature(jws) 获取,我们将其提取到 SafetynetResultModel 中,这是一个简单的数据类,我们将其打包以跨片段发送。下面是SafetynetResultModel.kt文件的代码。
科特林
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
我们将数据打包并通过 navController 将其发送到结果片段,我们在第一步中将其实现到 nav_graph 中。这样我们的 Result 片段就可以访问参数,因此我们可以提取数据并将其显示在一个简单的页面上。下面是Result.kt的代码 文件。
科特林
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
}
}
我们使用 navArgs 获取数据,这是通过在片段之间导航时将数据传递到 navController 生成的。类似于将数据传递给意图。然后displayData()函数可以将其显示到我们之前在布局中创建的视图中。这将创建一个基本的 SafetyNet 应用程序。用于创建用于分发的生产就绪应用程序。您必须添加一个后端来验证返回的数据并检查 API 是否被滥用或攻击,并防止它向其中添加检查。
项目链接:点此