[PM-10644] Re-prompt master password for protected passkeys (#3682)

This commit is contained in:
Patrick Honkonen 2024-08-06 11:43:12 -04:00 committed by GitHub
parent 3819916241
commit 1e5bee2917
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 137 additions and 1 deletions

View file

@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -1413,9 +1414,26 @@ class VaultItemListingViewModel @Inject constructor(
showFido2ErrorDialog()
return
}
if (state.hasMasterPassword &&
selectedCipher.reprompt == CipherRepromptType.PASSWORD
) {
repromptMasterPasswordForFido2Assertion(request.cipherId)
} else {
verifyUserAndAuthenticateCredential(request, selectedCipher)
}
}
}
private fun repromptMasterPasswordForFido2Assertion(cipherId: String) {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2MasterPasswordPrompt(
selectedCipherId = cipherId,
),
)
}
}
private fun verifyUserAndAuthenticateCredential(
request: Fido2CredentialAssertionRequest,

View file

@ -4,6 +4,7 @@ import android.content.pm.SigningInfo
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -2697,6 +2698,123 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should prompt for master password when passkey is protected and user has master password`() {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every { fido2CredentialManager.isUserVerified } returns true
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
repromptType = CipherRepromptType.PASSWORD,
),
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(number = 1)
every { authRepository.activeUserId } returns null
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2MasterPasswordPrompt(
selectedCipherId = mockAssertionRequest.cipherId!!,
),
viewModel.stateFlow.value.dialogState,
)
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should not re-prompt master password when user does not have a master password`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
repromptType = CipherRepromptType.PASSWORD,
)
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
DEFAULT_ACCOUNT.copy(
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
trustedDevice = UserState.TrustedDevice(
isDeviceTrusted = true,
hasMasterPassword = false,
hasAdminApproval = true,
hasLoginApprovingDevice = true,
hasResetPasswordPermission = true,
),
),
),
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every { fido2CredentialManager.isUserVerified } returns true
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(mockCipherView)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(number = 1)
coEvery {
fido2CredentialManager.authenticateFido2Credential(
DEFAULT_USER_STATE.activeUserId,
mockAssertionRequest,
mockCipherView,
)
} returns Fido2CredentialAssertionResult.Success("responseJson")
createVaultItemListingViewModel()
coVerify {
fido2CredentialManager.authenticateFido2Credential(
userId = any(),
request = any(),
selectedCipherView = any(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `UserVerificationLockout should display Fido2ErrorDialog and set isUserVerified to false`() {