diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index 487bade646..a84030ef50 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -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( @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index dc394247d7..964d097533 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -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( @@ -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 { diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index f4ee9bed60..6939abc1f4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -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(null) private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT) private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true) - private val fido2CredentialManager = mockk() private val settingsRepository = mockk { 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 { + 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 { - 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 { + every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest + every { getPasswordlessRequestDataIntentOrNull() } returns null + every { getAutofillSelectionDataOrNull() } returns null + every { getAutofillSaveItemOrNull() } returns null + every { isMyVaultShortcut } returns false + every { isPasswordGeneratorShortcut } returns false +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 555f70efb2..e95ad11125 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -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, ) }