mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
[PM-22441] Refactor DigitalAssetLinkService to use source website (#5351)
This commit is contained in:
parent
861a4281fa
commit
7de770ca03
@ -14,6 +14,8 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManagerImpl
|
||||
import com.x8bit.bitwarden.data.credentials.manager.OriginManager
|
||||
import com.x8bit.bitwarden.data.credentials.manager.OriginManagerImpl
|
||||
import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser
|
||||
import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParserImpl
|
||||
import com.x8bit.bitwarden.data.credentials.processor.CredentialProviderProcessor
|
||||
import com.x8bit.bitwarden.data.credentials.processor.CredentialProviderProcessorImpl
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
@ -121,4 +123,10 @@ object CredentialProviderModule {
|
||||
privilegedAppDiskSource = privilegedAppDiskSource,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRelyingPartyParser(
|
||||
json: Json,
|
||||
): RelyingPartyParser = RelyingPartyParserImpl(json)
|
||||
}
|
||||
|
||||
@ -11,11 +11,13 @@ interface OriginManager {
|
||||
/**
|
||||
* Validates the origin of a calling app.
|
||||
*
|
||||
* @param relyingPartyId The ID of the relying party that sent the request.
|
||||
* @param callingAppInfo The calling app info.
|
||||
*
|
||||
* @return The result of the validation.
|
||||
*/
|
||||
suspend fun validateOrigin(
|
||||
relyingPartyId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): ValidateOriginResult
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary
|
||||
import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
@ -26,25 +27,28 @@ class OriginManagerImpl(
|
||||
) : OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
relyingPartyId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): ValidateOriginResult {
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(callingAppInfo)
|
||||
validateCallingApplicationAssetLinks(relyingPartyId, callingAppInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
relyingPartyId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): ValidateOriginResult {
|
||||
return digitalAssetLinkService
|
||||
.checkDigitalAssetLinksRelations(
|
||||
packageName = callingAppInfo.packageName,
|
||||
certificateFingerprint = callingAppInfo
|
||||
sourceWebSite = relyingPartyId.prefixHttpsIfNecessary(),
|
||||
targetPackageName = callingAppInfo.packageName,
|
||||
targetCertificateFingerprint = callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
.orEmpty(),
|
||||
relation = DELEGATE_PERMISSION_HANDLE_ALL_URLS,
|
||||
relations = listOf(DELEGATE_PERMISSION_HANDLE_ALL_URLS),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
package com.x8bit.bitwarden.data.credentials.parser
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
|
||||
/**
|
||||
* A tool for parsing relying party data from the Credential Manager requests.
|
||||
*/
|
||||
interface RelyingPartyParser {
|
||||
/**
|
||||
* Parse the relying party ID from the [GetPublicKeyCredentialOption].
|
||||
*/
|
||||
fun parse(getPublicKeyCredentialOption: GetPublicKeyCredentialOption): String?
|
||||
|
||||
/**
|
||||
* Parse the relying party ID from the [CreatePublicKeyCredentialRequest].
|
||||
*/
|
||||
fun parse(createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest): String?
|
||||
|
||||
/**
|
||||
* Parse the relying party ID from the [BeginGetPublicKeyCredentialOption].
|
||||
*/
|
||||
fun parse(beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption): String?
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.x8bit.bitwarden.data.credentials.parser
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Default implementation of [RelyingPartyParser].
|
||||
*/
|
||||
class RelyingPartyParserImpl(
|
||||
private val json: Json,
|
||||
) : RelyingPartyParser {
|
||||
|
||||
override fun parse(
|
||||
getPublicKeyCredentialOption: GetPublicKeyCredentialOption,
|
||||
): String? = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(getPublicKeyCredentialOption.requestJson)
|
||||
?.relyingPartyId
|
||||
|
||||
override fun parse(
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
): String? = json
|
||||
.decodeFromStringOrNull<PasskeyAttestationOptions>(
|
||||
createPublicKeyCredentialRequest.requestJson,
|
||||
)
|
||||
?.relyingParty
|
||||
?.id
|
||||
|
||||
override fun parse(
|
||||
beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption,
|
||||
): String? = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(
|
||||
beginGetPublicKeyCredentialOption.requestJson,
|
||||
)
|
||||
?.relyingPartyId
|
||||
}
|
||||
@ -44,6 +44,7 @@ import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.credentials.util.getCreatePasskeyCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
@ -136,6 +137,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
private val snackbarRelayManager: SnackbarRelayManager,
|
||||
private val relyingPartyParser: RelyingPartyParser,
|
||||
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
@ -1028,6 +1030,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun authenticateFido2Credential(
|
||||
request: ProviderGetCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
@ -1048,10 +1051,20 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
)
|
||||
return
|
||||
}
|
||||
val relyingPartyId = relyingPartyParser.parse(option)
|
||||
?: run {
|
||||
showCredentialManagerErrorDialog(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
|
||||
val validateOriginResult = originManager
|
||||
.validateOrigin(callingAppInfo = request.callingAppInfo)
|
||||
.validateOrigin(
|
||||
relyingPartyId = relyingPartyId,
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
)
|
||||
|
||||
when (validateOriginResult) {
|
||||
is ValidateOriginResult.Error -> {
|
||||
@ -1796,9 +1809,20 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private fun handleRegisterFido2CredentialRequestReceive(
|
||||
action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive,
|
||||
) {
|
||||
val relyingPartyId = action.request.providerRequest
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let { relyingPartyParser.parse(it) }
|
||||
?: run {
|
||||
showCredentialManagerErrorDialog(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val validateOriginResult = originManager
|
||||
.validateOrigin(
|
||||
relyingPartyId = relyingPartyId,
|
||||
callingAppInfo = action.request.callingAppInfo,
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
@ -1861,8 +1885,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
val relyingPartyId = request
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.mapNotNull { relyingPartyParser.parse(it) }
|
||||
.distinct()
|
||||
.firstOrNull()
|
||||
?: run {
|
||||
showCredentialManagerErrorDialog(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val validateOriginResult = originManager.validateOrigin(
|
||||
relyingPartyId = relyingPartyId,
|
||||
callingAppInfo = callingAppInfo,
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
|
||||
@ -51,7 +51,7 @@ class OriginManagerTest {
|
||||
every { digest(any()) } returns DEFAULT_APP_SIGNATURE.toByteArray()
|
||||
}
|
||||
|
||||
private val fido2OriginManager = OriginManagerImpl(
|
||||
private val originManager = OriginManagerImpl(
|
||||
assetManager = mockAssetManager,
|
||||
digitalAssetLinkService = mockDigitalAssetLinkService,
|
||||
privilegedAppRepository = mockPrivilegedAppRepository,
|
||||
@ -83,7 +83,8 @@ class OriginManagerTest {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
} returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
val result = originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_ORIGIN,
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
@ -106,7 +107,8 @@ class OriginManagerTest {
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
} returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
val result = originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_ORIGIN,
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
@ -133,7 +135,8 @@ class OriginManagerTest {
|
||||
mockPrivilegedAppRepository.getUserTrustedAllowListJson()
|
||||
} returns DEFAULT_ALLOW_LIST
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
val result = originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_ORIGIN,
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
@ -161,7 +164,8 @@ class OriginManagerTest {
|
||||
mockPrivilegedAppRepository.getUserTrustedAllowListJson()
|
||||
} returns FAIL_ALLOW_LIST
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
val result = originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_ORIGIN,
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
)
|
||||
|
||||
@ -181,13 +185,15 @@ class OriginManagerTest {
|
||||
runTest {
|
||||
coEvery {
|
||||
mockDigitalAssetLinkService.checkDigitalAssetLinksRelations(
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
certificateFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
relation = "delegate_permission/common.handle_all_urls",
|
||||
sourceWebSite = "https://$DEFAULT_RELYING_PARTY_ID",
|
||||
targetPackageName = DEFAULT_PACKAGE_NAME,
|
||||
targetCertificateFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
relations = listOf("delegate_permission/common.handle_all_urls"),
|
||||
)
|
||||
} returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
val result = originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockNonPrivilegedAppInfo,
|
||||
)
|
||||
|
||||
@ -203,9 +209,10 @@ class OriginManagerTest {
|
||||
runTest {
|
||||
coEvery {
|
||||
mockDigitalAssetLinkService.checkDigitalAssetLinksRelations(
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
certificateFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
relation = "delegate_permission/common.handle_all_urls",
|
||||
sourceWebSite = "https://$DEFAULT_RELYING_PARTY_ID",
|
||||
targetPackageName = DEFAULT_PACKAGE_NAME,
|
||||
targetCertificateFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
relations = listOf("delegate_permission/common.handle_all_urls"),
|
||||
)
|
||||
} returns DEFAULT_ASSET_LINKS_CHECK_RESPONSE
|
||||
.copy(linked = false)
|
||||
@ -213,7 +220,10 @@ class OriginManagerTest {
|
||||
|
||||
assertEquals(
|
||||
ValidateOriginResult.Error.PasskeyNotSupportedForApp,
|
||||
fido2OriginManager.validateOrigin(callingAppInfo = mockNonPrivilegedAppInfo),
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockNonPrivilegedAppInfo,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -223,15 +233,19 @@ class OriginManagerTest {
|
||||
runTest {
|
||||
coEvery {
|
||||
mockDigitalAssetLinkService.checkDigitalAssetLinksRelations(
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
certificateFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
relation = "delegate_permission/common.handle_all_urls",
|
||||
sourceWebSite = "https://$DEFAULT_RELYING_PARTY_ID",
|
||||
targetPackageName = DEFAULT_PACKAGE_NAME,
|
||||
targetCertificateFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
relations = listOf("delegate_permission/common.handle_all_urls"),
|
||||
)
|
||||
} returns RuntimeException().asFailure()
|
||||
|
||||
assertEquals(
|
||||
ValidateOriginResult.Error.AssetLinkNotFound,
|
||||
fido2OriginManager.validateOrigin(callingAppInfo = mockNonPrivilegedAppInfo),
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockNonPrivilegedAppInfo,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -247,7 +261,8 @@ class OriginManagerTest {
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
} returns FAIL_ALLOW_LIST.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
val result = originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_ORIGIN,
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
)
|
||||
assertEquals(
|
||||
@ -266,6 +281,7 @@ 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_ORIGIN = "bitwarden.com"
|
||||
private const val DEFAULT_RELYING_PARTY_ID = "www.bitwarden.com"
|
||||
private const val GOOGLE_ALLOW_LIST_FILENAME = "fido2_privileged_google.json"
|
||||
private const val COMMUNITY_ALLOW_LIST_FILENAME = "fido2_privileged_community.json"
|
||||
private const val DEFAULT_ALLOW_LIST = """
|
||||
|
||||
@ -0,0 +1,180 @@
|
||||
package com.x8bit.bitwarden.data.credentials.parser
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.core.di.CoreModule
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertNull
|
||||
|
||||
class RelyingPartyParserTest {
|
||||
|
||||
private val relyingPartyParser = RelyingPartyParserImpl(json = CoreModule.providesJson())
|
||||
|
||||
@Test
|
||||
fun `parse GetPublicKeyCredentialOption should return relyingPartyId`() {
|
||||
val result = relyingPartyParser.parse(
|
||||
mockk<GetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns DEFAULT_ASSERTION_OPTIONS_JSON
|
||||
},
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_RELYING_PARTY_ID,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse GetPublicKeyCredentialOption should return null if relyingPartyId is missing`() {
|
||||
val result = relyingPartyParser.parse(
|
||||
mockk<GetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns INVALID_ASSERTION_OPTIONS_JSON
|
||||
},
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse CreatePublicKeyCredentialRequest should return relyingPartyId`() {
|
||||
val result = relyingPartyParser.parse(
|
||||
mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns DEFAULT_ATTESTATION_OPTIONS_JSON
|
||||
},
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_RELYING_PARTY_ID,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse CreatePublicKeyCredentialRequest should return null if relyingPartyId is missing`() {
|
||||
val result = relyingPartyParser.parse(
|
||||
mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns INVALID_ATTESTATION_OPTIONS_JSON
|
||||
},
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse BeginGetPublicKeyCredentialOption should return relyingPartyId`() {
|
||||
val result = relyingPartyParser.parse(
|
||||
mockk<BeginGetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns DEFAULT_ASSERTION_OPTIONS_JSON
|
||||
},
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_RELYING_PARTY_ID,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `parse BeginGetPublicKeyCredentialOption should return null if relyingPartyId is missing`() {
|
||||
val result = relyingPartyParser.parse(
|
||||
mockk<BeginGetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns INVALID_ASSERTION_OPTIONS_JSON
|
||||
},
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_RELYING_PARTY_ID = "www.bitwarden.com"
|
||||
private val DEFAULT_ATTESTATION_OPTIONS_JSON = """
|
||||
{
|
||||
"attestation": "direct",
|
||||
"authenticatorSelection": {
|
||||
"residentKey": "required",
|
||||
"userVerification": "preferred"
|
||||
},
|
||||
"challenge": "tZ1rLJ_paLC8IMmg",
|
||||
"excludeCredentials": [],
|
||||
"extensions": {
|
||||
"credProps": true
|
||||
},
|
||||
"pubKeyCredParams": [
|
||||
{
|
||||
"alg": -7,
|
||||
"type": "public-key"
|
||||
},
|
||||
{
|
||||
"alg": -257,
|
||||
"type": "public-key"
|
||||
}
|
||||
],
|
||||
"rp": {
|
||||
"id": "$DEFAULT_RELYING_PARTY_ID",
|
||||
"name": "mockRpName"
|
||||
},
|
||||
"user": {
|
||||
"displayName": "mockDisplayName",
|
||||
"id": "UmhpTE9NOUY",
|
||||
"name": "mockUserName"
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
private val INVALID_ATTESTATION_OPTIONS_JSON = """
|
||||
{
|
||||
"attestation": "direct",
|
||||
"authenticatorSelection": {
|
||||
"residentKey": "required",
|
||||
"userVerification": "preferred"
|
||||
},
|
||||
"challenge": "tZ1rLJ_paLC8IMmg",
|
||||
"excludeCredentials": [],
|
||||
"extensions": {
|
||||
"credProps": true
|
||||
},
|
||||
"pubKeyCredParams": [
|
||||
{
|
||||
"alg": -7,
|
||||
"type": "public-key"
|
||||
},
|
||||
{
|
||||
"alg": -257,
|
||||
"type": "public-key"
|
||||
}
|
||||
],
|
||||
"rp": {
|
||||
"name": "mockRpName"
|
||||
},
|
||||
"user": {
|
||||
"displayName": "mockDisplayName",
|
||||
"id": "UmhpTE9NOUY",
|
||||
"name": "mockUserName"
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
private val DEFAULT_ASSERTION_OPTIONS_JSON = """
|
||||
{
|
||||
"challenge": "FFeZc7g-BPSAPo",
|
||||
"allowCredentials": [],
|
||||
"timeout": 60000,
|
||||
"userVerification": "preferred",
|
||||
"rpId": "$DEFAULT_RELYING_PARTY_ID"
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
private val INVALID_ASSERTION_OPTIONS_JSON = """
|
||||
{
|
||||
"challenge": "FFeZc7g-BPSAPo",
|
||||
"allowCredentials": [],
|
||||
"timeout": 60000,
|
||||
"userVerification": "preferred",
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
@ -56,6 +56,7 @@ import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.createMockCreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.createMockGetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.parser.RelyingPartyParser
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
@ -151,7 +152,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk {
|
||||
every { setText(text = any<String>(), toastDescriptorOverride = any<Text>()) } just runs
|
||||
}
|
||||
@ -212,7 +212,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
coEvery { getCredentialEntries(any()) } returns emptyList<CredentialEntry>().asSuccess()
|
||||
}
|
||||
private val originManager: OriginManager = mockk {
|
||||
coEvery { validateOrigin(any()) } returns ValidateOriginResult.Success(null)
|
||||
coEvery {
|
||||
validateOrigin(
|
||||
relyingPartyId = any(),
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
} returns ValidateOriginResult.Success(null)
|
||||
}
|
||||
|
||||
private val organizationEventManager = mockk<OrganizationEventManager> {
|
||||
@ -235,17 +240,26 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
every { packageName } returns "mockPackageName"
|
||||
every { isOriginPopulated() } returns false
|
||||
}
|
||||
private val mockGetPublicKeyCredentialOption = mockk<GetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns "mockRequestJson"
|
||||
}
|
||||
private val mockProviderGetCredentialRequest = mockk<ProviderGetCredentialRequest> {
|
||||
every { credentialOptions } returns listOf(mockk<GetPublicKeyCredentialOption>())
|
||||
every { credentialOptions } returns listOf(mockGetPublicKeyCredentialOption)
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
}
|
||||
private val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
|
||||
private val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns "mockRequestJson"
|
||||
}
|
||||
private val mockBeginGetCredentialRequest = mockk<BeginGetCredentialRequest> {
|
||||
every { beginGetCredentialOptions } returns listOf(mockBeginGetPublicKeyCredentialOption)
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
}
|
||||
val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
|
||||
every { callingRequest } returns mockk<CreatePublicKeyCredentialRequest>(relaxed = true)
|
||||
private val mockCreatePublicKeyCredentialRequest = mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns "mockRequestJson"
|
||||
every { origin } returns "mockOrigin"
|
||||
}
|
||||
private val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
|
||||
every { callingRequest } returns mockCreatePublicKeyCredentialRequest
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
}
|
||||
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
|
||||
@ -255,6 +269,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
getSnackbarDataFlow(relay = any(), relays = anyVararg())
|
||||
} returns mutableSnackbarDataFlow
|
||||
}
|
||||
private val relyingPartyParser = mockk<RelyingPartyParser> {
|
||||
every { parse(any<BeginGetPublicKeyCredentialOption>()) } returns DEFAULT_RELYING_PARTY_ID
|
||||
every { parse(any<GetPublicKeyCredentialOption>()) } returns DEFAULT_RELYING_PARTY_ID
|
||||
every { parse(any<CreatePublicKeyCredentialRequest>()) } returns DEFAULT_RELYING_PARTY_ID
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
@ -309,7 +328,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createCredentialRequest = createCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(any())
|
||||
originManager.validateOrigin(any(), any())
|
||||
} returns ValidateOriginResult.Success(null)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
@ -1923,7 +1942,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Success(emptyList())
|
||||
coEvery {
|
||||
originManager.validateOrigin(any())
|
||||
originManager.validateOrigin(any(), any())
|
||||
} returns ValidateOriginResult.Success("")
|
||||
|
||||
mockFilteredCiphers = listOf(cipherView1)
|
||||
@ -1977,7 +1996,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(
|
||||
cipherViewList = listOf(cipherView1, cipherView2),
|
||||
)
|
||||
originManager.validateOrigin(any())
|
||||
originManager.validateOrigin(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2633,13 +2652,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Success("mockOrigin")
|
||||
|
||||
createVaultItemListingViewModel()
|
||||
|
||||
coVerify(ordering = Ordering.ORDERED) {
|
||||
originManager.validateOrigin(any())
|
||||
originManager.validateOrigin(any(), any())
|
||||
vaultRepository.vaultDataStateFlow
|
||||
}
|
||||
}
|
||||
@ -2651,7 +2673,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Error.Unknown
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@ -2674,7 +2699,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createCredentialRequest = createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@ -2698,7 +2726,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createCredentialRequest = createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@ -2721,7 +2752,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createCredentialRequest = createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@ -2744,7 +2778,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createCredentialRequest = createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Error.AssetLinkNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@ -2918,7 +2955,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
bitwardenCredentialManager.getCredentialEntries(any())
|
||||
} returns emptyList<PublicKeyCredentialEntry>().asSuccess()
|
||||
coEvery {
|
||||
originManager.validateOrigin(callingAppInfo = any())
|
||||
originManager.validateOrigin(relyingPartyId = any(), callingAppInfo = any())
|
||||
} returns ValidateOriginResult.Success("mockOrigin")
|
||||
every {
|
||||
vaultRepository
|
||||
@ -2996,7 +3033,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.CredentialManagerOperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
message =
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
@ -3066,7 +3105,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(callingAppInfo = any())
|
||||
originManager.validateOrigin(relyingPartyId = any(), callingAppInfo = any())
|
||||
} returns ValidateOriginResult.Error.Unknown
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
@ -3273,6 +3312,59 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2Assertion should show error dialog when relying party cannot be identified`() =
|
||||
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 { bitwardenCredentialManager.isUserVerified } returns true
|
||||
every {
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
} returns listOf(mockCipherView)
|
||||
every {
|
||||
relyingPartyParser.parse(mockGetPublicKeyCredentialOption)
|
||||
} returns 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
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
originManager.validateOrigin(any(), any())
|
||||
}
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.CredentialManagerOperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message =
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
),
|
||||
awaitItem().dialogState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2AssertionRequest should show error dialog when validateOrigin is not Success`() =
|
||||
runTest {
|
||||
@ -3295,7 +3387,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
.data
|
||||
} returns listOf(mockCipherView)
|
||||
coEvery {
|
||||
originManager.validateOrigin(any())
|
||||
originManager.validateOrigin(any(), any())
|
||||
} returns ValidateOriginResult.Error.Unknown
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
@ -3851,7 +3943,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = mockAssertionRequest,
|
||||
)
|
||||
|
||||
every {
|
||||
ProviderGetCredentialRequest.fromBundle(any())
|
||||
} returns mockk(relaxed = true) {
|
||||
@ -4579,7 +4670,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
mockCallingAppInfo.getSignatureFingerprintAsHexString()
|
||||
} returns "mockSignature"
|
||||
coEvery {
|
||||
originManager.validateOrigin(any())
|
||||
originManager.validateOrigin(any(), any())
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@ -4650,7 +4741,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
mockCallingAppInfo.getSignatureFingerprintAsHexString()
|
||||
} returns "mockSignature"
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
coEvery {
|
||||
bitwardenCredentialManager.getCredentialEntries(any())
|
||||
@ -4770,7 +4864,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
cipherId = cipherView.id!!,
|
||||
),
|
||||
)
|
||||
|
||||
every {
|
||||
mockCallingAppInfo.getSignatureFingerprintAsHexString()
|
||||
} returns "mockSignature"
|
||||
@ -4792,7 +4885,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
every { bitwardenCredentialManager.isUserVerified } returns true
|
||||
coEvery {
|
||||
originManager.validateOrigin(mockCallingAppInfo)
|
||||
originManager.validateOrigin(
|
||||
relyingPartyId = DEFAULT_RELYING_PARTY_ID,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
} returns ValidateOriginResult.Success("mockOrigin")
|
||||
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
@ -5016,6 +5112,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
privilegedAppRepository = privilegedAppRepository,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
relyingPartyParser = relyingPartyParser,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@ -5069,3 +5166,5 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(DEFAULT_ACCOUNT),
|
||||
)
|
||||
|
||||
private const val DEFAULT_RELYING_PARTY_ID = "www.bitwarden.com"
|
||||
|
||||
@ -13,19 +13,22 @@ import retrofit2.http.Query
|
||||
internal interface DigitalAssetLinkApi {
|
||||
|
||||
/**
|
||||
* Checks if the given [relation] exists in a digital asset link file.
|
||||
* Checks if the given [relations] are declared in the digital asset link file for the given
|
||||
* [sourceWebSite] for the given [targetPackageName] with a [targetCertificateFingerprint].
|
||||
*
|
||||
* @param sourceWebSite The host of the source digital asset links file.
|
||||
* @param targetPackageName The package name of the target application.
|
||||
* @param targetCertificateFingerprint The certificate fingerprint of the target application.
|
||||
*/
|
||||
@GET("v1/assetlinks:check")
|
||||
suspend fun checkDigitalAssetLinksRelations(
|
||||
@Query("source.androidApp.packageName")
|
||||
sourcePackageName: String,
|
||||
@Query("source.androidApp.certificate.sha256Fingerprint")
|
||||
sourceCertificateFingerprint: String,
|
||||
@Query("source.web.site")
|
||||
sourceWebSite: String,
|
||||
@Query("target.androidApp.packageName")
|
||||
targetPackageName: String,
|
||||
@Query("target.androidApp.certificate.sha256Fingerprint")
|
||||
targetCertificateFingerprint: String,
|
||||
@Query("relation")
|
||||
relation: String,
|
||||
relations: List<String>,
|
||||
): NetworkResult<DigitalAssetLinkCheckResponseJson>
|
||||
}
|
||||
|
||||
@ -7,12 +7,17 @@ import com.bitwarden.network.model.DigitalAssetLinkCheckResponseJson
|
||||
*/
|
||||
interface DigitalAssetLinkService {
|
||||
/**
|
||||
* Checks if the given [packageName] with a given [certificateFingerprint] has the given
|
||||
* [relation].
|
||||
* Checks if the given [relations] are declared in the digital asset link file for the given
|
||||
* [sourceWebSite] for the given [targetPackageName] with a [targetCertificateFingerprint].
|
||||
*
|
||||
* @param sourceWebSite The host of the source digital asset links file.
|
||||
* @param targetPackageName The package name of the target application.
|
||||
* @param targetCertificateFingerprint The certificate fingerprint of the target application.
|
||||
*/
|
||||
suspend fun checkDigitalAssetLinksRelations(
|
||||
packageName: String,
|
||||
certificateFingerprint: String,
|
||||
relation: String,
|
||||
sourceWebSite: String,
|
||||
targetPackageName: String,
|
||||
targetCertificateFingerprint: String,
|
||||
relations: List<String>,
|
||||
): Result<DigitalAssetLinkCheckResponseJson>
|
||||
}
|
||||
|
||||
@ -12,16 +12,16 @@ internal class DigitalAssetLinkServiceImpl(
|
||||
) : DigitalAssetLinkService {
|
||||
|
||||
override suspend fun checkDigitalAssetLinksRelations(
|
||||
packageName: String,
|
||||
certificateFingerprint: String,
|
||||
relation: String,
|
||||
sourceWebSite: String,
|
||||
targetPackageName: String,
|
||||
targetCertificateFingerprint: String,
|
||||
relations: List<String>,
|
||||
): Result<DigitalAssetLinkCheckResponseJson> = digitalAssetLinkApi
|
||||
.checkDigitalAssetLinksRelations(
|
||||
sourcePackageName = packageName,
|
||||
sourceCertificateFingerprint = certificateFingerprint,
|
||||
targetPackageName = packageName,
|
||||
targetCertificateFingerprint = certificateFingerprint,
|
||||
relation = relation,
|
||||
sourceWebSite = sourceWebSite,
|
||||
targetPackageName = targetPackageName,
|
||||
targetCertificateFingerprint = targetCertificateFingerprint,
|
||||
relations = relations,
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@ -28,10 +28,11 @@ class DigitalAssetLinkServiceTest : BaseServiceTest() {
|
||||
)
|
||||
.asSuccess(),
|
||||
digitalAssetLinkService.checkDigitalAssetLinksRelations(
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
certificateFingerprint =
|
||||
sourceWebSite = "https://www.bitwarden.com",
|
||||
targetPackageName = "com.x8bit.bitwarden",
|
||||
targetCertificateFingerprint =
|
||||
"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",
|
||||
relations = listOf("delegate_permission/common.handle_all_urls"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -42,4 +43,5 @@ private val CHECK_DIGITAL_ASSET_LINKS_RELATIONS_SUCCESS_JSON = """
|
||||
"linked": true,
|
||||
"maxAge": "47.535162130s"
|
||||
}
|
||||
""".trimIndent()
|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user