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

View file

@ -122,7 +122,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
} 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 {
every { vaultDataStateFlow } returns mutableVaultDataFlow
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")
@Test
fun `in add mode during fido2, SaveClick should show fido2 error dialog when create options are null`() =
runTest {
val mockUserId = "mockUserId"
val fido2CredentialRequest = Fido2CredentialRequest(
userId = mockUserId,
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest,
@ -910,14 +959,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Test
fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() =
runTest {
val mockUserId = "mockUserId"
val fido2CredentialRequest = Fido2CredentialRequest(
userId = mockUserId,
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest,
@ -963,14 +1005,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Test
fun `in add mode during fido2, SaveClick should emit fido user verification as required when request user verification option is REQUIRED`() =
runTest {
val mockUserId = "mockUserId"
val fido2CredentialRequest = Fido2CredentialRequest(
userId = mockUserId,
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
val fido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
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
fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() =
runTest {
@ -2869,9 +2949,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationLockout should display Fido2ErrorDialog`() {
fun `UserVerificationLockout should set isUserVerified to false and display Fido2ErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut)
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
@ -2882,10 +2963,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@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 {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled)
verify { fido2CredentialManager.isUserVerified = false }
assertNull(viewModel.stateFlow.value.dialog)
viewModel.eventFlow.test {
assertEquals(
@ -2899,9 +2981,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationFail should display Fido2ErrorDialog`() {
fun `UserVerificationFail should set isUserVerified to false, and display Fido2ErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail)
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
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")
@Test
fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is null`() =
@ -2985,8 +3079,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `UserVerificationSuccess should register FIDO 2 credential`() =
fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential`() =
runTest {
val mockRequest = createMockFido2CredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Success(
@ -3003,10 +3098,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
any(),
)
} returns mockResult
every { fido2CredentialManager.isUserVerified } returns true
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
coVerify {
fido2CredentialManager.isUserVerified = true
fido2CredentialManager.registerFido2Credential(
userId = any(),
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")
@Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =