From fe1fe770c7e9ef2136deadc04427624a3b9ffb16 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:39:04 -0400 Subject: [PATCH] Use Google's Digital Asset Links API to verify digital asset links (#5101) --- .../manager/Fido2CredentialManagerImpl.kt | 6 +- .../fido2/manager/Fido2OriginManager.kt | 2 - .../fido2/manager/Fido2OriginManagerImpl.kt | 91 +++------- .../model/Fido2CredentialAssertionResult.kt | 15 -- .../fido2/model/Fido2ValidateOriginResult.kt | 10 -- .../itemlisting/VaultItemListingViewModel.kt | 28 +-- ...ido2CredentialAssertionResultExtensions.kt | 8 - .../Fido2ValidateOriginResultExtensions.kt | 8 - .../com/x8bit/bitwarden/MainViewModelTest.kt | 17 -- .../manager/Fido2CredentialManagerTest.kt | 19 -- .../fido2/manager/Fido2OriginManagerTest.kt | 115 +++--------- .../VaultItemListingViewModelTest.kt | 163 ++---------------- .../network/BitwardenServiceClientImpl.kt | 4 +- .../network/api/DigitalAssetLinkApi.kt | 27 ++- .../DigitalAssetLinkCheckResponseJson.kt | 28 +++ .../model/DigitalAssetLinkResponseJson.kt | 32 ---- .../service/DigitalAssetLinkService.kt | 15 +- .../service/DigitalAssetLinkServiceImpl.kt | 24 +-- .../service/DigitalAssetLinkServiceTest.kt | 66 +++---- 19 files changed, 163 insertions(+), 515 deletions(-) create mode 100644 network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkCheckResponseJson.kt delete mode 100644 network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkResponseJson.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt index aa896785fa..404520811c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerImpl.kt @@ -113,9 +113,7 @@ class Fido2CredentialManagerImpl( packageName = callingAppInfo.packageName, sha256CertFingerprint = callingAppInfo .getSignatureFingerprintAsHexString() - ?: return Fido2CredentialAssertionResult - .Error - .InvalidAppSignature, + .orEmpty(), host = hostUrl, assetLinkUrl = hostUrl, ), @@ -183,7 +181,7 @@ class Fido2CredentialManagerImpl( val signatureFingerprint = callingAppInfo .getSignatureFingerprintAsHexString() - ?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature + .orEmpty() val sdkOrigin = Origin.Android( UnverifiedAssetLink( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt index 2457187540..16e0856f46 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManager.kt @@ -12,12 +12,10 @@ interface Fido2OriginManager { * Validates the origin of a calling app. * * @param callingAppInfo The calling app info. - * @param relyingPartyId The relying party ID. * * @return The result of the validation. */ suspend fun validateOrigin( callingAppInfo: CallingAppInfo, - relyingPartyId: String, ): Fido2ValidateOriginResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt index 5f5fc273a0..19c057eac1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerImpl.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager import androidx.credentials.provider.CallingAppInfo -import com.bitwarden.network.model.DigitalAssetLinkResponseJson import com.bitwarden.network.service.DigitalAssetLinkService import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.platform.manager.AssetManager @@ -11,11 +10,11 @@ import timber.log.Timber private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json" private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json" +private const val DELEGATE_PERMISSION_HANDLE_ALL_URLS = "delegate_permission/common.handle_all_urls" /** * Primary implementation of [Fido2OriginManager]. */ -@Suppress("TooManyFunctions") class Fido2OriginManagerImpl( private val assetManager: AssetManager, private val digitalAssetLinkService: DigitalAssetLinkService, @@ -23,49 +22,38 @@ class Fido2OriginManagerImpl( override suspend fun validateOrigin( callingAppInfo: CallingAppInfo, - relyingPartyId: String, ): Fido2ValidateOriginResult { return if (callingAppInfo.isOriginPopulated()) { validatePrivilegedAppOrigin(callingAppInfo) } else { - validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId) + validateCallingApplicationAssetLinks(callingAppInfo) } } private suspend fun validateCallingApplicationAssetLinks( callingAppInfo: CallingAppInfo, - relyingPartyId: String, - ): Fido2ValidateOriginResult = digitalAssetLinkService - .getDigitalAssetLinkForRp(relyingParty = relyingPartyId) - .onFailure { - return Fido2ValidateOriginResult.Error.AssetLinkNotFound - } - .mapCatching { statements -> - statements - .filterMatchingAppStatementsOrNull( - rpPackageName = callingAppInfo.packageName, - ) - ?: return Fido2ValidateOriginResult.Error.ApplicationNotFound - } - .mapCatching { matchingStatements -> - callingAppInfo - .getSignatureFingerprintAsHexString() - ?.let { certificateFingerprint -> - matchingStatements - .filterMatchingAppSignaturesOrNull( - signature = certificateFingerprint, - ) - } - ?: return Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified - } - .fold( - onSuccess = { - Fido2ValidateOriginResult.Success(null) - }, - onFailure = { - Fido2ValidateOriginResult.Error.Unknown - }, - ) + ): Fido2ValidateOriginResult { + return digitalAssetLinkService + .checkDigitalAssetLinksRelations( + packageName = callingAppInfo.packageName, + certificateFingerprint = callingAppInfo + .getSignatureFingerprintAsHexString() + .orEmpty(), + relation = DELEGATE_PERMISSION_HANDLE_ALL_URLS, + ) + .fold( + onSuccess = { + if (it.linked) { + Fido2ValidateOriginResult.Success(null) + } else { + Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp + } + }, + onFailure = { + Fido2ValidateOriginResult.Error.AssetLinkNotFound + }, + ) + } private suspend fun validatePrivilegedAppOrigin( callingAppInfo: CallingAppInfo, @@ -121,35 +109,4 @@ class Fido2OriginManagerImpl( Fido2ValidateOriginResult.Error.Unknown }, ) - - /** - * Returns statements targeting the calling Android application, or null. - */ - private fun List.filterMatchingAppStatementsOrNull( - rpPackageName: String, - ): List? = - filter { statement -> - val target = statement.target - target.namespace == "android_app" && - target.packageName == rpPackageName && - statement.relation.containsAll( - listOf( - "delegate_permission/common.handle_all_urls", - ), - ) - } - .takeUnless { it.isEmpty() } - - /** - * Returns statements that match the given [signature], or null. - */ - private fun List.filterMatchingAppSignaturesOrNull( - signature: String, - ): List? = - filter { statement -> - statement.target.sha256CertFingerprints - ?.contains(signature) - ?: false - } - .takeUnless { it.isEmpty() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt index 266445a96f..9045eab6f3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionResult.kt @@ -24,21 +24,6 @@ sealed class Fido2CredentialAssertionResult { */ data object MissingHostUrl : Error() - /** - * Indicates the calling application signature was invalid. - */ - data object InvalidAppSignature : Error() - - /** - * Indicates origin validation failed. - * - * @property originValidationError The specific error that caused the origin validation to - * fail. - */ - data class OriginValidationFailed( - val originValidationError: Fido2ValidateOriginResult.Error, - ) : Error() - /** * Indicates an internal error occurred. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2ValidateOriginResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2ValidateOriginResult.kt index 68fd5a6e11..01cacfc2c5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2ValidateOriginResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2ValidateOriginResult.kt @@ -22,16 +22,6 @@ sealed class Fido2ValidateOriginResult { */ data object AssetLinkNotFound : Error() - /** - * Indicates the application package name was not found in the digital asset links file. - */ - data object ApplicationNotFound : Error() - - /** - * Indicates the application fingerprint was not found the digital asset links file. - */ - data object ApplicationFingerprintNotVerified : Error() - /** * Indicates the calling application is privileged but its package name is not found within * the privileged app allow list. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index a52e3a0bbf..17c47a991f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -889,22 +889,10 @@ class VaultItemListingViewModel @Inject constructor( ) return } - val relyingPartyId = fido2CredentialManager - .getPasskeyAssertionOptionsOrNull(option.requestJson) - ?.relyingPartyId - ?: run { - showFido2ErrorDialog( - R.string.passkey_operation_failed_because_the_request_is_invalid.asText(), - ) - return - } viewModelScope.launch { val validateOriginResult = fido2OriginManager - .validateOrigin( - callingAppInfo = request.callingAppInfo, - relyingPartyId = relyingPartyId, - ) + .validateOrigin(callingAppInfo = request.callingAppInfo) when (validateOriginResult) { is Fido2ValidateOriginResult.Error -> { @@ -1638,23 +1626,10 @@ class VaultItemListingViewModel @Inject constructor( private fun handleFido2RegisterCredentialRequestReceive( action: VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive, ) { - val relyingPartyId = action.request - .providerRequest - .getCreatePasskeyCredentialRequestOrNull() - ?.let { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(it.requestJson) } - ?.relyingParty - ?.id - ?: run { - showFido2ErrorDialog( - R.string.passkey_operation_failed_because_the_request_is_invalid.asText(), - ) - return - } viewModelScope.launch { val validateOriginResult = fido2OriginManager .validateOrigin( callingAppInfo = action.request.callingAppInfo, - relyingPartyId = relyingPartyId, ) when (validateOriginResult) { is Fido2ValidateOriginResult.Error -> { @@ -1734,7 +1709,6 @@ class VaultItemListingViewModel @Inject constructor( viewModelScope.launch { val validateOriginResult = fido2OriginManager.validateOrigin( callingAppInfo = callingAppInfo, - relyingPartyId = relyingPartyId, ) when (validateOriginResult) { is Fido2ValidateOriginResult.Success -> { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2CredentialAssertionResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2CredentialAssertionResultExtensions.kt index 1f1021002c..a4418f80f0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2CredentialAssertionResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2CredentialAssertionResultExtensions.kt @@ -15,10 +15,6 @@ val Fido2CredentialAssertionResult.Error.messageResourceId: Int R.string.passkey_registration_failed_due_to_an_internal_error } - Fido2CredentialAssertionResult.Error.InvalidAppSignature -> { - R.string.passkey_operation_failed_because_app_signature_is_invalid - } - Fido2CredentialAssertionResult.Error.MissingHostUrl -> { R.string.passkey_operation_failed_because_host_url_is_not_present_in_request } @@ -26,8 +22,4 @@ val Fido2CredentialAssertionResult.Error.messageResourceId: Int Fido2CredentialAssertionResult.Error.MissingRpId -> { R.string.passkey_operation_failed_because_relying_party_cannot_be_identified } - - is Fido2CredentialAssertionResult.Error.OriginValidationFailed -> { - originValidationError.messageResourceId - } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2ValidateOriginResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2ValidateOriginResultExtensions.kt index fd2fb22a06..0b89a06143 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2ValidateOriginResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/Fido2ValidateOriginResultExtensions.kt @@ -11,14 +11,6 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult val Fido2ValidateOriginResult.Error.messageResourceId: Int @StringRes get() = when (this) { - Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified -> { - R.string.passkey_operation_failed_because_app_could_not_be_verified - } - - Fido2ValidateOriginResult.Error.ApplicationNotFound -> { - R.string.passkey_operation_failed_because_app_not_found_in_asset_links - } - Fido2ValidateOriginResult.Error.AssetLinkNotFound -> { R.string.passkey_operation_failed_because_of_missing_asset_links } diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index 634b98b9c1..db6c4fc084 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -24,11 +24,9 @@ import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager -import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest -import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest @@ -136,9 +134,6 @@ class MainViewModelTest : BaseViewModelTest() { every { isUserVerified } returns true every { isUserVerified = any() } just runs } - private val fido2OriginManager = mockk { - coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success(null) - } private val savedStateHandle = SavedStateHandle() private val appResumeManager: AppResumeManager = mockk { @@ -749,12 +744,6 @@ class MainViewModelTest : BaseViewModelTest() { val mockIntent = createMockIntent( mockFido2CreateCredentialRequest = fido2CreateCredentialRequest, ) - coEvery { - fido2OriginManager.validateOrigin( - callingAppInfo = any(), - relyingPartyId = any(), - ) - } returns Fido2ValidateOriginResult.Success(null) viewModel.trySendAction( MainAction.ReceiveFirstIntent( @@ -780,12 +769,6 @@ class MainViewModelTest : BaseViewModelTest() { val mockIntent = createMockIntent( mockFido2CreateCredentialRequest = fido2CreateCredentialRequest, ) - coEvery { - fido2OriginManager.validateOrigin( - callingAppInfo = any(), - relyingPartyId = any(), - ) - } returns Fido2ValidateOriginResult.Success(null) viewModel.trySendAction( MainAction.ReceiveFirstIntent( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt index 80c0b5e007..c28c348422 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2CredentialManagerTest.kt @@ -454,25 +454,6 @@ class Fido2CredentialManagerTest { ) } - @Suppress("MaxLineLength") - @Test - fun `registerFido2Credential should return InvalidAppSignature when calling app is not privileged and signature is invalid`() = - runTest { - every { mockCallingAppInfo.isOriginPopulated() } returns false - every { mockSigningInfo.hasMultipleSigners() } returns true - val result = fido2CredentialManager.registerFido2Credential( - userId = "mockUserId", - callingAppInfo = mockCallingAppInfo, - createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest, - selectedCipherView = createMockCipherView(number = 1), - ) - - assertEquals( - Fido2RegisterCredentialResult.Error.InvalidAppSignature, - result, - ) - } - @Suppress("MaxLineLength") @Test fun `registerFido2Credential should return MissingHostUrl when calling app if privileged and origin is missing`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerTest.kt index ba1a76918f..64e9d7bfc7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/manager/Fido2OriginManagerTest.kt @@ -5,7 +5,7 @@ import android.util.Base64 import androidx.credentials.provider.CallingAppInfo import com.bitwarden.core.data.util.asFailure import com.bitwarden.core.data.util.asSuccess -import com.bitwarden.network.model.DigitalAssetLinkResponseJson +import com.bitwarden.network.model.DigitalAssetLinkCheckResponseJson import com.bitwarden.network.service.DigitalAssetLinkService import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.platform.manager.AssetManager @@ -76,7 +76,6 @@ class Fido2OriginManagerTest { val result = fido2OriginManager.validateOrigin( callingAppInfo = mockPrivilegedAppInfo, - relyingPartyId = "relyingPartyId", ) coVerify(exactly = 1) { mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME) @@ -100,7 +99,6 @@ class Fido2OriginManagerTest { val result = fido2OriginManager.validateOrigin( callingAppInfo = mockPrivilegedAppInfo, - relyingPartyId = "relyingPartyId", ) coVerify(exactly = 1) { mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME) @@ -125,7 +123,6 @@ class Fido2OriginManagerTest { val result = fido2OriginManager.validateOrigin( callingAppInfo = mockPrivilegedAppInfo, - relyingPartyId = "relyingPartyId", ) coVerify(exactly = 1) { @@ -143,12 +140,15 @@ class Fido2OriginManagerTest { fun `validateOrigin should return Success when calling app is NonPrivileged and has a valid asset link entry`() = runTest { coEvery { - mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID) - } returns listOf(DEFAULT_STATEMENT).asSuccess() + mockDigitalAssetLinkService.checkDigitalAssetLinksRelations( + packageName = DEFAULT_PACKAGE_NAME, + certificateFingerprint = DEFAULT_CERT_FINGERPRINT, + relation = "delegate_permission/common.handle_all_urls", + ) + } returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE.asSuccess() val result = fido2OriginManager.validateOrigin( callingAppInfo = mockNonPrivilegedAppInfo, - relyingPartyId = DEFAULT_RP_ID, ) assertEquals( @@ -159,49 +159,21 @@ class Fido2OriginManagerTest { @Suppress("MaxLineLength") @Test - fun `validateOrigin should return ApplicationFingerprintNotVerified when calling app is NonPrivileged but signature does not match asset link entry`() = + fun `validateOrigin should return PasskeysNotSupportedForApp when calling app is NonPrivileged but signature does not match asset link entry`() = runTest { coEvery { - mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID) - } returns listOf( - DEFAULT_STATEMENT.copy( - target = DEFAULT_STATEMENT.target.copy( - sha256CertFingerprints = listOf("invalid_fingerprint"), - ), - ), - ) + mockDigitalAssetLinkService.checkDigitalAssetLinksRelations( + packageName = DEFAULT_PACKAGE_NAME, + certificateFingerprint = DEFAULT_CERT_FINGERPRINT, + relation = "delegate_permission/common.handle_all_urls", + ) + } returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE + .copy(linked = false) .asSuccess() assertEquals( - Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified, - fido2OriginManager.validateOrigin( - callingAppInfo = mockNonPrivilegedAppInfo, - relyingPartyId = DEFAULT_RP_ID, - ), - ) - } - - @Suppress("MaxLineLength") - @Test - fun `validateOrigin should return ApplicationNotFound when calling app is NonPrivileged and packageName has no asset link entry`() = - runTest { - coEvery { - mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID) - } returns listOf( - DEFAULT_STATEMENT.copy( - target = DEFAULT_STATEMENT.target.copy( - packageName = "invalid_package_name", - ), - ), - ) - .asSuccess() - - assertEquals( - Fido2ValidateOriginResult.Error.ApplicationNotFound, - fido2OriginManager.validateOrigin( - callingAppInfo = mockNonPrivilegedAppInfo, - relyingPartyId = DEFAULT_RP_ID, - ), + Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp, + fido2OriginManager.validateOrigin(callingAppInfo = mockNonPrivilegedAppInfo), ) } @@ -210,35 +182,16 @@ class Fido2OriginManagerTest { fun `validateOrigin should return AssetLinkNotFound when calling app is NonPrivileged and asset link does not exist`() = runTest { coEvery { - mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID) + mockDigitalAssetLinkService.checkDigitalAssetLinksRelations( + packageName = DEFAULT_PACKAGE_NAME, + certificateFingerprint = DEFAULT_CERT_FINGERPRINT, + relation = "delegate_permission/common.handle_all_urls", + ) } returns RuntimeException().asFailure() assertEquals( Fido2ValidateOriginResult.Error.AssetLinkNotFound, - fido2OriginManager.validateOrigin( - callingAppInfo = mockNonPrivilegedAppInfo, - relyingPartyId = DEFAULT_RP_ID, - ), - ) - } - - @Suppress("MaxLineLength") - @Test - fun `validateOrigin should return Unknown error when calling app is NonPrivileged and exception is caught while filtering asset links`() = - runTest { - coEvery { - mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID) - } returns listOf(DEFAULT_STATEMENT).asSuccess() - every { - mockNonPrivilegedAppInfo.packageName - } throws IllegalStateException() - - assertEquals( - Fido2ValidateOriginResult.Error.Unknown, - fido2OriginManager.validateOrigin( - callingAppInfo = mockNonPrivilegedAppInfo, - relyingPartyId = DEFAULT_RP_ID, - ), + fido2OriginManager.validateOrigin(callingAppInfo = mockNonPrivilegedAppInfo), ) } @@ -246,9 +199,6 @@ class Fido2OriginManagerTest { @Test fun `validateOrigin should return Unknown error when calling app is Privileged and allow list file read fails`() = runTest { - coEvery { - mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID) - } returns listOf(DEFAULT_STATEMENT).asSuccess() coEvery { mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME) } returns IllegalStateException().asFailure() @@ -260,7 +210,6 @@ class Fido2OriginManagerTest { Fido2ValidateOriginResult.Error.Unknown, fido2OriginManager.validateOrigin( callingAppInfo = mockPrivilegedAppInfo, - relyingPartyId = DEFAULT_RP_ID, ), ) } @@ -269,7 +218,6 @@ class Fido2OriginManagerTest { private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden" 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 const val DEFAULT_RP_ID = "bitwarden.com" private const val DEFAULT_ORIGIN = "bitwarden.com" private const val GOOGLE_ALLOW_LIST_FILENAME = "fido2_privileged_google.json" private const val COMMUNITY_ALLOW_LIST_FILENAME = "fido2_privileged_community.json" @@ -317,16 +265,9 @@ private const val FAIL_ALLOW_LIST = """ ] } """ -private val DEFAULT_STATEMENT = DigitalAssetLinkResponseJson( - relation = listOf( - "delegate_permission/common.get_login_creds", - "delegate_permission/common.handle_all_urls", - ), - target = DigitalAssetLinkResponseJson.Target( - namespace = "android_app", - packageName = DEFAULT_PACKAGE_NAME, - sha256CertFingerprints = listOf( - DEFAULT_CERT_FINGERPRINT, - ), - ), +private val DEFAULT_ASSET_LINKS_CHECK_RESPONSE = + DigitalAssetLinkCheckResponseJson( + linked = true, + maxAge = "30s", + debugString = null, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 88f0e46b07..c425d35bd9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -198,7 +198,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } returns UserVerificationRequirement.PREFERRED } private val fido2OriginManager: Fido2OriginManager = mockk { - coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success(null) + coEvery { validateOrigin(any()) } returns Fido2ValidateOriginResult.Success(null) } private val organizationEventManager = mockk { @@ -289,7 +289,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns createMockPasskeyAttestationOptions(number = 1) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Success(null) val viewModel = createVaultItemListingViewModel() @@ -1871,7 +1871,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } returns DecryptFido2CredentialAutofillViewResult.Success(emptyList()) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Success("") mockFilteredCiphers = listOf(cipherView1) @@ -1925,7 +1925,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { vaultRepository.getDecryptedFido2CredentialAutofillViews( cipherViewList = listOf(cipherView1, cipherView2), ) - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } } @@ -2594,16 +2594,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin( - callingAppInfo = any(), - relyingPartyId = any(), - ) + fido2OriginManager.validateOrigin(callingAppInfo = any()) } returns Fido2ValidateOriginResult.Success("mockOrigin") createVaultItemListingViewModel() coVerify(ordering = Ordering.ORDERED) { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) vaultRepository.vaultDataStateFlow } } @@ -2631,7 +2628,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Error.Unknown val viewModel = createVaultItemListingViewModel() @@ -2670,7 +2667,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed val viewModel = createVaultItemListingViewModel() @@ -2709,7 +2706,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound val viewModel = createVaultItemListingViewModel() @@ -2748,7 +2745,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp val viewModel = createVaultItemListingViewModel() @@ -2762,45 +2759,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } - @Suppress("MaxLineLength") - @Test - fun `Fido2ValidateOriginResult should update dialog state on ApplicationNotFound error`() = - runTest { - val fido2CreateCredentialRequest = Fido2CreateCredentialRequest( - userId = "mockUserId", - isUserPreVerified = false, - requestData = bundleOf(), - ) - - specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save( - fido2CreateCredentialRequest = fido2CreateCredentialRequest, - ) - - every { - ProviderCreateCredentialRequest.fromBundle(any()) - } returns mockk(relaxed = true) { - every { - callingRequest - } returns mockk(relaxed = true) - } - every { - fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) - } returns mockk(relaxed = true) - coEvery { - fido2OriginManager.validateOrigin(any(), any()) - } returns Fido2ValidateOriginResult.Error.ApplicationNotFound - - val viewModel = createVaultItemListingViewModel() - - assertEquals( - VaultItemListingState.DialogState.Fido2OperationFail( - R.string.an_error_has_occurred.asText(), - R.string.passkey_operation_failed_because_app_not_found_in_asset_links.asText(), - ), - viewModel.stateFlow.value.dialogState, - ) - } - @Suppress("MaxLineLength") @Test fun `Fido2ValidateOriginResult should update dialog state on AssetLinkNotFound error`() = @@ -2826,7 +2784,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Error.AssetLinkNotFound val viewModel = createVaultItemListingViewModel() @@ -2840,45 +2798,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } - @Suppress("MaxLineLength") - @Test - fun `Fido2ValidateOriginResult should update dialog state on ApplicationNotVerified error`() = - runTest { - val fido2CreateCredentialRequest = Fido2CreateCredentialRequest( - userId = "mockUserId", - isUserPreVerified = false, - requestData = bundleOf(), - ) - - specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save( - fido2CreateCredentialRequest = fido2CreateCredentialRequest, - ) - - every { - ProviderCreateCredentialRequest.fromBundle(any()) - } returns mockk(relaxed = true) { - every { - callingRequest - } returns mockk(relaxed = true) - } - every { - fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any()) - } returns mockk(relaxed = true) - coEvery { - fido2OriginManager.validateOrigin(any(), any()) - } returns Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified - - val viewModel = createVaultItemListingViewModel() - - assertEquals( - VaultItemListingState.DialogState.Fido2OperationFail( - R.string.an_error_has_occurred.asText(), - R.string.passkey_operation_failed_because_app_could_not_be_verified.asText(), - ), - viewModel.stateFlow.value.dialogState, - ) - } - @Suppress("MaxLineLength") @Test fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() = @@ -3038,10 +2957,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin( - callingAppInfo = any(), - relyingPartyId = any(), - ) + fido2OriginManager.validateOrigin(callingAppInfo = any()) } returns Fido2ValidateOriginResult.Success("mockOrigin") every { vaultRepository @@ -3247,10 +3163,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any()) } returns mockk(relaxed = true) coEvery { - fido2OriginManager.validateOrigin( - callingAppInfo = any(), - relyingPartyId = any(), - ) + fido2OriginManager.validateOrigin(callingAppInfo = any()) } returns Fido2ValidateOriginResult.Error.Unknown val dataState = DataState.Loaded( @@ -3463,54 +3376,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } - @Test - fun `Fido2AssertionRequest should show error dialog when relyingPartyId is null`() = runTest { - setupMockUri() - val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1) - .copy(cipherId = "mockId-1") - val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1) - val mockCipherView = createMockCipherView( - number = 1, - fido2Credentials = mockFido2CredentialList, - ) - specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion( - mockAssertionRequest, - ) - every { - vaultRepository - .ciphersStateFlow - .value - .data - } returns listOf(mockCipherView) - every { fido2CredentialManager.isUserVerified } returns true - every { - fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any()) - } returns createMockPasskeyAssertionOptions( - number = 1, - userVerificationRequirement = UserVerificationRequirement.DISCOURAGED, - relyingPartyId = null, - ) - - val dataState = DataState.Loaded( - data = VaultData( - cipherViewList = listOf(mockCipherView), - folderViewList = listOf(createMockFolderView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - sendViewList = listOf(createMockSendView(number = 1)), - ), - ) - val viewModel = createVaultItemListingViewModel() - mutableVaultDataStateFlow.value = dataState - - assertEquals( - VaultItemListingState.DialogState.Fido2OperationFail( - title = R.string.an_error_has_occurred.asText(), - message = R.string.passkey_operation_failed_because_the_request_is_invalid.asText(), - ), - viewModel.stateFlow.value.dialogState, - ) - } - @Test fun `Fido2AssertionRequest should show error dialog when validateOrigin is not Success`() = runTest { @@ -3540,7 +3405,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any()) } returns mockAssertionOptions coEvery { - fido2OriginManager.validateOrigin(any(), any()) + fido2OriginManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Error.Unknown val dataState = DataState.Loaded( diff --git a/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt b/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt index 335a03a924..d79bb8f2f3 100644 --- a/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/BitwardenServiceClientImpl.kt @@ -131,7 +131,9 @@ internal class BitwardenServiceClientImpl( override val digitalAssetLinkService: DigitalAssetLinkService by lazy { DigitalAssetLinkServiceImpl( - digitalAssetLinkApi = retrofits.createStaticRetrofit().create(), + digitalAssetLinkApi = retrofits + .createStaticRetrofit(baseUrl = "https://digitalassetlinks.googleapis.com/") + .create(), ) } diff --git a/network/src/main/kotlin/com/bitwarden/network/api/DigitalAssetLinkApi.kt b/network/src/main/kotlin/com/bitwarden/network/api/DigitalAssetLinkApi.kt index 164afea6a9..5a2fba9551 100644 --- a/network/src/main/kotlin/com/bitwarden/network/api/DigitalAssetLinkApi.kt +++ b/network/src/main/kotlin/com/bitwarden/network/api/DigitalAssetLinkApi.kt @@ -1,20 +1,31 @@ package com.bitwarden.network.api -import com.bitwarden.network.model.DigitalAssetLinkResponseJson +import androidx.annotation.Keep +import com.bitwarden.network.model.DigitalAssetLinkCheckResponseJson import com.bitwarden.network.model.NetworkResult import retrofit2.http.GET -import retrofit2.http.Url +import retrofit2.http.Query /** - * Defines calls to an RP digital asset link file. + * Defines calls to a digital asset link file. */ +@Keep interface DigitalAssetLinkApi { /** - * Attempts to download the asset links file from the RP. + * Checks if the given [relation] exists in a digital asset link file. */ - @GET - suspend fun getDigitalAssetLinks( - @Url url: String, - ): NetworkResult> + @GET("v1/assetlinks:check") + suspend fun checkDigitalAssetLinksRelations( + @Query("source.androidApp.packageName") + sourcePackageName: String, + @Query("source.androidApp.certificate.sha256Fingerprint") + sourceCertificateFingerprint: String, + @Query("target.androidApp.packageName") + targetPackageName: String, + @Query("target.androidApp.certificate.sha256Fingerprint") + targetCertificateFingerprint: String, + @Query("relation") + relation: String, + ): NetworkResult } diff --git a/network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkCheckResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkCheckResponseJson.kt new file mode 100644 index 0000000000..1d1bcfc201 --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkCheckResponseJson.kt @@ -0,0 +1,28 @@ +package com.bitwarden.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents the response from a digital asset link check. + * + * Modeled after the [CheckAssetLinks](https://developers.google.com/digital-asset-links/reference/rest/v1/assetlinks/check) + * response from the Google Digital Asset Links API. + * + * @property linked Indicates whether the asset link is linked. + * @property maxAge From serving time, how much longer the response should be considered valid + * barring further updates. A duration in seconds with up to nine fractional digits, terminated by + * 's'. Example: "3.5s". + * @property debugString Human-readable message containing information intended to help end users + * understand, reproduce and debug the result. + */ +@Suppress("MaxLineLength") +@Serializable +data class DigitalAssetLinkCheckResponseJson( + @SerialName("linked") + val linked: Boolean = false, + @SerialName("maxAge") + val maxAge: String?, + @SerialName("debugString") + val debugString: String?, +) diff --git a/network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkResponseJson.kt deleted file mode 100644 index e2a1cef320..0000000000 --- a/network/src/main/kotlin/com/bitwarden/network/model/DigitalAssetLinkResponseJson.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.bitwarden.network.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * Models a response from an RP digital asset link request. - */ -@Serializable -data class DigitalAssetLinkResponseJson( - @SerialName("relation") - val relation: List, - - @SerialName("target") - val target: Target, -) { - - /** - * Represents targets for an asset link statement. - */ - @Serializable - data class Target( - @SerialName("namespace") - val namespace: String, - - @SerialName("package_name") - val packageName: String?, - - @SerialName("sha256_cert_fingerprints") - val sha256CertFingerprints: List?, - ) -} diff --git a/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkService.kt b/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkService.kt index c4c3c1a16b..bda071850a 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkService.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkService.kt @@ -1,17 +1,18 @@ package com.bitwarden.network.service -import com.bitwarden.network.model.DigitalAssetLinkResponseJson +import com.bitwarden.network.model.DigitalAssetLinkCheckResponseJson /** * Provides an API for querying digital asset links. */ interface DigitalAssetLinkService { - /** - * Attempt to retrieve the asset links file from the provided [relyingParty]. + * Checks if the given [packageName] with a given [certificateFingerprint] has the given + * [relation]. */ - suspend fun getDigitalAssetLinkForRp( - scheme: String = "https://", - relyingParty: String, - ): Result> + suspend fun checkDigitalAssetLinksRelations( + packageName: String, + certificateFingerprint: String, + relation: String, + ): Result } diff --git a/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceImpl.kt b/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceImpl.kt index 496f0f29fe..59d662d22f 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceImpl.kt @@ -1,7 +1,7 @@ package com.bitwarden.network.service import com.bitwarden.network.api.DigitalAssetLinkApi -import com.bitwarden.network.model.DigitalAssetLinkResponseJson +import com.bitwarden.network.model.DigitalAssetLinkCheckResponseJson import com.bitwarden.network.util.toResult /** @@ -11,13 +11,17 @@ class DigitalAssetLinkServiceImpl( private val digitalAssetLinkApi: DigitalAssetLinkApi, ) : DigitalAssetLinkService { - override suspend fun getDigitalAssetLinkForRp( - scheme: String, - relyingParty: String, - ): Result> = - digitalAssetLinkApi - .getDigitalAssetLinks( - url = "$scheme$relyingParty/.well-known/assetlinks.json", - ) - .toResult() + override suspend fun checkDigitalAssetLinksRelations( + packageName: String, + certificateFingerprint: String, + relation: String, + ): Result = digitalAssetLinkApi + .checkDigitalAssetLinksRelations( + sourcePackageName = packageName, + sourceCertificateFingerprint = certificateFingerprint, + targetPackageName = packageName, + targetCertificateFingerprint = certificateFingerprint, + relation = relation, + ) + .toResult() } diff --git a/network/src/test/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceTest.kt index a9c6734bed..d9b1c31f6c 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/DigitalAssetLinkServiceTest.kt @@ -1,8 +1,9 @@ package com.bitwarden.network.service +import com.bitwarden.core.data.util.asSuccess import com.bitwarden.network.api.DigitalAssetLinkApi import com.bitwarden.network.base.BaseServiceTest -import com.bitwarden.network.model.DigitalAssetLinkResponseJson +import com.bitwarden.network.model.DigitalAssetLinkCheckResponseJson import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse import org.junit.jupiter.api.Assertions.assertEquals @@ -10,7 +11,6 @@ import org.junit.jupiter.api.Test import retrofit2.create class DigitalAssetLinkServiceTest : BaseServiceTest() { - private val digitalAssetLinkApi: DigitalAssetLinkApi = retrofit.create() private val digitalAssetLinkService: DigitalAssetLinkService = DigitalAssetLinkServiceImpl( @@ -18,50 +18,28 @@ class DigitalAssetLinkServiceTest : BaseServiceTest() { ) @Test - fun `getDigitalAssetLinkForRp should return the correct response`() = runTest { - server.enqueue(MockResponse().setBody(GET_DIGITAL_ASSET_LINK_SUCCESS_JSON)) - val result = digitalAssetLinkService.getDigitalAssetLinkForRp( - scheme = url.scheme, - relyingParty = url.host, - ) + fun `checkDigitalAssetLinksRelations should return the correct response`() = runTest { + server.enqueue(MockResponse().setBody(CHECK_DIGITAL_ASSET_LINKS_RELATIONS_SUCCESS_JSON)) assertEquals( - createDigitalAssetLinkResponse(), - result.getOrThrow(), + DigitalAssetLinkCheckResponseJson( + linked = true, + maxAge = "47.535162130s", + debugString = null, + ) + .asSuccess(), + digitalAssetLinkService.checkDigitalAssetLinksRelations( + packageName = "com.x8bit.bitwarden", + certificateFingerprint = + "00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:11:12:13", + relation = "delegate_permission/common.handle_all_urls", + ), ) } } -@Suppress("MaxLineLength") -private fun createDigitalAssetLinkResponse() = listOf( - DigitalAssetLinkResponseJson( - relation = listOf( - "delegate_permission/common.get_login_creds", - "delegate_permission/common.handle_all_urls", - ), - target = DigitalAssetLinkResponseJson.Target( - namespace = "android_app", - packageName = "com.mock.package", - sha256CertFingerprints = listOf( - "00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F", - ), - ), - ), -) - -private const val GET_DIGITAL_ASSET_LINK_SUCCESS_JSON = """ -[ - { - "relation": [ - "delegate_permission/common.get_login_creds", - "delegate_permission/common.handle_all_urls" - ], - "target": { - "namespace": "android_app", - "package_name": "com.mock.package", - "sha256_cert_fingerprints": [ - "00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F" - ] - } - } -] -""" +private val CHECK_DIGITAL_ASSET_LINKS_RELATIONS_SUCCESS_JSON = """ +{ + "linked": true, + "maxAge": "47.535162130s" +} +""".trimIndent()