@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.data.repository.NetworkService import com.topjohnwu.magisk.ktx.createClassLoader import com.topjohnwu.magisk.ktx.reflectField import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.signing.CryptoUtils 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 org.koin.core.KoinComponent import org.koin.core.inject import timber.log.Timber import java.io.ByteArrayInputStream import java.io.File import java.io.IOException import java.lang.reflect.InvocationHandler import java.security.GeneralSecurityException import java.security.SecureRandom import java.security.Signature import java.security.cert.X509Certificate class CheckSafetyNetEvent( private val callback: (SafetyNetResult) -> Unit = {} ) : ViewEventWithScope(), ContextExecutor, KoinComponent, SafetyNetHelper.Callback { private val svc by inject() private lateinit var apk: File private lateinit var dex: File private lateinit var nonce: ByteArray override fun invoke(context: Context) { apk = File("${context.filesDir.parent}/snet", "snet.jar") dex = File(apk.parent, "snet.dex") scope.launch(Dispatchers.IO) { attest(context) { // Download and retry Shell.sh("rm -rf " + apk.parent).exec() apk.parentFile?.mkdir() withContext(Dispatchers.Main) { showDialog(context) } } } } private suspend fun attest(context: Context, onError: suspend (Exception) -> Unit) { val helper: SafetyNetHelper try { val loader = createClassLoader(apk) // Scan through the dex and find our helper class var clazz: Class<*>? = null loop@for (dex in loader.getDexFiles()) { for (name in dex.entries()) { if (name.startsWith("x.")) { val cls = loader.loadClass(name) if (InvocationHandler::class.java.isAssignableFrom(cls)) { clazz = cls break@loop } } } } clazz ?: throw Exception("Cannot find SafetyNetHelper class") helper = clazz.getMethod("get", Class::class.java, Context::class.java, Any::class.java) .invoke(null, SafetyNetHelper::class.java, context, this) 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(nonce) } // 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(apk) 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) ?: return null val alg = when (header.algorithm) { "RS256" -> "SHA256withRSA" "ES256" -> { // Convert to DER encoding signature = ASN1Primitive.fromByteArray(signature).getEncoded(ASN1Encoding.DER) "SHA256withECDSA" } else -> return null } // Verify signature val certB64 = header.certificates?.first() ?: return null val certDer = certB64.decode() val bis = ByteArrayInputStream(certDer) val cert: X509Certificate try { cert = CryptoUtils.readCertificate(bis) val verifier = Signature.getInstance(alg) verifier.initVerify(cert.publicKey) verifier.update(signedBytes) if (!verifier.verify(signature)) return null } catch (e: GeneralSecurityException) { Timber.e(e) return null } // Verify hostname val hostNameVerifier = JsseDefaultHostnameAuthorizer(setOf()) try { if (!hostNameVerifier.verify("attest.android.com", cert)) return null } catch (e: IOException) { Timber.e(e) return null } val response = moshi.adapter(SafetyNetResponse::class.java).fromJson(payload) ?: return null // Verify results if (!response.nonce.decode().contentEquals(nonce)) return null return response } override fun onResponse(response: String?) { if (response != null) { scope.launch(Dispatchers.Default) { val res = response.parseJws() 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 = "" )