From 470fc97d1f62a92711ba6310a624b9fa27e93617 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Mon, 13 Sep 2021 01:41:31 -0700 Subject: [PATCH] Remove SafetyNet check --- app/shared/src/main/AndroidManifest.xml | 5 - .../topjohnwu/magisk/ui/home/HomeViewModel.kt | 11 +- .../ui/safetynet/CheckSafetyNetEvent.kt | 225 ----------------- .../magisk/ui/safetynet/SafetyNetHelper.kt | 14 -- .../magisk/ui/safetynet/SafetynetFragment.kt | 31 --- .../magisk/ui/safetynet/SafetynetViewModel.kt | 95 ------- app/src/main/res/layout/fragment_home_md2.xml | 16 +- .../res/layout/fragment_safetynet_md2.xml | 234 ------------------ app/src/main/res/navigation/main.xml | 14 -- 9 files changed, 3 insertions(+), 642 deletions(-) delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/safetynet/CheckSafetyNetEvent.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetyNetHelper.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt delete mode 100644 app/src/main/res/layout/fragment_safetynet_md2.xml diff --git a/app/shared/src/main/AndroidManifest.xml b/app/shared/src/main/AndroidManifest.xml index de79e1751..096449b52 100644 --- a/app/shared/src/main/AndroidManifest.xml +++ b/app/shared/src/main/AndroidManifest.xml @@ -25,9 +25,4 @@ android:usesCleartextTraffic="true" tools:ignore="UnusedAttribute" /> - - - diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt index 228770215..a78cb4134 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt @@ -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 { 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/CheckSafetyNetEvent.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/CheckSafetyNetEvent.kt deleted file mode 100644 index 30035ddb2..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/CheckSafetyNetEvent.kt +++ /dev/null @@ -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 { - 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? -) - -@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) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetyNetHelper.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetyNetHelper.kt deleted file mode 100644 index 92ef7c4ec..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetyNetHelper.kt +++ /dev/null @@ -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?) - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt deleted file mode 100644 index 191fda09f..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetFragment.kt +++ /dev/null @@ -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() { - - override val layoutRes = R.layout.fragment_safetynet_md2 - override val viewModel by viewModel() - - 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 - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt deleted file mode 100644 index 7d0c138b0..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt +++ /dev/null @@ -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 - } - -} diff --git a/app/src/main/res/layout/fragment_home_md2.xml b/app/src/main/res/layout/fragment_home_md2.xml index a1e70eef4..3ca1994a5 100644 --- a/app/src/main/res/layout/fragment_home_md2.xml +++ b/app/src/main/res/layout/fragment_home_md2.xml @@ -108,24 +108,10 @@ app:layout_constraintTop_toBottomOf="@+id/home_magisk_wrapper" /> -