mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
[PM-8137] Perform FIDO 2 verification on item listing when required (#3529)
This commit is contained in:
parent
94781bc1a9
commit
36270ec55a
2 changed files with 87 additions and 14 deletions
|
@ -255,12 +255,14 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserVerificationLockOut() {
|
private fun handleUserVerificationLockOut() {
|
||||||
|
fido2CredentialManager.isUserVerified = false
|
||||||
showFido2ErrorDialog()
|
showFido2ErrorDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserVerificationSuccess(
|
private fun handleUserVerificationSuccess(
|
||||||
action: VaultItemListingsAction.UserVerificationSuccess,
|
action: VaultItemListingsAction.UserVerificationSuccess,
|
||||||
) {
|
) {
|
||||||
|
fido2CredentialManager.isUserVerified = true
|
||||||
specialCircumstanceManager
|
specialCircumstanceManager
|
||||||
.specialCircumstance
|
.specialCircumstance
|
||||||
?.toFido2RequestOrNull()
|
?.toFido2RequestOrNull()
|
||||||
|
@ -274,10 +276,12 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserVerificationFail() {
|
private fun handleUserVerificationFail() {
|
||||||
|
fido2CredentialManager.isUserVerified = false
|
||||||
showFido2ErrorDialog()
|
showFido2ErrorDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserVerificationCancelled() {
|
private fun handleUserVerificationCancelled() {
|
||||||
|
fido2CredentialManager.isUserVerified = false
|
||||||
clearDialogState()
|
clearDialogState()
|
||||||
sendEvent(
|
sendEvent(
|
||||||
VaultItemListingEvent.CompleteFido2Registration(
|
VaultItemListingEvent.CompleteFido2Registration(
|
||||||
|
@ -287,6 +291,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserVerificationNotSupported() {
|
private fun handleUserVerificationNotSupported() {
|
||||||
|
fido2CredentialManager.isUserVerified = false
|
||||||
showFido2ErrorDialog()
|
showFido2ErrorDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,7 +389,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
?: run {
|
?: run {
|
||||||
// This scenario should not occur because `isFido2Creation` is false when
|
// This scenario should not occur because `isFido2Creation` is false when
|
||||||
// `fido2CredentialRequest` is null. We show the FIDO 2 error dialog to inform
|
// `fido2CredentialRequest` is null. We show the FIDO 2 error dialog to inform
|
||||||
// the user and terminate the flow.
|
// the user and terminate the flow just in case it does occur.
|
||||||
showFido2ErrorDialog()
|
showFido2ErrorDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -395,13 +400,28 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (fido2CredentialManager.isUserVerified) {
|
||||||
|
// The user has performed verification implicitly so we continue FIDO 2 registration
|
||||||
|
// without checking the request's user verification settings.
|
||||||
|
registerFido2CredentialToCipher(
|
||||||
|
request = credentialRequest,
|
||||||
|
cipherView = cipherView,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
performUserVerificationIfRequired(credentialRequest, cipherView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performUserVerificationIfRequired(
|
||||||
|
credentialRequest: Fido2CredentialRequest,
|
||||||
|
cipherView: CipherView,
|
||||||
|
) {
|
||||||
val createOptions = fido2CredentialManager
|
val createOptions = fido2CredentialManager
|
||||||
.getPasskeyCreateOptionsOrNull(credentialRequest.requestJson)
|
.getPasskeyCreateOptionsOrNull(credentialRequest.requestJson)
|
||||||
?: run {
|
?: run {
|
||||||
showFido2ErrorDialog()
|
showFido2ErrorDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
when (createOptions.authenticatorSelection.userVerification) {
|
when (createOptions.authenticatorSelection.userVerification) {
|
||||||
UserVerificationRequirement.DISCOURAGED -> {
|
UserVerificationRequirement.DISCOURAGED -> {
|
||||||
registerFido2CredentialToCipher(
|
registerFido2CredentialToCipher(
|
||||||
|
|
|
@ -141,6 +141,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
private val fido2CredentialManager: Fido2CredentialManager = mockk {
|
private val fido2CredentialManager: Fido2CredentialManager = mockk {
|
||||||
coEvery { validateOrigin(any()) } returns Fido2ValidateOriginResult.Success
|
coEvery { validateOrigin(any()) } returns Fido2ValidateOriginResult.Success
|
||||||
|
every { isUserVerified } returns false
|
||||||
|
every { isUserVerified = any() } just runs
|
||||||
}
|
}
|
||||||
|
|
||||||
private val organizationEventManager = mockk<OrganizationEventManager> {
|
private val organizationEventManager = mockk<OrganizationEventManager> {
|
||||||
|
@ -486,6 +488,51 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `ItemClick for vault item during FIDO 2 registration should skip user verification when user is verified`() {
|
||||||
|
setupMockUri()
|
||||||
|
val cipherView = createMockCipherView(number = 1)
|
||||||
|
val mockFido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
|
||||||
|
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||||
|
fido2CredentialRequest = mockFido2CredentialRequest,
|
||||||
|
)
|
||||||
|
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(cipherView),
|
||||||
|
folderViewList = emptyList(),
|
||||||
|
collectionViewList = emptyList(),
|
||||||
|
sendViewList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
every {
|
||||||
|
fido2CredentialManager.getPasskeyCreateOptionsOrNull(any())
|
||||||
|
} returns createMockPublicKeyCredentialCreationOptions(
|
||||||
|
number = 1,
|
||||||
|
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
fido2CredentialManager.registerFido2Credential(
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
)
|
||||||
|
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||||
|
every { fido2CredentialManager.isUserVerified } returns true
|
||||||
|
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
|
||||||
|
|
||||||
|
coVerify { fido2CredentialManager.isUserVerified }
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
fido2CredentialManager.registerFido2Credential(
|
||||||
|
userId = DEFAULT_USER_STATE.activeUserId,
|
||||||
|
fido2CredentialRequest = mockFido2CredentialRequest,
|
||||||
|
selectedCipherView = cipherView,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
@ -2051,10 +2098,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `UserVerificationLockout should display Fido2ErrorDialog`() {
|
fun `UserVerificationLockout should display Fido2ErrorDialog and set isUserVerified to false`() {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationLockOut)
|
viewModel.trySendAction(VaultItemListingsAction.UserVerificationLockOut)
|
||||||
|
|
||||||
|
verify { fido2CredentialManager.isUserVerified = false }
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
@ -2066,11 +2114,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `UserVerificationCancelled should clear dialog state and emit CompleteFido2Create with cancelled result`() =
|
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Create with cancelled result`() =
|
||||||
runTest {
|
runTest {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
|
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
|
||||||
|
|
||||||
|
verify { fido2CredentialManager.isUserVerified = false }
|
||||||
assertNull(viewModel.stateFlow.value.dialogState)
|
assertNull(viewModel.stateFlow.value.dialogState)
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -2082,16 +2131,17 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
@Test
|
||||||
fun `UserVerificationFail should display Fido2ErrorDialog`() {
|
fun `UserVerificationFail should display Fido2ErrorDialog and set isUserVerified to false`() {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationFail)
|
viewModel.trySendAction(VaultItemListingsAction.UserVerificationFail)
|
||||||
|
|
||||||
|
verify { fido2CredentialManager.isUserVerified = false }
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||||
|
.asText(),
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value.dialogState,
|
viewModel.stateFlow.value.dialogState,
|
||||||
)
|
)
|
||||||
|
@ -2192,7 +2242,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `UserVerificationSuccess should register FIDO 2 credential when registration result is received`() =
|
fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential when registration result is received`() =
|
||||||
runTest {
|
runTest {
|
||||||
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
||||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||||
|
@ -2216,6 +2266,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
|
fido2CredentialManager.isUserVerified = true
|
||||||
fido2CredentialManager.registerFido2Credential(
|
fido2CredentialManager.registerFido2Credential(
|
||||||
userId = DEFAULT_ACCOUNT.userId,
|
userId = DEFAULT_ACCOUNT.userId,
|
||||||
fido2CredentialRequest = mockRequest,
|
fido2CredentialRequest = mockRequest,
|
||||||
|
@ -2224,11 +2275,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `UserVerificationNotSupported should display Fido2ErrorDialog`() {
|
fun `UserVerificationNotSupported should display Fido2ErrorDialog and set isUserVerified to false`() {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationNotSupported)
|
viewModel.trySendAction(VaultItemListingsAction.UserVerificationNotSupported)
|
||||||
|
|
||||||
|
verify { fido2CredentialManager.isUserVerified = false }
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
|
Loading…
Reference in a new issue