[PM-8137] Perform FIDO 2 verification on item listing when required (#3529)

This commit is contained in:
Patrick Honkonen 2024-07-16 17:01:55 -04:00 committed by GitHub
parent 94781bc1a9
commit 36270ec55a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 14 deletions

View file

@ -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(

View file

@ -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(),