mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
Use Google's Digital Asset Links API to verify digital asset links (#5101)
This commit is contained in:
parent
240bca3c2f
commit
fe1fe770c7
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<DigitalAssetLinkResponseJson>.filterMatchingAppStatementsOrNull(
|
||||
rpPackageName: String,
|
||||
): List<DigitalAssetLinkResponseJson>? =
|
||||
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<DigitalAssetLinkResponseJson>.filterMatchingAppSignaturesOrNull(
|
||||
signature: String,
|
||||
): List<DigitalAssetLinkResponseJson>? =
|
||||
filter { statement ->
|
||||
statement.target.sha256CertFingerprints
|
||||
?.contains(signature)
|
||||
?: false
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 -> {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<Fido2OriginManager> {
|
||||
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(
|
||||
|
||||
@ -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`() =
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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<OrganizationEventManager> {
|
||||
@ -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<CreatePublicKeyCredentialRequest>(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<CreatePublicKeyCredentialRequest>(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(
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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<List<DigitalAssetLinkResponseJson>>
|
||||
@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<DigitalAssetLinkCheckResponseJson>
|
||||
}
|
||||
|
||||
@ -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?,
|
||||
)
|
||||
@ -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<String>,
|
||||
|
||||
@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<String>?,
|
||||
)
|
||||
}
|
||||
@ -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<List<DigitalAssetLinkResponseJson>>
|
||||
suspend fun checkDigitalAssetLinksRelations(
|
||||
packageName: String,
|
||||
certificateFingerprint: String,
|
||||
relation: String,
|
||||
): Result<DigitalAssetLinkCheckResponseJson>
|
||||
}
|
||||
|
||||
@ -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<List<DigitalAssetLinkResponseJson>> =
|
||||
digitalAssetLinkApi
|
||||
.getDigitalAssetLinks(
|
||||
url = "$scheme$relyingParty/.well-known/assetlinks.json",
|
||||
)
|
||||
.toResult()
|
||||
override suspend fun checkDigitalAssetLinksRelations(
|
||||
packageName: String,
|
||||
certificateFingerprint: String,
|
||||
relation: String,
|
||||
): Result<DigitalAssetLinkCheckResponseJson> = digitalAssetLinkApi
|
||||
.checkDigitalAssetLinksRelations(
|
||||
sourcePackageName = packageName,
|
||||
sourceCertificateFingerprint = certificateFingerprint,
|
||||
targetPackageName = packageName,
|
||||
targetCertificateFingerprint = certificateFingerprint,
|
||||
relation = relation,
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user