[PM-8137] Set initial FIDO 2 user verification state (#3463)

This commit is contained in:
Patrick Honkonen 2024-07-16 09:02:59 -04:00 committed by GitHub
parent 5ea2f1c736
commit 7653d71b3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 88 additions and 12 deletions

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
@ -44,9 +45,10 @@ class MainViewModel @Inject constructor(
autofillSelectionManager: AutofillSelectionManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val fido2CredentialManager: Fido2CredentialManager,
private val intentManager: IntentManager,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
private val savedStateHandle: SavedStateHandle,
) : BaseViewModel<MainState, MainEvent, MainAction>(
@ -218,6 +220,10 @@ class MainViewModel @Inject constructor(
}
fido2CredentialRequestData != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CredentialManager.isUserVerified = false
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequestData,

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -43,6 +44,7 @@ class VaultUnlockViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepo: VaultRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val fido2CredentialManager: Fido2CredentialManager,
environmentRepo: EnvironmentRepository,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
@ -253,6 +255,10 @@ class VaultUnlockViewModel @Inject constructor(
return
}
// Mark the user verified for this session if the unlock result is Success.
fido2CredentialManager.isUserVerified =
action.vaultUnlockResult is VaultUnlockResult.Success
when (action.vaultUnlockResult) {
VaultUnlockResult.AuthenticationError -> {
mutableStateFlow.update {

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
@ -55,7 +56,6 @@ class MainViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT)
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
private val fido2CredentialManager = mockk<Fido2CredentialManager>()
private val settingsRepository = mockk<SettingsRepository> {
every { appTheme } returns AppTheme.DEFAULT
every { appThemeStateFlow } returns mutableAppThemeFlow
@ -78,6 +78,10 @@ class MainViewModelTest : BaseViewModelTest() {
private val intentManager: IntentManager = mockk {
every { getShareDataFromIntent(any()) } returns null
}
private val fido2CredentialManager = mockk<Fido2CredentialManager> {
every { isUserVerified } returns true
every { isUserVerified = any() } just runs
}
private val savedStateHandle = SavedStateHandle()
@BeforeEach
@ -362,22 +366,16 @@ class MainViewModelTest : BaseViewModelTest() {
signingInfo = SigningInfo(),
origin = "mockOrigin",
)
val mockIntent = mockk<Intent> {
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
}
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
val fido2Intent = createMockFido2RegistrationIntent(fido2CredentialRequest)
every { intentManager.getShareDataFromIntent(fido2Intent) } returns null
coEvery {
fido2CredentialManager.validateOrigin(any())
} returns Fido2ValidateOriginResult.Success
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = mockIntent,
intent = fido2Intent,
),
)
@ -389,6 +387,22 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `on ReceiveFirstIntent with fido2 request data should set the user to unverified`() {
val viewModel = createViewModel()
val fido2Intent = createMockFido2RegistrationIntent()
viewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = fido2Intent,
),
)
verify {
fido2CredentialManager.isUserVerified = false
}
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with fido2 request data should switch users if active user is not selected`() {
@ -633,6 +647,7 @@ class MainViewModelTest : BaseViewModelTest() {
autofillSelectionManager = autofillSelectionManager,
specialCircumstanceManager = specialCircumstanceManager,
garbageCollectionManager = garbageCollectionManager,
fido2CredentialManager = fido2CredentialManager,
intentManager = intentManager,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
@ -669,3 +684,14 @@ private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(DEFAULT_ACCOUNT),
)
private fun createMockFido2RegistrationIntent(
fido2CredentialRequest: Fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
): Intent = mockk<Intent> {
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { isMyVaultShortcut } returns false
every { isPasswordGeneratorShortcut } returns false
}

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -67,6 +68,10 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
)
} returns false
}
private val fido2CredentialManager: Fido2CredentialManager = mockk {
every { isUserVerified } returns true
every { isUserVerified = any() } just runs
}
@Test
fun `on init with biometrics enabled and valid should emit PromptForBiometrics`() = runTest {
@ -871,6 +876,38 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveVaultUnlockResult should set FIDO 2 user verification state to verified when result is Success`() {
val viewModel = createViewModel()
viewModel.trySendAction(
VaultUnlockAction.Internal.ReceiveVaultUnlockResult(
userId = "activeUserId",
vaultUnlockResult = VaultUnlockResult.Success,
isBiometricLogin = true,
),
)
verify { fido2CredentialManager.isUserVerified = true }
}
@Suppress("MaxLineLength")
@Test
fun `on ReceiveVaultUnlockResult should set FIDO 2 user verification state to not verified when result is not Success`() {
val viewModel = createViewModel()
viewModel.trySendAction(
VaultUnlockAction.Internal.ReceiveVaultUnlockResult(
userId = "activeUserId",
vaultUnlockResult = VaultUnlockResult.InvalidStateError,
isBiometricLogin = false,
),
)
verify { fido2CredentialManager.isUserVerified = false }
}
private fun createViewModel(
state: VaultUnlockState? = null,
unlockType: UnlockType = UnlockType.STANDARD,
@ -886,6 +923,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
vaultRepo = vaultRepo,
environmentRepo = environmentRepo,
biometricsEncryptionManager = biometricsEncryptionManager,
fido2CredentialManager = fido2CredentialManager,
)
}