diff --git a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt index 308322f8aa..e606ddfde2 100644 --- a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -1,5 +1,6 @@ package com.tangem.tap.core.security +import android.os.Build import com.dexprotector.rtc.RtcStatus import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.utils.logging.TangemLogger @@ -12,6 +13,63 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { override val isXposed: Boolean 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? { return try { RtcStatus.getRtcStatus() @@ -20,4 +78,21 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { 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", + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 7b156a7889..2efdd84d9e 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.main +import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents +import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.routing.configurator.AppRouterConfig @@ -83,6 +85,7 @@ internal class MainViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val appRouterConfig: AppRouterConfig, private val sellService: SellService, + private val deviceSecurityInfoProvider: DeviceSecurityInfoProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -123,6 +126,7 @@ internal class MainViewModel @Inject constructor( deleteDeprecatedLogsUseCase() sendKeyboardIdentifierEvent() + sendMediaTekVulnerabilityEvent() preloadImages() } @@ -341,7 +345,6 @@ internal class MainViewModel @Inject constructor( listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true) } - @Suppress("NullableToStringCall") private fun sendKeyboardIdentifierEvent() { viewModelScope.launch { 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 private fun preloadImages() { try { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt index 3e71bc838b..fe1501ef27 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt @@ -35,4 +35,19 @@ sealed class TechAnalyticsEvent( 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, + ), + ) } \ No newline at end of file diff --git a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt index 09d16c0a94..f76c699a21 100644 --- a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt +++ b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt @@ -4,6 +4,7 @@ interface DeviceSecurityInfoProvider { val isRooted: Boolean val isBootloaderUnlocked: Boolean val isXposed: Boolean + val isVulnerableToMediaTekExploit: Boolean } fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed \ No newline at end of file