mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
[PM-21567] Implement CredentialEntryBuilder interface (#5177)
This commit is contained in:
parent
860a2e265f
commit
7f4e65d7e4
@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.credentials.builder
|
||||
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
|
||||
/**
|
||||
* Builder for credential entries.
|
||||
*/
|
||||
interface CredentialEntryBuilder {
|
||||
|
||||
/**
|
||||
* Build public key credential entries from the given cipher views and options.
|
||||
*/
|
||||
fun buildPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
|
||||
beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
|
||||
isUserVerified: Boolean,
|
||||
): List<PublicKeyCredentialEntry>
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.x8bit.bitwarden.data.credentials.builder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Primary implementation of [CredentialEntryBuilder].
|
||||
*/
|
||||
class CredentialEntryBuilderImpl(
|
||||
private val context: Context,
|
||||
private val intentManager: IntentManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
) : CredentialEntryBuilder {
|
||||
|
||||
override fun buildPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
|
||||
beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
|
||||
isUserVerified: Boolean,
|
||||
): List<PublicKeyCredentialEntry> = beginGetPublicKeyCredentialOptions
|
||||
.flatMap { option ->
|
||||
fido2CredentialAutofillViews
|
||||
.toPublicKeyCredentialEntryList(
|
||||
userId = userId,
|
||||
option = option,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
|
||||
userId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
isUserVerified: Boolean,
|
||||
): List<PublicKeyCredentialEntry> = this
|
||||
.map { fido2AutofillView ->
|
||||
PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = fido2AutofillView.userNameForUi
|
||||
?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = fido2AutofillView.credentialId.toString(),
|
||||
cipherId = fido2AutofillView.cipherId,
|
||||
isUserVerified = isUserVerified,
|
||||
requestCode = Random.nextInt(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.setIcon(
|
||||
getCredentialEntryIcon(
|
||||
isPasskey = true,
|
||||
),
|
||||
)
|
||||
.also { builder ->
|
||||
if (!isUserVerified) {
|
||||
builder.setBiometricPromptDataIfSupported(
|
||||
cipher = biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId),
|
||||
isSingleTapAuthEnabled = featureFlagManager
|
||||
.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
// TODO: [PM-20176] Enable web icons in credential entries
|
||||
// Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
|
||||
// are addressed. See https://issuetracker.google.com/issues/355141766 for details.
|
||||
private fun getCredentialEntryIcon(isPasskey: Boolean): Icon = IconCompat
|
||||
.createWithResource(
|
||||
context,
|
||||
if (isPasskey) {
|
||||
R.drawable.ic_bw_passkey
|
||||
} else {
|
||||
R.drawable.ic_globe
|
||||
},
|
||||
)
|
||||
.toIcon(context)
|
||||
}
|
||||
@ -7,6 +7,8 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilderImpl
|
||||
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManagerImpl
|
||||
import com.x8bit.bitwarden.data.credentials.manager.OriginManager
|
||||
@ -16,7 +18,6 @@ import com.x8bit.bitwarden.data.credentials.processor.CredentialProviderProcesso
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@ -63,28 +64,20 @@ object CredentialProviderModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBitwardenCredentialManager(
|
||||
@ApplicationContext context: Context,
|
||||
intentManager: IntentManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
json: Json,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
dispatcherManager: DispatcherManager,
|
||||
credentialEntryBuilder: CredentialEntryBuilder,
|
||||
): BitwardenCredentialManager =
|
||||
BitwardenCredentialManagerImpl(
|
||||
context = context,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
intentManager = intentManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
json = json,
|
||||
environmentRepository = environmentRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
dispatcherManager = dispatcherManager,
|
||||
credentialEntryBuilder = credentialEntryBuilder,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ -97,4 +90,18 @@ object CredentialProviderModule {
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCredentialEntryBuilder(
|
||||
@ApplicationContext context: Context,
|
||||
intentManager: IntentManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
): CredentialEntryBuilder = CredentialEntryBuilderImpl(
|
||||
context = context,
|
||||
intentManager = intentManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@ package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
|
||||
|
||||
@ -91,10 +91,9 @@ interface BitwardenCredentialManager {
|
||||
|
||||
/**
|
||||
* Retrieve a list of [CredentialEntry] objects representing vault items matching the given
|
||||
* request [options].
|
||||
* [getCredentialsRequest].
|
||||
*/
|
||||
suspend fun getCredentialEntries(
|
||||
userId: String,
|
||||
options: List<BeginGetCredentialOption>,
|
||||
getCredentialsRequest: GetCredentialsRequest,
|
||||
): Result<List<CredentialEntry>>
|
||||
}
|
||||
|
||||
@ -1,52 +1,34 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.takeUntilLoaded
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.util.baseIconUrl
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
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.isBuildVersionBelow
|
||||
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
|
||||
@ -55,32 +37,22 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKe
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.fold
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.crypto.Cipher
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Primary implementation of [BitwardenCredentialManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
class BitwardenCredentialManagerImpl(
|
||||
private val context: Context,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val intentManager: IntentManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val credentialEntryBuilder: CredentialEntryBuilder,
|
||||
private val json: Json,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : BitwardenCredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
@ -194,8 +166,7 @@ class BitwardenCredentialManagerImpl(
|
||||
?: fallbackRequirement
|
||||
|
||||
override suspend fun getCredentialEntries(
|
||||
userId: String,
|
||||
options: List<BeginGetCredentialOption>,
|
||||
getCredentialsRequest: GetCredentialsRequest,
|
||||
): Result<List<CredentialEntry>> = withContext(ioScope.coroutineContext) {
|
||||
val cipherViews = vaultRepository
|
||||
.ciphersStateFlow
|
||||
@ -209,33 +180,18 @@ class BitwardenCredentialManagerImpl(
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
.ifEmpty {
|
||||
return@withContext emptyList<CredentialEntry>().asSuccess()
|
||||
}
|
||||
|
||||
val publicKeyCredentialOptions = options
|
||||
.filterIsInstance<BeginGetPublicKeyCredentialOption>()
|
||||
.ifEmpty { return@withContext emptyList<CredentialEntry>().asSuccess() }
|
||||
|
||||
when (
|
||||
val decryptResult =
|
||||
vaultRepository.getDecryptedFido2CredentialAutofillViews(cipherViews)
|
||||
) {
|
||||
is DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
GetCredentialUnknownException(
|
||||
"Error decrypting user's FIDO 2 credentials.",
|
||||
)
|
||||
.asFailure()
|
||||
}
|
||||
|
||||
is DecryptFido2CredentialAutofillViewResult.Success -> {
|
||||
publicKeyCredentialOptions.toPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
cipherViews = cipherViews,
|
||||
fido2CredentialAutofillViews = decryptResult.fido2CredentialAutofillViews,
|
||||
)
|
||||
}
|
||||
}
|
||||
getCredentialsRequest
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.toPublicKeyCredentialEntries(
|
||||
userId = getCredentialsRequest.userId,
|
||||
cipherViewsWithPublicKeyCredentials = cipherViews,
|
||||
)
|
||||
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
|
||||
}
|
||||
|
||||
private fun getPasskeyAssertionOptionsOrNull(
|
||||
@ -244,157 +200,38 @@ class BitwardenCredentialManagerImpl(
|
||||
|
||||
private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
cipherViews: List<CipherView>,
|
||||
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
|
||||
): Result<List<PublicKeyCredentialEntry>> {
|
||||
val baseIconUrl = environmentRepository
|
||||
.environment
|
||||
.environmentUrlData
|
||||
.baseIconUrl
|
||||
cipherViewsWithPublicKeyCredentials: List<CipherView>,
|
||||
): Result<List<CredentialEntry>> {
|
||||
val relyingPartyIds = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
|
||||
.distinct()
|
||||
.ifEmpty {
|
||||
return GetCredentialUnknownException("Relying party id required.").asFailure()
|
||||
}
|
||||
|
||||
var options: PasskeyAssertionOptions
|
||||
var relyingPartyId: String
|
||||
return this
|
||||
.flatMap { option ->
|
||||
options = getPasskeyAssertionOptionsOrNull(option.requestJson)
|
||||
?: return GetCredentialUnknownException(
|
||||
"Invalid passkey request. Could not deserialize request options.",
|
||||
)
|
||||
.asFailure()
|
||||
val decryptResult = vaultRepository
|
||||
.getDecryptedFido2CredentialAutofillViews(cipherViewsWithPublicKeyCredentials)
|
||||
|
||||
relyingPartyId = options.relyingPartyId
|
||||
?: return GetCredentialUnknownException(
|
||||
"Invalid passkey request. Relying party ID is required.",
|
||||
)
|
||||
.asFailure()
|
||||
return when (decryptResult) {
|
||||
is DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
|
||||
}
|
||||
|
||||
val autofillViews = fido2CredentialAutofillViews
|
||||
.filter { it.rpId == relyingPartyId }
|
||||
.ifEmpty {
|
||||
return emptyList<PublicKeyCredentialEntry>().asSuccess()
|
||||
}
|
||||
|
||||
val cipherIdsToMatch = autofillViews
|
||||
.map { it.cipherId }
|
||||
.toSet()
|
||||
|
||||
cipherViews
|
||||
.filter { cipherView -> cipherView.id in cipherIdsToMatch }
|
||||
.associateWith { cipherView ->
|
||||
// We can safely call first() here because we know the cipherId exists in
|
||||
// the collection of autofill views.
|
||||
autofillViews.first { it.cipherId == cipherView.id }
|
||||
}
|
||||
.toPublicKeyCredentialEntryList(
|
||||
baseIconUrl = baseIconUrl,
|
||||
is DecryptFido2CredentialAutofillViewResult.Success -> {
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
option = option,
|
||||
)
|
||||
}
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
private suspend fun Map<CipherView, Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
|
||||
baseIconUrl: String,
|
||||
userId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
): List<PublicKeyCredentialEntry> = this.map { (cipherView, autofillView) ->
|
||||
val loginIconData = cipherView.login
|
||||
?.uris
|
||||
.toLoginIconData(
|
||||
// TODO: [PM-20176] Enable web icons in passkey credential entries
|
||||
// Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
|
||||
// are addressed. See https://issuetracker.google.com/issues/355141766 for details.
|
||||
isIconLoadingDisabled = true,
|
||||
baseIconUrl = baseIconUrl,
|
||||
usePasskeyDefaultIcon = true,
|
||||
)
|
||||
val iconCompat = when (loginIconData) {
|
||||
is IconData.Local -> {
|
||||
IconCompat.createWithResource(context, loginIconData.iconRes)
|
||||
}
|
||||
|
||||
is IconData.Network -> {
|
||||
loginIconData.toIconCompat()
|
||||
}
|
||||
}
|
||||
|
||||
val pkEntryBuilder = PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = autofillView.userNameForUi
|
||||
?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = autofillView.credentialId.toString(),
|
||||
cipherId = autofillView.cipherId,
|
||||
fido2CredentialAutofillViews = decryptResult
|
||||
.fido2CredentialAutofillViews
|
||||
.filter { it.rpId in relyingPartyIds },
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
requestCode = Random.nextInt(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.setIcon(iconCompat.toIcon(context))
|
||||
|
||||
if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication) &&
|
||||
!isUserVerified
|
||||
) {
|
||||
biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId)
|
||||
?.let { cipher ->
|
||||
pkEntryBuilder
|
||||
.setBiometricPromptDataIfSupported(cipher = cipher)
|
||||
}
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
pkEntryBuilder.build()
|
||||
}
|
||||
|
||||
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): PublicKeyCredentialEntry.Builder =
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
this
|
||||
} else {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
private fun buildPromptDataWithCipher(
|
||||
cipher: Cipher,
|
||||
): BiometricPromptData = BiometricPromptData.Builder()
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Converts a network icon to an [IconCompat]. Performs a blocking network request to fetch the
|
||||
* icon, so only call this method from a background thread or coroutine.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@WorkerThread
|
||||
private suspend fun IconData.Network.toIconCompat(): IconCompat = try {
|
||||
val futureTargetBitmap = Glide
|
||||
.with(context)
|
||||
.asBitmap()
|
||||
.load(this.uri)
|
||||
.placeholder(R.drawable.ic_bw_passkey)
|
||||
.submit()
|
||||
|
||||
IconCompat.createWithBitmap(futureTargetBitmap.get())
|
||||
} catch (_: ExecutionException) {
|
||||
null
|
||||
} catch (_: InterruptedException) {
|
||||
null
|
||||
}
|
||||
?: IconCompat.createWithResource(
|
||||
context,
|
||||
this.fallbackIconRes,
|
||||
)
|
||||
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
|
||||
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.credentials.model
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
@ -30,7 +31,7 @@ data class GetCredentialsRequest(
|
||||
|
||||
/**
|
||||
* The [BeginGetPublicKeyCredentialOption]s of the [providerRequest], or an empty list if no
|
||||
* public key credentials are present.
|
||||
* public key options are present.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption> by lazy {
|
||||
@ -40,6 +41,18 @@ data class GetCredentialsRequest(
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* The [BeginGetPasswordOption]s of the [providerRequest], or an empty list if no password
|
||||
* options are present.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val beginGetPasswordOptions: List<BeginGetPasswordOption> by lazy {
|
||||
providerRequest
|
||||
?.beginGetCredentialOptions
|
||||
?.filterIsInstance<BeginGetPasswordOption>()
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* The [CallingAppInfo] of the [providerRequest], or null if the [providerRequest] is not a
|
||||
* [BeginGetCredentialRequest].
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.data.credentials.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* A wrapper around [ProviderGetCredentialRequest] that includes additional information needed to
|
||||
* fulfill the request.
|
||||
*
|
||||
* @param userId The ID of the user that owns the credential being requested.
|
||||
* @param cipherId The ID of the cipher containing the password to be retrieved.
|
||||
* @param isUserVerified Whether the user has been verified prior to this request.
|
||||
* @param requestData The original request data from the system.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProviderGetPasswordCredentialRequest(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
val isUserVerified: Boolean,
|
||||
val requestData: Bundle,
|
||||
) : Parcelable
|
||||
@ -22,16 +22,15 @@ import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
@ -124,15 +123,14 @@ class CredentialProviderProcessorImpl(
|
||||
|
||||
// Otherwise, find all matching credentials from the current vault.
|
||||
val getCredentialJob = ioScope.launch {
|
||||
getMatchingFido2CredentialEntries(
|
||||
userId = userState.activeUserId,
|
||||
request = request,
|
||||
)
|
||||
.onSuccess {
|
||||
callback.onResult(
|
||||
BeginGetCredentialResponse(credentialEntries = it),
|
||||
)
|
||||
}
|
||||
bitwardenCredentialManager
|
||||
.getCredentialEntries(
|
||||
getCredentialsRequest = GetCredentialsRequest(
|
||||
userId = userState.activeUserId,
|
||||
BeginGetCredentialRequest.asBundle(request),
|
||||
),
|
||||
)
|
||||
.onSuccess { callback.onResult(BeginGetCredentialResponse(credentialEntries = it)) }
|
||||
.onFailure { callback.onError(GetCredentialUnknownException(it.message)) }
|
||||
}
|
||||
cancellationSignal.setOnCancelListener {
|
||||
@ -213,18 +211,6 @@ class CredentialProviderProcessorImpl(
|
||||
return entryBuilder.build()
|
||||
}
|
||||
|
||||
private suspend fun getMatchingFido2CredentialEntries(
|
||||
userId: String,
|
||||
request: BeginGetCredentialRequest,
|
||||
): Result<List<CredentialEntry>> =
|
||||
bitwardenCredentialManager
|
||||
.getCredentialEntries(
|
||||
userId = userId,
|
||||
options = request
|
||||
.beginGetCredentialOptions
|
||||
.filterIsInstance<BeginGetPublicKeyCredentialOption>(),
|
||||
)
|
||||
|
||||
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): CreateEntry.Builder {
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.credentials.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* Builds a [BiometricPromptData] instance with the provided [Cipher].
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
fun buildPromptDataWithCipher(
|
||||
cipher: Cipher,
|
||||
): BiometricPromptData = BiometricPromptData.Builder()
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
|
||||
.build()
|
||||
@ -0,0 +1,27 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.credentials.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* Sets the biometric prompt data on the [PublicKeyCredentialEntry.Builder] if supported.
|
||||
*/
|
||||
fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher?,
|
||||
isSingleTapAuthEnabled: Boolean,
|
||||
): PublicKeyCredentialEntry.Builder =
|
||||
if (!isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
|
||||
cipher != null &&
|
||||
isSingleTapAuthEnabled
|
||||
) {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
@ -1253,6 +1254,10 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
VaultItemListingsAction.Internal.InternetConnectionErrorReceived -> {
|
||||
handleInternetConnectionErrorReceived()
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive -> {
|
||||
handleGetCredentialEntriesResultReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1690,14 +1695,6 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private fun handleProviderGetCredentialsRequest(
|
||||
request: GetCredentialsRequest,
|
||||
) {
|
||||
val beginGetCredentialOption = request
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.ifEmpty {
|
||||
showCredentialManagerErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
val callingAppInfo = request.callingAppInfo
|
||||
?: run {
|
||||
showCredentialManagerErrorDialog(
|
||||
@ -1712,17 +1709,11 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
is ValidateOriginResult.Success -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteProviderGetCredentialsRequest(
|
||||
GetCredentialsResult.Success(
|
||||
credentialEntries = bitwardenCredentialManager
|
||||
.getCredentialEntries(
|
||||
userId = request.userId,
|
||||
options = beginGetCredentialOption,
|
||||
)
|
||||
.getOrNull()
|
||||
.orEmpty(),
|
||||
userId = request.userId,
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive(
|
||||
userId = request.userId,
|
||||
result = bitwardenCredentialManager.getCredentialEntries(
|
||||
getCredentialsRequest = request,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -1861,6 +1852,27 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGetCredentialEntriesResultReceive(
|
||||
action: VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive,
|
||||
) {
|
||||
action.result
|
||||
.onFailure {
|
||||
showCredentialManagerErrorDialog(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
)
|
||||
}
|
||||
.onSuccess { credentialEntries ->
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteProviderGetCredentialsRequest(
|
||||
GetCredentialsResult.Success(
|
||||
credentialEntries = credentialEntries,
|
||||
userId = action.userId,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
@ -2916,6 +2928,14 @@ sealed class VaultItemListingsAction {
|
||||
* Indicates that the there is not internet connection.
|
||||
*/
|
||||
data object InternetConnectionErrorReceived : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a result for building credential entries has been received.
|
||||
*/
|
||||
data class GetCredentialEntriesResultReceive(
|
||||
val userId: String,
|
||||
val result: Result<List<CredentialEntry>>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,243 @@
|
||||
package com.x8bit.bitwarden.data.credentials.builder
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CredentialEntryBuilderTest {
|
||||
|
||||
private val mockContext = mockk<Context>()
|
||||
private val mockGetPublicKeyCredentialIntent = mockk<PendingIntent>(relaxed = true)
|
||||
private val mockIntentManager = mockk<IntentManager> {
|
||||
every {
|
||||
createFido2GetCredentialPendingIntent(
|
||||
action = any(),
|
||||
userId = any(),
|
||||
cipherId = any(),
|
||||
credentialId = any(),
|
||||
requestCode = any(),
|
||||
isUserVerified = any(),
|
||||
)
|
||||
} returns mockGetPublicKeyCredentialIntent
|
||||
}
|
||||
private val mockFeatureFlagManager = mockk<FeatureFlagManager>()
|
||||
private val mockBiometricsEncryptionManager = mockk<BiometricsEncryptionManager>()
|
||||
private val mockBeginGetPublicKeyOption = mockk<BeginGetPublicKeyCredentialOption>()
|
||||
private val credentialEntryBuilder = CredentialEntryBuilderImpl(
|
||||
context = mockContext,
|
||||
intentManager = mockIntentManager,
|
||||
featureFlagManager = mockFeatureFlagManager,
|
||||
biometricsEncryptionManager = mockBiometricsEncryptionManager,
|
||||
)
|
||||
private val mockPublicKeyCredentialEntry = mockk<PublicKeyCredentialEntry>(relaxed = true)
|
||||
private val mockIcon = mockk<Icon>()
|
||||
|
||||
@BeforeEach
|
||||
@Test
|
||||
fun setUp() {
|
||||
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
mockkStatic(IconCompat::class)
|
||||
mockBuilder<PublicKeyCredentialEntry.Builder> { it.setIcon(any()) }
|
||||
every { IconCompat.createWithResource(any(), any()) } returns mockk {
|
||||
every { toIcon(mockContext) } returns mockIcon
|
||||
}
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockPublicKeyCredentialEntry
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@Test
|
||||
fun tearDown() {
|
||||
unmockkStatic(IconCompat::class)
|
||||
unmockkStatic(::isBuildVersionBelow)
|
||||
unmockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `buildPublicKeyCredentialEntries should return Success with empty list when options list is empty`() =
|
||||
runTest {
|
||||
val options = emptyList<BeginGetPublicKeyCredentialOption>()
|
||||
val fido2AutofillViews: List<Fido2CredentialAutofillView> = listOf(
|
||||
createMockFido2CredentialAutofillView(number = 1),
|
||||
)
|
||||
|
||||
val result = credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `buildPublicKeyCredentialEntries should return Success with empty list when fido2AutofillViews is empty`() =
|
||||
runTest {
|
||||
val options = listOf(mockBeginGetPublicKeyOption)
|
||||
val fido2AutofillViews = emptyList<Fido2CredentialAutofillView>()
|
||||
val result = credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `buildPublicKeyCredentialEntries should return Success with list of PublicKeyCredentialEntry`() =
|
||||
runTest {
|
||||
val options = listOf(mockBeginGetPublicKeyOption)
|
||||
val fido2AutofillViews: List<Fido2CredentialAutofillView> = listOf(
|
||||
createMockFido2CredentialAutofillView(number = 1),
|
||||
)
|
||||
|
||||
every {
|
||||
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
|
||||
} returns false
|
||||
every {
|
||||
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
|
||||
} returns null
|
||||
|
||||
val result = credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
|
||||
assertTrue(result.isNotEmpty())
|
||||
|
||||
verify {
|
||||
mockIntentManager.createFido2GetCredentialPendingIntent(
|
||||
action = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY",
|
||||
userId = "userId",
|
||||
cipherId = "mockCipherId-1",
|
||||
credentialId = fido2AutofillViews.first().credentialId.toString(),
|
||||
requestCode = any(),
|
||||
isUserVerified = false,
|
||||
)
|
||||
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setIcon(mockIcon)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildPublicKeyCredentialEntries should set biometric prompt data correctly`() = runTest {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
val options = listOf(mockBeginGetPublicKeyOption)
|
||||
val fido2AutofillViews: List<Fido2CredentialAutofillView> = listOf(
|
||||
createMockFido2CredentialAutofillView(number = 1),
|
||||
)
|
||||
|
||||
// Verify biometric prompt data is not set when flag is false, buildVersion is < 35, and
|
||||
// cipher is null.
|
||||
every {
|
||||
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
|
||||
} returns false
|
||||
every {
|
||||
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
|
||||
} returns null
|
||||
every { isBuildVersionBelow(any()) } returns false
|
||||
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
|
||||
// Verify biometric prompt data is not set when flag is true, buildVersion is < 35, and
|
||||
// cipher is null.
|
||||
every {
|
||||
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
|
||||
} returns true
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
|
||||
// Verify biometric prompt data is not set when flag is true, buildVersion is >= 35, and
|
||||
// cipher is null
|
||||
every { isBuildVersionBelow(any()) } returns false
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
|
||||
// Verify biometric prompt data is not set when user is verified
|
||||
every {
|
||||
mockBiometricsEncryptionManager.getOrCreateCipher(any())
|
||||
} returns mockk(relaxed = true)
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = true,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
|
||||
// Verify biometric prompt data is set when flag is true, buildVersion is >= 35, cipher is
|
||||
// not null, and user is not verified
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = "userId",
|
||||
isUserVerified = false,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = options,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.Signature
|
||||
import android.content.pm.SigningInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
@ -25,20 +23,18 @@ import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.BitwardenException
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2AttestationResponse
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||
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
|
||||
@ -51,14 +47,12 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2Cre
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkConstructor
|
||||
@ -82,6 +76,9 @@ class BitwardenCredentialManagerTest {
|
||||
|
||||
private lateinit var bitwardenCredentialManager: BitwardenCredentialManager
|
||||
|
||||
private val mutableCipherStateFlow =
|
||||
MutableStateFlow<DataState<List<CipherView>>>(DataState.Loading)
|
||||
|
||||
private val json = mockk<Json> {
|
||||
every {
|
||||
decodeFromStringOrNull<PasskeyAttestationOptions>(any())
|
||||
@ -122,11 +119,10 @@ class BitwardenCredentialManagerTest {
|
||||
}
|
||||
private val mockVaultSdkSource = mockk<VaultSdkSource>()
|
||||
private val mockFido2CredentialStore = mockk<Fido2CredentialStore>()
|
||||
private val mockIntentManager = mockk<IntentManager>()
|
||||
private val mockVaultRepository = mockk<VaultRepository>()
|
||||
private val mockFeatureFlagManager = mockk<FeatureFlagManager>()
|
||||
private val mockBiometricsEncryptionManager = mockk<BiometricsEncryptionManager>()
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
private val mockVaultRepository = mockk<VaultRepository> {
|
||||
every { ciphersStateFlow } returns mutableCipherStateFlow
|
||||
}
|
||||
private val mockCredentialEntryBuilder = mockk<CredentialEntryBuilder>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
@ -138,16 +134,12 @@ class BitwardenCredentialManagerTest {
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
|
||||
bitwardenCredentialManager = BitwardenCredentialManagerImpl(
|
||||
context = mockk(relaxed = true),
|
||||
vaultSdkSource = mockVaultSdkSource,
|
||||
fido2CredentialStore = mockFido2CredentialStore,
|
||||
json = json,
|
||||
intentManager = mockIntentManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
vaultRepository = mockVaultRepository,
|
||||
featureFlagManager = mockFeatureFlagManager,
|
||||
biometricsEncryptionManager = mockBiometricsEncryptionManager,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
credentialEntryBuilder = mockCredentialEntryBuilder,
|
||||
)
|
||||
}
|
||||
|
||||
@ -879,65 +871,75 @@ class BitwardenCredentialManagerTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getCredentialEntries should return empty list when cipherViews are empty`() =
|
||||
fun `getCredentialEntries with public key credential options should return empty list when no ciphers have FIDO 2 credentials`() =
|
||||
runTest {
|
||||
val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
|
||||
val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every {
|
||||
beginGetPublicKeyCredentialOptions
|
||||
} returns listOf(mockBeginGetPublicKeyCredentialOption)
|
||||
}
|
||||
every {
|
||||
mockBeginGetPublicKeyCredentialOption.requestJson
|
||||
} returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
every {
|
||||
mockVaultRepository.ciphersStateFlow
|
||||
} returns MutableStateFlow(DataState.Loaded(emptyList()))
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockBeginGetPublicKeyCredentialOption),
|
||||
)
|
||||
mutableCipherStateFlow.value = DataState.Loaded(emptyList())
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
|
||||
assertEquals(emptyList<CredentialEntry>(), result.getOrNull())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getCredentialEntries should return error when FIDO 2 credential decryption fails`() = runTest {
|
||||
val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
|
||||
every {
|
||||
mockBeginGetPublicKeyCredentialOption.requestJson
|
||||
} returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
every {
|
||||
mockVaultRepository.ciphersStateFlow
|
||||
} returns MutableStateFlow(
|
||||
DataState.Loaded(
|
||||
fun `getCredentialEntries with public key credential options should return error when FIDO 2 credential decryption fails`() =
|
||||
runTest {
|
||||
val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
|
||||
val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every {
|
||||
beginGetPublicKeyCredentialOptions
|
||||
} returns listOf(mockBeginGetPublicKeyCredentialOption)
|
||||
every { beginGetPasswordOptions } returns emptyList()
|
||||
every { userId } returns "mockUserId"
|
||||
}
|
||||
every {
|
||||
mockBeginGetPublicKeyCredentialOption.requestJson
|
||||
} returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
mutableCipherStateFlow.value = DataState.Loaded(
|
||||
listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Error(
|
||||
BitwardenException.E("Error decrypting credentials."),
|
||||
)
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockBeginGetPublicKeyCredentialOption),
|
||||
)
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result.exceptionOrNull() is GetCredentialUnknownException)
|
||||
}
|
||||
)
|
||||
coEvery {
|
||||
mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Error(
|
||||
BitwardenException.E("Error decrypting credentials."),
|
||||
)
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result.exceptionOrNull() is GetCredentialUnknownException)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getCredentialEntries should return error when passkey assertion options are null`() =
|
||||
fun `getCredentialEntries with public key credential options should return error when passkey assertion options are null`() =
|
||||
runTest {
|
||||
val mockRequest = mockk<BeginGetPublicKeyCredentialOption> {
|
||||
val mockOption = mockk<BeginGetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns ""
|
||||
}
|
||||
every {
|
||||
mockVaultRepository.ciphersStateFlow
|
||||
} returns MutableStateFlow(
|
||||
DataState.Loaded(
|
||||
val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every {
|
||||
beginGetPublicKeyCredentialOptions
|
||||
} returns listOf(mockOption)
|
||||
every { beginGetPasswordOptions } returns emptyList()
|
||||
every { userId } returns "mockUserId"
|
||||
}
|
||||
mutableCipherStateFlow.value = DataState.Loaded(
|
||||
listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
@ -945,7 +947,6 @@ class BitwardenCredentialManagerTest {
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
|
||||
@ -961,25 +962,28 @@ class BitwardenCredentialManagerTest {
|
||||
every {
|
||||
json.decodeFromStringOrNull<PasskeyAssertionOptions>(any())
|
||||
} returns null
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockRequest),
|
||||
)
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
|
||||
assertTrue(
|
||||
result.exceptionOrNull() is GetCredentialUnknownException,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getCredentialEntries should return error when FIDO 2 relyingPartyId is null`() =
|
||||
fun `getCredentialEntries with public key credential options should return error when FIDO 2 relyingPartyId is null`() =
|
||||
runTest {
|
||||
val mockRequest = mockk<BeginGetPublicKeyCredentialOption> {
|
||||
val mockOption = mockk<BeginGetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns ""
|
||||
}
|
||||
every {
|
||||
mockVaultRepository.ciphersStateFlow
|
||||
} returns MutableStateFlow(
|
||||
DataState.Loaded(
|
||||
val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every {
|
||||
beginGetPublicKeyCredentialOptions
|
||||
} returns listOf(mockOption)
|
||||
every { beginGetPasswordOptions } returns emptyList()
|
||||
every { userId } returns "mockUserId"
|
||||
}
|
||||
mutableCipherStateFlow.value = DataState.Loaded(
|
||||
listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
@ -987,7 +991,6 @@ class BitwardenCredentialManagerTest {
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
|
||||
@ -1003,10 +1006,8 @@ class BitwardenCredentialManagerTest {
|
||||
every {
|
||||
json.decodeFromStringOrNull<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(number = 1, relyingPartyId = null)
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockRequest),
|
||||
)
|
||||
val result = bitwardenCredentialManager
|
||||
.getCredentialEntries(mockGetCredentialsRequest)
|
||||
assertTrue(
|
||||
result.exceptionOrNull() is GetCredentialUnknownException,
|
||||
)
|
||||
@ -1014,116 +1015,56 @@ class BitwardenCredentialManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getCredentialEntries should return list of PublicKeyCredentialEntry when FIDO 2 credential decryption succeeds`() =
|
||||
fun `getCredentialEntries should build public key credential entries when decryption succeeds`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
mockkStatic(IconCompat::class)
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockk()
|
||||
val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
|
||||
val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every {
|
||||
beginGetPublicKeyCredentialOptions
|
||||
} returns listOf(mockBeginGetPublicKeyCredentialOption)
|
||||
every { userId } returns "mockUserId"
|
||||
}
|
||||
val fido2CredentialAutofillViews = listOf(
|
||||
createMockFido2CredentialAutofillView(
|
||||
number = 1,
|
||||
cipherId = "mockId-1",
|
||||
rpId = "mockRelyingPartyId-1",
|
||||
),
|
||||
)
|
||||
every {
|
||||
mockBeginGetPublicKeyCredentialOption.requestJson
|
||||
} returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
every {
|
||||
mockVaultRepository.ciphersStateFlow
|
||||
} returns MutableStateFlow(
|
||||
DataState.Loaded(
|
||||
listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
login = createMockLoginView(number = 1, hasUris = false),
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
),
|
||||
mutableCipherStateFlow.value = DataState.Loaded(
|
||||
listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
login = createMockLoginView(number = 1, hasUris = false),
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Success(
|
||||
listOf(
|
||||
createMockFido2CredentialAutofillView(
|
||||
number = 1,
|
||||
cipherId = "mockId-1",
|
||||
rpId = "mockRelyingPartyId-1",
|
||||
),
|
||||
),
|
||||
fido2CredentialAutofillViews,
|
||||
)
|
||||
val mockIcon = mockk<Icon>()
|
||||
every {
|
||||
IconCompat.createWithResource(any<Context>(), any<Int>())
|
||||
} returns mockk {
|
||||
every { toIcon(any()) } returns mockIcon
|
||||
}
|
||||
every {
|
||||
mockIntentManager.createFido2GetCredentialPendingIntent(
|
||||
action = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY",
|
||||
mockCredentialEntryBuilder.buildPublicKeyCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
credentialId = any(),
|
||||
cipherId = "mockId-1",
|
||||
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = listOf(
|
||||
mockBeginGetPublicKeyCredentialOption,
|
||||
),
|
||||
isUserVerified = false,
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockk()
|
||||
every {
|
||||
mockBiometricsEncryptionManager.getOrCreateCipher("mockUserId")
|
||||
} returns mockk()
|
||||
every {
|
||||
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
|
||||
} returns false
|
||||
mockBuilder<PublicKeyCredentialEntry.Builder> { it.setIcon(mockIcon) }
|
||||
mockBuilder<PublicKeyCredentialEntry.Builder> { it.setBiometricPromptData(any()) }
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockBeginGetPublicKeyCredentialOption),
|
||||
)
|
||||
} returns listOf(mockk<PublicKeyCredentialEntry>())
|
||||
|
||||
val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(1, result.getOrNull()?.size)
|
||||
assertTrue(result.getOrNull()?.first() is PublicKeyCredentialEntry)
|
||||
verify {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setIcon(mockIcon)
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
|
||||
// Verify biometric prompt data IS NOT set when single tap feature flag is enabled and
|
||||
// build version is < 35
|
||||
every {
|
||||
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
|
||||
} returns true
|
||||
every { isBuildVersionBelow(35) } returns true
|
||||
bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockBeginGetPublicKeyCredentialOption),
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
|
||||
// Verify biometric prompt data IS set when single tap feature flag is enabled and build
|
||||
// version is 35+
|
||||
every {
|
||||
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
|
||||
} returns true
|
||||
every { isBuildVersionBelow(35) } returns false
|
||||
bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId",
|
||||
options = listOf(mockBeginGetPublicKeyCredentialOption),
|
||||
)
|
||||
verify {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri() {
|
||||
mockkStatic(Uri::class)
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.host } returns "www.mockuri.com"
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden"
|
||||
|
||||
@ -69,9 +69,7 @@ class CredentialProviderProcessorTest {
|
||||
}
|
||||
private val credentialEntries = listOf(mockk<CredentialEntry>(relaxed = true))
|
||||
private val bitwardenCredentialManager: BitwardenCredentialManager = mockk {
|
||||
coEvery {
|
||||
getCredentialEntries(any(), any())
|
||||
} returns credentialEntries.asSuccess()
|
||||
coEvery { getCredentialEntries(any()) } returns credentialEntries.asSuccess()
|
||||
}
|
||||
private val intentManager: IntentManager = mockk()
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
@ -459,10 +457,7 @@ class CredentialProviderProcessorTest {
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
coEvery {
|
||||
bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
options = listOf(mockOption),
|
||||
)
|
||||
bitwardenCredentialManager.getCredentialEntries(any())
|
||||
} returns Result.failure(Exception("Error decrypting credentials."))
|
||||
|
||||
credentialProviderProcessor.processGetCredentialRequest(
|
||||
|
||||
@ -95,11 +95,13 @@ fun createMockCipherView(
|
||||
/**
|
||||
* Create a mock [LoginView] with a given [number].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun createMockLoginView(
|
||||
number: Int,
|
||||
totp: String? = "mockTotp-$number",
|
||||
clock: Clock = FIXED_CLOCK,
|
||||
hasUris: Boolean = true,
|
||||
uris: List<LoginUriView>? = listOf(createMockUriView(number = number)),
|
||||
fido2Credentials: List<Fido2Credential>? = createMockSdkFido2CredentialList(number, clock),
|
||||
): LoginView =
|
||||
LoginView(
|
||||
@ -107,7 +109,7 @@ fun createMockLoginView(
|
||||
password = "mockPassword-$number",
|
||||
passwordRevisionDate = clock.instant(),
|
||||
autofillOnPageLoad = false,
|
||||
uris = listOf(createMockUriView(number = number)).takeIf { hasUris },
|
||||
uris = uris.takeIf { hasUris },
|
||||
totp = totp,
|
||||
fido2Credentials = fido2Credentials,
|
||||
)
|
||||
@ -162,9 +164,9 @@ fun createMockFido2CredentialAutofillView(
|
||||
/**
|
||||
* Create a mock [LoginUriView] with a given [number].
|
||||
*/
|
||||
fun createMockUriView(number: Int): LoginUriView =
|
||||
fun createMockUriView(number: Int, uri: String = "www.mockuri$number.com"): LoginUriView =
|
||||
LoginUriView(
|
||||
uri = "www.mockuri$number.com",
|
||||
uri = uri,
|
||||
match = UriMatchType.HOST,
|
||||
uriChecksum = "mockUriChecksum-$number",
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
@ -12,6 +13,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
@ -2967,10 +2969,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
mockGetCredentialsRequest,
|
||||
)
|
||||
coEvery {
|
||||
bitwardenCredentialManager.getCredentialEntries(
|
||||
userId = "mockUserId-1",
|
||||
options = listOf(mockBeginGetPublicKeyCredentialOption),
|
||||
)
|
||||
bitwardenCredentialManager.getCredentialEntries(any())
|
||||
} returns emptyList<PublicKeyCredentialEntry>().asSuccess()
|
||||
coEvery {
|
||||
originManager.validateOrigin(callingAppInfo = any())
|
||||
@ -3007,8 +3006,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `GetCredentialsRequest should display error dialog when getCredentialEntries is failure`() =
|
||||
fun `GetCredentialsRequest should emit GetCredentialEntriesResultReceive when result is received`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val mockGetCredentialsRequest = createMockGetCredentialsRequest(number = 1)
|
||||
@ -3031,6 +3031,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
every {
|
||||
mockBeginGetCredentialRequest.beginGetCredentialOptions
|
||||
} returns emptyList()
|
||||
coEvery {
|
||||
bitwardenCredentialManager.getCredentialEntries(any())
|
||||
} returns GetCredentialUnknownException("Internal error").asFailure()
|
||||
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
@ -3047,8 +3050,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.CredentialManagerOperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message =
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user