[PM-8137] Perform FIDO 2 verification on item add/edit when required (#3532)
Some checks failed
Crowdin Push / Crowdin Push (push) Waiting to run
Scan / Check PR run (push) Failing after 0s
Scan / SAST scan (push) Has been skipped
Scan / Quality scan (push) Has been skipped
Test / Check PR run (push) Failing after 0s
Test / Test (push) Has been skipped

This commit is contained in:
Patrick Honkonen 2024-07-16 17:02:16 -04:00 committed by GitHub
parent 36270ec55a
commit 9b19c71d95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 138 additions and 51 deletions

View file

@ -398,6 +398,12 @@ class VaultAddEditViewModel @Inject constructor(
request: Fido2CredentialRequest, request: Fido2CredentialRequest,
content: VaultAddEditState.ViewState.Content, content: VaultAddEditState.ViewState.Content,
) { ) {
if (fido2CredentialManager.isUserVerified) {
registerFido2CredentialToCipher(request, content.toCipherView())
return
}
val createOptions = fido2CredentialManager val createOptions = fido2CredentialManager
.getPasskeyCreateOptionsOrNull(request.requestJson) .getPasskeyCreateOptionsOrNull(request.requestJson)
?: run { ?: run {
@ -514,10 +520,12 @@ class VaultAddEditViewModel @Inject constructor(
} }
private fun handleUserVerificationLockOut() { private fun handleUserVerificationLockOut() {
fido2CredentialManager.isUserVerified = false
showFido2ErrorDialog() showFido2ErrorDialog()
} }
private fun handleUserVerificationSuccess() { private fun handleUserVerificationSuccess() {
fido2CredentialManager.isUserVerified = true
specialCircumstanceManager specialCircumstanceManager
.specialCircumstance .specialCircumstance
?.toFido2RequestOrNull() ?.toFido2RequestOrNull()
@ -533,10 +541,12 @@ class VaultAddEditViewModel @Inject constructor(
} }
private fun handleUserVerificationFail() { private fun handleUserVerificationFail() {
fido2CredentialManager.isUserVerified = false
showFido2ErrorDialog() showFido2ErrorDialog()
} }
private fun handleFido2ErrorDialogDismissed() { private fun handleFido2ErrorDialogDismissed() {
fido2CredentialManager.isUserVerified = false
clearDialogState() clearDialogState()
sendEvent( sendEvent(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteFido2Registration(
@ -546,6 +556,7 @@ class VaultAddEditViewModel @Inject constructor(
} }
private fun handleUserVerificationCancelled() { private fun handleUserVerificationCancelled() {
fido2CredentialManager.isUserVerified = false
clearDialogState() clearDialogState()
sendEvent( sendEvent(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteFido2Registration(
@ -555,12 +566,8 @@ class VaultAddEditViewModel @Inject constructor(
} }
private fun handleUserVerificationNotSupported() { private fun handleUserVerificationNotSupported() {
clearDialogState() fido2CredentialManager.isUserVerified = false
sendEvent( showFido2ErrorDialog()
VaultAddEditEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
),
)
} }
private fun handleAddNewCustomFieldClick( private fun handleAddNewCustomFieldClick(

View file

@ -122,7 +122,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP) getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
} returns emptyList() } returns emptyList()
} }
private val fido2CredentialManager = mockk<Fido2CredentialManager>() private val fido2CredentialManager = mockk<Fido2CredentialManager> {
every { isUserVerified } returns false
every { isUserVerified = any() } just runs
}
private val vaultRepository: VaultRepository = mockk { private val vaultRepository: VaultRepository = mockk {
every { vaultDataStateFlow } returns mutableVaultDataFlow every { vaultDataStateFlow } returns mutableVaultDataFlow
every { totpCodeFlow } returns totpTestCodeFlow every { totpCodeFlow } returns totpTestCodeFlow
@ -855,18 +858,64 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `in add mode during fido2, SaveClick should skip user verification when user is verified`() =
runTest {
val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest,
)
val stateWithName = createVaultAddItemState(
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
)
.copy(shouldExitOnSave = true)
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(),
)
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
),
)
every { authRepository.activeUserId } returns fido2CredentialRequest.userId
every { fido2CredentialManager.isUserVerified } returns true
coEvery {
fido2CredentialManager.registerFido2Credential(
userId = fido2CredentialRequest.userId,
fido2CredentialRequest = fido2CredentialRequest,
selectedCipherView = any(),
)
} returns Fido2RegisterCredentialResult.Success(registrationResponse = "mockResponse")
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
coVerify {
fido2CredentialManager.registerFido2Credential(
userId = fido2CredentialRequest.userId,
fido2CredentialRequest = fido2CredentialRequest,
selectedCipherView = any(),
)
}
verify(exactly = 0) {
fido2CredentialManager.getPasskeyCreateOptionsOrNull(any())
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in add mode during fido2, SaveClick should show fido2 error dialog when create options are null`() = fun `in add mode during fido2, SaveClick should show fido2 error dialog when create options are null`() =
runTest { runTest {
val mockUserId = "mockUserId" val mockUserId = "mockUserId"
val fido2CredentialRequest = Fido2CredentialRequest( val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
userId = mockUserId,
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save( SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest, fido2CredentialRequest = fido2CredentialRequest,
@ -910,14 +959,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Test @Test
fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() = fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() =
runTest { runTest {
val mockUserId = "mockUserId" val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
val fido2CredentialRequest = Fido2CredentialRequest(
userId = mockUserId,
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save( SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest, fido2CredentialRequest = fido2CredentialRequest,
@ -963,14 +1005,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Test @Test
fun `in add mode during fido2, SaveClick should emit fido user verification as required when request user verification option is REQUIRED`() = fun `in add mode during fido2, SaveClick should emit fido user verification as required when request user verification option is REQUIRED`() =
runTest { runTest {
val mockUserId = "mockUserId" val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
val fido2CredentialRequest = Fido2CredentialRequest(
userId = mockUserId,
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save( SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest, fido2CredentialRequest = fido2CredentialRequest,
@ -1012,6 +1047,51 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `in add mode during fido2, SaveClick should show Fido2ErrorDialog when user is not verified and registration user verification option is null`() =
runTest {
val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
val stateWithName = createVaultAddItemState(
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
)
.copy(shouldExitOnSave = true)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest,
)
every {
fido2CredentialManager.getPasskeyCreateOptionsOrNull(
requestJson = fido2CredentialRequest.requestJson,
)
} returns createMockPublicKeyCredentialCreationOptions(
number = 1,
userVerificationRequirement = null,
)
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(),
)
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
),
)
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
viewModel.stateFlow.value.dialog,
)
}
@Test @Test
fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() = fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() =
runTest { runTest {
@ -2869,9 +2949,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationLockout should display Fido2ErrorDialog`() { fun `UserVerificationLockout should set isUserVerified to false and display Fido2ErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut)
verify { fido2CredentialManager.isUserVerified = false }
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.Fido2Error(
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(),
@ -2882,10 +2963,11 @@ class VaultAddEditViewModelTest : 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 {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled)
verify { fido2CredentialManager.isUserVerified = false }
assertNull(viewModel.stateFlow.value.dialog) assertNull(viewModel.stateFlow.value.dialog)
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
@ -2899,9 +2981,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationFail should display Fido2ErrorDialog`() { fun `UserVerificationFail should set isUserVerified to false, and display Fido2ErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail)
verify { fido2CredentialManager.isUserVerified = false }
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.Fido2Error(
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(),
@ -2910,6 +2993,17 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test
fun `UserVerificationNotSupported should set isUserVerified to false and show Fido2ErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultAddEditState.DialogState.Fido2Error(R.string.passkey_operation_failed_because_user_could_not_be_verified.asText()),
viewModel.stateFlow.value.dialog,
)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is null`() = fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is null`() =
@ -2985,8 +3079,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationSuccess should register FIDO 2 credential`() = fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential`() =
runTest { runTest {
val mockRequest = createMockFido2CredentialRequest(number = 1) val mockRequest = createMockFido2CredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Success( val mockResult = Fido2RegisterCredentialResult.Success(
@ -3003,10 +3098,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
any(), any(),
) )
} returns mockResult } returns mockResult
every { fido2CredentialManager.isUserVerified } returns true
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
coVerify { coVerify {
fido2CredentialManager.isUserVerified = true
fido2CredentialManager.registerFido2Credential( fido2CredentialManager.registerFido2Credential(
userId = any(), userId = any(),
fido2CredentialRequest = mockRequest, fido2CredentialRequest = mockRequest,
@ -3026,23 +3123,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `UserVerificationNotSupported should clear dialog state and send CompleteFido2Registration event with Error`() =
runTest {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialog)
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() = fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =