mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BITAU-112 Support deep link into add item flow from Authenticator app (#4128)
This commit is contained in:
parent
f1d7d1a530
commit
fa248243b6
15 changed files with 365 additions and 21 deletions
Binary file not shown.
|
@ -5,6 +5,7 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
|
@ -23,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
|||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -33,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
|||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -58,6 +61,7 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
|||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
|
@ -234,7 +238,20 @@ class MainViewModel @Inject constructor(
|
|||
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
|
||||
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
|
||||
val shareData = intentManager.getShareDataFromIntent(intent)
|
||||
val totpData = intent.getTotpDataOrNull()
|
||||
val totpData: TotpData? =
|
||||
// First grab TOTP URI directly from the intent data:
|
||||
intent.getTotpDataOrNull()
|
||||
?: run {
|
||||
// Then check to see if the intent is coming from the Authenticator app:
|
||||
if (intent.isAddTotpLoginItemFromAuthenticator()) {
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData.also {
|
||||
// Clear pending add TOTP data so it is only handled once:
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP
|
||||
* item.
|
||||
*/
|
||||
interface AddTotpItemFromAuthenticatorManager {
|
||||
|
||||
/**
|
||||
* Current pending [TotpData] to be added from the Authenticator app.
|
||||
*/
|
||||
var pendingAddTotpLoginItemData: TotpData?
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager].
|
||||
*/
|
||||
class AddTotpItemFromAuthenticatorManagerImpl : AddTotpItemFromAuthenticatorManager {
|
||||
|
||||
override var pendingAddTotpLoginItemData: TotpData? = null
|
||||
}
|
|
@ -7,6 +7,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServ
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
|
@ -124,4 +126,9 @@ object AuthManagerModule {
|
|||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
|
||||
AddTotpItemFromAuthenticatorManagerImpl()
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
|
@ -84,10 +85,14 @@ object PlatformManagerModule {
|
|||
@Singleton
|
||||
fun provideAuthenticatorBridgeProcessor(
|
||||
authenticatorBridgeRepository: AuthenticatorBridgeRepository,
|
||||
addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
@ApplicationContext context: Context,
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): AuthenticatorBridgeProcessor = AuthenticatorBridgeProcessorImpl(
|
||||
authenticatorBridgeRepository = authenticatorBridgeRepository,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemFromAuthenticatorManager,
|
||||
context = context,
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
package com.x8bit.bitwarden.data.platform.processor
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.IInterface
|
||||
import android.os.RemoteCallbackList
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedAddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyFingerprintData
|
||||
import com.bitwarden.authenticatorbridge.util.AUTHENTICATOR_BRIDGE_SDK_VERSION
|
||||
import com.bitwarden.authenticatorbridge.util.decrypt
|
||||
import com.bitwarden.authenticatorbridge.util.encrypt
|
||||
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -26,10 +31,13 @@ import kotlinx.coroutines.launch
|
|||
*/
|
||||
class AuthenticatorBridgeProcessorImpl(
|
||||
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
context: Context,
|
||||
) : AuthenticatorBridgeProcessor {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private val callbacks by lazy { RemoteCallbackList<IAuthenticatorBridgeServiceCallback>() }
|
||||
private val scope by lazy { CoroutineScope(dispatcherManager.default) }
|
||||
|
||||
|
@ -101,13 +109,18 @@ class AuthenticatorBridgeProcessorImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun createAddTotpLoginItemIntent(): Intent {
|
||||
// TODO: BITAU-112
|
||||
return Intent()
|
||||
}
|
||||
|
||||
override fun setPendingAddTotpLoginItemData(data: EncryptedAddTotpLoginItemData?) {
|
||||
// TODO: BITAU-112
|
||||
override fun startAddTotpLoginItemFlow(data: EncryptedAddTotpLoginItemData): Boolean {
|
||||
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return false
|
||||
val intent = createAddTotpItemFromAuthenticatorIntent(context = applicationContext)
|
||||
val totpData = data.decrypt(symmetricEncryptionKey)
|
||||
.getOrNull()
|
||||
?.totpUri
|
||||
?.toUri()
|
||||
?.getTotpDataOrNull()
|
||||
?: return false
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData
|
||||
applicationContext.startActivity(intent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY = "add-totp-item-from-authenticator-key"
|
||||
|
||||
/**
|
||||
* Creates an intent for launching add TOTP item flow from the Authenticator app.
|
||||
*/
|
||||
fun createAddTotpItemFromAuthenticatorIntent(
|
||||
context: Context,
|
||||
): Intent =
|
||||
Intent(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
)
|
||||
.apply {
|
||||
putExtra(
|
||||
ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY,
|
||||
true,
|
||||
)
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(FLAG_ACTIVITY_SINGLE_TOP)
|
||||
addFlags(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Intent was started by the Authenticator app to add a TOTP item. The TOTP
|
||||
* item can be found in [AddTotpItemFromAuthenticatorManager].
|
||||
*/
|
||||
fun Intent.isAddTotpLoginItemFromAuthenticator(): Boolean =
|
||||
getBooleanExtra(ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY, false)
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
|
@ -43,6 +44,7 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
|||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
|
@ -79,6 +81,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl()
|
||||
private val accessibilitySelectionManager: AccessibilitySelectionManager =
|
||||
AccessibilitySelectionManagerImpl()
|
||||
private val addTotpItemAuthenticatorManager = AddTotpItemFromAuthenticatorManagerImpl()
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT)
|
||||
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
|
||||
|
@ -131,6 +134,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
Intent::getFido2AssertionRequestOrNull,
|
||||
Intent::getFido2CredentialRequestOrNull,
|
||||
Intent::getFido2GetCredentialsRequestOrNull,
|
||||
Intent::isAddTotpLoginItemFromAuthenticator,
|
||||
)
|
||||
mockkStatic(
|
||||
Intent::isMyVaultShortcut,
|
||||
|
@ -150,6 +154,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
Intent::getFido2AssertionRequestOrNull,
|
||||
Intent::getFido2CredentialRequestOrNull,
|
||||
Intent::getFido2GetCredentialsRequestOrNull,
|
||||
Intent::isAddTotpLoginItemFromAuthenticator,
|
||||
)
|
||||
unmockkStatic(
|
||||
Intent::isMyVaultShortcut,
|
||||
|
@ -321,6 +326,38 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with TOTP data from Authenticator app should set the special circumstance to AddTotpLoginItem and clear pendingAddTotpLoginItemData`() {
|
||||
val viewModel = createViewModel()
|
||||
val totpData = mockk<TotpData>()
|
||||
val mockIntent = createMockIntent(
|
||||
mockIsAddTotpLoginItemFromAuthenticator = true,
|
||||
)
|
||||
addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData = totpData
|
||||
|
||||
viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent))
|
||||
assertEquals(
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
assertNull(addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent when intent is from Authenticator app but pending item is null should not set special circumstance`() {
|
||||
val viewModel = createViewModel()
|
||||
val mockIntent = createMockIntent(
|
||||
mockIsAddTotpLoginItemFromAuthenticator = true,
|
||||
)
|
||||
addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData = null
|
||||
|
||||
viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent))
|
||||
assertNull(specialCircumstanceManager.specialCircumstance)
|
||||
assertNull(addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() {
|
||||
|
@ -748,6 +785,38 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveNewIntent with TOTP data from Authenticator app should set the special circumstance to AddTotpLoginItem and clear pendingAddTotpLoginItemData`() {
|
||||
val viewModel = createViewModel()
|
||||
val totpData = mockk<TotpData>()
|
||||
val mockIntent = createMockIntent(
|
||||
mockIsAddTotpLoginItemFromAuthenticator = true,
|
||||
)
|
||||
addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData = totpData
|
||||
|
||||
viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent))
|
||||
assertEquals(
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
assertNull(addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveNewIntent when intent is from Authenticator app but pending item is null should not set special circumstance`() {
|
||||
val viewModel = createViewModel()
|
||||
val mockIntent = createMockIntent(
|
||||
mockIsAddTotpLoginItemFromAuthenticator = true,
|
||||
)
|
||||
addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData = null
|
||||
|
||||
viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent))
|
||||
assertNull(specialCircumstanceManager.specialCircumstance)
|
||||
assertNull(addTotpItemAuthenticatorManager.pendingAddTotpLoginItemData)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() {
|
||||
|
@ -943,6 +1012,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
) = MainViewModel(
|
||||
accessibilitySelectionManager = accessibilitySelectionManager,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemAuthenticatorManager,
|
||||
autofillSelectionManager = autofillSelectionManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
garbageCollectionManager = garbageCollectionManager,
|
||||
|
@ -1013,6 +1083,7 @@ private fun createMockIntent(
|
|||
mockIsMyVaultShortcut: Boolean = false,
|
||||
mockIsPasswordGeneratorShortcut: Boolean = false,
|
||||
mockIsAccountSecurityShortcut: Boolean = false,
|
||||
mockIsAddTotpLoginItemFromAuthenticator: Boolean = false,
|
||||
): Intent = mockk<Intent> {
|
||||
every { getTotpDataOrNull() } returns mockTotpData
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns mockPasswordlessRequestData
|
||||
|
@ -1025,6 +1096,7 @@ private fun createMockIntent(
|
|||
every { isMyVaultShortcut } returns mockIsMyVaultShortcut
|
||||
every { isPasswordGeneratorShortcut } returns mockIsPasswordGeneratorShortcut
|
||||
every { isAccountSecurityShortcut } returns mockIsAccountSecurityShortcut
|
||||
every { isAddTotpLoginItemFromAuthenticator() } returns mockIsAddTotpLoginItemFromAuthenticator
|
||||
}
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
|
||||
class AddTotpItemFromAuthenticatorTest {
|
||||
|
||||
@Test
|
||||
fun `pendingAddTotpLoginItemData should start as null and keep value when set`() {
|
||||
val manager = AddTotpItemFromAuthenticatorManagerImpl()
|
||||
assertNull(manager.pendingAddTotpLoginItemData)
|
||||
|
||||
val totpData: TotpData = mockk()
|
||||
manager.pendingAddTotpLoginItemData = totpData
|
||||
assertEquals(
|
||||
totpData,
|
||||
manager.pendingAddTotpLoginItemData,
|
||||
)
|
||||
|
||||
manager.pendingAddTotpLoginItemData = null
|
||||
assertNull(manager.pendingAddTotpLoginItemData)
|
||||
}
|
||||
}
|
|
@ -1,29 +1,42 @@
|
|||
package com.x8bit.bitwarden.data.platform.processor
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.RemoteCallbackList
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||
import com.bitwarden.authenticatorbridge.model.AddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedAddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.util.AUTHENTICATOR_BRIDGE_SDK_VERSION
|
||||
import com.bitwarden.authenticatorbridge.util.decrypt
|
||||
import com.bitwarden.authenticatorbridge.util.encrypt
|
||||
import com.bitwarden.authenticatorbridge.util.generateSecretKey
|
||||
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
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.Assertions.assertFalse
|
||||
|
@ -37,23 +50,44 @@ import org.junit.jupiter.api.Test
|
|||
class AuthenticatorBridgeProcessorTest {
|
||||
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>()
|
||||
private val addTotpItemFromAuthenticatorManager = AddTotpItemFromAuthenticatorManagerImpl()
|
||||
private val authenticatorBridgeRepository = mockk<AuthenticatorBridgeRepository>()
|
||||
private val context = mockk<Context> {
|
||||
every { applicationContext } returns this@mockk
|
||||
}
|
||||
|
||||
private lateinit var bridgeServiceProcessor: AuthenticatorBridgeProcessorImpl
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
bridgeServiceProcessor = AuthenticatorBridgeProcessorImpl(
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemFromAuthenticatorManager,
|
||||
authenticatorBridgeRepository = authenticatorBridgeRepository,
|
||||
context = context,
|
||||
featureFlagManager = featureFlagManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
mockkStatic(::createAddTotpItemFromAuthenticatorIntent)
|
||||
mockkStatic(
|
||||
SharedAccountData::encrypt,
|
||||
EncryptedAddTotpLoginItemData::decrypt,
|
||||
Uri::parse,
|
||||
Uri::getTotpDataOrNull,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(::isBuildVersionBelow)
|
||||
unmockkStatic(SharedAccountData::encrypt)
|
||||
unmockkStatic(
|
||||
::createAddTotpItemFromAuthenticatorIntent,
|
||||
::isBuildVersionBelow,
|
||||
)
|
||||
unmockkStatic(
|
||||
SharedAccountData::encrypt,
|
||||
EncryptedAddTotpLoginItemData::decrypt,
|
||||
Uri::parse,
|
||||
Uri::getTotpDataOrNull,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -139,6 +173,80 @@ class AuthenticatorBridgeProcessorTest {
|
|||
assertEquals(SYMMETRIC_KEY, binder.symmetricEncryptionKeyData)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `startAddTotpLoginItemFlow should return false when symmetricEncryptionKeyData is null`() {
|
||||
val binder = getDefaultBinder()
|
||||
every { authenticatorBridgeRepository.authenticatorSyncSymmetricKey } returns null
|
||||
val data: EncryptedAddTotpLoginItemData = mockk()
|
||||
assertFalse(binder.startAddTotpLoginItemFlow(data))
|
||||
verify { authenticatorBridgeRepository.authenticatorSyncSymmetricKey }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `startAddTotpLoginItemFlow should return false when decryption fails`() {
|
||||
val binder = getDefaultBinder()
|
||||
val intent: Intent = mockk()
|
||||
val data: EncryptedAddTotpLoginItemData = mockk()
|
||||
every {
|
||||
authenticatorBridgeRepository.authenticatorSyncSymmetricKey
|
||||
} returns SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
every { createAddTotpItemFromAuthenticatorIntent(context) } returns intent
|
||||
every { data.decrypt(SYMMETRIC_KEY) } returns Result.failure(RuntimeException())
|
||||
assertFalse(binder.startAddTotpLoginItemFlow(data))
|
||||
verify { authenticatorBridgeRepository.authenticatorSyncSymmetricKey }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `startAddTotpLoginItemFlow should return false when getTotpDataOrNull returns null`() {
|
||||
val binder = getDefaultBinder()
|
||||
val intent: Intent = mockk()
|
||||
val totpUri = "totpUri"
|
||||
val uri: Uri = mockk()
|
||||
every { Uri.parse(totpUri) } returns uri
|
||||
val data: EncryptedAddTotpLoginItemData = mockk()
|
||||
val decryptedData: AddTotpLoginItemData = mockk()
|
||||
every {
|
||||
authenticatorBridgeRepository.authenticatorSyncSymmetricKey
|
||||
} returns SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
every { createAddTotpItemFromAuthenticatorIntent(context) } returns intent
|
||||
every { data.decrypt(SYMMETRIC_KEY) } returns Result.success(decryptedData)
|
||||
every { decryptedData.totpUri } returns totpUri
|
||||
every { uri.getTotpDataOrNull() } returns null
|
||||
assertFalse(binder.startAddTotpLoginItemFlow(data))
|
||||
verify { authenticatorBridgeRepository.authenticatorSyncSymmetricKey }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `startAddTotpLoginItemFlow should return true and set pendingAddTotpLoginItemData when getTotpDataOrNull succeeds`() {
|
||||
val binder = getDefaultBinder()
|
||||
val intent: Intent = mockk()
|
||||
val totpUri = "totpUri"
|
||||
val uri: Uri = mockk()
|
||||
every { Uri.parse(totpUri) } returns uri
|
||||
val expectedPendingData: TotpData = mockk()
|
||||
val data: EncryptedAddTotpLoginItemData = mockk()
|
||||
val decryptedData: AddTotpLoginItemData = mockk()
|
||||
every {
|
||||
authenticatorBridgeRepository.authenticatorSyncSymmetricKey
|
||||
} returns SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
every { createAddTotpItemFromAuthenticatorIntent(context) } returns intent
|
||||
every { data.decrypt(SYMMETRIC_KEY) } returns Result.success(decryptedData)
|
||||
every { decryptedData.totpUri } returns totpUri
|
||||
every { uri.getTotpDataOrNull() } returns expectedPendingData
|
||||
every { context.startActivity(intent) } just runs
|
||||
assertTrue(binder.startAddTotpLoginItemFlow(data))
|
||||
assertEquals(
|
||||
expectedPendingData,
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData,
|
||||
)
|
||||
verify { context.startActivity(intent) }
|
||||
verify { authenticatorBridgeRepository.authenticatorSyncSymmetricKey }
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SyncAccountsTest {
|
||||
|
||||
|
|
|
@ -49,13 +49,8 @@ interface IAuthenticatorBridgeService {
|
|||
// Add TOTP Item
|
||||
// ==============
|
||||
|
||||
// Returns an intent that can be launched to navigate the user to the add Totp item flow
|
||||
// in the main password manager app.
|
||||
Intent createAddTotpLoginItemIntent();
|
||||
|
||||
// Give the given TOTP item data to the main Bitwarden app before launching the add TOTP
|
||||
// item flow. This should be called before launching the intent returned from
|
||||
// createAddTotpLoginItemIntent().
|
||||
void setPendingAddTotpLoginItemData(in EncryptedAddTotpLoginItemData data);
|
||||
// Start the add TOTP item flow in the main Bitwarden app with the given data.
|
||||
// Returns true if the flow was successfully launched and false otherwise.
|
||||
boolean startAddTotpLoginItemFlow(in EncryptedAddTotpLoginItemData data);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.bitwarden.authenticatorbridge.manager
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -14,4 +16,12 @@ interface AuthenticatorBridgeManager {
|
|||
* State flow representing the current [AccountSyncState].
|
||||
*/
|
||||
val accountSyncStateFlow: StateFlow<AccountSyncState>
|
||||
|
||||
/**
|
||||
* Start the add TOTP item flow in the main Bitwarden app with the given data.
|
||||
*
|
||||
* @param totpUri TOTP URI to add to the main Bitwarden app.
|
||||
* @return true if the flow was successfully launched, false otherwise.
|
||||
*/
|
||||
fun startAddTotpLoginItemFlow(totpUri: String): Boolean
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.content.ServiceConnection
|
|||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
|
@ -14,11 +15,13 @@ import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
|||
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||
import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType
|
||||
import com.bitwarden.authenticatorbridge.manager.util.toPackageName
|
||||
import com.bitwarden.authenticatorbridge.model.AddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.provider.AuthenticatorBridgeCallbackProvider
|
||||
import com.bitwarden.authenticatorbridge.provider.StubAuthenticatorBridgeCallbackProvider
|
||||
import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider
|
||||
import com.bitwarden.authenticatorbridge.util.decrypt
|
||||
import com.bitwarden.authenticatorbridge.util.encrypt
|
||||
import com.bitwarden.authenticatorbridge.util.isBuildVersionBelow
|
||||
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -103,6 +106,21 @@ internal class AuthenticatorBridgeManagerImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun startAddTotpLoginItemFlow(totpUri: String): Boolean =
|
||||
bridgeService
|
||||
?.safeCall {
|
||||
// Grab symmetric key data from local storage:
|
||||
val symmetricKey = symmetricKeyStorageProvider.symmetricKey ?: return@safeCall false
|
||||
// Encrypt the given URI:
|
||||
val addTotpData = AddTotpLoginItemData(totpUri).encrypt(symmetricKey).getOrThrow()
|
||||
return@safeCall this.startAddTotpLoginItemFlow(addTotpData)
|
||||
}
|
||||
?.fold(
|
||||
onFailure = { false },
|
||||
onSuccess = { true }
|
||||
)
|
||||
?: false
|
||||
|
||||
private fun bindService() {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.S)) {
|
||||
mutableSharedAccountsStateFlow.value = AccountSyncState.OsVersionNotSupported
|
||||
|
@ -119,11 +137,17 @@ internal class AuthenticatorBridgeManagerImpl(
|
|||
)
|
||||
}
|
||||
|
||||
val flags = if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
Context.BIND_AUTO_CREATE
|
||||
} else {
|
||||
Context.BIND_AUTO_CREATE or Context.BIND_ALLOW_ACTIVITY_STARTS
|
||||
}
|
||||
|
||||
val isBound = try {
|
||||
applicationContext.bindService(
|
||||
intent,
|
||||
bridgeServiceConnection,
|
||||
Context.BIND_AUTO_CREATE,
|
||||
flags,
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
unbindService()
|
||||
|
|
|
@ -118,7 +118,7 @@ internal fun AddTotpLoginItemData.encrypt(
|
|||
*
|
||||
* @param symmetricEncryptionKeyData Symmetric key used for decryption.
|
||||
*/
|
||||
internal fun EncryptedAddTotpLoginItemData.decrypt(
|
||||
fun EncryptedAddTotpLoginItemData.decrypt(
|
||||
symmetricEncryptionKeyData: SymmetricEncryptionKeyData,
|
||||
): Result<AddTotpLoginItemData> = runCatching {
|
||||
val encodedKey = symmetricEncryptionKeyData
|
||||
|
|
Loading…
Reference in a new issue