mirror of
https://github.com/bitwarden/android.git
synced 2026-03-22 06:11:38 -05:00
[PM-15609] Move FIDO2 origin validation logic to Fido2OriginManager (#4426)
This commit is contained in:
@@ -8,6 +8,8 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
@@ -58,17 +60,26 @@ object Fido2ProviderModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFido2CredentialManager(
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
fido2OriginManager: Fido2OriginManager,
|
||||
json: Json,
|
||||
): Fido2CredentialManager =
|
||||
Fido2CredentialManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFido2OriginManager(
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
): Fido2OriginManager =
|
||||
Fido2OriginManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
|
||||
@@ -26,14 +24,6 @@ interface Fido2CredentialManager {
|
||||
*/
|
||||
var authenticationAttempts: Int
|
||||
|
||||
/**
|
||||
* Attempt to validate the RP and origin of the provided [callingAppInfo] and [relyingPartyId].
|
||||
*/
|
||||
suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Attempt to extract FIDO 2 passkey attestation options from the system [requestJson], or null.
|
||||
*/
|
||||
|
||||
@@ -6,8 +6,6 @@ import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
@@ -15,12 +13,10 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResu
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
@@ -31,18 +27,14 @@ import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
|
||||
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class Fido2CredentialManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val fido2OriginManager: Fido2OriginManager,
|
||||
private val json: Json,
|
||||
) : Fido2CredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
@@ -108,16 +100,14 @@ class Fido2CredentialManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
private suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
|
||||
}
|
||||
}
|
||||
): Fido2ValidateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
override fun getPasskeyAttestationOptionsOrNull(
|
||||
requestJson: String,
|
||||
@@ -168,7 +158,7 @@ class Fido2CredentialManagerImpl(
|
||||
Fido2CredentialAssertionResult.Error
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
@@ -200,127 +190,6 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
return digitalAssetLinkService
|
||||
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
.onFailure {
|
||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
}
|
||||
.map { statements ->
|
||||
statements
|
||||
.filterMatchingAppStatementsOrNull(
|
||||
rpPackageName = callingAppInfo.packageName,
|
||||
)
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
}
|
||||
.map { matchingStatements ->
|
||||
callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?.let { certificateFingerprint ->
|
||||
matchingStatements
|
||||
.filterMatchingAppSignaturesOrNull(
|
||||
signature = certificateFingerprint,
|
||||
)
|
||||
}
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotVerified
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
Fido2ValidateOriginResult.Success
|
||||
},
|
||||
onFailure = {
|
||||
Fido2ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun validatePrivilegedAppOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult {
|
||||
val googleAllowListResult =
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
return when (googleAllowListResult) {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
// Application was found and successfully validated against the Google allow list so
|
||||
// we can return the result as the final validation result.
|
||||
googleAllowListResult
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
// Check the community allow list if the Google allow list failed, and return the
|
||||
// result as the final validation result.
|
||||
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
fileName: String,
|
||||
): Fido2ValidateOriginResult =
|
||||
assetManager
|
||||
.readAsset(fileName)
|
||||
.map { allowList ->
|
||||
callingAppInfo.validatePrivilegedApp(
|
||||
allowList = allowList,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { 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.get_login_creds",
|
||||
"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() }
|
||||
|
||||
override fun hasAuthenticationAttemptsRemaining(): Boolean =
|
||||
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Responsible for managing FIDO2 origin validation.
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* Returns the privileged app origin, or null if the calling app is not allowed.
|
||||
*
|
||||
* @param callingAppInfo The calling app info.
|
||||
*
|
||||
* @return The privileged app origin, or null.
|
||||
*/
|
||||
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
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"
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2OriginManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class Fido2OriginManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
) : Fido2OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
|
||||
if (!callingAppInfo.isOriginPopulated()) return null
|
||||
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
|
||||
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
|
||||
?.takeUnless { !callingAppInfo.isOriginPopulated() }
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppOrigin(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult {
|
||||
val googleAllowListResult =
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
return when (googleAllowListResult) {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
// Application was found and successfully validated against the Google allow list so
|
||||
// we can return the result as the final validation result.
|
||||
googleAllowListResult
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
// Check the community allow list if the Google allow list failed, and return the
|
||||
// result as the final validation result.
|
||||
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
): Fido2ValidateOriginResult =
|
||||
validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo = callingAppInfo,
|
||||
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
|
||||
)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithAllowList(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
fileName: String,
|
||||
): Fido2ValidateOriginResult =
|
||||
assetManager
|
||||
.readAsset(fileName)
|
||||
.mapCatching { allowList ->
|
||||
callingAppInfo.validatePrivilegedApp(
|
||||
allowList = allowList,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { 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.get_login_creds",
|
||||
"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() }
|
||||
|
||||
private suspend fun getGoogleAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
|
||||
.getOrNull()
|
||||
|
||||
private suspend fun getCommunityAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
|
||||
.getOrNull()
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Models the result of validating the origin of a FIDO2 request.
|
||||
*/
|
||||
@@ -7,49 +10,75 @@ sealed class Fido2ValidateOriginResult {
|
||||
|
||||
/**
|
||||
* Represents a successful origin validation.
|
||||
*
|
||||
* @param origin The origin of the calling app, or null if the calling app is not privileged.
|
||||
*/
|
||||
data object Success : Fido2ValidateOriginResult()
|
||||
data class Success(val origin: String?) : Fido2ValidateOriginResult()
|
||||
|
||||
/**
|
||||
* Represents a validation error.
|
||||
*/
|
||||
sealed class Error : Fido2ValidateOriginResult() {
|
||||
/**
|
||||
* The string resource ID of the error message.
|
||||
*/
|
||||
@get:StringRes
|
||||
abstract val messageResId: Int
|
||||
|
||||
/**
|
||||
* Indicates the digital asset links file could not be located.
|
||||
*/
|
||||
data object AssetLinkNotFound : Error()
|
||||
data object AssetLinkNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_of_missing_asset_links
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the application package name was not found in the digital asset links file.
|
||||
*/
|
||||
data object ApplicationNotFound : Error()
|
||||
data object ApplicationNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the application fingerprint was not found the digital asset links file.
|
||||
*/
|
||||
data object ApplicationNotVerified : Error()
|
||||
data object ApplicationFingerprintNotVerified : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the calling application is privileged but its package name is not found within
|
||||
* the privileged app allow list.
|
||||
*/
|
||||
data object PrivilegedAppNotAllowed : Error()
|
||||
data object PrivilegedAppNotAllowed : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_browser_is_not_privileged
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the calling app is privileged but but no matching signing certificate signature
|
||||
* is present in the allow list.
|
||||
*/
|
||||
data object PrivilegedAppSignatureNotFound : Error()
|
||||
data object PrivilegedAppSignatureNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_browser_signature_does_not_match
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates passkeys are not supported for the requesting application.
|
||||
*/
|
||||
data object PasskeyNotSupportedForApp : Error()
|
||||
data object PasskeyNotSupportedForApp : Error() {
|
||||
override val messageResId = R.string.passkeys_not_supported_for_this_app
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates an unknown error was encountered while validating the origin.
|
||||
*/
|
||||
data object Unknown : Error()
|
||||
data object Unknown : Error() {
|
||||
override val messageResId = R.string.generic_error_message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,16 +41,17 @@ fun CallingAppInfo.validatePrivilegedApp(allowList: String): Fido2ValidateOrigin
|
||||
}
|
||||
|
||||
return try {
|
||||
if (getOrigin(allowList) != null) {
|
||||
Fido2ValidateOriginResult.Success
|
||||
} else {
|
||||
val origin = getOrigin(allowList)
|
||||
if (origin.isNullOrEmpty()) {
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
} else {
|
||||
Fido2ValidateOriginResult.Success(origin)
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
} catch (_: IllegalStateException) {
|
||||
// We know the package name is in the allow list so we can infer that this exception is
|
||||
// thrown because no matching signature is found.
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// The allow list is not formatted correctly so we notify the user passkeys are not
|
||||
// supported for this application
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||
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.Fido2CredentialAssertionResult
|
||||
@@ -106,6 +107,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val fido2OriginManager: Fido2OriginManager,
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
|
||||
initialState = run {
|
||||
@@ -727,7 +729,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val validateOriginResult = fido2CredentialManager
|
||||
val validateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
@@ -737,7 +739,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
handleFido2OriginValidationFail(validateOriginResult)
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2AssertionResultReceive(
|
||||
result = fido2CredentialManager.authenticateFido2Credential(
|
||||
@@ -1384,7 +1386,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
showFido2ErrorDialog()
|
||||
return@launch
|
||||
}
|
||||
val validateOriginResult = fido2CredentialManager
|
||||
val validateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = action.request.callingAppInfo,
|
||||
relyingPartyId = options.relyingParty.id,
|
||||
@@ -1394,7 +1396,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
handleFido2OriginValidationFail(validateOriginResult)
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
observeVaultData()
|
||||
}
|
||||
}
|
||||
@@ -1427,7 +1429,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotVerified -> {
|
||||
Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified -> {
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -121,6 +122,9 @@ 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()
|
||||
|
||||
@BeforeEach
|
||||
@@ -614,11 +618,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(
|
||||
fido2OriginManager.validateOrigin(
|
||||
fido2CreateCredentialRequest.callingAppInfo,
|
||||
fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
@@ -668,11 +672,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(
|
||||
fido2OriginManager.validateOrigin(
|
||||
fido2CreateCredentialRequest.callingAppInfo,
|
||||
fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
@@ -698,11 +702,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(
|
||||
fido2OriginManager.validateOrigin(
|
||||
fido2CreateCredentialRequest.callingAppInfo,
|
||||
fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
|
||||
@@ -3,16 +3,12 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
import android.content.pm.Signature
|
||||
import android.content.pm.SigningInfo
|
||||
import android.util.Base64
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
|
||||
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.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2PublicKeyCredential
|
||||
@@ -21,8 +17,6 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
@@ -34,7 +28,6 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPublicKeyAt
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||
import io.mockk.Ordering
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
@@ -61,13 +54,8 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
private lateinit var fido2CredentialManager: Fido2CredentialManager
|
||||
|
||||
private val assetManager: AssetManager = mockk {
|
||||
coEvery { readAsset(any()) } returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||
}
|
||||
private val digitalAssetLinkService = mockk<DigitalAssetLinkService> {
|
||||
coEvery {
|
||||
getDigitalAssetLinkForRp(relyingParty = any())
|
||||
} returns DEFAULT_STATEMENT_LIST.asSuccess()
|
||||
private val fido2OriginManager = mockk<Fido2OriginManager> {
|
||||
coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success(null)
|
||||
}
|
||||
private val json = mockk<Json> {
|
||||
every {
|
||||
@@ -80,28 +68,10 @@ class Fido2CredentialManagerTest {
|
||||
decodeFromStringOrNull<PasskeyAssertionOptions>(DEFAULT_FIDO2_AUTH_REQUEST_JSON)
|
||||
} returns createMockPasskeyAssertionOptions(number = 1)
|
||||
}
|
||||
private val mockPrivilegedCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { packageName } returns DEFAULT_PACKAGE_NAME
|
||||
every { isOriginPopulated() } returns true
|
||||
every { getOrigin(any()) } returns DEFAULT_PACKAGE_NAME
|
||||
}
|
||||
private val mockPrivilegedAppRequest = mockk<Fido2CreateCredentialRequest> {
|
||||
every { callingAppInfo } returns mockPrivilegedCallingAppInfo
|
||||
every { requestJson } returns "{}"
|
||||
}
|
||||
private val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature("0987654321ABCDEF"))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
private val mockUnprivilegedCallingAppInfo = CallingAppInfo(
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
signingInfo = mockSigningInfo,
|
||||
origin = null,
|
||||
)
|
||||
private val mockUnprivilegedAppRequest = mockk<Fido2CreateCredentialRequest> {
|
||||
every { callingAppInfo } returns mockUnprivilegedCallingAppInfo
|
||||
every { requestJson } returns "{}"
|
||||
}
|
||||
private val mockMessageDigest = mockk<MessageDigest> {
|
||||
every { digest(any()) } returns DEFAULT_APP_SIGNATURE.toByteArray()
|
||||
}
|
||||
@@ -114,10 +84,9 @@ class Fido2CredentialManagerTest {
|
||||
every { MessageDigest.getInstance(any()) } returns mockMessageDigest
|
||||
|
||||
fido2CredentialManager = Fido2CredentialManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
vaultSdkSource = mockVaultSdkSource,
|
||||
fido2CredentialStore = mockFido2CredentialStore,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
json = json,
|
||||
)
|
||||
}
|
||||
@@ -130,201 +99,6 @@ class Fido2CredentialManagerTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should load allow list when origin is populated`() =
|
||||
runTest {
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
assetManager.readAsset(
|
||||
fileName = GOOGLE_ALLOW_LIST_FILENAME,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should validate with community allow list when google allow list validation fails`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
} returns MISSING_PACKAGE_ALLOW_LIST.asSuccess()
|
||||
every {
|
||||
mockPrivilegedCallingAppInfo.getOrigin(
|
||||
privilegedAllowlist = MISSING_PACKAGE_ALLOW_LIST,
|
||||
)
|
||||
} throws IllegalStateException()
|
||||
coEvery {
|
||||
assetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
} returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||
every {
|
||||
mockPrivilegedCallingAppInfo.getOrigin(
|
||||
privilegedAllowlist = DEFAULT_ALLOW_LIST,
|
||||
)
|
||||
} returns DEFAULT_PACKAGE_NAME
|
||||
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
)
|
||||
|
||||
coVerify(ordering = Ordering.ORDERED) {
|
||||
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
assetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return Success when privileged app is allowed`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return PrivilegedAppSignatureNotFound when privileged app signature is not found in allow list`() =
|
||||
runTest {
|
||||
every { mockPrivilegedCallingAppInfo.getOrigin(any()) } throws IllegalStateException()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return PrivilegedAppNotAllowed when privileged app package name is not found in allow list`() =
|
||||
runTest {
|
||||
coEvery { assetManager.readAsset(any()) } returns MISSING_PACKAGE_ALLOW_LIST.asSuccess()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedCallingAppInfo, "{}"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return error when allow list is unreadable`() = runTest {
|
||||
coEvery { assetManager.readAsset(any()) } returns IllegalStateException().asFailure()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.Unknown,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockPrivilegedAppRequest.callingAppInfo,
|
||||
mockPrivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return PasskeyNotSupportedForApp when allow list is invalid`() =
|
||||
runTest {
|
||||
every {
|
||||
mockPrivilegedCallingAppInfo.getOrigin(any())
|
||||
} throws IllegalArgumentException()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp,
|
||||
fido2CredentialManager.validateOrigin(mockPrivilegedCallingAppInfo, "{}"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return success when asset links contains matching statement`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return error when asset links are unavailable`() = runTest {
|
||||
coEvery {
|
||||
digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = any())
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
assertEquals(
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
Fido2ValidateOriginResult.Error.AssetLinkNotFound,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return error when asset links does not contain package name`() =
|
||||
runTest {
|
||||
every { mockUnprivilegedAppRequest.callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "its.a.trap",
|
||||
signingInfo = mockSigningInfo,
|
||||
origin = null,
|
||||
)
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotFound,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return error when asset links does not contain android app namespace`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = any())
|
||||
} returns listOf(
|
||||
DEFAULT_STATEMENT.copy(
|
||||
target = DEFAULT_STATEMENT.target.copy(
|
||||
namespace = "its_a_trap",
|
||||
),
|
||||
),
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotFound,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validateOrigin should return error when asset links certificate hash no match`() =
|
||||
runTest {
|
||||
every {
|
||||
mockMessageDigest.digest(any())
|
||||
} returns "ITSATRAP".toByteArray()
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotVerified,
|
||||
fido2CredentialManager.validateOrigin(
|
||||
mockUnprivilegedAppRequest.callingAppInfo,
|
||||
mockUnprivilegedAppRequest.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasskeyAttestationOptionsOrNull should return passkey options when deserialized`() =
|
||||
runTest {
|
||||
@@ -967,7 +741,6 @@ class Fido2CredentialManagerTest {
|
||||
)
|
||||
|
||||
coVerify {
|
||||
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
@@ -1021,8 +794,8 @@ class Fido2CredentialManagerTest {
|
||||
val mockSelectedCipherView = createMockCipherView(number = 1)
|
||||
|
||||
coEvery {
|
||||
digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = "mockRelyingPartyId-1")
|
||||
} returns IllegalStateException().asFailure()
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
@@ -1053,22 +826,6 @@ private val DEFAULT_ORIGIN = Origin.Android(
|
||||
assetLinkUrl = "bitwarden.com",
|
||||
),
|
||||
)
|
||||
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 const val GOOGLE_ALLOW_LIST_FILENAME = "fido2_privileged_google.json"
|
||||
private const val COMMUNITY_ALLOW_LIST_FILENAME = "fido2_privileged_community.json"
|
||||
private val DEFAULT_STATEMENT_LIST = listOf(DEFAULT_STATEMENT)
|
||||
private const val DEFAULT_ALLOW_LIST = """
|
||||
{
|
||||
"apps": [
|
||||
@@ -1091,28 +848,6 @@ private const val DEFAULT_ALLOW_LIST = """
|
||||
]
|
||||
}
|
||||
"""
|
||||
private const val MISSING_PACKAGE_ALLOW_LIST = """
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.android.chrome",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
private const val DEFAULT_FIDO2_AUTH_REQUEST_JSON = """
|
||||
{
|
||||
"allowCredentials": [
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import android.content.pm.Signature
|
||||
import android.util.Base64
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.security.MessageDigest
|
||||
|
||||
class Fido2OriginManagerTest {
|
||||
|
||||
private val mockAssetManager = mockk<AssetManager>()
|
||||
private val mockDigitalAssetLinkService = mockk<DigitalAssetLinkService>()
|
||||
private val mockPrivilegedAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns true
|
||||
every { packageName } returns DEFAULT_PACKAGE_NAME
|
||||
every { getOrigin(any()) } returns DEFAULT_ORIGIN
|
||||
}
|
||||
private val mockNonPrivilegedAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns false
|
||||
every { packageName } returns DEFAULT_PACKAGE_NAME
|
||||
every { getOrigin(any()) } returns null
|
||||
every { signingInfo } returns mockk {
|
||||
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
}
|
||||
private val mockMessageDigest = mockk<MessageDigest> {
|
||||
every { digest(any()) } returns DEFAULT_APP_SIGNATURE.toByteArray()
|
||||
}
|
||||
|
||||
private val fido2OriginManager = Fido2OriginManagerImpl(
|
||||
assetManager = mockAssetManager,
|
||||
digitalAssetLinkService = mockDigitalAssetLinkService,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(
|
||||
MessageDigest::class,
|
||||
Base64::class,
|
||||
)
|
||||
every { MessageDigest.getInstance(any()) } returns mockMessageDigest
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
MessageDigest::class,
|
||||
Base64::class,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return Success when calling app is Privileged and is in Google allow list`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
} returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
relyingPartyId = "relyingPartyId",
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
}
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success(DEFAULT_ORIGIN),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return Success when calling app is Privileged and is in the Community allow list but not the Google allow list`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
} returns FAIL_ALLOW_LIST.asSuccess()
|
||||
coEvery {
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
} returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
relyingPartyId = "relyingPartyId",
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
}
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success(DEFAULT_ORIGIN),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return ApplicationNotFound when calling app is Privileged but not in either allow list`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
} returns FAIL_ALLOW_LIST.asSuccess()
|
||||
coEvery {
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
} returns FAIL_ALLOW_LIST.asSuccess()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
relyingPartyId = "relyingPartyId",
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
mockAssetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
}
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
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()
|
||||
|
||||
val result = fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = mockNonPrivilegedAppInfo,
|
||||
relyingPartyId = DEFAULT_RP_ID,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success(null),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return ApplicationFingerprintNotVerified 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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
.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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `validateOrigin should return AssetLinkNotFound when calling app is NonPrivileged and asset link does not exist`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockDigitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = DEFAULT_RP_ID)
|
||||
} 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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()
|
||||
coEvery {
|
||||
mockAssetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||
} returns IllegalStateException().asFailure()
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Error.Unknown,
|
||||
fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = mockPrivilegedAppInfo,
|
||||
relyingPartyId = DEFAULT_RP_ID,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
private const val DEFAULT_ALLOW_LIST = """
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.x8bit.bitwarden",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
private const val FAIL_ALLOW_LIST = """
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.not.bitwarden",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -120,7 +120,7 @@ class CallingAppInfoExtensionsTest {
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Fido2ValidateOriginResult.Success,
|
||||
Fido2ValidateOriginResult.Success("origin"),
|
||||
mockAppInfo.validatePrivilegedApp(
|
||||
allowList = DEFAULT_ALLOW_LIST,
|
||||
),
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
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.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
@@ -165,7 +166,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()
|
||||
}
|
||||
private val fido2CredentialManager: Fido2CredentialManager = mockk {
|
||||
coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success
|
||||
every { isUserVerified } returns false
|
||||
every { isUserVerified = any() } just runs
|
||||
every { authenticationAttempts } returns 0
|
||||
@@ -173,6 +173,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
every { hasAuthenticationAttemptsRemaining() } returns true
|
||||
every { getPasskeyAttestationOptionsOrNull(any()) } returns mockk(relaxed = true)
|
||||
}
|
||||
private val fido2OriginManager: Fido2OriginManager = mockk {
|
||||
coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success(null)
|
||||
}
|
||||
|
||||
private val organizationEventManager = mockk<OrganizationEventManager> {
|
||||
every { trackEvent(event = any()) } just runs
|
||||
@@ -1562,8 +1565,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Success(emptyList())
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Success
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Success("")
|
||||
|
||||
mockFilteredCiphers = listOf(cipherView1)
|
||||
|
||||
@@ -1618,7 +1621,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(
|
||||
cipherViewList = listOf(cipherView1, cipherView2),
|
||||
)
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2163,7 +2166,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
createVaultItemListingViewModel()
|
||||
|
||||
coVerify(ordering = Ordering.ORDERED) {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
vaultRepository.vaultDataStateFlow
|
||||
}
|
||||
}
|
||||
@@ -2181,7 +2184,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -2212,7 +2215,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -2243,7 +2246,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -2274,7 +2277,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -2305,7 +2308,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -2336,7 +2339,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -2367,8 +2370,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.ApplicationNotVerified
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
@@ -2790,7 +2793,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns mockAssertionOptions
|
||||
coEvery {
|
||||
fido2CredentialManager.validateOrigin(any(), any())
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -4054,6 +4057,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
policyManager = policyManager,
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
organizationEventManager = organizationEventManager,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
||||
Reference in New Issue
Block a user