mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-11884] Perform origin validation during FIDO 2 auth (#3896)
This commit is contained in:
parent
74ae39a665
commit
4f55d622cb
8 changed files with 422 additions and 261 deletions
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
|
@ -26,10 +27,11 @@ interface Fido2CredentialManager {
|
|||
var authenticationAttempts: Int
|
||||
|
||||
/**
|
||||
* Attempt to validate the RP and origin of the provided [fido2CredentialRequest].
|
||||
* Attempt to validate the RP and origin of the provided [callingAppInfo] and [relyingPartyId].
|
||||
*/
|
||||
suspend fun validateOrigin(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,9 +14,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
|||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
|
@ -95,13 +93,13 @@ class Fido2CredentialManagerImpl(
|
|||
}
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
val callingAppInfo = fido2CredentialRequest.callingAppInfo
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(fido2CredentialRequest)
|
||||
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,40 +134,52 @@ class Fido2CredentialManagerImpl(
|
|||
val clientData = request.clientDataHash
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
|
||||
val origin = request.origin
|
||||
val origin = callingAppInfo.origin
|
||||
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
val relyingPartyId = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
|
||||
return vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidFido2PublicKeyCredential() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = { Fido2CredentialAssertionResult.Error },
|
||||
)
|
||||
val validateOriginResult = validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
return when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
Fido2CredentialAssertionResult.Error
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidFido2PublicKeyCredential() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = { Fido2CredentialAssertionResult.Error },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
val callingAppInfo = fido2CredentialRequest.callingAppInfo
|
||||
return fido2CredentialRequest
|
||||
.requestJson
|
||||
.getRpId(json)
|
||||
.flatMap { rpId ->
|
||||
digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = rpId)
|
||||
}
|
||||
return digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
.onFailure {
|
||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
}
|
||||
|
@ -247,20 +257,6 @@ class Fido2CredentialManagerImpl(
|
|||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
private fun String.getRpId(json: Json): Result<String> {
|
||||
return try {
|
||||
json
|
||||
.decodeFromString<PasskeyAttestationOptions>(this)
|
||||
.relyingParty
|
||||
.id
|
||||
.asSuccess()
|
||||
} catch (e: SerializationException) {
|
||||
e.asFailure()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasAuthenticationAttemptsRemaining(): Boolean =
|
||||
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
|
||||
|
||||
|
|
|
@ -157,8 +157,8 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
.fido2CredentialRequest
|
||||
?.let { request ->
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.ValidateFido2OriginResultReceive(
|
||||
result = fido2CredentialManager.validateOrigin(request),
|
||||
VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive(
|
||||
request = request,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -692,6 +692,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun authenticateFido2Credential(
|
||||
relyingPartyId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
|
@ -701,23 +702,28 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = fido2CredentialManager
|
||||
.authenticateFido2Credential(
|
||||
userId = activeUserId,
|
||||
selectedCipherView = cipherView,
|
||||
request = Fido2CredentialAssertionRequest(
|
||||
cipherId = request.cipherId,
|
||||
credentialId = request.credentialId,
|
||||
requestJson = request.requestJson,
|
||||
clientDataHash = request.clientDataHash,
|
||||
packageName = request.packageName,
|
||||
signingInfo = request.signingInfo,
|
||||
origin = request.origin,
|
||||
),
|
||||
val validateOriginResult = fido2CredentialManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2AssertionResultReceive(result),
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
handleFido2OriginValidationFail(validateOriginResult)
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2AssertionResultReceive(
|
||||
result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = activeUserId,
|
||||
selectedCipherView = cipherView,
|
||||
request = request,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -956,8 +962,8 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
handlePolicyUpdateReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.ValidateFido2OriginResultReceive -> {
|
||||
handleValidateFido2OriginResultReceive(action)
|
||||
is VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive -> {
|
||||
handleFido2RegisterCredentialRequestReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
|
||||
|
@ -1224,10 +1230,16 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
.specialCircumstance
|
||||
?.toFido2AssertionRequestOrNull()
|
||||
?.let { request ->
|
||||
authenticateFido2Credential(
|
||||
request = request,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?.let { relyingPartyId ->
|
||||
authenticateFido2Credential(
|
||||
relyingPartyId = relyingPartyId,
|
||||
request = request,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
}
|
||||
?: showFido2ErrorDialog()
|
||||
}
|
||||
|
@ -1317,6 +1329,33 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleFido2RegisterCredentialRequestReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val options = fido2CredentialManager
|
||||
.getPasskeyAttestationOptionsOrNull(requestJson = action.request.requestJson)
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return@launch
|
||||
}
|
||||
val validateOriginResult = fido2CredentialManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = action.request.callingAppInfo,
|
||||
relyingPartyId = options.relyingParty.id,
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
handleFido2OriginValidationFail(validateOriginResult)
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
observeVaultData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFido2RegisterCredentialResultReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive,
|
||||
) {
|
||||
|
@ -1337,20 +1376,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
sendEvent(VaultItemListingEvent.CompleteFido2Registration(action.result))
|
||||
}
|
||||
|
||||
private fun handleValidateFido2OriginResultReceive(
|
||||
action: VaultItemListingsAction.Internal.ValidateFido2OriginResultReceive,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
handleFido2OriginValidationFail(result)
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
handleFido2OriginValidationSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFido2OriginValidationFail(error: Fido2ValidateOriginResult.Error) {
|
||||
val messageResId = when (error) {
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotFound -> {
|
||||
|
@ -1391,10 +1416,6 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleFido2OriginValidationSuccess() {
|
||||
observeVaultData()
|
||||
}
|
||||
|
||||
private fun handleFido2AssertionDataReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2AssertionDataReceive,
|
||||
) {
|
||||
|
@ -1453,14 +1474,24 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val relyingPartyId = assertionOptions.relyingPartyId
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
authenticateFido2Credential(request, selectedCipher)
|
||||
authenticateFido2Credential(relyingPartyId, request, selectedCipher)
|
||||
return
|
||||
}
|
||||
|
||||
when (assertionOptions.userVerification) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
authenticateFido2Credential(request, selectedCipher)
|
||||
authenticateFido2Credential(
|
||||
relyingPartyId,
|
||||
request,
|
||||
selectedCipher,
|
||||
)
|
||||
}
|
||||
|
||||
UserVerificationRequirement.PREFERRED -> {
|
||||
|
@ -2425,11 +2456,10 @@ sealed class VaultItemListingsAction {
|
|||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a result for validating the relying party's origin during a FIDO 2
|
||||
* request.
|
||||
* Indicates that a FIDO 2 credential registration has been received.
|
||||
*/
|
||||
data class ValidateFido2OriginResultReceive(
|
||||
val result: Fido2ValidateOriginResult,
|
||||
data class Fido2RegisterCredentialRequestReceive(
|
||||
val request: Fido2CredentialRequest,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
|
|
|
@ -614,7 +614,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
|
||||
every { intentManager.getShareDataFromIntent(fido2Intent) } returns null
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any())
|
||||
fido2CredentialManager.validateOrigin(
|
||||
fido2CredentialRequest.callingAppInfo,
|
||||
fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
|
||||
viewModel.trySendAction(
|
||||
|
@ -670,7 +673,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any())
|
||||
fido2CredentialManager.validateOrigin(
|
||||
fido2CredentialRequest.callingAppInfo,
|
||||
fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
|
||||
viewModel.trySendAction(
|
||||
|
@ -704,7 +710,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any())
|
||||
fido2CredentialManager.validateOrigin(
|
||||
fido2CredentialRequest.callingAppInfo,
|
||||
fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
|
||||
viewModel.trySendAction(
|
||||
|
|
|
@ -7,7 +7,6 @@ import androidx.credentials.provider.CallingAppInfo
|
|||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
|
||||
|
@ -23,6 +22,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRe
|
|||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
|
@ -73,21 +73,25 @@ class Fido2CredentialManagerTest {
|
|||
every {
|
||||
decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(number = 1)
|
||||
every {
|
||||
decodeFromStringOrNull<PasskeyAssertionOptions>(DEFAULT_FIDO2_AUTH_REQUEST_JSON)
|
||||
} returns createMockPasskeyAssertionOptions(number = 1)
|
||||
}
|
||||
private val mockPrivilegedCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { packageName } returns "com.x8bit.bitwarden"
|
||||
every { packageName } returns DEFAULT_PACKAGE_NAME
|
||||
every { isOriginPopulated() } returns true
|
||||
every { getOrigin(any()) } returns "com.x8bit.bitwarden"
|
||||
every { getOrigin(any()) } returns DEFAULT_PACKAGE_NAME
|
||||
}
|
||||
private val mockPrivilegedAppRequest = mockk<Fido2CredentialRequest> {
|
||||
every { callingAppInfo } returns mockPrivilegedCallingAppInfo
|
||||
every { requestJson } returns "{}"
|
||||
}
|
||||
private val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature("0987654321ABCDEF"))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
private val mockUnprivilegedCallingAppInfo = CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
signingInfo = mockSigningInfo,
|
||||
origin = null,
|
||||
)
|
||||
|
@ -126,11 +130,14 @@ class Fido2CredentialManagerTest {
|
|||
@Test
|
||||
fun `validateOrigin should load allow list when origin is populated`() =
|
||||
runTest {
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedAppRequest)
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
assetManager.readAsset(
|
||||
fileName = "fido2_privileged_allow_list.json",
|
||||
fileName = DEFAULT_ALLOW_LIST_FILENAME,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +147,10 @@ class Fido2CredentialManagerTest {
|
|||
runTest {
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -152,7 +162,10 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -164,7 +177,7 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedCallingAppInfo, "{}"),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -174,7 +187,10 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.Unknown,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -187,7 +203,7 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedCallingAppInfo, "{}"),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -196,32 +212,10 @@ class Fido2CredentialManagerTest {
|
|||
runTest {
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success,
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return error when request cannot be decoded`() = runTest {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} throws SerializationException()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.AssetLinkNotFound,
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return error when request cannot be cast to object type`() =
|
||||
runTest {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} throws IllegalArgumentException()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.AssetLinkNotFound,
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -232,7 +226,10 @@ class Fido2CredentialManagerTest {
|
|||
} returns Throwable().asFailure()
|
||||
|
||||
assertEquals(
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
Fido2ValidateOriginResult.Error.AssetLinkNotFound,
|
||||
)
|
||||
}
|
||||
|
@ -247,7 +244,10 @@ class Fido2CredentialManagerTest {
|
|||
)
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotFound,
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -268,7 +268,10 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotFound,
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -280,7 +283,10 @@ class Fido2CredentialManagerTest {
|
|||
} returns "ITSATRAP".toByteArray()
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotVerified,
|
||||
fido2CredentialManager.validateOrigin(mockUnprivilegedAppRequest),
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -624,19 +630,16 @@ class Fido2CredentialManagerTest {
|
|||
runTest {
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRequest = createMockFido2CredentialAssertionRequest(number = 1)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
|
||||
number = 1,
|
||||
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
|
||||
mockRequest = mockRequest,
|
||||
mockCipherView = mockCipherView,
|
||||
val mockRequest = createMockFido2AssertionRequest(
|
||||
mockClientDataHash = byteArrayOf(),
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
|
||||
coEvery {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = mockSdkRequest,
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} returns mockSdkResponse.asSuccess()
|
||||
|
@ -656,7 +659,7 @@ class Fido2CredentialManagerTest {
|
|||
assertEquals(
|
||||
AuthenticateFido2CredentialRequest(
|
||||
userId = "activeUserId",
|
||||
origin = "mockOrigin-1",
|
||||
origin = DEFAULT_ORIGIN,
|
||||
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
|
||||
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
|
||||
selectedCipherView = mockCipherView,
|
||||
|
@ -677,32 +680,16 @@ class Fido2CredentialManagerTest {
|
|||
mockSigningInfo.hasMultipleSigners()
|
||||
} returns false
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRequest = Fido2CredentialAssertionRequest(
|
||||
cipherId = "mockCipherId",
|
||||
credentialId = "mockCredentialId",
|
||||
requestJson = "requestJson",
|
||||
clientDataHash = null,
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockSigningInfo,
|
||||
origin = "mockOrigin",
|
||||
val mockRequest = createMockFido2AssertionRequest(
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkRequest = AuthenticateFido2CredentialRequest(
|
||||
userId = "activeUserId",
|
||||
origin = mockRequest.origin!!,
|
||||
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
|
||||
clientData = ClientData.DefaultWithExtraData(
|
||||
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
|
||||
),
|
||||
selectedCipherView = mockCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
)
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
coEvery {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = mockSdkRequest,
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} returns mockSdkResponse.asSuccess()
|
||||
|
@ -738,27 +725,19 @@ class Fido2CredentialManagerTest {
|
|||
@Test
|
||||
fun `authenticateFido2Credential should construct correct assertion request when calling app is unprivileged`() =
|
||||
runTest {
|
||||
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(
|
||||
number = 1,
|
||||
origin = null,
|
||||
clientDataHash = null,
|
||||
signingInfo = mockSigningInfo,
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockOrigin = null,
|
||||
mockClientDataHash = null,
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val mockAssertionOptions = createMockPasskeyAssertionOptions(number = 1)
|
||||
val mockSelectedCipher = createMockCipherView(number = 1)
|
||||
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
|
||||
number = 1,
|
||||
origin = "https://${mockAssertionOptions.relyingPartyId}",
|
||||
clientData = ClientData.DefaultWithExtraData(
|
||||
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
|
||||
),
|
||||
)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
coEvery {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = mockSdkRequest,
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} returns createMockPublicKeyAssertionResponse(number = 1).asSuccess()
|
||||
|
@ -785,11 +764,10 @@ class Fido2CredentialManagerTest {
|
|||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `authenticateFido2Credential should return Error when origin is null`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(
|
||||
number = 1,
|
||||
origin = null,
|
||||
clientDataHash = null,
|
||||
signingInfo = mockSigningInfo,
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockOrigin = null,
|
||||
mockClientDataHash = null,
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
|
||||
val mockSelectedCipher = createMockCipherView(number = 1)
|
||||
|
@ -822,17 +800,11 @@ class Fido2CredentialManagerTest {
|
|||
@Test
|
||||
fun `authenticateFidoCredential should convert SDK response to AndroidFido2PublicKeyCredential, deserialize the response to JSON, and return Success with response JSON`() =
|
||||
runTest {
|
||||
mockkStatic(PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential)
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRequest = createMockFido2CredentialAssertionRequest(number = 1)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
|
||||
number = 1,
|
||||
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
|
||||
mockRequest = mockRequest,
|
||||
mockCipherView = mockCipherView,
|
||||
mockkStatic(
|
||||
PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential,
|
||||
)
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockRequest = createMockFido2AssertionRequest(mockSigningInfo = mockSigningInfo)
|
||||
val mockPublicKeyCredential = mockk<Fido2PublicKeyCredential>()
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true) {
|
||||
|
@ -840,7 +812,7 @@ class Fido2CredentialManagerTest {
|
|||
}
|
||||
coEvery {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = mockSdkRequest,
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} returns mockSdkResponse.asSuccess()
|
||||
|
@ -854,7 +826,7 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
coVerify {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = capture(requestCaptureSlot),
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
mockSdkResponse.toAndroidFido2PublicKeyCredential()
|
||||
|
@ -873,15 +845,8 @@ class Fido2CredentialManagerTest {
|
|||
runTest {
|
||||
mockkStatic(PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential)
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRequest = createMockFido2CredentialAssertionRequest(number = 1)
|
||||
val mockRequest = createMockFido2AssertionRequest(mockSigningInfo = mockSigningInfo)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
|
||||
number = 1,
|
||||
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
|
||||
mockRequest = mockRequest,
|
||||
mockCipherView = mockCipherView,
|
||||
)
|
||||
val mockPublicKeyCredential = mockk<Fido2PublicKeyCredential>()
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true) {
|
||||
|
@ -889,7 +854,7 @@ class Fido2CredentialManagerTest {
|
|||
}
|
||||
coEvery {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = mockSdkRequest,
|
||||
request = capture(requestCaptureSlot),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} returns mockSdkResponse.asSuccess()
|
||||
|
@ -902,8 +867,9 @@ class Fido2CredentialManagerTest {
|
|||
)
|
||||
|
||||
coVerify {
|
||||
assetManager.readAsset(DEFAULT_ALLOW_LIST_FILENAME)
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = capture(requestCaptureSlot),
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
mockSdkResponse.toAndroidFido2PublicKeyCredential()
|
||||
|
@ -915,8 +881,68 @@ class Fido2CredentialManagerTest {
|
|||
authResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticateFido2Credential should return Error when relyingPartyId is null`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val mockSelectedCipherView = createMockCipherView(number = 1)
|
||||
|
||||
every {
|
||||
json.decodeFromStringOrNull<PasskeyAssertionOptions>(DEFAULT_FIDO2_AUTH_REQUEST_JSON)
|
||||
} returns createMockPasskeyAssertionOptions(number = 1, relyingPartyId = null)
|
||||
|
||||
val result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockAssertionRequest,
|
||||
selectedCipherView = mockSelectedCipherView,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionResult.Error,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticateFido2Credential should return Error when validateOrigin is Error`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockOrigin = null,
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val mockSelectedCipherView = createMockCipherView(number = 1)
|
||||
|
||||
coEvery {
|
||||
digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = "mockRelyingPartyId-1")
|
||||
} returns IllegalStateException().asFailure()
|
||||
|
||||
val result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockAssertionRequest,
|
||||
selectedCipherView = mockSelectedCipherView,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionResult.Error,
|
||||
result,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
mockVaultSdkSource.authenticateFido2Credential(any(), any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden"
|
||||
private const val DEFAULT_ORIGIN = "bitwarden.com"
|
||||
private const val DEFAULT_APP_SIGNATURE = "0987654321ABCDEF"
|
||||
private const val DEFAULT_CERT_FINGERPRINT = "30:39:38:37:36:35:34:33:32:31:41:42:43:44:45:46"
|
||||
private val DEFAULT_STATEMENT = DigitalAssetLinkResponseJson(
|
||||
|
@ -926,12 +952,13 @@ private val DEFAULT_STATEMENT = DigitalAssetLinkResponseJson(
|
|||
),
|
||||
target = DigitalAssetLinkResponseJson.Target(
|
||||
namespace = "android_app",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
sha256CertFingerprints = listOf(
|
||||
DEFAULT_CERT_FINGERPRINT,
|
||||
),
|
||||
),
|
||||
)
|
||||
private const val DEFAULT_ALLOW_LIST_FILENAME = "fido2_privileged_allow_list.json"
|
||||
private val DEFAULT_STATEMENT_LIST = listOf(DEFAULT_STATEMENT)
|
||||
private const val DEFAULT_ALLOW_LIST = """
|
||||
{
|
||||
|
@ -977,34 +1004,43 @@ private const val MISSING_PACKAGE_ALLOW_LIST = """
|
|||
]
|
||||
}
|
||||
"""
|
||||
private const val DEFAULT_FIDO2_AUTH_REQUEST_JSON = """
|
||||
{
|
||||
"allowCredentials": [
|
||||
{
|
||||
"id": "mockCredentialId-1",
|
||||
"transports": [
|
||||
"internal"
|
||||
],
|
||||
"type": "public-key"
|
||||
},
|
||||
{
|
||||
"id": "mockCredentialId-2",
|
||||
"transports": [
|
||||
"internal"
|
||||
],
|
||||
"type": "public-key"
|
||||
}
|
||||
],
|
||||
"challenge": "mockChallenge",
|
||||
"rpId": "bitwarden.com",
|
||||
"userVerification": "preferred"
|
||||
}
|
||||
"""
|
||||
|
||||
private fun createMockFido2CredentialAssertionRequest(
|
||||
number: Int,
|
||||
clientDataHash: ByteArray? = byteArrayOf(0),
|
||||
signingInfo: SigningInfo = SigningInfo(),
|
||||
origin: String? = "mockOrigin-$number",
|
||||
) = Fido2CredentialAssertionRequest(
|
||||
cipherId = "mockCipherId-$number",
|
||||
credentialId = "mockCredentialId-$number",
|
||||
requestJson = "requestJson-$number",
|
||||
clientDataHash = clientDataHash,
|
||||
packageName = "mockPackageName-$number",
|
||||
signingInfo = signingInfo,
|
||||
origin = origin,
|
||||
)
|
||||
|
||||
private fun createAuthenticateFido2CredentialRequest(
|
||||
number: Int,
|
||||
clientData: ClientData,
|
||||
mockRequest: Fido2CredentialAssertionRequest =
|
||||
createMockFido2CredentialAssertionRequest(number),
|
||||
mockCipherView: CipherView = createMockCipherView(number),
|
||||
origin: String = mockRequest.origin!!,
|
||||
) = AuthenticateFido2CredentialRequest(
|
||||
userId = "activeUserId",
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = mockCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
)
|
||||
private fun createMockFido2AssertionRequest(
|
||||
mockOrigin: String? = DEFAULT_ORIGIN,
|
||||
mockClientDataHash: ByteArray? = null,
|
||||
mockSigningInfo: SigningInfo,
|
||||
) = mockk<Fido2CredentialAssertionRequest> {
|
||||
every { origin } returns mockOrigin
|
||||
every { requestJson } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
every { clientDataHash } returns mockClientDataHash
|
||||
every { callingAppInfo } returns mockk {
|
||||
every { origin } returns mockOrigin
|
||||
every { packageName } returns DEFAULT_PACKAGE_NAME
|
||||
every { getOrigin(DEFAULT_ALLOW_LIST) } returns mockOrigin
|
||||
every { signingInfo } returns mockSigningInfo
|
||||
every { isOriginPopulated() } returns mockOrigin.isNullOrEmpty().not()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1249,7 +1249,7 @@ private val DEFAULT_FIDO_2_REGISTER_CREDENTIAL_REQUEST = RegisterFido2Credential
|
|||
isUserVerificationSupported = true,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
val DEFAULT_FIDO_2_AUTH_REQUEST = AuthenticateFido2CredentialRequest(
|
||||
private val DEFAULT_FIDO_2_AUTH_REQUEST = AuthenticateFido2CredentialRequest(
|
||||
userId = "mockUserId",
|
||||
origin = "www.bitwarden.com",
|
||||
requestJson = "requestJson",
|
||||
|
|
|
@ -12,6 +12,7 @@ fun createMockPasskeyAssertionOptions(
|
|||
number: Int,
|
||||
userVerificationRequirement: UserVerificationRequirement =
|
||||
UserVerificationRequirement.PREFERRED,
|
||||
relyingPartyId: String? = "mockRelyingPartyId-$number",
|
||||
) = PasskeyAssertionOptions(
|
||||
challenge = "mockChallenge-$number",
|
||||
allowCredentials = listOf(
|
||||
|
@ -21,6 +22,6 @@ fun createMockPasskeyAssertionOptions(
|
|||
transports = listOf("mockPublicKeyCredentialDescriptorTransports-$number"),
|
||||
),
|
||||
),
|
||||
relyingPartyId = "mockRelyingPartyId-$number",
|
||||
relyingPartyId = relyingPartyId,
|
||||
userVerification = userVerificationRequirement,
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
|||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.net.Uri
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
|
@ -162,12 +163,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()
|
||||
}
|
||||
private val fido2CredentialManager: Fido2CredentialManager = mockk {
|
||||
coEvery { validateOrigin(any()) } returns Fido2ValidateOriginResult.Success
|
||||
coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success
|
||||
every { isUserVerified } returns false
|
||||
every { isUserVerified = any() } just runs
|
||||
every { authenticationAttempts } returns 0
|
||||
every { authenticationAttempts = any() } just runs
|
||||
every { hasAuthenticationAttemptsRemaining() } returns true
|
||||
every { getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true)
|
||||
}
|
||||
|
||||
private val organizationEventManager = mockk<OrganizationEventManager> {
|
||||
|
@ -1367,7 +1369,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Success(emptyList())
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any())
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
|
||||
mockFilteredCiphers = listOf(cipherView1)
|
||||
|
@ -1423,7 +1425,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
vaultRepository.getDecryptedFido2CredentialAutofillViews(
|
||||
cipherViewList = listOf(cipherView1, cipherView2),
|
||||
)
|
||||
fido2CredentialManager.validateOrigin(any())
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1959,27 +1961,25 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
createVaultItemListingViewModel()
|
||||
|
||||
coVerify(ordering = Ordering.ORDERED) {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
vaultRepository.vaultDataStateFlow
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2ValidateOriginResult should update dialog state on Unknown error`() = runTest {
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = "mockUserId",
|
||||
requestJson = "{}",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = null,
|
||||
)
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo>(relaxed = true)
|
||||
val mock = mockk<Fido2CredentialRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every { requestJson } returns "{}"
|
||||
}
|
||||
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
fido2CredentialRequest = mock,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2010,7 +2010,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2041,7 +2041,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2072,7 +2072,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2103,7 +2103,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2134,7 +2134,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2165,7 +2165,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(fido2CredentialRequest)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.ApplicationNotVerified
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -2517,6 +2517,93 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
verify(exactly = 0) { fido2CredentialManager.isUserVerified }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2AssertionRequest should show error dialog when relyingPartyId is null`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
|
||||
.copy(cipherId = "mockId-1")
|
||||
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
|
||||
mockAssertionRequest,
|
||||
)
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialList,
|
||||
),
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
|
||||
mockAssertionRequest.requestJson,
|
||||
)
|
||||
} returns createMockPasskeyAssertionOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
relyingPartyId = null,
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
verify(exactly = 0) { fido2CredentialManager.isUserVerified }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2AssertionRequest should show error dialog when validateOrigin is not Success`() =
|
||||
runTest {
|
||||
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
|
||||
.copy(cipherId = "mockId-1")
|
||||
val mockAssertionOptions = createMockPasskeyAssertionOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
)
|
||||
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
|
||||
mockAssertionRequest,
|
||||
)
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = mockFido2CredentialList,
|
||||
),
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
|
||||
requestJson = mockAssertionRequest.requestJson,
|
||||
)
|
||||
} returns mockAssertionOptions
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
awaitItem().dialogState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2AssertionRequest should observe vault data when request does not contain a cipherId`() =
|
||||
|
|
Loading…
Reference in a new issue