diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/AccessibilityActivity.kt b/app/src/main/kotlin/com/x8bit/bitwarden/AccessibilityActivity.kt index d4a8ec79f2..c0dc427beb 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/AccessibilityActivity.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/AccessibilityActivity.kt @@ -1,8 +1,11 @@ package com.x8bit.bitwarden +import android.app.ComponentCaller +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.ui.platform.util.validate /** * An activity to be launched and then immediately closed so that the OS Shade can be collapsed @@ -11,7 +14,16 @@ import com.bitwarden.annotation.OmitFromCoverage @OmitFromCoverage class AccessibilityActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { + intent = intent.validate() super.onCreate(savedInstanceState) finish() } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent.validate()) + } + + override fun onNewIntent(intent: Intent, caller: ComponentCaller) { + super.onNewIntent(intent.validate(), caller) + } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/AuthCallbackActivity.kt b/app/src/main/kotlin/com/x8bit/bitwarden/AuthCallbackActivity.kt index 330ff15737..e86fd51d65 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/AuthCallbackActivity.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/AuthCallbackActivity.kt @@ -1,10 +1,12 @@ package com.x8bit.bitwarden +import android.app.ComponentCaller import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.ui.platform.util.validate import dagger.hilt.android.AndroidEntryPoint /** @@ -21,6 +23,7 @@ class AuthCallbackActivity : AppCompatActivity() { private val viewModel: AuthCallbackViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { + intent = intent.validate() super.onCreate(savedInstanceState) viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = intent)) @@ -35,4 +38,12 @@ class AuthCallbackActivity : AppCompatActivity() { startActivity(intent) finish() } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent.validate()) + } + + override fun onNewIntent(intent: Intent, caller: ComponentCaller) { + super.onNewIntent(intent.validate(), caller) + } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/AutofillTotpCopyActivity.kt b/app/src/main/kotlin/com/x8bit/bitwarden/AutofillTotpCopyActivity.kt index e1693a5aff..299bb911fd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/AutofillTotpCopyActivity.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/AutofillTotpCopyActivity.kt @@ -1,10 +1,13 @@ package com.x8bit.bitwarden +import android.app.ComponentCaller +import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.ui.platform.util.validate import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn @@ -26,6 +29,7 @@ class AutofillTotpCopyActivity : AppCompatActivity() { private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { + intent = intent.validate() super.onCreate(savedInstanceState) observeViewModelEvents() @@ -37,6 +41,14 @@ class AutofillTotpCopyActivity : AppCompatActivity() { ) } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent.validate()) + } + + override fun onNewIntent(intent: Intent, caller: ComponentCaller) { + super.onNewIntent(intent.validate(), caller) + } + private fun observeViewModelEvents() { autofillTotpCopyViewModel .eventFlow diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt b/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt index a616cbaa7c..a804e5750e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden +import android.app.ComponentCaller import android.content.Intent import android.os.Build import android.os.Bundle @@ -23,6 +24,7 @@ import com.bitwarden.annotation.OmitFromCoverage import com.bitwarden.ui.platform.base.util.EventsEffect import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.platform.util.setupEdgeToEdge +import com.bitwarden.ui.platform.util.validate import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager @@ -67,6 +69,7 @@ class MainActivity : AppCompatActivity() { lateinit var debugLaunchManager: DebugMenuLaunchManager override fun onCreate(savedInstanceState: Bundle?) { + intent = intent.validate() var shouldShowSplashScreen = true installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen } super.onCreate(savedInstanceState) @@ -114,8 +117,15 @@ class MainActivity : AppCompatActivity() { } override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = intent)) + val newIntent = intent.validate() + super.onNewIntent(newIntent) + mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent)) + } + + override fun onNewIntent(intent: Intent, caller: ComponentCaller) { + val newIntent = intent.validate() + super.onNewIntent(newIntent, caller) + mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent)) } override fun onResume() { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/util/PasswordlessRequestDataUtils.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/util/PasswordlessRequestDataUtils.kt index 3b8b4957ea..6ca544b39f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/util/PasswordlessRequestDataUtils.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/util/PasswordlessRequestDataUtils.kt @@ -4,7 +4,7 @@ import android.content.Context import android.content.Intent import com.x8bit.bitwarden.MainActivity import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData -import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra +import com.bitwarden.ui.platform.util.getSafeParcelableExtra private const val NOTIFICATION_DATA: String = "notificationData" diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt index 898c7fd668..b587e440e2 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt @@ -18,7 +18,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData -import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra +import com.bitwarden.ui.platform.util.getSafeParcelableExtra import kotlin.random.Random private const val AUTOFILL_SAVE_ITEM_DATA_KEY = "autofill-save-item-data" diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt index 0613802fec..47a14e13d1 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt @@ -1,5 +1,6 @@ package com.bitwarden.authenticator +import android.app.ComponentCaller import android.content.Intent import android.os.Bundle import android.view.KeyEvent @@ -14,13 +15,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.bitwarden.authenticator.data.platform.util.isSuspicious import com.bitwarden.authenticator.ui.platform.composition.LocalManagerProvider import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager import com.bitwarden.authenticator.ui.platform.feature.debugmenu.navigateToDebugMenuScreen import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavScreen import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme import com.bitwarden.ui.platform.util.setupEdgeToEdge +import com.bitwarden.ui.platform.util.validate import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -39,7 +40,7 @@ class MainActivity : AppCompatActivity() { lateinit var debugLaunchManager: DebugMenuLaunchManager override fun onCreate(savedInstanceState: Bundle?) { - sanitizeIntent() + intent = intent.validate() var shouldShowSplashScreen = true installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen } super.onCreate(savedInstanceState) @@ -72,20 +73,15 @@ class MainActivity : AppCompatActivity() { } override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - sanitizeIntent() - mainViewModel.trySendAction( - MainAction.ReceiveNewIntent(intent = intent), - ) + val newIntent = intent.validate() + super.onNewIntent(newIntent) + mainViewModel.trySendAction(MainAction.ReceiveNewIntent(intent = newIntent)) } - private fun sanitizeIntent() { - if (intent.isSuspicious) { - intent = Intent( - /* packageContext = */ this, - /* cls = */ MainActivity::class.java, - ) - } + override fun onNewIntent(intent: Intent, caller: ComponentCaller) { + val newIntent = intent.validate() + super.onNewIntent(newIntent, caller) + mainViewModel.trySendAction(MainAction.ReceiveNewIntent(intent = newIntent)) } private fun observeViewModelEvents(navController: NavHostController) { diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/util/IntentExtensions.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/util/IntentExtensions.kt deleted file mode 100644 index 5a8ac5ae8a..0000000000 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/util/IntentExtensions.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.bitwarden.authenticator.data.platform.util - -import android.content.Intent - -/** - * Returns true if this intent contains unexpected or suspicious data. - */ -val Intent.isSuspicious: Boolean - get() { - return try { - val containsSuspiciousExtras = extras?.isEmpty() == false - val containsSuspiciousData = data != null - containsSuspiciousData || containsSuspiciousExtras - } catch (_: Exception) { - // `unparcel()` throws an exception on Android 12 and below if the bundle contains - // suspicious data, so we catch the exception and return true. - true - } - } diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/util/IntentExtensionsTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/util/IntentExtensionsTest.kt deleted file mode 100644 index a158eb8ff8..0000000000 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/util/IntentExtensionsTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.bitwarden.authenticator.data.platform.util - -import android.content.Intent -import io.mockk.every -import io.mockk.mockk -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class IntentExtensionsTest { - @Test - fun `isSuspicious should return true when extras are not empty`() { - val intent = mockk { - every { data } returns mockk() - every { extras } returns mockk { - every { isEmpty } returns false - } - } - - assertTrue(intent.isSuspicious) - } - - @Test - fun `isSuspicious should return true when extras are null`() { - val intent = mockk { - every { data } returns mockk() - every { extras } returns null - } - - assertTrue(intent.isSuspicious) - } - - @Test - fun `isSuspicious should return true when data is not null`() { - val intent = mockk { - every { data } returns mockk() - every { extras } returns null - } - - assertTrue(intent.isSuspicious) - } - - @Test - fun `isSuspicious should return false when data and extras are null`() { - val intent = mockk { - every { data } returns null - every { extras } returns null - } - - assertFalse(intent.isSuspicious) - } - - @Test - fun `isSuspicious should return false when data is null and extras are empty`() { - val intent = mockk { - every { data } returns null - every { extras } returns mockk { - every { isEmpty } returns true - } - } - - assertFalse(intent.isSuspicious) - } -} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/IntentExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/IntentExtensions.kt similarity index 50% rename from app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/IntentExtensions.kt rename to ui/src/main/kotlin/com/bitwarden/ui/platform/util/IntentExtensions.kt index 8f20b46f94..bec1c9dc4f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/IntentExtensions.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/IntentExtensions.kt @@ -1,8 +1,9 @@ @file:OmitFromCoverage -package com.x8bit.bitwarden.data.platform.util +package com.bitwarden.ui.platform.util import android.content.Intent +import android.os.BadParcelableException import android.os.Bundle import android.os.Parcelable import androidx.core.content.IntentCompat @@ -24,3 +25,21 @@ inline fun Intent.getSafeParcelableExtra( inline fun Bundle.getSafeParcelableExtra( name: String, ): T? = BundleCompat.getParcelable(this, name, T::class.java) + +/** + * Validate if there's anything suspicious with the intent received and returns a new valid intent. + */ +fun Intent.validate(): Intent = + try { + // This will force Android to attempt to fetch each item and verify it is valid + this.extras?.let { bundle -> + bundle.keySet().forEach { @Suppress("DEPRECATION") bundle.get(it) } + } + this + } catch (_: BadParcelableException) { + this.replaceExtras(null as Bundle?) + } catch (_: ClassNotFoundException) { + this.replaceExtras(null as Bundle?) + } catch (@Suppress("TooGenericExceptionCaught") _: RuntimeException) { + this.replaceExtras(null as Bundle?) + } diff --git a/ui/src/test/kotlin/com/bitwarden/ui/platform/util/IntentExtensionsTest.kt b/ui/src/test/kotlin/com/bitwarden/ui/platform/util/IntentExtensionsTest.kt new file mode 100644 index 0000000000..761de65c00 --- /dev/null +++ b/ui/src/test/kotlin/com/bitwarden/ui/platform/util/IntentExtensionsTest.kt @@ -0,0 +1,68 @@ +package com.bitwarden.ui.platform.util + +import android.content.Intent +import android.os.BadParcelableException +import android.os.Bundle +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class IntentExtensionsTest { + + @BeforeEach + fun setUp() { + mockkStatic(Intent::class) + } + + @AfterEach + fun tearDown() { + unmockkStatic(Intent::class) + } + + @Test + fun `validate should not modify the current intent`() { + val intent = mockk(relaxed = true) { + every { getStringExtra("token") } returns "myToken" + } + + intent.validate() + + assertEquals("myToken", intent.getStringExtra("token")) + } + + @Test + fun `validate should remove extras if BadParcelableException is thrown`() { + val mockIntent = mockk(relaxed = true) + + every { mockIntent.extras } throws BadParcelableException("Bad parcel") + + mockIntent.validate() + verify { mockIntent.replaceExtras(null as Bundle?) } + } + + @Test + fun `validate should remove extras if ClassNotFoundException is thrown`() { + val mockIntent = mockk(relaxed = true) + + every { mockIntent.extras } throws ClassNotFoundException("Bad parcel") + + mockIntent.validate() + verify { mockIntent.replaceExtras(null as Bundle?) } + } + + @Test + fun `validate should remove extras if RuntimeException is thrown`() { + val mockIntent = mockk(relaxed = true) + + every { mockIntent.extras } throws RuntimeException("Bad parcel") + + mockIntent.validate() + verify { mockIntent.replaceExtras(null as Bundle?) } + } +}