Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-31 16:26:45 +04:00
parent 821c6b5697
commit 9a7cc4e717
4 changed files with 116 additions and 1 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.tap.core.security package com.tangem.tap.core.security
import android.os.Build
import com.dexprotector.rtc.RtcStatus import com.dexprotector.rtc.RtcStatus
import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
@ -12,6 +13,63 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
override val isXposed: Boolean override val isXposed: Boolean
get() = getRtcStatusSafely()?.xposed == true get() = getRtcStatusSafely()?.xposed == true
override val isVulnerableToMediaTekExploit: Boolean by lazy {
val isAffected by lazy { isAffectedMediaTekDevice() }
val isPatched by lazy { hasSecurityPatch() }
val isVulnerable = isAffected && !isPatched
TangemLogger.i(
"CVE-2026-20435 check: isAffectedMediaTek=$isAffected, " +
"isPatched=$isPatched, isVulnerable=$isVulnerable",
)
isVulnerable
}
private fun isAffectedMediaTekDevice(): Boolean {
val socModel = resolveMediaTekSocModel()
val isAffected = socModel != null && socModel in AFFECTED_MEDIATEK_SOCS
TangemLogger.i("CVE-2026-20435 SoC result: model=$socModel, isAffected=$isAffected")
return isAffected
}
private fun resolveMediaTekSocModel(): String? {
// Layer 1: API 31+ provides direct SoC info (public API, most reliable)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manufacturer = Build.SOC_MANUFACTURER
val model = Build.SOC_MODEL
TangemLogger.i("CVE-2026-20435 Layer 1: SOC_MANUFACTURER=$manufacturer, SOC_MODEL=$model")
if (manufacturer.equals("MediaTek", ignoreCase = true)) {
extractSocModel(model)?.let { return it }
}
}
// Layer 2: Build.HARDWARE often contains "mtXXXX" on MediaTek devices (public API)
val hardware = Build.HARDWARE
TangemLogger.i("CVE-2026-20435 Layer 2: HARDWARE=$hardware")
extractSocModel(hardware)?.let { return it }
return null
}
private fun extractSocModel(value: String): String? {
val match = MEDIATEK_SOC_PATTERN.find(value.uppercase()) ?: return null
return match.value
}
private fun hasSecurityPatch(): Boolean {
val patch = Build.VERSION.SECURITY_PATCH
val isPatched = try {
patch >= MEDIATEK_CVE_FIX_PATCH_LEVEL
} catch (e: Exception) {
TangemLogger.w("CVE-2026-20435 patch check: failed to parse SECURITY_PATCH=$patch", e)
false // fail-safe: treat unknown patch level as unpatched
}
TangemLogger.i(
"CVE-2026-20435 patch check: SECURITY_PATCH=$patch, " +
"required=$MEDIATEK_CVE_FIX_PATCH_LEVEL, isPatched=$isPatched",
)
return isPatched
}
private fun getRtcStatusSafely(): RtcStatus? { private fun getRtcStatusSafely(): RtcStatus? {
return try { return try {
RtcStatus.getRtcStatus() RtcStatus.getRtcStatus()
@ -20,4 +78,21 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
null null
} }
} }
private companion object {
/** Android security patch level that includes the fix for CVE-2026-20435 */
const val MEDIATEK_CVE_FIX_PATCH_LEVEL = "2026-03-05"
/** Regex to extract MediaTek SoC model number (e.g., MT6789) */
val MEDIATEK_SOC_PATTERN = Regex("MT\\d{4}")
/** Affected MediaTek SoC models per Ledger Donjon disclosure */
val AFFECTED_MEDIATEK_SOCS = setOf(
"MT6739", "MT6761", "MT6765", "MT6768", "MT6781",
"MT6789", "MT6813", "MT6833", "MT6853", "MT6855",
"MT6877", "MT6878", "MT6879", "MT6880", "MT6885",
"MT6886", "MT6890", "MT6893", "MT6895", "MT6897",
"MT6983", "MT6985", "MT6989", "MT6990", "MT6993",
)
}
} }

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.main package com.tangem.tap.features.main
import android.os.Build
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.BlockchainSDKFactory
@ -39,6 +40,7 @@ import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.AppRouterConfig
@ -83,6 +85,7 @@ internal class MainViewModel @Inject constructor(
private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig, private val appRouterConfig: AppRouterConfig,
private val sellService: SellService, private val sellService: SellService,
private val deviceSecurityInfoProvider: DeviceSecurityInfoProvider,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() { ) : ViewModel() {
@ -123,6 +126,7 @@ internal class MainViewModel @Inject constructor(
deleteDeprecatedLogsUseCase() deleteDeprecatedLogsUseCase()
sendKeyboardIdentifierEvent() sendKeyboardIdentifierEvent()
sendMediaTekVulnerabilityEvent()
preloadImages() preloadImages()
} }
@ -341,7 +345,6 @@ internal class MainViewModel @Inject constructor(
listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true) listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true)
} }
@Suppress("NullableToStringCall")
private fun sendKeyboardIdentifierEvent() { private fun sendKeyboardIdentifierEvent() {
viewModelScope.launch { viewModelScope.launch {
val keyboardId = keyboardValidator.getKeyboardId() val keyboardId = keyboardValidator.getKeyboardId()
@ -364,6 +367,27 @@ internal class MainViewModel @Inject constructor(
} }
} }
private fun sendMediaTekVulnerabilityEvent() {
viewModelScope.launch(dispatchers.io) {
val isVulnerable = deviceSecurityInfoProvider.isVulnerableToMediaTekExploit
if (isVulnerable) {
analyticsEventHandler.send(
event = TechAnalyticsEvent.MediaTekVulnerability(
model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Build.SOC_MODEL else "unknown",
manufacturer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Build.SOC_MANUFACTURER
} else {
"unknown"
},
hardware = Build.HARDWARE,
patch = Build.VERSION.SECURITY_PATCH,
),
)
}
}
}
// Preload stories to display on startup before navigation to targeted screen // Preload stories to display on startup before navigation to targeted screen
private fun preloadImages() { private fun preloadImages() {
try { try {

View file

@ -35,4 +35,19 @@ sealed class TechAnalyticsEvent(
put("isTrusted", isTrusted.toString()) put("isTrusted", isTrusted.toString())
}, },
) )
class MediaTekVulnerability(
model: String,
manufacturer: String,
hardware: String,
patch: String,
) : TechAnalyticsEvent(
event = "MediaTek Vulnerability",
params = mapOf(
"SocModel" to model,
"SocManufacturer" to manufacturer,
"SocHardware" to hardware,
"Patch" to patch,
),
)
} }

View file

@ -4,6 +4,7 @@ interface DeviceSecurityInfoProvider {
val isRooted: Boolean val isRooted: Boolean
val isBootloaderUnlocked: Boolean val isBootloaderUnlocked: Boolean
val isXposed: Boolean val isXposed: Boolean
val isVulnerableToMediaTekExploit: Boolean
} }
fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed