Remove SafetyNet check

This commit is contained in:
topjohnwu 2021-09-13 01:41:31 -07:00
parent 8d59caf635
commit 470fc97d1f
9 changed files with 3 additions and 642 deletions

View File

@ -25,9 +25,4 @@
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute" />
<!-- Hardcode GMS version -->
<meta-data
android:name="com.google.android.gms.version"
android:value="12451000" />
</manifest>

View File

@ -75,9 +75,6 @@ class HomeViewModel(
var stateManagerProgress = 0
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
@get:Bindable
val showSafetyNet get() = Info.hasGMS && isConnected.get()
val itemBinding = itemBindingOf<IconLink> {
it.bindExtra(BR.viewModel, this)
}
@ -86,7 +83,6 @@ class HomeViewModel(
override fun refresh() = viewModelScope.launch {
state = State.LOADING
notifyPropertyChanged(BR.showSafetyNet)
Info.getRemote(svc)?.apply {
state = State.LOADED
@ -101,10 +97,10 @@ class HomeViewModel(
launch {
ensureEnv()
}
} ?: {
} ?: run {
state = State.LOADING_FAILED
managerRemoteVersion = R.string.not_available.asText()
}()
}
}
val showTest = false
@ -134,9 +130,6 @@ class HomeViewModel(
HomeFragmentDirections.actionHomeFragmentToInstallFragment().navigate()
}
fun onSafetyNetPressed() =
HomeFragmentDirections.actionHomeFragmentToSafetynetFragment().navigate()
fun hideNotice() {
Config.safetyNotice = false
isNoticeVisible = false

View File

@ -1,225 +0,0 @@
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk.ui.safetynet
import android.content.Context
import android.util.Base64
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.ViewEventWithScope
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.createClassLoader
import com.topjohnwu.magisk.ktx.reflectField
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.signing.CryptoUtils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import dalvik.system.BaseDexClassLoader
import dalvik.system.DexFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.bouncycastle.asn1.ASN1Encoding
import org.bouncycastle.asn1.ASN1Primitive
import org.bouncycastle.est.jcajce.JsseDefaultHostnameAuthorizer
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.File
import java.io.IOException
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Proxy
import java.security.SecureRandom
import java.security.Signature
class CheckSafetyNetEvent(
private val callback: (SafetyNetResult) -> Unit = {}
) : ViewEventWithScope(), ContextExecutor, SafetyNetHelper.Callback {
private val svc get() = ServiceLocator.networkService
private lateinit var jar: File
private lateinit var nonce: ByteArray
override fun invoke(context: Context) {
jar = File("${context.filesDir.parent}/snet", "snet.jar")
scope.launch(Dispatchers.IO) {
attest(context) {
// Download and retry
Shell.sh("rm -rf " + jar.parent).exec()
jar.parentFile?.mkdir()
withContext(Dispatchers.Main) {
showDialog(context)
}
}
}
}
private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) {
val helper: SafetyNetHelper
try {
val loader = createClassLoader(jar)
// Scan through the dex and find our helper class
var clazz: Class<*>? = null
loop@for (dex in loader.getDexFiles()) {
for (name in dex.entries()) {
val cls = loader.loadClass(name)
if (InvocationHandler::class.java.isAssignableFrom(cls)) {
clazz = cls
break@loop
}
}
}
clazz ?: throw Exception("Cannot find SafetyNetHelper implementation")
helper = Proxy.newProxyInstance(
loader, arrayOf(SafetyNetHelper::class.java),
clazz.newInstance() as InvocationHandler) as SafetyNetHelper
if (helper.version != Const.SNET_EXT_VER)
throw Exception("snet extension version mismatch")
} catch (e: Exception) {
onError(e)
return
}
val random = SecureRandom()
nonce = ByteArray(24)
random.nextBytes(nonce)
helper.attest(context, nonce, this)
}
// All of these fields are whitelisted
private fun BaseDexClassLoader.getDexFiles(): List<DexFile> {
val pathList = BaseDexClassLoader::class.java.reflectField("pathList").get(this)
val dexElements = pathList.javaClass.reflectField("dexElements").get(pathList) as Array<*>
val fileField = dexElements.javaClass.componentType.reflectField("dexFile")
return dexElements.map { fileField.get(it) as DexFile }
}
private fun download(context: Context) = scope.launch(Dispatchers.IO) {
val abort: suspend (Exception) -> Unit = {
Timber.e(it)
withContext(Dispatchers.Main) {
callback(SafetyNetResult())
}
}
try {
svc.fetchSafetynet().byteStream().writeTo(jar)
attest(context, abort)
} catch (e: IOException) {
abort(e)
}
}
private fun showDialog(context: Context) {
MagiskDialog(context)
.applyTitle(R.string.proprietary_title)
.applyMessage(R.string.proprietary_notice)
.cancellable(false)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = android.R.string.ok
onClick { download(context) }
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
onClick { callback(SafetyNetResult(dismiss = true)) }
}
.onCancel {
callback(SafetyNetResult(dismiss = true))
}
.reveal()
}
private fun String.decode(): ByteArray {
return if (contains("[+/]".toRegex()))
Base64.decode(this, Base64.DEFAULT)
else
Base64.decode(this, Base64.URL_SAFE)
}
private fun String.parseJws(): SafetyNetResponse {
val jws = split('.')
val secondDot = lastIndexOf('.')
val rawHeader = String(jws[0].decode())
val payload = String(jws[1].decode())
var signature = jws[2].decode()
val signedBytes = substring(0, secondDot).toByteArray()
val moshi = Moshi.Builder().build()
val header = moshi.adapter(JwsHeader::class.java).fromJson(rawHeader)
?: error("Invalid JWS header")
val alg = when (header.algorithm) {
"RS256" -> "SHA256withRSA"
"ES256" -> {
// Convert to DER encoding
signature = ASN1Primitive.fromByteArray(signature).getEncoded(ASN1Encoding.DER)
"SHA256withECDSA"
}
else -> error("Unsupported algorithm: ${header.algorithm}")
}
// Verify signature
val certB64 = header.certificates?.first() ?: error("Cannot find certificate in JWS")
val bis = ByteArrayInputStream(certB64.decode())
val cert = CryptoUtils.readCertificate(bis)
val verifier = Signature.getInstance(alg)
verifier.initVerify(cert.publicKey)
verifier.update(signedBytes)
if (!verifier.verify(signature))
error("Signature mismatch")
// Verify hostname
val hostnameVerifier = JsseDefaultHostnameAuthorizer(setOf())
if (!hostnameVerifier.verify("attest.android.com", cert))
error("Hostname mismatch")
val response = moshi.adapter(SafetyNetResponse::class.java).fromJson(payload)
?: error("Invalid SafetyNet response")
// Verify results
if (!response.nonce.decode().contentEquals(nonce))
error("nonce mismatch")
return response
}
override fun onResponse(response: String?) {
if (response != null) {
scope.launch(Dispatchers.Default) {
val res = runCatching { response.parseJws() }.getOrElse {
Timber.e(it)
INVALID_RESPONSE
}
withContext(Dispatchers.Main) {
callback(SafetyNetResult(res))
}
}
} else {
callback(SafetyNetResult())
}
}
}
@JsonClass(generateAdapter = true)
data class JwsHeader(
@Json(name = "alg") val algorithm: String,
@Json(name = "x5c") val certificates: List<String>?
)
@JsonClass(generateAdapter = true)
data class SafetyNetResponse(
val nonce: String,
val ctsProfileMatch: Boolean,
val basicIntegrity: Boolean,
val evaluationType: String = ""
)
// Special instance to indicate invalid SafetyNet response
val INVALID_RESPONSE = SafetyNetResponse("", ctsProfileMatch = false, basicIntegrity = false)

View File

@ -1,14 +0,0 @@
package com.topjohnwu.magisk.ui.safetynet
import android.content.Context
interface SafetyNetHelper {
val version: Int
fun attest(context: Context, nonce: ByteArray, callback: Callback)
interface Callback {
fun onResponse(response: String?)
}
}

View File

@ -1,31 +0,0 @@
package com.topjohnwu.magisk.ui.safetynet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentSafetynetMd2Binding
import com.topjohnwu.magisk.di.viewModel
class SafetynetFragment : BaseUIFragment<SafetynetViewModel, FragmentSafetynetMd2Binding>() {
override val layoutRes = R.layout.fragment_safetynet_md2
override val viewModel by viewModel<SafetynetViewModel>()
override fun onStart() {
super.onStart()
activity.setTitle(R.string.safetynet)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
// Set barrier reference IDs in code, since resource IDs will be stripped in release mode
binding.snetBarrier.referencedIds = intArrayOf(R.id.basic_text, R.id.cts_text)
return binding.root
}
}

View File

@ -1,95 +0,0 @@
package com.topjohnwu.magisk.ui.safetynet
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.databinding.set
class SafetyNetResult(
val response: SafetyNetResponse? = null,
val dismiss: Boolean = false
)
class SafetynetViewModel : BaseViewModel() {
@get:Bindable
var safetyNetTitle = R.string.empty
set(value) = set(value, field, { field = it }, BR.safetyNetTitle)
@get:Bindable
var ctsState = false
set(value) = set(value, field, { field = it }, BR.ctsState)
@get:Bindable
var basicIntegrityState = false
set(value) = set(value, field, { field = it }, BR.basicIntegrityState)
@get:Bindable
var evalType = ""
set(value) = set(value, field, { field = it }, BR.evalType)
@get:Bindable
var isChecking = false
set(value) = set(value, field, { field = it }, BR.checking)
@get:Bindable
var isSuccess = false
set(value) = set(value, field, { field = it }, BR.success, BR.textColorAttr)
@get:Bindable
val textColorAttr get() = if (isSuccess) R.attr.colorOnPrimary else R.attr.colorOnError
init {
cachedResult?.also {
handleResult(SafetyNetResult(it))
} ?: attest()
}
private fun attest() {
isChecking = true
CheckSafetyNetEvent(::handleResult).publish()
}
fun reset() = attest()
private fun handleResult(result: SafetyNetResult) {
isChecking = false
if (result.dismiss) {
back()
return
}
result.response?.apply {
cachedResult = this
if (this === INVALID_RESPONSE) {
isSuccess = false
ctsState = false
basicIntegrityState = false
evalType = "N/A"
safetyNetTitle = R.string.safetynet_res_invalid
} else {
val success = ctsProfileMatch && basicIntegrity
isSuccess = success
ctsState = ctsProfileMatch
basicIntegrityState = basicIntegrity
evalType = if (evaluationType.contains("HARDWARE")) "HARDWARE" else "BASIC"
safetyNetTitle =
if (success) R.string.safetynet_attest_success
else R.string.safetynet_attest_failure
}
} ?: run {
isSuccess = false
ctsState = false
basicIntegrityState = false
evalType = "N/A"
safetyNetTitle = R.string.safetynet_api_error
}
}
companion object {
private var cachedResult: SafetyNetResponse? = null
}
}

View File

@ -108,24 +108,10 @@
app:layout_constraintTop_toBottomOf="@+id/home_magisk_wrapper" />
<Space
gone="@{!viewModel.showSafetyNet &amp;&amp; !Info.env.isActive}"
goneUnless="@{Info.env.isActive}"
android:layout_width="match_parent"
android:layout_height="@dimen/l1" />
<Button
style="@style/WidgetFoundation.Button.Outlined"
gone="@{!viewModel.showSafetyNet}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:onClick="@{() -> viewModel.onSafetyNetPressed()}"
android:text="@string/home_check_safetynet"
android:textAllCaps="false"
android:textSize="12sp"
app:cornerRadius="@dimen/r1"
app:icon="@drawable/ic_safetynet_md2" />
<Button
style="@style/WidgetFoundation.Button.Outlined.Error"
goneUnless="@{Info.env.isActive}"

View File

@ -1,234 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<import type="android.os.Build" />
<import type="com.topjohnwu.magisk.core.Info" />
<import type="com.topjohnwu.magisk.BuildConfig" />
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.safetynet.SafetynetViewModel" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
tools:paddingBottom="48dp"
tools:paddingTop="24dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
invisibleUnless="@{viewModel.checking}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/safetynet_attest_loading"
android:textAppearance="@style/AppearanceFoundation.Title" />
<androidx.constraintlayout.widget.ConstraintLayout
invisible="@{viewModel.checking}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1">
<com.google.android.material.card.MaterialCardView
android:id="@+id/safetynet_attestation"
style="@style/WidgetFoundation.Card.Elevated"
cardBackgroundColorAttr="@{viewModel.success ? R.attr.colorPrimary : R.attr.colorError}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l2"
android:layout_marginEnd="@dimen/l1"
app:cardCornerRadius="@dimen/l1"
app:cardElevation="@dimen/l1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".35"
app:layout_constraintWidth_max="300dp"
tools:cardBackgroundColor="?colorPrimary">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/safetynet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l2"
android:layout_marginEnd="@dimen/l1"
android:gravity="center"
android:text="@{viewModel.safetyNetTitle}"
android:textAppearance="@style/AppearanceFoundation.Display.OnPrimary"
android:textStyle="bold"
app:textColorAttr="@{viewModel.textColorAttr}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/safetynet_attest_success" />
<ImageView
android:id="@+id/safetynet_divider"
android:layout_width="50dp"
android:layout_height="4dp"
android:layout_marginTop="@dimen/l2"
app:srcCompat="@drawable/bg_divider_rounded_on_primary"
app:tintAttr="@{viewModel.textColorAttr}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_title"
tools:tint="?colorOnPrimary"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/checkbox_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/l1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_divider">
<TextView
android:id="@+id/basic_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:text="basicIntegrity"
android:textAppearance="@style/AppearanceFoundation.Body.OnPrimary"
android:textStyle="bold"
android:layout_marginTop="@dimen/l2"
app:textColorAttr="@{viewModel.textColorAttr}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/snet_barrier"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/cts_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:text="ctsProfile"
android:textAppearance="@style/AppearanceFoundation.Body.OnPrimary"
android:textStyle="bold"
android:layout_marginTop="@dimen/l2"
app:textColorAttr="@{viewModel.textColorAttr}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/snet_barrier"
app:layout_constraintTop_toBottomOf="@id/basic_text"
tools:ignore="HardcodedText" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/snet_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="basic_text,cts_text"/>
<ImageView
style="@style/WidgetFoundation.Icon.OnPrimary"
isSelected="@{viewModel.basicIntegrityState}"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_check_circle_md2"
app:tintAttr="@{viewModel.textColorAttr}"
app:layout_constraintStart_toEndOf="@id/snet_barrier"
app:layout_constraintTop_toTopOf="@id/basic_text"
app:layout_constraintBottom_toBottomOf="@id/basic_text"
tools:tint="?colorOnPrimary"/>
<ImageView
style="@style/WidgetFoundation.Icon.OnPrimary"
isSelected="@{viewModel.ctsState}"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_check_circle_md2"
app:tintAttr="@{viewModel.textColorAttr}"
app:layout_constraintStart_toEndOf="@id/snet_barrier"
app:layout_constraintTop_toTopOf="@id/cts_text"
app:layout_constraintBottom_toBottomOf="@id/cts_text"
tools:tint="?colorOnPrimary"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="@dimen/l2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/checkbox_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:text="evalType"
android:textAppearance="@style/AppearanceFoundation.Body.OnPrimary"
android:textStyle="bold"
app:textColorAttr="@{viewModel.textColorAttr}"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAppearance="@style/AppearanceFoundation.Body.OnPrimary"
android:textStyle="bold"
android:layout_marginStart="@dimen/l1"
android:text="@{viewModel.evalType}"
app:textColorAttr="@{viewModel.textColorAttr}"
tools:text="HARDWARE"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<Button
style="@style/WidgetFoundation.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:onClick="@{() -> viewModel.reset()}"
android:text="@string/safetynet_attest_restart"
android:textAllCaps="false"
android:textColor="?colorOnSurfaceVariant"
app:icon="@drawable/ic_refresh_safetynet_md2"
app:iconTint="?colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="@+id/safetynet_attestation"
app:layout_constraintTop_toBottomOf="@+id/safetynet_attestation" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</androidx.core.widget.NestedScrollView>
</layout>

View File

@ -33,14 +33,6 @@
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop" />
<action
android:id="@+id/action_homeFragment_to_safetynetFragment"
app:destination="@id/safetynetFragment"
app:enterAnim="@anim/fragment_enter"
app:exitAnim="@anim/fragment_exit"
app:popEnterAnim="@anim/fragment_enter_pop"
app:popExitAnim="@anim/fragment_exit_pop" />
</fragment>
<fragment
@ -84,12 +76,6 @@
android:label="ModuleFragment"
tools:layout="@layout/fragment_module_md2" />
<fragment
android:id="@+id/safetynetFragment"
android:name="com.topjohnwu.magisk.ui.safetynet.SafetynetFragment"
android:label="SafetynetFragment"
tools:layout="@layout/fragment_safetynet_md2" />
<fragment
android:id="@+id/settingsFragment"
android:name="com.topjohnwu.magisk.ui.settings.SettingsFragment"