mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
Update androidx.credentials (#4919)
This commit is contained in:
1 parent
bf1ed690a4
commit
8767f2dd24
50 files changed
+2234
-2130
No files matched your search
@@ -309,10 +309,10 @@ class MainViewModel @Inject constructor(
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
val fido2CreateCredentialRequestData = intent.getFido2CreateCredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2CreateCredentialRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
@@ -370,34 +370,39 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fido2CreateCredentialRequestData != null -> {
|
||||
fido2CreateCredentialRequest != null -> {
|
||||
// Set the user's verification status when a new FIDO 2 request is received to force
|
||||
// explicit verification if the user's vault is unlocked when the request is
|
||||
// received.
|
||||
fido2CreateCredentialRequestData.isUserVerified
|
||||
fido2CreateCredentialRequest.providerRequest
|
||||
.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequestData,
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
|
||||
// Switch accounts if the selected user is not the active user.
|
||||
if (authRepository.activeUserId != null &&
|
||||
authRepository.activeUserId != fido2CreateCredentialRequestData.userId
|
||||
authRepository.activeUserId != fido2CreateCredentialRequest.userId
|
||||
) {
|
||||
authRepository.switchAccount(fido2CreateCredentialRequestData.userId)
|
||||
authRepository.switchAccount(fido2CreateCredentialRequest.userId)
|
||||
}
|
||||
}
|
||||
|
||||
fido2CredentialAssertionRequest != null -> {
|
||||
fido2AssertCredentialRequest != null -> {
|
||||
// If device biometric verification was performed as part of single-tap
|
||||
// authentication, set the user's verification state to the device result.
|
||||
// Otherwise, retain the verification state as-is.
|
||||
fido2CredentialAssertionRequest.isUserVerified
|
||||
fido2AssertCredentialRequest.providerRequest.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = fido2CredentialAssertionRequest,
|
||||
fido2AssertionRequest = fido2AssertCredentialRequest,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,13 +68,11 @@ object Fido2ProviderModule {
|
||||
fun provideFido2CredentialManager(
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
fido2OriginManager: Fido2OriginManager,
|
||||
json: Json,
|
||||
): Fido2CredentialManager =
|
||||
Fido2CredentialManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
json = json,
|
||||
)
|
||||
|
||||
|
||||
+38
-4
@@ -1,12 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
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.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||
|
||||
/**
|
||||
* Responsible for managing FIDO 2 credential registration and authentication.
|
||||
@@ -43,7 +46,8 @@ interface Fido2CredentialManager {
|
||||
*/
|
||||
suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult
|
||||
|
||||
@@ -52,12 +56,42 @@ interface Fido2CredentialManager {
|
||||
*/
|
||||
suspend fun authenticateFido2Credential(
|
||||
userId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
request: GetPublicKeyCredentialOption,
|
||||
selectedCipherView: CipherView,
|
||||
origin: String?,
|
||||
): Fido2CredentialAssertionResult
|
||||
|
||||
/**
|
||||
* Whether or not the user has authentication attempts remaining.
|
||||
*/
|
||||
fun hasAuthenticationAttemptsRemaining(): Boolean
|
||||
|
||||
/**
|
||||
* Determines the user verification requirement for a given FIDO2 assertion request.
|
||||
*
|
||||
* @param request The FIDO2 credential assertion request.
|
||||
* @param fallbackRequirement The fallback requirement to use if the request doesn't specify
|
||||
* one.
|
||||
* Defaults to [UserVerificationRequirement.REQUIRED].
|
||||
* @return The user verification requirement for the request.
|
||||
*/
|
||||
fun getUserVerificationRequirement(
|
||||
request: ProviderGetCredentialRequest,
|
||||
fallbackRequirement: UserVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
): UserVerificationRequirement
|
||||
|
||||
/**
|
||||
* Determines the user verification requirement for a given FIDO2 registration request.
|
||||
*
|
||||
* @param request The FIDO2 credential request.
|
||||
* @param fallbackRequirement The fallback requirement to use if the request doesn't specify
|
||||
* one.
|
||||
* Defaults to [UserVerificationRequirement.REQUIRED].
|
||||
* @return The user verification requirement for the request.
|
||||
*/
|
||||
fun getUserVerificationRequirement(
|
||||
request: CreatePublicKeyCredentialRequest,
|
||||
fallbackRequirement: UserVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
): UserVerificationRequirement
|
||||
}
|
||||
+154
-128
@@ -1,21 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
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
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
@@ -36,7 +34,6 @@ import timber.log.Timber
|
||||
class Fido2CredentialManagerImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val fido2OriginManager: Fido2OriginManager,
|
||||
private val json: Json,
|
||||
) : Fido2CredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
@@ -47,76 +44,111 @@ class Fido2CredentialManagerImpl(
|
||||
|
||||
override suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
|
||||
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
|
||||
} else {
|
||||
callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
|
||||
)
|
||||
}
|
||||
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
|
||||
val host = getOriginUrlFromAttestationOptionsOrNull(
|
||||
requestJson = fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
)
|
||||
Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
host = host,
|
||||
assetLinkUrl = host,
|
||||
),
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
registerFido2CredentialForPrivilegedApp(
|
||||
userId = userId,
|
||||
callingAppInfo = callingAppInfo,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
)
|
||||
} else {
|
||||
Origin.Web(fido2CreateCredentialRequest.origin)
|
||||
registerFido2CredentialForUnprivilegedApp(
|
||||
userId = userId,
|
||||
callingAppInfo = callingAppInfo,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
)
|
||||
}
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidAttestationResponse() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = {
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun validateOrigin(
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = ClientData.DefaultWithExtraData(callingAppInfo.packageName)
|
||||
|
||||
val host = getOriginUrlFromAttestationOptionsOrNull(
|
||||
requestJson = createPublicKeyCredentialRequest.requestJson,
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
|
||||
|
||||
val signatureFingerprint = callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
|
||||
|
||||
val sdkOrigin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = signatureFingerprint,
|
||||
host = host,
|
||||
assetLinkUrl = host,
|
||||
),
|
||||
)
|
||||
|
||||
return registerFido2CredentialInternal(
|
||||
userId = userId,
|
||||
sdkOrigin = sdkOrigin,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForPrivilegedApp(
|
||||
userId: String,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
|
||||
|
||||
val sdkOrigin = createPublicKeyCredentialRequest.origin
|
||||
?.let { Origin.Web(it) }
|
||||
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
|
||||
|
||||
return registerFido2CredentialInternal(
|
||||
userId = userId,
|
||||
sdkOrigin = sdkOrigin,
|
||||
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
clientData = clientData,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialInternal(
|
||||
userId: String,
|
||||
sdkOrigin: Origin,
|
||||
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
clientData: ClientData,
|
||||
): Fido2RegisterCredentialResult = vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${createPublicKeyCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidAttestationResponse() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = { Fido2RegisterCredentialResult.Error.InternalError },
|
||||
)
|
||||
|
||||
override fun getPasskeyAttestationOptionsOrNull(
|
||||
@@ -148,85 +180,80 @@ class Fido2CredentialManagerImpl(
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun authenticateFido2Credential(
|
||||
userId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
request: GetPublicKeyCredentialOption,
|
||||
selectedCipherView: CipherView,
|
||||
origin: String?,
|
||||
): Fido2CredentialAssertionResult {
|
||||
val callingAppInfo = request.callingAppInfo
|
||||
val clientData = request.clientDataHash
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
|
||||
val relyingPartyId = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
|
||||
val validateOriginResult = validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
|
||||
Origin.Web(request.origin)
|
||||
val sdkOrigin = if (!origin.isNullOrEmpty()) {
|
||||
Origin.Web(origin)
|
||||
} else {
|
||||
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
)
|
||||
?: return Fido2CredentialAssertionResult.Error.MissingHostUrl
|
||||
Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = callingAppInfo.packageName,
|
||||
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
sha256CertFingerprint = callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult
|
||||
.Error
|
||||
.InvalidAppSignature,
|
||||
host = hostUrl,
|
||||
assetLinkUrl = hostUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidFido2PublicKeyCredential() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to authenticate FIDO2 credential.")
|
||||
Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_authentication_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidFido2PublicKeyCredential() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to authenticate FIDO2 credential.")
|
||||
Fido2CredentialAssertionResult.Error.InternalError
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun hasAuthenticationAttemptsRemaining(): Boolean =
|
||||
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
|
||||
|
||||
override fun getUserVerificationRequirement(
|
||||
request: ProviderGetCredentialRequest,
|
||||
fallbackRequirement: UserVerificationRequirement,
|
||||
): UserVerificationRequirement = request
|
||||
.credentialOptions
|
||||
.filterIsInstance<GetPublicKeyCredentialOption>()
|
||||
.firstOrNull()
|
||||
?.let { option ->
|
||||
getPasskeyAssertionOptionsOrNull(option.requestJson)
|
||||
?.userVerification
|
||||
}
|
||||
?: fallbackRequirement
|
||||
|
||||
override fun getUserVerificationRequirement(
|
||||
request: CreatePublicKeyCredentialRequest,
|
||||
fallbackRequirement: UserVerificationRequirement,
|
||||
): UserVerificationRequirement = getPasskeyAttestationOptionsOrNull(request.requestJson)
|
||||
?.authenticatorSelection
|
||||
?.userVerification
|
||||
?: fallbackRequirement
|
||||
|
||||
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAssertionOptionsOrNull(requestJson)
|
||||
?.relyingPartyId
|
||||
@@ -240,4 +267,3 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
|
||||
private const val MAX_AUTHENTICATION_ATTEMPTS = 5
|
||||
private const val HTTPS = "https://"
|
||||
+50
-15
@@ -1,31 +1,66 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents raw data from the a user deciding to create a passkey in their vault via the
|
||||
* credential manager framework.
|
||||
*
|
||||
* @property userId The user under which the passkey should be saved.
|
||||
* @property requestJson JSON payload containing the RP request.
|
||||
* @property callingAppInfo Information about the application that initiated the request.
|
||||
* @property userId The ID of the user creating the passkey.
|
||||
* @property requestData Provider request data in the form of a [Bundle].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CreateCredentialRequest(
|
||||
val userId: String,
|
||||
val requestJson: String,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
val requestData: Bundle,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(
|
||||
packageName = packageName,
|
||||
signingInfo = signingInfo,
|
||||
origin = origin,
|
||||
)
|
||||
|
||||
/**
|
||||
* The [ProviderCreateCredentialRequest] from the [requestData].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val providerRequest: ProviderCreateCredentialRequest by lazy {
|
||||
ProviderCreateCredentialRequest.fromBundle(requestData)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [CallingAppInfo] of the [providerRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val callingAppInfo: CallingAppInfo by lazy { providerRequest.callingAppInfo }
|
||||
|
||||
/**
|
||||
* The [CreatePublicKeyCredentialRequest] of the [providerRequest], or null if the calling
|
||||
* request is not a [CreatePublicKeyCredentialRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest? by lazy {
|
||||
providerRequest.callingRequest as? CreatePublicKeyCredentialRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* The [requestJson] of the [createPublicKeyCredentialRequest], or null if the calling request
|
||||
* is not a [CreatePublicKeyCredentialRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val requestJson: String? by lazy { createPublicKeyCredentialRequest?.requestJson }
|
||||
|
||||
/**
|
||||
* Returns the ID of the relying party, or null if the relying party cannot be identified.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val relyingPartyIdOrNull: String? by lazy {
|
||||
if (callingAppInfo.isOriginPopulated()) {
|
||||
providerRequest.callingRequest.origin?.toHostOrPathOrNull()
|
||||
} else {
|
||||
callingAppInfo.packageName
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-23
@@ -1,38 +1,50 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 credential authentication request parsed from the launching intent.
|
||||
*
|
||||
* @param userId The ID of the Bitwarden user to authenticate.
|
||||
* @param cipherId The ID of the cipher that contains the passkey to authenticate.
|
||||
* @param credentialId The ID of the credential to authenticate.
|
||||
* @param requestJson The JSON representation of the FIDO 2 request.
|
||||
* @param clientDataHash The hash of the client data.
|
||||
* @param packageName The package name of the calling app.
|
||||
* @param signingInfo The signing info of the calling app.
|
||||
* @param origin The origin of the calling app. Only populated if the calling application is a
|
||||
* privileged application. I.e., a web browser.
|
||||
* @param isUserVerified Whether the user has been verified prior to receiving this request. Only
|
||||
* populated if device biometric verification was performed. If null, the application is responsible
|
||||
* for prompting user verification when it is deemed necessary.
|
||||
* @param userId ID of the user requesting credential authentication.
|
||||
* @param cipherId ID of the cipher to be authenticated against.
|
||||
* @param credentialId ID of the credential to authenticate.
|
||||
* @param requestData Provider request data in the form of a [Bundle].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CredentialAssertionRequest(
|
||||
val userId: String,
|
||||
val cipherId: String?,
|
||||
val credentialId: String?,
|
||||
val requestJson: String,
|
||||
val clientDataHash: ByteArray?,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
val isUserVerified: Boolean?,
|
||||
val cipherId: String,
|
||||
val credentialId: String,
|
||||
private val requestData: Bundle,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
|
||||
/**
|
||||
* The [ProviderGetCredentialRequest] from the [requestData].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val providerRequest: ProviderGetCredentialRequest by lazy {
|
||||
ProviderGetCredentialRequest.fromBundle(requestData)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [CallingAppInfo] from the [providerRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val callingAppInfo: CallingAppInfo by lazy { providerRequest.callingAppInfo }
|
||||
|
||||
/**
|
||||
* The [GetPublicKeyCredentialOption] from the [providerRequest], or null if one is not found
|
||||
* in the request options list.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val option: GetPublicKeyCredentialOption? by lazy {
|
||||
providerRequest.credentialOptions
|
||||
.firstNotNullOfOrNull { it as? GetPublicKeyCredentialOption }
|
||||
}
|
||||
}
|
||||
+31
-3
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* Represents possible outcomes of a FIDO 2 credential assertion request.
|
||||
*/
|
||||
@@ -15,5 +13,35 @@ sealed class Fido2CredentialAssertionResult {
|
||||
/**
|
||||
* Indicates there was an error and the assertion was not successful.
|
||||
*/
|
||||
data class Error(val message: Text) : Fido2CredentialAssertionResult()
|
||||
sealed class Error : Fido2CredentialAssertionResult() {
|
||||
/**
|
||||
* Indicates the relying party ID was missing from the request.
|
||||
*/
|
||||
data object MissingRpId : Error()
|
||||
|
||||
/**
|
||||
* Indicates the host URL was missing from the request.
|
||||
*/
|
||||
data object MissingHostUrl : Error()
|
||||
|
||||
/**
|
||||
* Indicates the calling application signature was invalid.
|
||||
*/
|
||||
data object InvalidAppSignature : Error()
|
||||
|
||||
/**
|
||||
* Indicates origin validation failed.
|
||||
*
|
||||
* @property originValidationError The specific error that caused the origin validation to
|
||||
* fail.
|
||||
*/
|
||||
data class OriginValidationFailed(
|
||||
val originValidationError: Fido2ValidateOriginResult.Error,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates an internal error occurred.
|
||||
*/
|
||||
data object InternalError : Error()
|
||||
}
|
||||
}
|
||||
+45
-17
@@ -1,34 +1,62 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 request to retrieve FIDO credentials parsed from the launching intent.
|
||||
*
|
||||
* @param userId The ID of the user's vault to search.
|
||||
* @param requestData Provider request data in the form of a [Bundle].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2GetCredentialsRequest(
|
||||
val candidateQueryData: Bundle,
|
||||
val id: String,
|
||||
val userId: String,
|
||||
val requestJson: String,
|
||||
val clientDataHash: ByteArray? = null,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
private val requestData: Bundle,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
/**
|
||||
* The [BeginGetCredentialRequest] from the [requestData], or null if the [requestData] does not
|
||||
* contain a [BeginGetCredentialRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val providerRequest: BeginGetCredentialRequest? by lazy {
|
||||
BeginGetCredentialRequest.fromBundle(requestData)
|
||||
}
|
||||
|
||||
val option: BeginGetPublicKeyCredentialOption
|
||||
get() = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData,
|
||||
id,
|
||||
requestJson,
|
||||
clientDataHash,
|
||||
)
|
||||
/**
|
||||
* The first [BeginGetPublicKeyCredentialOption] of the [providerRequest], or null if the
|
||||
* [providerRequest] is not a [BeginGetCredentialRequest] or does not contain a
|
||||
* [BeginGetPublicKeyCredentialOption].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption? by lazy {
|
||||
providerRequest
|
||||
?.beginGetCredentialOptions
|
||||
?.filterIsInstance<BeginGetPublicKeyCredentialOption>()
|
||||
?.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* The [CallingAppInfo] of the [providerRequest], or null if the [providerRequest] is not a
|
||||
* [BeginGetCredentialRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val callingAppInfo: CallingAppInfo? by lazy { providerRequest?.callingAppInfo }
|
||||
|
||||
/**
|
||||
* The first [BeginGetPublicKeyCredentialOption] of the [providerRequest], or null if the
|
||||
* [providerRequest] does not contain a [BeginGetPublicKeyCredentialOption].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val option: BeginGetPublicKeyCredentialOption? by lazy {
|
||||
providerRequest?.beginGetCredentialOptions
|
||||
?.firstNotNullOfOrNull {
|
||||
it as? BeginGetPublicKeyCredentialOption
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-7
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* Models the data returned from creating a FIDO 2 credential.
|
||||
*/
|
||||
@@ -17,10 +15,21 @@ sealed class Fido2RegisterCredentialResult {
|
||||
/**
|
||||
* Indicates there was an error and the credential was not registered.
|
||||
*/
|
||||
data class Error(val message: Text) : Fido2RegisterCredentialResult()
|
||||
sealed class Error : Fido2RegisterCredentialResult() {
|
||||
|
||||
/**
|
||||
* Indicates the user cancelled the request.
|
||||
*/
|
||||
data object Cancelled : Fido2RegisterCredentialResult()
|
||||
/**
|
||||
* Indicates the host URL was missing from the request.
|
||||
*/
|
||||
data object MissingHostUrl : Error()
|
||||
|
||||
/**
|
||||
* Indicates the app signature was invalid.
|
||||
*/
|
||||
data object InvalidAppSignature : Error()
|
||||
|
||||
/**
|
||||
* Indicates an internal error occurred.
|
||||
*/
|
||||
data object InternalError : Error()
|
||||
}
|
||||
}
|
||||
+7
-34
@@ -1,8 +1,5 @@
|
||||
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.
|
||||
*/
|
||||
@@ -19,66 +16,42 @@ sealed class 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() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_of_missing_asset_links
|
||||
}
|
||||
data object AssetLinkNotFound : Error()
|
||||
|
||||
/**
|
||||
* Indicates the application package name was not found in the digital asset links file.
|
||||
*/
|
||||
data object ApplicationNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
|
||||
}
|
||||
data object ApplicationNotFound : Error()
|
||||
|
||||
/**
|
||||
* Indicates the application fingerprint was not found the digital asset links file.
|
||||
*/
|
||||
data object ApplicationFingerprintNotVerified : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified
|
||||
}
|
||||
data object ApplicationFingerprintNotVerified : Error()
|
||||
|
||||
/**
|
||||
* Indicates the calling application is privileged but its package name is not found within
|
||||
* the privileged app allow list.
|
||||
*/
|
||||
data object PrivilegedAppNotAllowed : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_browser_is_not_privileged
|
||||
}
|
||||
data object PrivilegedAppNotAllowed : Error()
|
||||
|
||||
/**
|
||||
* Indicates the calling app is privileged but but no matching signing certificate signature
|
||||
* is present in the allow list.
|
||||
*/
|
||||
data object PrivilegedAppSignatureNotFound : Error() {
|
||||
override val messageResId =
|
||||
R.string.passkey_operation_failed_because_browser_signature_does_not_match
|
||||
}
|
||||
data object PrivilegedAppSignatureNotFound : Error()
|
||||
|
||||
/**
|
||||
* Indicates passkeys are not supported for the requesting application.
|
||||
*/
|
||||
data object PasskeyNotSupportedForApp : Error() {
|
||||
override val messageResId = R.string.passkeys_not_supported_for_this_app
|
||||
}
|
||||
data object PasskeyNotSupportedForApp : Error()
|
||||
|
||||
/**
|
||||
* Indicates an unknown error was encountered while validating the origin.
|
||||
*/
|
||||
data object Unknown : Error() {
|
||||
override val messageResId = R.string.generic_error_message
|
||||
}
|
||||
data object Unknown : Error()
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ package com.x8bit.bitwarden.data.autofill.fido2.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
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
|
||||
@@ -21,13 +21,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
|
||||
fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveProviderCreateCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val createPublicKeyRequest = systemRequest
|
||||
.callingRequest
|
||||
as? CreatePublicKeyCredentialRequest
|
||||
val systemRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val userId = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
@@ -35,11 +29,7 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
|
||||
|
||||
return Fido2CreateCredentialRequest(
|
||||
userId = userId,
|
||||
requestJson = createPublicKeyRequest.requestJson,
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
isUserVerified = systemRequest.biometricPromptResult?.isSuccessful,
|
||||
requestData = ProviderCreateCredentialRequest.asBundle(systemRequest),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,11 +44,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
.retrieveProviderGetCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val option: GetPublicKeyCredentialOption = systemRequest
|
||||
.credentialOptions
|
||||
.firstNotNullOfOrNull { it as? GetPublicKeyCredentialOption }
|
||||
?: return null
|
||||
|
||||
val credentialId = getStringExtra(EXTRA_KEY_CREDENTIAL_ID)
|
||||
?: return null
|
||||
|
||||
@@ -68,18 +53,11 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
val isUserVerified = systemRequest.biometricPromptResult?.isSuccessful
|
||||
|
||||
return Fido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
credentialId = credentialId,
|
||||
requestJson = option.requestJson,
|
||||
clientDataHash = option.clientDataHash,
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
isUserVerified = isUserVerified,
|
||||
requestData = ProviderGetCredentialRequest.asBundle(systemRequest),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,26 +72,11 @@ fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? {
|
||||
.retrieveBeginGetCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val option: BeginGetPublicKeyCredentialOption = systemRequest
|
||||
.beginGetCredentialOptions
|
||||
.firstNotNullOfOrNull { it as? BeginGetPublicKeyCredentialOption }
|
||||
?: return null
|
||||
|
||||
val callingAppInfo = systemRequest
|
||||
.callingAppInfo
|
||||
?: return null
|
||||
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
return Fido2GetCredentialsRequest(
|
||||
candidateQueryData = option.candidateQueryData,
|
||||
id = option.id,
|
||||
userId = userId,
|
||||
requestJson = option.requestJson,
|
||||
clientDataHash = option.clientDataHash,
|
||||
packageName = callingAppInfo.packageName,
|
||||
signingInfo = callingAppInfo.signingInfo,
|
||||
origin = callingAppInfo.origin,
|
||||
requestData = BeginGetCredentialRequest.asBundle(systemRequest),
|
||||
)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.util
|
||||
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
|
||||
/**
|
||||
* Retrieves the [CreatePublicKeyCredentialRequest] from a [ProviderCreateCredentialRequest],
|
||||
* otherwise null.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
fun ProviderCreateCredentialRequest.getCreatePasskeyCredentialRequestOrNull(): CreatePublicKeyCredentialRequest? =
|
||||
callingRequest as? CreatePublicKeyCredentialRequest
|
||||
@@ -3,21 +3,8 @@ package com.x8bit.bitwarden.data.platform.util
|
||||
import android.util.Base64
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Returns the name of the RP. If this [CallingAppInfo] is a privileged app the RP host name will be
|
||||
* returned. If this [CallingAppInfo] is a native RP application the package name will be returned.
|
||||
* Otherwise, `null` is returned.
|
||||
*/
|
||||
fun CallingAppInfo.getFido2RpIdOrNull(): String? =
|
||||
if (isOriginPopulated()) {
|
||||
origin?.toHostOrPathOrNull()
|
||||
} else {
|
||||
packageName
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the application's signing certificate hash formatted as a hex string if it has a single
|
||||
* signing certificate. Otherwise `null` is returned.
|
||||
|
||||
+5
-5
@@ -36,8 +36,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.inputFieldVisibilityToggleTestTag
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenInputLabel
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenInputTestTag
|
||||
@@ -45,6 +43,8 @@ import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenKeyboard
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenMessage
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenTitle
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
@@ -130,13 +130,13 @@ fun VaultUnlockScreen(
|
||||
|
||||
is VaultUnlockEvent.Fido2CredentialAssertionError -> {
|
||||
fido2CompletionManager.completeFido2Assertion(
|
||||
result = Fido2CredentialAssertionResult.Error(event.message),
|
||||
result = AssertFido2CredentialResult.Error(message = event.message),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultUnlockEvent.Fido2GetCredentialsError -> {
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(
|
||||
result = Fido2GetCredentialsResult.Error(message = event.message),
|
||||
fido2CompletionManager.completeFido2GetCredentialsRequest(
|
||||
result = GetFido2CredentialsResult.Error(message = event.message),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.ui.autofill.fido2.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
|
||||
/**
|
||||
* A manager for completing the FIDO 2 creation process.
|
||||
@@ -12,15 +12,15 @@ interface Fido2CompletionManager {
|
||||
/**
|
||||
* Completes the FIDO 2 registration process with the provided [result].
|
||||
*/
|
||||
fun completeFido2Registration(result: Fido2RegisterCredentialResult)
|
||||
fun completeFido2Registration(result: RegisterFido2CredentialResult)
|
||||
|
||||
/**
|
||||
* Complete the FIDO 2 credential assertion process with the provided [result].
|
||||
*/
|
||||
fun completeFido2Assertion(result: Fido2CredentialAssertionResult)
|
||||
fun completeFido2Assertion(result: AssertFido2CredentialResult)
|
||||
|
||||
/**
|
||||
* Complete the FIDO 2 "Get credentials" process with the provided [result].
|
||||
*/
|
||||
fun completeFido2GetCredentialRequest(result: Fido2GetCredentialsResult)
|
||||
fun completeFido2GetCredentialsRequest(result: GetFido2CredentialsResult)
|
||||
}
|
||||
+30
-17
@@ -10,15 +10,16 @@ import androidx.credentials.GetCredentialResponse
|
||||
import androidx.credentials.PublicKeyCredential
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialCancellationException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlin.random.Random
|
||||
|
||||
@@ -32,19 +33,21 @@ class Fido2CompletionManagerImpl(
|
||||
private val intentManager: IntentManager,
|
||||
) : Fido2CompletionManager {
|
||||
|
||||
override fun completeFido2Registration(result: Fido2RegisterCredentialResult) {
|
||||
override fun completeFido2Registration(result: RegisterFido2CredentialResult) {
|
||||
activity.also {
|
||||
val intent = Intent()
|
||||
when (result) {
|
||||
is Fido2RegisterCredentialResult.Error -> {
|
||||
is RegisterFido2CredentialResult.Error -> {
|
||||
PendingIntentHandler
|
||||
.setCreateCredentialException(
|
||||
intent = intent,
|
||||
exception = CreateCredentialUnknownException(),
|
||||
exception = CreateCredentialUnknownException(
|
||||
errorMessage = result.message.invoke(it.resources),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2RegisterCredentialResult.Success -> {
|
||||
is RegisterFido2CredentialResult.Success -> {
|
||||
PendingIntentHandler
|
||||
.setCreateCredentialResponse(
|
||||
intent = intent,
|
||||
@@ -54,7 +57,7 @@ class Fido2CompletionManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2RegisterCredentialResult.Cancelled -> {
|
||||
is RegisterFido2CredentialResult.Cancelled -> {
|
||||
PendingIntentHandler
|
||||
.setCreateCredentialException(
|
||||
intent = intent,
|
||||
@@ -67,19 +70,21 @@ class Fido2CompletionManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun completeFido2Assertion(result: Fido2CredentialAssertionResult) {
|
||||
override fun completeFido2Assertion(result: AssertFido2CredentialResult) {
|
||||
activity.also {
|
||||
val intent = Intent()
|
||||
when (result) {
|
||||
is Fido2CredentialAssertionResult.Error -> {
|
||||
is AssertFido2CredentialResult.Error -> {
|
||||
PendingIntentHandler
|
||||
.setGetCredentialException(
|
||||
intent = intent,
|
||||
exception = GetCredentialUnknownException(),
|
||||
exception = GetCredentialUnknownException(
|
||||
errorMessage = result.message.invoke(it.resources),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2CredentialAssertionResult.Success -> {
|
||||
is AssertFido2CredentialResult.Success -> {
|
||||
PendingIntentHandler
|
||||
.setGetCredentialResponse(
|
||||
intent = intent,
|
||||
@@ -88,17 +93,25 @@ class Fido2CompletionManagerImpl(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is AssertFido2CredentialResult.Cancelled -> {
|
||||
PendingIntentHandler
|
||||
.setGetCredentialException(
|
||||
intent = intent,
|
||||
exception = GetCredentialCancellationException(),
|
||||
)
|
||||
}
|
||||
}
|
||||
it.setResult(Activity.RESULT_OK, intent)
|
||||
it.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialsResult) {
|
||||
override fun completeFido2GetCredentialsRequest(result: GetFido2CredentialsResult) {
|
||||
val resultIntent = Intent()
|
||||
val responseBuilder = BeginGetCredentialResponse.Builder()
|
||||
when (result) {
|
||||
is Fido2GetCredentialsResult.Success -> {
|
||||
is GetFido2CredentialsResult.Success -> {
|
||||
val entries = result
|
||||
.credentials
|
||||
.map {
|
||||
@@ -116,7 +129,7 @@ class Fido2CompletionManagerImpl(
|
||||
username = it.userNameForUi
|
||||
?: activity.getString(R.string.no_username),
|
||||
pendingIntent = pendingIntent,
|
||||
beginGetPublicKeyCredentialOption = result.options,
|
||||
beginGetPublicKeyCredentialOption = result.option,
|
||||
)
|
||||
.setIcon(
|
||||
Icon
|
||||
@@ -139,11 +152,11 @@ class Fido2CompletionManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2GetCredentialsResult.Error -> {
|
||||
is GetFido2CredentialsResult.Error -> {
|
||||
PendingIntentHandler.setGetCredentialException(
|
||||
resultIntent,
|
||||
GetCredentialUnknownException(
|
||||
errorMessage = result.message.toString(activity.resources),
|
||||
errorMessage = result.message.invoke(activity.resources),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,17 +1,17 @@
|
||||
package com.x8bit.bitwarden.ui.autofill.fido2.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
|
||||
/**
|
||||
* A no-op implementation of [Fido2CompletionManagerImpl] provided when the build version is below
|
||||
* UPSIDE_DOWN_CAKE (34). These versions do not support [androidx.credentials.CredentialProvider].
|
||||
*/
|
||||
object Fido2CompletionManagerUnsupportedApiImpl : Fido2CompletionManager {
|
||||
override fun completeFido2Registration(result: Fido2RegisterCredentialResult) = Unit
|
||||
override fun completeFido2Registration(result: RegisterFido2CredentialResult) = Unit
|
||||
|
||||
override fun completeFido2Assertion(result: Fido2CredentialAssertionResult) = Unit
|
||||
override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit
|
||||
|
||||
override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialsResult) = Unit
|
||||
override fun completeFido2GetCredentialsRequest(result: GetFido2CredentialsResult) = Unit
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.ui.autofill.fido2.manager.model
|
||||
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* Represents the result of FIDO2 authentication.
|
||||
*/
|
||||
sealed class AssertFido2CredentialResult {
|
||||
/**
|
||||
* Represents a successful authentication of FIDO2 credentials.
|
||||
*/
|
||||
data class Success(val responseJson: String) : AssertFido2CredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates the user cancelled authentication.
|
||||
*/
|
||||
data object Cancelled : AssertFido2CredentialResult()
|
||||
|
||||
/**
|
||||
* Represents an error during FIDO2 credential assertion.
|
||||
*/
|
||||
data class Error(val message: Text) : AssertFido2CredentialResult()
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.x8bit.bitwarden.ui.autofill.fido2.manager.model
|
||||
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* Represents the result of fetching FIDO2 credentials.
|
||||
*/
|
||||
sealed class GetFido2CredentialsResult {
|
||||
|
||||
/**
|
||||
* Represents a successful result with a list of matching credentials, the original request
|
||||
* option, and associated user ID.
|
||||
*/
|
||||
data class Success(
|
||||
val credentials: List<Fido2CredentialAutofillView>,
|
||||
val option: BeginGetPublicKeyCredentialOption,
|
||||
val userId: String,
|
||||
) : GetFido2CredentialsResult()
|
||||
|
||||
/**
|
||||
* Represents an error result with a message.
|
||||
*
|
||||
* @property message The error message.
|
||||
*/
|
||||
data class Error(val message: Text) : GetFido2CredentialsResult()
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.ui.autofill.fido2.manager.model
|
||||
|
||||
import com.bitwarden.ui.util.Text
|
||||
|
||||
/**
|
||||
* Represents the result of a FIDO2 credential registration attempt.
|
||||
*/
|
||||
sealed class RegisterFido2CredentialResult {
|
||||
/**
|
||||
* Indicates that the registration was successful.
|
||||
*/
|
||||
data class Success(val responseJson: String) : RegisterFido2CredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred during registration.
|
||||
*/
|
||||
data class Error(val message: Text) : RegisterFido2CredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates that the registration was cancelled by the user.
|
||||
*/
|
||||
data object Cancelled : RegisterFido2CredentialResult()
|
||||
}
|
||||
+6
-3
@@ -133,21 +133,24 @@ class RootNavViewModel @Inject constructor(
|
||||
is SpecialCircumstance.Fido2Save -> {
|
||||
RootNavState.VaultUnlockedForFido2Save(
|
||||
activeUserId = userState.activeUserId,
|
||||
fido2CreateCredentialRequest = specialCircumstance.fido2CreateCredentialRequest,
|
||||
fido2CreateCredentialRequest =
|
||||
specialCircumstance.fido2CreateCredentialRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is SpecialCircumstance.Fido2Assertion -> {
|
||||
RootNavState.VaultUnlockedForFido2Assertion(
|
||||
activeUserId = userState.activeUserId,
|
||||
fido2CredentialAssertionRequest = specialCircumstance.fido2AssertionRequest,
|
||||
fido2CredentialAssertionRequest =
|
||||
specialCircumstance.fido2AssertionRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is SpecialCircumstance.Fido2GetCredentials -> {
|
||||
RootNavState.VaultUnlockedForFido2GetCredentials(
|
||||
activeUserId = userState.activeUserId,
|
||||
fido2GetCredentialsRequest = specialCircumstance.fido2GetCredentialsRequest,
|
||||
fido2GetCredentialsRequest =
|
||||
specialCircumstance.fido2GetCredentialsRequest,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+114
-71
@@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.DateTime
|
||||
@@ -20,6 +23,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getCreatePasskeyCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
@@ -44,6 +48,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
|
||||
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||
@@ -58,6 +63,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.messageResourceId
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
|
||||
@@ -134,13 +140,14 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
val totpData = specialCircumstance?.toTotpDataOrNull()
|
||||
// Check for Fido2 data to pre-populate
|
||||
val fido2CreationRequest = specialCircumstance?.toFido2CreateRequestOrNull()
|
||||
val fido2AttestationOptions = fido2CreationRequest?.let { request ->
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(request.requestJson)
|
||||
}
|
||||
val fido2AttestationOptions = fido2CreationRequest?.requestJson
|
||||
?.let { request ->
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(request)
|
||||
}
|
||||
|
||||
// Exit on save if handling an autofill, Fido2 Attestation, or TOTP link
|
||||
val shouldExitOnSave = autofillSaveItem != null ||
|
||||
fido2AttestationOptions != null
|
||||
fido2CreationRequest != null
|
||||
|
||||
val dialogState = if (!settingsRepository.initialAutofillDialogShown &&
|
||||
vaultAddEditType is VaultAddEditType.AddItem &&
|
||||
@@ -180,8 +187,9 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
dialog = dialogState,
|
||||
bottomSheetState = null,
|
||||
totpData = totpData,
|
||||
fido2CreateCredentialRequest = fido2CreationRequest,
|
||||
// Set special conditions for autofill and fido2 save
|
||||
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
|
||||
shouldShowCloseButton = autofillSaveItem == null && fido2CreationRequest == null,
|
||||
shouldExitOnSave = shouldExitOnSave,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldClearSpecialCircumstance = autofillSelectionData == null,
|
||||
@@ -405,10 +413,12 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
specialCircumstanceManager.specialCircumstance
|
||||
?.toFido2CreateRequestOrNull()
|
||||
state.fido2CreateCredentialRequest
|
||||
?.let { request ->
|
||||
handleFido2RequestSpecialCircumstance(request, content.toCipherView())
|
||||
handleProviderCreateCredentialRequest(
|
||||
request.providerRequest,
|
||||
content.toCipherView(),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
|
||||
@@ -435,65 +445,67 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFido2RequestSpecialCircumstance(
|
||||
request: Fido2CreateCredentialRequest,
|
||||
private fun handleProviderCreateCredentialRequest(
|
||||
request: ProviderCreateCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
request
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let { handleCreatePublicKeyCredentialRequest(request.callingAppInfo, it, cipherView) }
|
||||
?: run { handleUnsupportedProviderCreateCredentialRequest() }
|
||||
}
|
||||
|
||||
private fun handleCreatePublicKeyCredentialRequest(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
request: CreatePublicKeyCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
if (cipherView.isActiveWithFido2Credentials) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.OverwritePasskeyConfirmationPrompt)
|
||||
}
|
||||
} else {
|
||||
registerFido2Credential(request, cipherView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerFido2Credential(
|
||||
request: Fido2CreateCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
registerFido2CredentialToCipher(request, cipherView)
|
||||
return
|
||||
}
|
||||
|
||||
val createOptions = fido2CredentialManager
|
||||
.getPasskeyAttestationOptionsOrNull(request.requestJson)
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
registerFido2CredentialToCipher(callingAppInfo, request, cipherView)
|
||||
return
|
||||
}
|
||||
|
||||
when (createOptions.authenticatorSelection.userVerification) {
|
||||
when (val requirement = fido2CredentialManager.getUserVerificationRequirement(request)) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
registerFido2CredentialToCipher(request, cipherView)
|
||||
registerFido2CredentialToCipher(callingAppInfo, request, cipherView)
|
||||
}
|
||||
|
||||
UserVerificationRequirement.PREFERRED -> {
|
||||
sendEvent(VaultAddEditEvent.Fido2UserVerification(isRequired = false))
|
||||
}
|
||||
|
||||
UserVerificationRequirement.REQUIRED -> {
|
||||
sendEvent(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
else -> {
|
||||
sendEvent(
|
||||
VaultAddEditEvent.Fido2UserVerification(
|
||||
isRequired = requirement == UserVerificationRequirement.REQUIRED,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerFido2CredentialToCipher(
|
||||
request: Fido2CreateCredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
request: CreatePublicKeyCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val userId = authRepository.activeUserId
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val result: Fido2RegisterCredentialResult =
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = userId,
|
||||
fido2CreateCredentialRequest = request,
|
||||
callingAppInfo = callingAppInfo,
|
||||
createPublicKeyCredentialRequest = request,
|
||||
selectedCipherView = cipherView,
|
||||
)
|
||||
sendAction(
|
||||
@@ -502,6 +514,12 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUnsupportedProviderCreateCredentialRequest() {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_unsupported.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleAttachmentsClick() {
|
||||
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
|
||||
}
|
||||
@@ -568,20 +586,27 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleConfirmOverwriteExistingPasskeyClick() {
|
||||
specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2CreateRequestOrNull()
|
||||
state
|
||||
.fido2CreateCredentialRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
onContent { content ->
|
||||
registerFido2Credential(request, content.toCipherView())
|
||||
handleProviderCreateCredentialRequest(
|
||||
request,
|
||||
content.toCipherView(),
|
||||
)
|
||||
}
|
||||
}
|
||||
?: showFido2ErrorDialog()
|
||||
?: showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationLockOut() {
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationSuccess() {
|
||||
@@ -590,22 +615,25 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun getRequestAndRegisterCredential() =
|
||||
specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2CreateRequestOrNull()
|
||||
state.fido2CreateCredentialRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
onContent { content ->
|
||||
registerFido2CredentialToCipher(
|
||||
handleProviderCreateCredentialRequest(
|
||||
request = request,
|
||||
cipherView = content.toCipherView(),
|
||||
)
|
||||
}
|
||||
}
|
||||
?: showFido2ErrorDialog()
|
||||
?: showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_unsupported.asText(),
|
||||
)
|
||||
|
||||
private fun handleUserVerificationFail() {
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleFido2ErrorDialogDismissed(
|
||||
@@ -615,9 +643,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error(
|
||||
message = action.message,
|
||||
),
|
||||
result = RegisterFido2CredentialResult.Error(action.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -627,7 +653,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -640,7 +666,9 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
.value
|
||||
?.activeAccount
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -725,7 +753,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleDismissFido2VerificationDialogClick() {
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleSelectOrAddFolderForItem() {
|
||||
@@ -1821,17 +1852,24 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
when (action.result) {
|
||||
is Fido2RegisterCredentialResult.Error -> {
|
||||
sendEvent(VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText()))
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
action.result.messageResourceId.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2RegisterCredentialResult.Success -> {
|
||||
sendEvent(VaultAddEditEvent.ShowToast(R.string.item_updated.asText()))
|
||||
}
|
||||
|
||||
Fido2RegisterCredentialResult.Cancelled -> {
|
||||
// no-op: The OS will handle re-displaying the system prompt.
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(action.result.responseJson),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
sendEvent(VaultAddEditEvent.CompleteFido2Registration(action.result))
|
||||
}
|
||||
|
||||
private fun handleValidateFido2PasswordResultReceive(
|
||||
@@ -1841,7 +1879,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
|
||||
when (action.result) {
|
||||
is ValidatePasswordResult.Error -> {
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
is ValidatePasswordResult.Success -> {
|
||||
@@ -1863,7 +1904,9 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
|
||||
when (action.result) {
|
||||
is ValidatePinResult.Error -> {
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
is ValidatePinResult.Success -> {
|
||||
@@ -1885,7 +1928,9 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
it.copy(dialog = errorDialogState)
|
||||
}
|
||||
} else {
|
||||
showFido2ErrorDialog()
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1907,13 +1952,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
private fun showFido2ErrorDialog() {
|
||||
private fun showFido2ErrorDialog(message: Text) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error(message),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2080,6 +2122,7 @@ data class VaultAddEditState(
|
||||
val shouldExitOnSave: Boolean = false,
|
||||
val shouldClearSpecialCircumstance: Boolean = true,
|
||||
val totpData: TotpData? = null,
|
||||
val fido2CreateCredentialRequest: Fido2CreateCredentialRequest? = null,
|
||||
private val shouldShowCoachMarkTour: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
@@ -2697,7 +2740,7 @@ sealed class VaultAddEditEvent {
|
||||
* @property result the result of FIDO 2 credential registration.
|
||||
*/
|
||||
data class CompleteFido2Registration(
|
||||
val result: Fido2RegisterCredentialResult,
|
||||
val result: RegisterFido2CredentialResult,
|
||||
) : BackgroundEvent, VaultAddEditEvent()
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -17,10 +17,10 @@ fun Fido2CreateCredentialRequest.toDefaultAddTypeContent(
|
||||
isIndividualVaultDisabled: Boolean,
|
||||
): VaultAddEditState.ViewState.Content {
|
||||
|
||||
val rpUri = origin
|
||||
val rpUri = providerRequest.callingRequest.origin
|
||||
?.toUriOrNull()
|
||||
?.toString()
|
||||
?: packageName
|
||||
?: callingAppInfo.packageName
|
||||
.toAndroidAppUriString()
|
||||
|
||||
val rpName = attestationOptions
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
@@ -185,7 +185,7 @@ fun VaultItemListingScreen(
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.CompleteFido2GetCredentialsRequest -> {
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(event.result)
|
||||
fido2CompletionManager.completeFido2GetCredentialsRequest(event.result)
|
||||
}
|
||||
|
||||
VaultItemListingEvent.ExitApp -> exitManager.exitApplication()
|
||||
|
||||
+287
-191
@@ -2,11 +2,17 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.map
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -22,10 +28,10 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialReques
|
||||
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.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
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.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getCreatePasskeyCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
@@ -45,7 +51,6 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.platform.util.getFido2RpIdOrNull
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
@@ -53,11 +58,11 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toAndroidAppUriString
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
@@ -70,6 +75,7 @@ import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.components.util.toVaultItemCipherTypeOrNull
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.messageResourceId
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toSearchType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toVaultItemCipherType
|
||||
@@ -123,7 +129,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val specialCircumstance = specialCircumstanceManager.specialCircumstance
|
||||
val fido2CredentialRequest = specialCircumstance?.toFido2CreateRequestOrNull()
|
||||
val fido2CreateCredentialRequest = specialCircumstance?.toFido2CreateRequestOrNull()
|
||||
val fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull()
|
||||
val fido2AssertCredentialRequest = specialCircumstance?.toFido2AssertionRequestOrNull()
|
||||
VaultItemListingState(
|
||||
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
|
||||
.vaultItemListingType
|
||||
@@ -136,7 +144,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
dialogState = fido2CredentialRequest
|
||||
dialogState = fido2CreateCredentialRequest
|
||||
?.let { VaultItemListingState.DialogState.Loading(R.string.loading.asText()) },
|
||||
policyDisablesSend = policyManager
|
||||
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
|
||||
@@ -144,9 +152,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull(),
|
||||
hasMasterPassword = userState.activeAccount.hasMasterPassword,
|
||||
totpData = specialCircumstance?.toTotpDataOrNull(),
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
fido2CredentialAssertionRequest = specialCircumstance?.toFido2AssertionRequestOrNull(),
|
||||
fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull(),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
fido2CredentialAssertionRequest = fido2AssertCredentialRequest,
|
||||
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
|
||||
isPremium = userState.activeAccount.isPremium,
|
||||
isRefreshing = false,
|
||||
)
|
||||
@@ -165,24 +173,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
.onEach { sendAction(VaultItemListingsAction.Internal.IconLoadingSettingReceive(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
state
|
||||
.fido2CreateCredentialRequest
|
||||
?.let { request ->
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive(
|
||||
request = request,
|
||||
),
|
||||
)
|
||||
}
|
||||
?: observeVaultData()
|
||||
}
|
||||
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND)
|
||||
.map { VaultItemListingsAction.Internal.PolicyUpdateReceive(it.any()) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
specialCircumstanceManager.specialCircumstance
|
||||
?.toFido2CreateRequestOrNull()
|
||||
?.let { request ->
|
||||
trySendAction(
|
||||
VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive(
|
||||
request = request,
|
||||
),
|
||||
)
|
||||
}
|
||||
?: observeVaultData()
|
||||
}
|
||||
|
||||
private fun observeVaultData() {
|
||||
@@ -193,7 +199,6 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
it
|
||||
.filterForAutofillIfNecessary()
|
||||
.filterForFido2CreationIfNecessary()
|
||||
.filterForFidoGetCredentialsIfNecessary()
|
||||
.filterForTotpIfNecessary(),
|
||||
)
|
||||
}
|
||||
@@ -364,11 +369,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private fun handleUserVerificationCancelled() {
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
state.fido2CreateCredentialRequest
|
||||
?.let {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
?: state.fido2CredentialAssertionRequest
|
||||
?.let {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(
|
||||
result = AssertFido2CredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserVerificationNotSupported(
|
||||
@@ -679,10 +695,11 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isFido2Creation) {
|
||||
handleFido2RegistrationRequestReceive(action)
|
||||
return
|
||||
}
|
||||
state.fido2CreateCredentialRequest
|
||||
?.let {
|
||||
handleItemClickForProviderCreateCredentialRequest(action, it)
|
||||
return
|
||||
}
|
||||
|
||||
val event = when (state.itemListingType) {
|
||||
is VaultItemListingState.ItemListingType.Vault -> {
|
||||
@@ -699,7 +716,10 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
sendEvent(event)
|
||||
}
|
||||
|
||||
private fun handleFido2RegistrationRequestReceive(action: VaultItemListingsAction.ItemClick) {
|
||||
private fun handleItemClickForProviderCreateCredentialRequest(
|
||||
action: VaultItemListingsAction.ItemClick,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
) {
|
||||
val cipherView = getCipherViewOrNull(action.id)
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
@@ -709,12 +729,34 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
fido2CreateCredentialRequest
|
||||
.providerRequest
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let {
|
||||
handleItemClickForCreatePublicKeyCredentialRequest(
|
||||
cipherId = action.id,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_unsupported.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleItemClickForCreatePublicKeyCredentialRequest(
|
||||
cipherId: String,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
if (cipherView.isActiveWithFido2Credentials) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState
|
||||
.DialogState
|
||||
.OverwritePasskeyConfirmationPrompt(cipherViewId = action.id),
|
||||
.OverwritePasskeyConfirmationPrompt(
|
||||
cipherViewId = cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -723,17 +765,6 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun registerFido2Credential(cipherView: CipherView) {
|
||||
val credentialRequest = state
|
||||
.fido2CreateCredentialRequest
|
||||
?: run {
|
||||
// This scenario should not occur because `isFido2Creation` is false when
|
||||
// `fido2CredentialRequest` is null. We show the FIDO 2 error dialog to inform
|
||||
// the user and terminate the flow just in case it does occur.
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Loading(
|
||||
@@ -741,35 +772,45 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
// The user has performed verification implicitly so we continue FIDO 2 registration
|
||||
// without checking the request's user verification settings.
|
||||
registerFido2CredentialToCipher(
|
||||
request = credentialRequest,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
} else {
|
||||
performUserVerificationIfRequired(credentialRequest, cipherView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performUserVerificationIfRequired(
|
||||
credentialRequest: Fido2CreateCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val attestationOptions = fido2CredentialManager
|
||||
.getPasskeyAttestationOptionsOrNull(credentialRequest.requestJson)
|
||||
val providerRequest = state
|
||||
.fido2CreateCredentialRequest
|
||||
?.providerRequest
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
when (attestationOptions.authenticatorSelection.userVerification) {
|
||||
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
// The user has performed verification implicitly so we continue FIDO 2 registration
|
||||
// without checking the request's user verification settings.
|
||||
registerFido2CredentialToCipher(cipherView, providerRequest)
|
||||
} else {
|
||||
performUserVerificationIfRequired(cipherView, providerRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performUserVerificationIfRequired(
|
||||
cipherView: CipherView,
|
||||
providerRequest: ProviderCreateCredentialRequest,
|
||||
) {
|
||||
val userVerificationRequirement = providerRequest
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let { fido2CredentialManager.getUserVerificationRequirement(it) }
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_unsupported.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
when (userVerificationRequirement) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
registerFido2CredentialToCipher(
|
||||
request = credentialRequest,
|
||||
cipherView = cipherView,
|
||||
providerRequest = providerRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -794,8 +835,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun registerFido2CredentialToCipher(
|
||||
request: Fido2CreateCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
providerRequest: ProviderCreateCredentialRequest,
|
||||
) {
|
||||
val activeUserId = authRepository.activeUserId
|
||||
?: run {
|
||||
@@ -804,11 +845,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val createRequest = providerRequest
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result: Fido2RegisterCredentialResult =
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = activeUserId,
|
||||
fido2CreateCredentialRequest = request,
|
||||
callingAppInfo = providerRequest.callingAppInfo,
|
||||
createPublicKeyCredentialRequest = createRequest,
|
||||
selectedCipherView = cipherView,
|
||||
)
|
||||
sendAction(
|
||||
@@ -818,8 +870,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun authenticateFido2Credential(
|
||||
relyingPartyId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
request: ProviderGetCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val activeUserId = authRepository.activeUserId
|
||||
@@ -829,12 +880,32 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
)
|
||||
return
|
||||
}
|
||||
val option = request.credentialOptions
|
||||
.filterIsInstance<GetPublicKeyCredentialOption>()
|
||||
.firstOrNull()
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
val relyingPartyId = fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(option.requestJson)
|
||||
?.relyingPartyId
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
|
||||
val validateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
handleFido2OriginValidationFail(validateOriginResult)
|
||||
@@ -846,7 +917,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = activeUserId,
|
||||
selectedCipherView = cipherView,
|
||||
request = request,
|
||||
request = option,
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
origin = validateOriginResult.origin,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -975,9 +1048,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
state.fido2CreateCredentialRequest != null -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error(
|
||||
action.message,
|
||||
),
|
||||
result = RegisterFido2CredentialResult.Error(action.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -985,7 +1056,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
state.fido2CredentialAssertionRequest != null -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(
|
||||
result = Fido2CredentialAssertionResult.Error(
|
||||
result = AssertFido2CredentialResult.Error(
|
||||
message = action.message,
|
||||
),
|
||||
),
|
||||
@@ -995,7 +1066,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
state.fido2GetCredentialsRequest != null -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
|
||||
result = Fido2GetCredentialsResult.Error(
|
||||
result = GetFido2CredentialsResult.Error(
|
||||
message = action.message,
|
||||
),
|
||||
),
|
||||
@@ -1467,29 +1538,21 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun continueFido2Operation(cipherView: CipherView) {
|
||||
specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2CreateRequestOrNull()
|
||||
state.fido2CreateCredentialRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
registerFido2CredentialToCipher(
|
||||
request = request,
|
||||
cipherView = cipherView,
|
||||
providerRequest = request,
|
||||
)
|
||||
}
|
||||
?: specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2AssertionRequestOrNull()
|
||||
?: state.fido2CredentialAssertionRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?.let { relyingPartyId ->
|
||||
authenticateFido2Credential(
|
||||
relyingPartyId = relyingPartyId,
|
||||
request = request,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
authenticateFido2Credential(
|
||||
request = request,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
@@ -1518,34 +1581,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
|
||||
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
||||
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||
|
||||
state.fido2GetCredentialsRequest
|
||||
?.let { fido2GetCredentialsRequest ->
|
||||
val relyingPartyId = fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(
|
||||
requestJson = fido2GetCredentialsRequest.option.requestJson,
|
||||
)
|
||||
?.relyingPartyId
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid
|
||||
.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
|
||||
Fido2GetCredentialsResult.Success(
|
||||
userId = fido2GetCredentialsRequest.userId,
|
||||
options = fido2GetCredentialsRequest.option,
|
||||
credentials = vaultData
|
||||
.data
|
||||
.fido2CredentialAutofillViewList
|
||||
?.filter { it.rpId == relyingPartyId }
|
||||
?: emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
?.let { handleFido2GetCredentialsRequest(it, vaultData.data) }
|
||||
?: state.fido2CredentialAssertionRequest
|
||||
?.let { request ->
|
||||
trySendAction(
|
||||
@@ -1600,19 +1638,23 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private fun handleFido2RegisterCredentialRequestReceive(
|
||||
action: VaultItemListingsAction.Internal.Fido2RegisterCredentialRequestReceive,
|
||||
) {
|
||||
val relyingPartyId = action.request
|
||||
.providerRequest
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let { fido2CredentialManager.getPasskeyAttestationOptionsOrNull(it.requestJson) }
|
||||
?.relyingParty
|
||||
?.id
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val options = fido2CredentialManager
|
||||
.getPasskeyAttestationOptionsOrNull(requestJson = action.request.requestJson)
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val validateOriginResult = fido2OriginManager
|
||||
.validateOrigin(
|
||||
callingAppInfo = action.request.callingAppInfo,
|
||||
relyingPartyId = options.relyingParty.id,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
@@ -1632,18 +1674,90 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
clearDialogState()
|
||||
when (action.result) {
|
||||
is Fido2RegisterCredentialResult.Error -> {
|
||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()))
|
||||
handleFido2RegisterCredentialResultErrorReceive(action.result)
|
||||
}
|
||||
|
||||
is Fido2RegisterCredentialResult.Success -> {
|
||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.item_updated.asText()))
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(action.result.responseJson),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fido2RegisterCredentialResult.Cancelled -> {
|
||||
// no-op: The OS will handle re-displaying the system prompt.
|
||||
private fun handleFido2RegisterCredentialResultErrorReceive(
|
||||
error: Fido2RegisterCredentialResult.Error,
|
||||
) {
|
||||
sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()))
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
message = error.messageResourceId.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleFido2GetCredentialsRequest(
|
||||
request: Fido2GetCredentialsRequest,
|
||||
vaultData: VaultData,
|
||||
) {
|
||||
val beginGetCredentialOption = request
|
||||
.beginGetPublicKeyCredentialOption
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
val relyingPartyId = fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(requestJson = beginGetCredentialOption.requestJson)
|
||||
?.relyingPartyId
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
val callingAppInfo = request.callingAppInfo
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val validateOriginResult = fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
|
||||
GetFido2CredentialsResult.Success(
|
||||
userId = request.userId,
|
||||
option = beginGetCredentialOption,
|
||||
credentials = vaultData
|
||||
.toFido2CredentialAutofillViews()
|
||||
.orEmpty()
|
||||
.filter { it.rpId == relyingPartyId },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
handleFido2OriginValidationFail(validateOriginResult)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
sendEvent(VaultItemListingEvent.CompleteFido2Registration(action.result))
|
||||
}
|
||||
|
||||
private fun handleFido2OriginValidationFail(error: Fido2ValidateOriginResult.Error) {
|
||||
@@ -1651,7 +1765,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Fido2OperationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = error.messageResId.asText(),
|
||||
message = error.messageResourceId.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1674,13 +1788,15 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
.data
|
||||
.orEmpty()
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
if (request.cipherId.isNullOrEmpty()) {
|
||||
val selectedCipherId = request.cipherId
|
||||
|
||||
if (selectedCipherId.isEmpty()) {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_no_item_was_selected.asText(),
|
||||
)
|
||||
} else {
|
||||
val selectedCipher = ciphers
|
||||
.find { it.id == request.cipherId }
|
||||
.find { it.id == selectedCipherId }
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_selected_item_does_not_exist
|
||||
@@ -1692,9 +1808,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
if (state.hasMasterPassword &&
|
||||
selectedCipher.reprompt == CipherRepromptType.PASSWORD
|
||||
) {
|
||||
repromptMasterPasswordForFido2Assertion(request.cipherId)
|
||||
repromptMasterPasswordForFido2Assertion(selectedCipherId)
|
||||
} else {
|
||||
verifyUserAndAuthenticateCredential(request, selectedCipher)
|
||||
verifyUserAndAuthenticateCredential(request.providerRequest, selectedCipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1710,37 +1826,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun verifyUserAndAuthenticateCredential(
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
request: ProviderGetCredentialRequest,
|
||||
selectedCipher: CipherView,
|
||||
) {
|
||||
val assertionOptions = fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(request.requestJson)
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val relyingPartyId = assertionOptions.relyingPartyId
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (fido2CredentialManager.isUserVerified) {
|
||||
authenticateFido2Credential(relyingPartyId, request, selectedCipher)
|
||||
authenticateFido2Credential(request, selectedCipher)
|
||||
return
|
||||
}
|
||||
|
||||
when (assertionOptions.userVerification) {
|
||||
val userVerificationRequirement =
|
||||
fido2CredentialManager.getUserVerificationRequirement(request)
|
||||
when (userVerificationRequirement) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
authenticateFido2Credential(
|
||||
relyingPartyId,
|
||||
request,
|
||||
selectedCipher,
|
||||
request = request,
|
||||
cipherView = selectedCipher,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1759,9 +1860,27 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
) {
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(action.result),
|
||||
)
|
||||
when (action.result) {
|
||||
is Fido2CredentialAssertionResult.Error -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(
|
||||
AssertFido2CredentialResult.Error(
|
||||
message = action.result.messageResourceId.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Fido2CredentialAssertionResult.Success -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Assertion(
|
||||
AssertFido2CredentialResult.Success(
|
||||
responseJson = action.result.responseJson,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
|
||||
@@ -1848,30 +1967,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private suspend fun DataState<VaultData>.filterForFido2CreationIfNecessary(): DataState<VaultData> {
|
||||
val request = state.fido2CreateCredentialRequest ?: return this
|
||||
return this.map { vaultData ->
|
||||
val matchUri = request.origin
|
||||
?: request.packageName
|
||||
.toAndroidAppUriString()
|
||||
|
||||
vaultData.copy(
|
||||
cipherViewList = cipherMatchingManager.filterCiphersForMatches(
|
||||
ciphers = vaultData.cipherViewList,
|
||||
matchUri = matchUri,
|
||||
),
|
||||
fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the given vault data and filters it for FIDO 2 credential selection.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
private suspend fun DataState<VaultData>.filterForFidoGetCredentialsIfNecessary(): DataState<VaultData> {
|
||||
val request = state.fido2GetCredentialsRequest ?: return this
|
||||
return this.map { vaultData ->
|
||||
val matchUri = request.origin
|
||||
?: request.packageName
|
||||
.toAndroidAppUriString()
|
||||
val matchUri = request.providerRequest.callingRequest.origin
|
||||
?: request.callingAppInfo.packageName.toAndroidAppUriString()
|
||||
|
||||
vaultData.copy(
|
||||
cipherViewList = cipherMatchingManager.filterCiphersForMatches(
|
||||
@@ -1994,8 +2091,7 @@ data class VaultItemListingState(
|
||||
?.toHostOrPathOrNull()
|
||||
?.let { R.string.items_for_uri.asText(it) }
|
||||
?: fido2CreateCredentialRequest
|
||||
?.callingAppInfo
|
||||
?.getFido2RpIdOrNull()
|
||||
?.relyingPartyIdOrNull
|
||||
?.let { R.string.items_for_uri.asText(it) }
|
||||
?: totpData?.let { R.string.items_for_uri.asText(it.issuer ?: it.accountName ?: "--") }
|
||||
?: itemListingType.titleText
|
||||
@@ -2478,7 +2574,7 @@ sealed class VaultItemListingEvent {
|
||||
* @property result The result of FIDO 2 credential registration.
|
||||
*/
|
||||
data class CompleteFido2Registration(
|
||||
val result: Fido2RegisterCredentialResult,
|
||||
val result: RegisterFido2CredentialResult,
|
||||
) : BackgroundEvent, VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
@@ -2496,7 +2592,7 @@ sealed class VaultItemListingEvent {
|
||||
* @property result The result of the FIDO 2 credential assertion.
|
||||
*/
|
||||
data class CompleteFido2Assertion(
|
||||
val result: Fido2CredentialAssertionResult,
|
||||
val result: AssertFido2CredentialResult,
|
||||
) : BackgroundEvent, VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
@@ -2505,7 +2601,7 @@ sealed class VaultItemListingEvent {
|
||||
* @property result The result of querying for matching FIDO 2 credentials.
|
||||
*/
|
||||
data class CompleteFido2GetCredentialsRequest(
|
||||
val result: Fido2GetCredentialsResult,
|
||||
val result: GetFido2CredentialsResult,
|
||||
) : BackgroundEvent, VaultItemListingEvent()
|
||||
}
|
||||
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
|
||||
/**
|
||||
* Returns the string resource ID corresponding to the error message for the given
|
||||
* [Fido2CredentialAssertionResult.Error].
|
||||
*/
|
||||
val Fido2CredentialAssertionResult.Error.messageResourceId: Int
|
||||
@StringRes
|
||||
get() = when (this) {
|
||||
Fido2CredentialAssertionResult.Error.InternalError -> {
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error
|
||||
}
|
||||
|
||||
Fido2CredentialAssertionResult.Error.InvalidAppSignature -> {
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
}
|
||||
|
||||
Fido2CredentialAssertionResult.Error.MissingHostUrl -> {
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
}
|
||||
|
||||
Fido2CredentialAssertionResult.Error.MissingRpId -> {
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
}
|
||||
|
||||
is Fido2CredentialAssertionResult.Error.OriginValidationFailed -> {
|
||||
originValidationError.messageResourceId
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
|
||||
/**
|
||||
* Returns the string resource ID corresponding to the error message for the given
|
||||
* [Fido2RegisterCredentialResult.Error].
|
||||
*/
|
||||
val Fido2RegisterCredentialResult.Error.messageResourceId: Int
|
||||
@StringRes
|
||||
get() = when (this) {
|
||||
Fido2RegisterCredentialResult.Error.InternalError -> {
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error
|
||||
}
|
||||
|
||||
Fido2RegisterCredentialResult.Error.InvalidAppSignature -> {
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
}
|
||||
|
||||
Fido2RegisterCredentialResult.Error.MissingHostUrl -> {
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Returns the string resource ID corresponding to the error message for the given
|
||||
* [Fido2ValidateOriginResult.Error].
|
||||
*/
|
||||
val Fido2ValidateOriginResult.Error.messageResourceId: Int
|
||||
@StringRes
|
||||
get() = when (this) {
|
||||
Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified -> {
|
||||
R.string.passkey_operation_failed_because_app_could_not_be_verified
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.ApplicationNotFound -> {
|
||||
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.AssetLinkNotFound -> {
|
||||
R.string.passkey_operation_failed_because_of_missing_asset_links
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp -> {
|
||||
R.string.passkeys_not_supported_for_this_app
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppNotAllowed -> {
|
||||
R.string.passkey_operation_failed_because_browser_is_not_privileged
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound -> {
|
||||
R.string.passkey_operation_failed_because_browser_signature_does_not_match
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Error.Unknown -> {
|
||||
R.string.generic_error_message
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -6,6 +6,7 @@ import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -17,7 +18,6 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||
@@ -175,7 +175,7 @@ fun VaultData.toViewState(
|
||||
?.toHostOrPathOrNull()
|
||||
?.let { R.string.no_items_for_uri.asText(it) }
|
||||
?: fido2CreationData
|
||||
?.origin
|
||||
?.relyingPartyIdOrNull
|
||||
?.toHostOrPathOrNull()
|
||||
?.let { R.string.no_items_for_uri.asText(it) }
|
||||
?: totpData?.let { R.string.search_for_a_login_or_add_a_new_login.asText() }
|
||||
|
||||
@@ -1198,18 +1198,17 @@ Do you want to switch to this account?</string>
|
||||
<string name="system_certificates_are_not_as_secure_as_importing_certificates_to_bitwarden">Using a system certificate is less secure than storing the certificate with Bitwarden. Continuing will display a list of available system certificates if one is already installed.</string>
|
||||
<string name="certificate_alias_already_exists">Certificate alias already exists</string>
|
||||
<string name="link">Link</string>
|
||||
<string name="passkey_operation_failed_because_app_is_signed_incorrectly">Passkey operation failed because app is signed incorrectly.</string>
|
||||
<string name="passkey_operation_failed_because_host_url_is_not_present_in_request">Passkey operation failed because host URL is not present in request.</string>
|
||||
<string name="passkey_operation_failed_because_app_signature_is_invalid">Passkey operation failed because app signature is invalid.</string>
|
||||
<string name="passkey_registration_failed_due_to_an_internal_error">Passkey registration failed due to an internal error.</string>
|
||||
<string name="passkey_operation_failed_because_relying_party_cannot_be_identified">Passkey operation failed because relying party cannot be identified.</string>
|
||||
<string name="passkey_authentication_failed_due_to_an_internal_error">Passkey authentication failed due to an internal error.</string>
|
||||
<string name="passkey_operation_failed_because_the_selected_item_does_not_exist">Passkey operation failed because the selected item does not exist.</string>
|
||||
<string name="passkey_operation_failed_because_user_is_locked_out">Passkey operation failed because user is locked out.</string>
|
||||
<string name="passkey_operation_failed_because_user_verification_was_cancelled">Passkey operation failed because user verification was cancelled.</string>
|
||||
<string name="passkey_operation_failed_because_the_request_is_invalid">Passkey operation failed because the request is invalid.</string>
|
||||
<string name="passkey_operation_failed_because_user_verification_attempts_exceeded">Passkey operation failed because user verification attempts exceeded.</string>
|
||||
<string name="passkey_operation_failed_because_no_item_was_selected">Passkey operation failed because no item was selected.</string>
|
||||
<string name="passkey_operation_failed_because_the_request_is_unsupported">Passkey operation failed because the request is unsupported.</string>
|
||||
<string name="self_host_server_url">Self-host server URL</string>
|
||||
<string name="use_chrome_autofill_integration">Use Chrome autofill integration</string>
|
||||
<string name="use_chrome_beta_autofill_integration">Use Chrome autofill integration (Beta)</string>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BiometricPromptResult
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
|
||||
@@ -53,7 +58,6 @@ import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticato
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@@ -66,8 +70,10 @@ import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -147,6 +153,20 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
|
||||
} returns mutableMobileErrorReportingFeatureFlow
|
||||
}
|
||||
private val mockBiometricsPromptResult = mockk<BiometricPromptResult>(relaxed = true) {
|
||||
every { isSuccessful } returns true
|
||||
}
|
||||
private val mockProviderCreateCredentialRequest =
|
||||
mockk<ProviderCreateCredentialRequest>(relaxed = true) {
|
||||
every { biometricPromptResult } returns mockBiometricsPromptResult
|
||||
}
|
||||
private val mockProviderGetCredentialRequest =
|
||||
mockk<ProviderGetCredentialRequest>(relaxed = true) {
|
||||
every { biometricPromptResult } returns mockBiometricsPromptResult
|
||||
every { credentialOptions } returns listOf(
|
||||
mockk<GetPublicKeyCredentialOption>(relaxed = true),
|
||||
)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
@@ -166,6 +186,16 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
Intent::isPasswordGeneratorShortcut,
|
||||
Intent::isAccountSecurityShortcut,
|
||||
)
|
||||
mockkObject(
|
||||
ProviderCreateCredentialRequest.Companion,
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
)
|
||||
every {
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderCreateCredentialRequest
|
||||
every {
|
||||
ProviderGetCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderGetCredentialRequest
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -186,6 +216,10 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
Intent::isPasswordGeneratorShortcut,
|
||||
Intent::isAccountSecurityShortcut,
|
||||
)
|
||||
unmockkObject(
|
||||
ProviderCreateCredentialRequest.Companion,
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -514,33 +548,35 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should set the special circumstance to ExpiredRegistration if token is not valid`() {
|
||||
val viewModel = createViewModel()
|
||||
val intentEmail = "email"
|
||||
val token = "token"
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns intentEmail
|
||||
every { verificationToken } returns token
|
||||
}
|
||||
val mockIntent = createMockIntent(mockCompleteRegistrationData = completeRegistrationData)
|
||||
every { authRepository.activeUserId } returns null
|
||||
coEvery {
|
||||
authRepository.validateEmailToken(
|
||||
email = intentEmail,
|
||||
token = token,
|
||||
)
|
||||
} returns EmailTokenResult.Expired
|
||||
fun `on ReceiveFirstIntent with complete registration data should set the special circumstance to ExpiredRegistration if token is not valid`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val intentEmail = "email"
|
||||
val token = "token"
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData> {
|
||||
every { email } returns intentEmail
|
||||
every { verificationToken } returns token
|
||||
}
|
||||
val mockIntent =
|
||||
createMockIntent(mockCompleteRegistrationData = completeRegistrationData)
|
||||
every { authRepository.activeUserId } returns null
|
||||
coEvery {
|
||||
authRepository.validateEmailToken(
|
||||
email = intentEmail,
|
||||
token = token,
|
||||
)
|
||||
} returns EmailTokenResult.Expired
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink,
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
}
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink,
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
@@ -651,27 +687,16 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with fido2 request data should set the special circumstance to Fido2Save`() {
|
||||
fun `on ReceiveFirstIntent with fido2 create intent data should set the special circumstance to Fido2Save`() {
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
requestJson = """{"mockRequestJson":1}""",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
val fido2Intent = createMockIntent(
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
fido2CreateCredentialRequest.callingAppInfo,
|
||||
fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = fido2Intent,
|
||||
@@ -702,72 +727,68 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
verify {
|
||||
fido2CredentialManager.isUserVerified = createCredentialRequest.isUserVerified ?: false
|
||||
fido2CredentialManager.isUserVerified = true
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with fido2 request data should switch users if active user is not selected`() {
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "selectedUserId",
|
||||
requestJson = """{"mockRequestJson":1}""",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
isUserVerified = true,
|
||||
)
|
||||
val mockIntent = createMockIntent(
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
fido2CreateCredentialRequest.callingAppInfo,
|
||||
fido2CreateCredentialRequest.requestJson,
|
||||
fun `on ReceiveFirstIntent with fido2 create intent data should switch users if active user is not selected`() =
|
||||
runTest {
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "selectedUserId",
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
val mockIntent = createMockIntent(
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = any(),
|
||||
relyingPartyId = any(),
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
|
||||
verify(exactly = 1) { authRepository.switchAccount(fido2CreateCredentialRequest.userId) }
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
authRepository.switchAccount(fido2CreateCredentialRequest.userId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with fido2 request data should not switch users if active user is selected`() {
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
requestJson = """{"mockRequestJson":1}""",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
isUserVerified = true,
|
||||
)
|
||||
val mockIntent = createMockIntent(
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
fido2CreateCredentialRequest.callingAppInfo,
|
||||
fido2CreateCredentialRequest.requestJson,
|
||||
fun `on ReceiveFirstIntent with fido2 request data should not switch users if active user is selected`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
val mockIntent = createMockIntent(
|
||||
mockFido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(
|
||||
callingAppInfo = any(),
|
||||
relyingPartyId = any(),
|
||||
)
|
||||
} returns Fido2ValidateOriginResult.Success(null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
|
||||
verify(exactly = 0) { authRepository.switchAccount(fido2CreateCredentialRequest.userId) }
|
||||
}
|
||||
verify(exactly = 0) { authRepository.switchAccount(fido2CreateCredentialRequest.userId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
|
||||
+350
-272
@@ -3,6 +3,10 @@ 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.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.fido.ClientData
|
||||
@@ -10,17 +14,14 @@ import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
|
||||
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
|
||||
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
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
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
|
||||
@@ -37,13 +38,13 @@ import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -55,13 +56,13 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
private lateinit var fido2CredentialManager: Fido2CredentialManager
|
||||
|
||||
private val fido2OriginManager = mockk<Fido2OriginManager> {
|
||||
coEvery { validateOrigin(any(), any()) } returns Fido2ValidateOriginResult.Success(null)
|
||||
}
|
||||
private val json = mockk<Json> {
|
||||
every {
|
||||
decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} returns createMockPasskeyAttestationOptions(number = 1)
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
relyingPartyId = DEFAULT_HOST,
|
||||
)
|
||||
every {
|
||||
decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(number = 1)
|
||||
@@ -70,33 +71,57 @@ class Fido2CredentialManagerTest {
|
||||
} returns createMockPasskeyAssertionOptions(number = 1)
|
||||
}
|
||||
private val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature("0987654321ABCDEF"))
|
||||
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()
|
||||
}
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns true
|
||||
every { packageName } returns "com.x8bit.bitwarden"
|
||||
every { signingInfo } returns mockSigningInfo
|
||||
}
|
||||
val mockCreatePublicKeyCredentialRequest = mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns DEFAULT_FIDO2_CREATE_REQUEST_JSON
|
||||
every { clientDataHash } returns byteArrayOf()
|
||||
}
|
||||
val mockGetPublicKeyCredentialOption = mockk<GetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
every { clientDataHash } returns byteArrayOf()
|
||||
}
|
||||
val mockProviderGetCredentialRequest = mockk<ProviderGetCredentialRequest> {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every { credentialOptions } returns listOf(mockGetPublicKeyCredentialOption)
|
||||
}
|
||||
private val mockVaultSdkSource = mockk<VaultSdkSource>()
|
||||
private val mockFido2CredentialStore = mockk<Fido2CredentialStore>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(MessageDigest::class, Base64::class)
|
||||
mockkStatic(
|
||||
MessageDigest::class,
|
||||
Base64::class,
|
||||
)
|
||||
every { MessageDigest.getInstance(any()) } returns mockMessageDigest
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
|
||||
fido2CredentialManager = Fido2CredentialManagerImpl(
|
||||
vaultSdkSource = mockVaultSdkSource,
|
||||
fido2CredentialStore = mockFido2CredentialStore,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
json = json,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(MessageDigest::class, Base64::class)
|
||||
unmockkStatic(
|
||||
MessageDigest::class,
|
||||
Base64::class,
|
||||
)
|
||||
unmockkStatic(
|
||||
PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential,
|
||||
CallingAppInfo::getAppSigningSignatureFingerprint,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,9 +129,9 @@ class Fido2CredentialManagerTest {
|
||||
fun `getPasskeyAttestationOptionsOrNull should return passkey options when deserialized`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
createMockPasskeyAttestationOptions(number = 1),
|
||||
createMockPasskeyAttestationOptions(number = 1, relyingPartyId = DEFAULT_HOST),
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(
|
||||
requestJson = "",
|
||||
requestJson = DEFAULT_FIDO2_AUTH_REQUEST_JSON,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -169,110 +194,125 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `registerFido2Credential should construct ClientData DefaultWithCustomHash when callingAppInfo origin is populated`() =
|
||||
fun `registerFido2Credential should construct correct RegisterFido2CredentialRequest when callingAppInfo origin is populated`() =
|
||||
runTest {
|
||||
val mockFido2CreateCredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
origin = "origin",
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
val mockCipherView = createMockCipherView(1)
|
||||
val mockRegistrationResponse = createMockPublicKeyAttestationResponse(number = 1)
|
||||
mockkStatic(CallingAppInfo::getAppSigningSignatureFingerprint)
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
every { json.encodeToString<Fido2AttestationResponse>(any(), any()) } returns ""
|
||||
val requestCaptureSlot = slot<RegisterFido2CredentialRequest>()
|
||||
val selectedCipherView = createMockCipherView(number = 1)
|
||||
val slot = slot<RegisterFido2CredentialRequest>()
|
||||
|
||||
every {
|
||||
mockCallingAppInfo.getAppSigningSignatureFingerprint()
|
||||
} returns DEFAULT_APP_SIGNATURE.toByteArray()
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns DEFAULT_WEB_ORIGIN.v1
|
||||
coEvery {
|
||||
mockVaultSdkSource.registerFido2Credential(
|
||||
request = capture(requestCaptureSlot),
|
||||
request = capture(slot),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
mockRegistrationResponse
|
||||
.asSuccess()
|
||||
}
|
||||
} returns createMockPublicKeyAttestationResponse(number = 1).asSuccess()
|
||||
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2CreateCredentialRequest,
|
||||
selectedCipherView = mockCipherView,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = selectedCipherView,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
requestCaptureSlot.captured.clientData is ClientData.DefaultWithCustomHash,
|
||||
verify { mockCallingAppInfo.getAppSigningSignatureFingerprint() }
|
||||
assertEquals(
|
||||
"mockUserId",
|
||||
slot.captured.userId,
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_WEB_ORIGIN,
|
||||
slot.captured.origin,
|
||||
)
|
||||
assertEquals(
|
||||
"""{"publicKey": $DEFAULT_FIDO2_CREATE_REQUEST_JSON}""",
|
||||
slot.captured.requestJson,
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_APP_SIGNATURE,
|
||||
(slot.captured.clientData as ClientData.DefaultWithCustomHash)
|
||||
.hash
|
||||
.decodeToString(),
|
||||
)
|
||||
assertEquals(selectedCipherView, slot.captured.selectedCipherView)
|
||||
assertTrue(slot.captured.isUserVerificationSupported)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `registerFido2Credential should construct ClientData DefaultWithExtraData when callingAppInfo origin is null`() =
|
||||
fun `registerFido2Credential should construct correct RegisterFido2CredentialRequest when callingAppInfo origin is null`() =
|
||||
runTest {
|
||||
val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
val mockFido2Request = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRegistrationResponse = createMockPublicKeyAttestationResponse(number = 1)
|
||||
val slot = slot<RegisterFido2CredentialRequest>()
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
every { json.encodeToString<Fido2AttestationResponse>(any(), any()) } returns ""
|
||||
val requestCaptureSlot = slot<RegisterFido2CredentialRequest>()
|
||||
every { mockCallingAppInfo.isOriginPopulated() } returns false
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns null
|
||||
coEvery {
|
||||
mockVaultSdkSource.registerFido2Credential(
|
||||
request = capture(requestCaptureSlot),
|
||||
request = capture(slot),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
mockRegistrationResponse
|
||||
.asSuccess()
|
||||
}
|
||||
} returns mockRegistrationResponse.asSuccess()
|
||||
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2Request,
|
||||
selectedCipherView = createMockCipherView(1),
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = mockCipherView,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"mockUserId",
|
||||
slot.captured.userId,
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_ANDROID_ORIGIN,
|
||||
slot.captured.origin,
|
||||
)
|
||||
assertEquals(
|
||||
"""{"publicKey": ${DEFAULT_FIDO2_CREATE_REQUEST_JSON}}""",
|
||||
slot.captured.requestJson,
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_PACKAGE_NAME,
|
||||
(slot.captured.clientData as ClientData.DefaultWithExtraData)
|
||||
.androidPackageName,
|
||||
)
|
||||
assertEquals(mockCipherView, slot.captured.selectedCipherView)
|
||||
assertTrue(slot.captured.isUserVerificationSupported)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `registerFido2Credential should wrap request in webauthn json object`() =
|
||||
runTest {
|
||||
val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
val mockFido2CreateCredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
origin = "origin",
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
val mockCipherView = createMockCipherView(1)
|
||||
val mockRegistrationResponse = createMockPublicKeyAttestationResponse(number = 1)
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
every { json.encodeToString<Fido2AttestationResponse>(any(), any()) } returns ""
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns DEFAULT_WEB_ORIGIN.v1
|
||||
val requestCaptureSlot = slot<RegisterFido2CredentialRequest>()
|
||||
coEvery {
|
||||
mockVaultSdkSource.registerFido2Credential(
|
||||
request = capture(requestCaptureSlot),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
mockRegistrationResponse
|
||||
.asSuccess()
|
||||
}
|
||||
} returns mockRegistrationResponse.asSuccess()
|
||||
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2CreateCredentialRequest,
|
||||
selectedCipherView = mockCipherView,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""{"publicKey": ${mockFido2CreateCredentialRequest.requestJson}}""",
|
||||
"""{"publicKey": ${mockCreatePublicKeyCredentialRequest.requestJson}}""",
|
||||
requestCaptureSlot.captured.requestJson,
|
||||
)
|
||||
}
|
||||
@@ -280,46 +320,32 @@ class Fido2CredentialManagerTest {
|
||||
@Test
|
||||
fun `registerFido2Credential should register FIDO 2 credential to active user ID`() =
|
||||
runTest {
|
||||
val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
val mockFido2CreateCredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
origin = "origin",
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
|
||||
val mockCipherView = createMockCipherView(1)
|
||||
val mockRegistrationResponse = createMockPublicKeyAttestationResponse(number = 1)
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
every { json.encodeToString<Fido2AttestationResponse>(any(), any()) } returns ""
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns DEFAULT_WEB_ORIGIN.v1
|
||||
val requestCaptureSlot = slot<RegisterFido2CredentialRequest>()
|
||||
coEvery {
|
||||
mockVaultSdkSource.registerFido2Credential(
|
||||
request = capture(requestCaptureSlot),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
mockRegistrationResponse
|
||||
.asSuccess()
|
||||
}
|
||||
} returns mockRegistrationResponse.asSuccess()
|
||||
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2CreateCredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = mockCipherView,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"mockUserId",
|
||||
requestCaptureSlot.captured.userId,
|
||||
)
|
||||
|
||||
assertNotEquals(
|
||||
"mockUserId",
|
||||
mockFido2CreateCredentialRequest.userId,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -329,23 +355,16 @@ class Fido2CredentialManagerTest {
|
||||
val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { hasMultipleSigners() } returns true
|
||||
}
|
||||
val mockFido2CredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
origin = "origin",
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
|
||||
every { mockCallingAppInfo.signingInfo } returns mockSigningInfo
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2CredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_is_signed_incorrectly
|
||||
.asText(),
|
||||
),
|
||||
Fido2RegisterCredentialResult.Error.InvalidAppSignature,
|
||||
result,
|
||||
)
|
||||
}
|
||||
@@ -357,22 +376,17 @@ class Fido2CredentialManagerTest {
|
||||
val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { hasMultipleSigners() } returns true
|
||||
}
|
||||
val mockFido2CredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
every { mockCallingAppInfo.signingInfo } returns mockSigningInfo
|
||||
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2CredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_app_signature_is_invalid
|
||||
.asText(),
|
||||
),
|
||||
Fido2RegisterCredentialResult.Error.InvalidAppSignature,
|
||||
result,
|
||||
)
|
||||
}
|
||||
@@ -380,15 +394,7 @@ class Fido2CredentialManagerTest {
|
||||
@Test
|
||||
fun `registerFido2Credential should return Error when deserialization fails`() =
|
||||
runTest {
|
||||
val mockSigningInfo = mockk<SigningInfo> {
|
||||
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
|
||||
every { hasMultipleSigners() } returns false
|
||||
}
|
||||
val mockFido2CredentialRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
origin = "origin",
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
every { mockCallingAppInfo.signingInfo } returns mockSigningInfo
|
||||
val mockRegistrationResponse = createMockPublicKeyAttestationResponse(number = 1)
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
@@ -398,49 +404,41 @@ class Fido2CredentialManagerTest {
|
||||
any(),
|
||||
)
|
||||
} throws IllegalArgumentException()
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns DEFAULT_WEB_ORIGIN.v1
|
||||
coEvery {
|
||||
mockVaultSdkSource.registerFido2Credential(
|
||||
request = any(),
|
||||
fido2CredentialStore = any(),
|
||||
)
|
||||
} coAnswers {
|
||||
mockRegistrationResponse
|
||||
.asSuccess()
|
||||
}
|
||||
} returns mockRegistrationResponse.asSuccess()
|
||||
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CreateCredentialRequest = mockFido2CredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
),
|
||||
Fido2RegisterCredentialResult.Error.InternalError,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `registerFido2Credential should return Error when origin is null`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2CreateCredentialRequest(
|
||||
number = 1,
|
||||
origin = null,
|
||||
signingInfo = mockSigningInfo,
|
||||
)
|
||||
val mockSelectedCipher = createMockCipherView(number = 1)
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} throws SerializationException()
|
||||
every { mockCallingAppInfo.isOriginPopulated() } returns false
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns null
|
||||
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
userId = "activeUserId",
|
||||
fido2CreateCredentialRequest = mockAssertionRequest,
|
||||
selectedCipherView = mockSelectedCipher,
|
||||
userId = "mockUserId",
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
@@ -451,14 +449,49 @@ class Fido2CredentialManagerTest {
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Fido2RegisterCredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
),
|
||||
Fido2RegisterCredentialResult.Error.MissingHostUrl,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `registerFido2Credential should return InvalidAppSignature when calling app is not privileged and signature is invalid`() =
|
||||
runTest {
|
||||
every { mockCallingAppInfo.isOriginPopulated() } returns false
|
||||
every { mockSigningInfo.hasMultipleSigners() } returns true
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2RegisterCredentialResult.Error.InvalidAppSignature,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `registerFido2Credential should return MissingHostUrl when calling app if privileged and origin is missing`() =
|
||||
runTest {
|
||||
every { mockCreatePublicKeyCredentialRequest.origin } returns null
|
||||
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2RegisterCredentialResult.Error.MissingHostUrl,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `hasAuthenticationAttemptsRemaining returns true when authenticationAttempts is less than 5`() {
|
||||
@@ -478,13 +511,11 @@ class Fido2CredentialManagerTest {
|
||||
runTest {
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRequest = createMockFido2AssertionRequest(
|
||||
mockClientDataHash = byteArrayOf(),
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
|
||||
every { mockCallingAppInfo.isOriginPopulated() } returns true
|
||||
every { mockGetPublicKeyCredentialOption.clientDataHash } returns byteArrayOf(1, 2, 3)
|
||||
coEvery {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
request = any(),
|
||||
@@ -494,8 +525,10 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
request = mockGetPublicKeyCredentialOption,
|
||||
selectedCipherView = mockCipherView,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
origin = DEFAULT_WEB_ORIGIN.v1,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
@@ -508,8 +541,10 @@ class Fido2CredentialManagerTest {
|
||||
AuthenticateFido2CredentialRequest(
|
||||
userId = "activeUserId",
|
||||
origin = DEFAULT_WEB_ORIGIN,
|
||||
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
|
||||
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
|
||||
requestJson = """{"publicKey": ${mockGetPublicKeyCredentialOption.requestJson}}""",
|
||||
clientData = ClientData.DefaultWithCustomHash(
|
||||
hash = mockGetPublicKeyCredentialOption.clientDataHash!!,
|
||||
),
|
||||
selectedCipherView = mockCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
@@ -527,10 +562,10 @@ class Fido2CredentialManagerTest {
|
||||
every {
|
||||
mockSigningInfo.hasMultipleSigners()
|
||||
} returns false
|
||||
every { mockCallingAppInfo.isOriginPopulated() } returns true
|
||||
|
||||
every { mockGetPublicKeyCredentialOption.clientDataHash } returns null
|
||||
val mockCipherView = createMockCipherView(number = 1)
|
||||
val mockRequest = createMockFido2AssertionRequest(
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
|
||||
@@ -544,8 +579,10 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockRequest,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
request = mockGetPublicKeyCredentialOption,
|
||||
selectedCipherView = mockCipherView,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
origin = DEFAULT_WEB_ORIGIN.v1,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
@@ -558,7 +595,8 @@ class Fido2CredentialManagerTest {
|
||||
AuthenticateFido2CredentialRequest(
|
||||
userId = "activeUserId",
|
||||
origin = DEFAULT_WEB_ORIGIN,
|
||||
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
|
||||
requestJson =
|
||||
"""{"publicKey": ${mockGetPublicKeyCredentialOption.requestJson}}""",
|
||||
clientData = ClientData.DefaultWithExtraData(
|
||||
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
|
||||
),
|
||||
@@ -573,11 +611,6 @@ class Fido2CredentialManagerTest {
|
||||
@Test
|
||||
fun `authenticateFido2Credential should construct correct assertion request when calling app is unprivileged`() =
|
||||
runTest {
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockOrigin = null,
|
||||
mockClientDataHash = null,
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val mockAssertionOptions = createMockPasskeyAssertionOptions(number = 1)
|
||||
val mockSelectedCipher = createMockCipherView(number = 1)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
@@ -592,8 +625,10 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockAssertionRequest,
|
||||
request = mockGetPublicKeyCredentialOption,
|
||||
selectedCipherView = mockSelectedCipher,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
origin = null,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
@@ -619,12 +654,6 @@ class Fido2CredentialManagerTest {
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `authenticateFido2Credential should return Error when origin is null`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockOrigin = null,
|
||||
mockClientDataHash = null,
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
|
||||
val mockSelectedCipher = createMockCipherView(number = 1)
|
||||
|
||||
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
|
||||
@@ -634,8 +663,10 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
val result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockAssertionRequest,
|
||||
request = mockGetPublicKeyCredentialOption,
|
||||
selectedCipherView = mockSelectedCipher,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
origin = null,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
@@ -646,10 +677,7 @@ class Fido2CredentialManagerTest {
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
|
||||
.asText(),
|
||||
),
|
||||
Fido2CredentialAssertionResult.Error.MissingHostUrl,
|
||||
result,
|
||||
)
|
||||
}
|
||||
@@ -662,7 +690,6 @@ class Fido2CredentialManagerTest {
|
||||
PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential,
|
||||
)
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockRequest = createMockFido2AssertionRequest(mockSigningInfo = mockSigningInfo)
|
||||
val mockPublicKeyCredential = mockk<Fido2PublicKeyCredential>()
|
||||
val mockSdkResponse =
|
||||
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true) {
|
||||
@@ -678,8 +705,10 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
val authResult = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockRequest,
|
||||
request = mockGetPublicKeyCredentialOption,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
origin = null,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
@@ -703,7 +732,6 @@ class Fido2CredentialManagerTest {
|
||||
runTest {
|
||||
mockkStatic(PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential)
|
||||
every { Base64.encodeToString(any(), any()) } returns ""
|
||||
val mockRequest = createMockFido2AssertionRequest(mockSigningInfo = mockSigningInfo)
|
||||
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
|
||||
val mockPublicKeyCredential = mockk<Fido2PublicKeyCredential>()
|
||||
val mockSdkResponse =
|
||||
@@ -720,8 +748,10 @@ class Fido2CredentialManagerTest {
|
||||
|
||||
val authResult = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockRequest,
|
||||
request = mockGetPublicKeyCredentialOption,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
origin = null,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
@@ -734,74 +764,143 @@ class Fido2CredentialManagerTest {
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_authentication_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
),
|
||||
Fido2CredentialAssertionResult.Error.InternalError,
|
||||
authResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `authenticateFido2Credential should return Error when relyingPartyId is null`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val mockSelectedCipherView = createMockCipherView(number = 1)
|
||||
|
||||
fun `getUserVerificationRequirement should return correct verification required based on attestation options`() {
|
||||
// Verify REQUIRED is mapped correctly
|
||||
every {
|
||||
json.decodeFromStringOrNull<PasskeyAssertionOptions>(DEFAULT_FIDO2_AUTH_REQUEST_JSON)
|
||||
} returns createMockPasskeyAssertionOptions(number = 1, relyingPartyId = null)
|
||||
|
||||
val result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockAssertionRequest,
|
||||
selectedCipherView = mockSelectedCipherView,
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
mockVaultSdkSource.authenticateFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
assertEquals(
|
||||
UserVerificationRequirement.REQUIRED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockCreatePublicKeyCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.PREFERRED,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify PREFERRED is mapped correctly
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UserVerificationRequirement.PREFERRED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockCreatePublicKeyCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.REQUIRED,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify DISCOURAGED is mapped correctly
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UserVerificationRequirement.DISCOURAGED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockCreatePublicKeyCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.PREFERRED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getUserVerificationRequirement should return fallback requirement when attestation options are invalid`() {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} throws SerializationException()
|
||||
assertEquals(
|
||||
UserVerificationRequirement.PREFERRED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockCreatePublicKeyCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.PREFERRED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getUserVerificationRequirement should return fallback requirement based on assertion options`() =
|
||||
runTest {
|
||||
// Verify REQUIRED is mapped correctly
|
||||
every {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
assertEquals(
|
||||
UserVerificationRequirement.REQUIRED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockProviderGetCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.PREFERRED,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify PREFERRED is mapped correctly
|
||||
every {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
assertEquals(
|
||||
UserVerificationRequirement.REQUIRED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockProviderGetCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.PREFERRED,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify DISCOURAGED is mapped correctly
|
||||
every {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
)
|
||||
assertEquals(
|
||||
UserVerificationRequirement.DISCOURAGED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockProviderGetCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.PREFERRED,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionResult.Error(
|
||||
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
|
||||
.asText(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `authenticateFido2Credential should return Error when validateOrigin is Error`() = runTest {
|
||||
val mockAssertionRequest = createMockFido2AssertionRequest(
|
||||
mockOrigin = null,
|
||||
mockSigningInfo = mockSigningInfo,
|
||||
)
|
||||
val mockSelectedCipherView = createMockCipherView(number = 1)
|
||||
|
||||
coEvery {
|
||||
fido2OriginManager.validateOrigin(any(), any())
|
||||
} returns Fido2ValidateOriginResult.Error.Unknown
|
||||
|
||||
val result = fido2CredentialManager.authenticateFido2Credential(
|
||||
userId = "activeUserId",
|
||||
request = mockAssertionRequest,
|
||||
selectedCipherView = mockSelectedCipherView,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionResult.Error(R.string.generic_error_message.asText()),
|
||||
result,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
mockVaultSdkSource.authenticateFido2Credential(any(), any())
|
||||
fun `getUserVerificationRequirement should return fallback requirement when assertion options are invalid`() =
|
||||
runTest {
|
||||
every {
|
||||
mockProviderGetCredentialRequest.credentialOptions
|
||||
} returns emptyList()
|
||||
assertEquals(
|
||||
UserVerificationRequirement.REQUIRED,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockProviderGetCredentialRequest,
|
||||
fallbackRequirement = UserVerificationRequirement.REQUIRED,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden"
|
||||
@@ -812,33 +911,11 @@ private val DEFAULT_ANDROID_ORIGIN = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = DEFAULT_PACKAGE_NAME,
|
||||
sha256CertFingerprint = DEFAULT_CERT_FINGERPRINT,
|
||||
host = DEFAULT_HOST,
|
||||
assetLinkUrl = "bitwarden.com",
|
||||
host = "https://$DEFAULT_HOST",
|
||||
assetLinkUrl = "https://$DEFAULT_HOST",
|
||||
),
|
||||
)
|
||||
private val DEFAULT_WEB_ORIGIN = Origin.Web("bitwarden.com")
|
||||
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 DEFAULT_FIDO2_AUTH_REQUEST_JSON = """
|
||||
{
|
||||
"allowCredentials": [
|
||||
@@ -862,20 +939,21 @@ private const val DEFAULT_FIDO2_AUTH_REQUEST_JSON = """
|
||||
"userVerification": "preferred"
|
||||
}
|
||||
"""
|
||||
|
||||
private fun createMockFido2AssertionRequest(
|
||||
mockOrigin: String? = "bitwarden.com",
|
||||
mockClientDataHash: ByteArray? = null,
|
||||
mockSigningInfo: SigningInfo,
|
||||
) = mockk<Fido2CredentialAssertionRequest> {
|
||||
every { origin } returns mockOrigin
|
||||
every { requestJson } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
|
||||
every { clientDataHash } returns mockClientDataHash
|
||||
every { callingAppInfo } returns mockk {
|
||||
every { origin } returns mockOrigin
|
||||
every { packageName } returns DEFAULT_PACKAGE_NAME
|
||||
every { getOrigin(DEFAULT_ALLOW_LIST) } returns mockOrigin
|
||||
every { signingInfo } returns mockSigningInfo
|
||||
every { isOriginPopulated() } returns mockOrigin.isNullOrEmpty().not()
|
||||
}
|
||||
private const val DEFAULT_FIDO2_CREATE_REQUEST_JSON = """
|
||||
{
|
||||
"authenticatorSelection": { "residentKey":"required", "userVerification":"preferred" },
|
||||
"challenge":"TrvS2AJTFN877BNxAisWaMRvi_2wlT83ZD8IMry2QQs",
|
||||
"excludeCredentials":[],
|
||||
"extensions": { "credProps":true },
|
||||
"pubKeyCredParams":[
|
||||
{ "alg":-7, "type":"public-key" },
|
||||
{ "alg":-257, "type":"public-key" }
|
||||
],
|
||||
"rp": { "id":"learnpasskeys.io", "name":"Learn Passkeys" },
|
||||
"user": {
|
||||
"displayName":"Clyde O'Hara",
|
||||
"id":"TE8tc3VWR184emowN3liOVpwbDN6azA4SnA3cTUtUmo",
|
||||
"name":"overjoyed.clyde.0553"
|
||||
}
|
||||
}
|
||||
"""
|
||||
+2
-8
@@ -1,20 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
fun createMockFido2CredentialAssertionRequest(
|
||||
number: Int = 1,
|
||||
userId: String = "mockUserId-$number",
|
||||
isUserVerified: Boolean = false,
|
||||
): Fido2CredentialAssertionRequest =
|
||||
Fido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = "mockCipherId-$number",
|
||||
credentialId = "mockCredentialId-$number",
|
||||
requestJson = "mockRequestJson-$number",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
packageName = "mockPackageName-$number",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin-$number",
|
||||
isUserVerified = isUserVerified,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
+7
-12
@@ -1,20 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
/**
|
||||
* Creates a mock [Fido2CreateCredentialRequest] with a given [number].
|
||||
*/
|
||||
fun createMockFido2CreateCredentialRequest(
|
||||
number: Int,
|
||||
origin: String? = null,
|
||||
signingInfo: SigningInfo = SigningInfo(),
|
||||
): Fido2CreateCredentialRequest =
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-$number",
|
||||
requestJson = """{"request": {"number": $number}}""",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
signingInfo = signingInfo,
|
||||
origin = origin,
|
||||
isUserVerified = true,
|
||||
)
|
||||
requestData: Bundle = bundleOf(),
|
||||
): Fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-$number",
|
||||
requestData = requestData,
|
||||
)
|
||||
+2
-11
@@ -1,20 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
fun createMockFido2GetCredentialsRequest(
|
||||
number: Int,
|
||||
userId: String = "mockUserId-$number",
|
||||
signingInfo: SigningInfo = SigningInfo(),
|
||||
origin: String? = null,
|
||||
): Fido2GetCredentialsRequest = Fido2GetCredentialsRequest(
|
||||
candidateQueryData = Bundle(),
|
||||
id = "mockId-$number",
|
||||
userId = userId,
|
||||
requestJson = "requestJson-$number",
|
||||
clientDataHash = null,
|
||||
packageName = "mockPackageName-$number",
|
||||
signingInfo = signingInfo,
|
||||
origin = origin,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
+12
-61
@@ -22,7 +22,6 @@ import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
@@ -134,11 +133,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should invoke callback with error when user id is null`() {
|
||||
val request: BeginCreateCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
every { candidateQueryData } returns Bundle()
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
|
||||
@@ -162,11 +157,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should invoke callback with error on password create request`() {
|
||||
val request: BeginCreatePasswordCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
every { candidateQueryData } returns Bundle()
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
|
||||
@@ -187,11 +178,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should invoke callback with error when json is null or empty`() {
|
||||
val request: BeginCreatePublicKeyCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val candidateQueryData: Bundle = mockk()
|
||||
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
|
||||
@@ -215,11 +202,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should invoke callback with error when user state null`() {
|
||||
val request: BeginCreatePublicKeyCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val candidateQueryData: Bundle = mockk()
|
||||
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
|
||||
@@ -244,11 +227,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should invoke callback with result when user state is valid`() {
|
||||
val request: BeginCreatePublicKeyCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val candidateQueryData: Bundle = mockk()
|
||||
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
|
||||
@@ -286,11 +265,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should generate correct entries based on state`() {
|
||||
val request: BeginCreatePublicKeyCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val candidateQueryData: Bundle = mockk()
|
||||
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
|
||||
@@ -375,11 +350,7 @@ class Fido2ProviderProcessorTest {
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with error when user state is null`() {
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
every {
|
||||
beginGetCredentialOptions
|
||||
} returns listOf(
|
||||
@@ -412,11 +383,7 @@ class Fido2ProviderProcessorTest {
|
||||
every { candidateQueryData } returns Bundle()
|
||||
},
|
||||
)
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<BeginGetCredentialResponse>()
|
||||
@@ -477,11 +444,7 @@ class Fido2ProviderProcessorTest {
|
||||
every { candidateQueryData } returns Bundle()
|
||||
},
|
||||
)
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
@@ -508,11 +471,7 @@ class Fido2ProviderProcessorTest {
|
||||
)
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
@@ -542,11 +501,7 @@ class Fido2ProviderProcessorTest {
|
||||
)
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
@@ -593,11 +548,7 @@ class Fido2ProviderProcessorTest {
|
||||
)
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
every { callingAppInfo } returns CallingAppInfo(
|
||||
packageName = "com.x8bit.bitwarden.dev",
|
||||
signingInfo = mockk(),
|
||||
origin = null,
|
||||
)
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<BeginGetCredentialResponse>()
|
||||
|
||||
+59
-364
@@ -1,22 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.SigningInfo
|
||||
import android.service.credentials.BeginGetCredentialRequest
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPasswordOption
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
|
||||
@@ -39,14 +28,24 @@ class Fido2IntentUtilsTest {
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
mockkObject(PendingIntentHandler.Companion)
|
||||
mockkObject(
|
||||
PendingIntentHandler.Companion,
|
||||
BeginGetCredentialRequest.Companion,
|
||||
ProviderCreateCredentialRequest.Companion,
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
)
|
||||
every { isBuildVersionBelow(any()) } returns false
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(::isBuildVersionBelow)
|
||||
unmockkObject(PendingIntentHandler.Companion)
|
||||
unmockkObject(
|
||||
BeginGetCredentialRequest.Companion,
|
||||
PendingIntentHandler.Companion,
|
||||
ProviderCreateCredentialRequest.Companion,
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -55,82 +54,16 @@ class Fido2IntentUtilsTest {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
}
|
||||
val mockCallingRequest = mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns "requestJson"
|
||||
every { clientDataHash } returns byteArrayOf(0)
|
||||
every { preferImmediatelyAvailableCredentials } returns false
|
||||
every { origin } returns "mockOrigin"
|
||||
every { isAutoSelectAllowed } returns true
|
||||
}
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
val mockProviderRequest = ProviderCreateCredentialRequest(
|
||||
callingRequest = mockCallingRequest,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
every { ProviderCreateCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns mockProviderRequest
|
||||
} returns mockk()
|
||||
|
||||
val createRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
assertEquals(
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
requestJson = mockCallingRequest.requestJson,
|
||||
packageName = mockCallingAppInfo.packageName,
|
||||
signingInfo = mockCallingAppInfo.signingInfo,
|
||||
origin = mockCallingAppInfo.origin,
|
||||
isUserVerified = null,
|
||||
),
|
||||
createRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2CreateCredentialRequestOrNull should set isUserVerified when biometric prompt result is present`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
}
|
||||
val mockCallingRequest = mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns "requestJson"
|
||||
every { clientDataHash } returns byteArrayOf(0)
|
||||
every { preferImmediatelyAvailableCredentials } returns false
|
||||
every { origin } returns "mockOrigin"
|
||||
every { isAutoSelectAllowed } returns true
|
||||
}
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
val mockProviderRequest = ProviderCreateCredentialRequest(
|
||||
callingRequest = mockCallingRequest,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
biometricPromptResult = mockk {
|
||||
every { isSuccessful } returns true
|
||||
},
|
||||
)
|
||||
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns mockProviderRequest
|
||||
|
||||
val createRequest = intent.getFido2CreateCredentialRequestOrNull()
|
||||
assertEquals(
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
requestJson = mockCallingRequest.requestJson,
|
||||
packageName = mockCallingAppInfo.packageName,
|
||||
signingInfo = mockCallingAppInfo.signingInfo,
|
||||
origin = mockCallingAppInfo.origin,
|
||||
isUserVerified = true,
|
||||
),
|
||||
createRequest,
|
||||
"mockUserId",
|
||||
createRequest?.userId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -146,35 +79,9 @@ class Fido2IntentUtilsTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2CreateCredentialRequestOrNull should return null when intent is not a provider create credential request`() {
|
||||
val intent = mockk<Intent>()
|
||||
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns null
|
||||
|
||||
assertNull(intent.getFido2CreateCredentialRequestOrNull())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2CreateCredentialRequestOrNull should return null when calling request is not a public key credential create request`() {
|
||||
val intent = mockk<Intent>()
|
||||
val mockCallingRequest = mockk<CreatePasswordRequest>()
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
val mockProviderRequest = ProviderCreateCredentialRequest(
|
||||
callingRequest = mockCallingRequest,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns mockProviderRequest
|
||||
|
||||
assertNull(intent.getFido2CreateCredentialRequestOrNull())
|
||||
fun `getFido2CreateCredentialRequestOrNull should return null when retrieveProviderCreateCredentialRequest is null`() {
|
||||
every { PendingIntentHandler.retrieveProviderCreateCredentialRequest(any()) } returns null
|
||||
assertNull(mockk<Intent>().getFido2CreateCredentialRequestOrNull())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -183,26 +90,9 @@ class Fido2IntentUtilsTest {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns null
|
||||
}
|
||||
val mockCallingRequest = mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns "requestJson"
|
||||
every { clientDataHash } returns byteArrayOf(0)
|
||||
every { preferImmediatelyAvailableCredentials } returns false
|
||||
every { origin } returns "mockOrigin"
|
||||
every { isAutoSelectAllowed } returns true
|
||||
}
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
val mockProviderRequest = ProviderCreateCredentialRequest(
|
||||
callingRequest = mockCallingRequest,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||
} returns mockProviderRequest
|
||||
} returns mockk()
|
||||
|
||||
assertNull(intent.getFido2CreateCredentialRequestOrNull())
|
||||
}
|
||||
@@ -214,96 +104,24 @@ class Fido2IntentUtilsTest {
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
}
|
||||
val mockOption = GetPublicKeyCredentialOption(
|
||||
requestJson = "requestJson",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
allowedProviders = emptySet(),
|
||||
)
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
|
||||
credentialOptions = listOf(mockOption),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
every { ProviderGetCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockProviderGetCredentialRequest
|
||||
} returns mockk()
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
|
||||
assertNotNull(assertionRequest)
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionRequest(
|
||||
userId = "mockUserId",
|
||||
cipherId = "mockCipherId",
|
||||
credentialId = "mockCredentialId",
|
||||
requestJson = mockOption.requestJson,
|
||||
clientDataHash = mockOption.clientDataHash,
|
||||
packageName = mockCallingAppInfo.packageName,
|
||||
signingInfo = mockCallingAppInfo.signingInfo,
|
||||
origin = mockCallingAppInfo.origin,
|
||||
isUserVerified = null,
|
||||
),
|
||||
assertionRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should set isUserVerified when biometric prompt result is present`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
}
|
||||
val mockOption = GetPublicKeyCredentialOption(
|
||||
requestJson = "requestJson",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
allowedProviders = emptySet(),
|
||||
)
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
|
||||
credentialOptions = listOf(mockOption),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
biometricPromptResult = mockk {
|
||||
every { isSuccessful } returns true
|
||||
},
|
||||
)
|
||||
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockProviderGetCredentialRequest
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
|
||||
assertNotNull(assertionRequest)
|
||||
assertEquals(
|
||||
Fido2CredentialAssertionRequest(
|
||||
userId = "mockUserId",
|
||||
cipherId = "mockCipherId",
|
||||
credentialId = "mockCredentialId",
|
||||
requestJson = mockOption.requestJson,
|
||||
clientDataHash = mockOption.clientDataHash,
|
||||
packageName = mockCallingAppInfo.packageName,
|
||||
signingInfo = mockCallingAppInfo.signingInfo,
|
||||
origin = mockCallingAppInfo.origin,
|
||||
isUserVerified = true,
|
||||
),
|
||||
assertionRequest,
|
||||
)
|
||||
assertEquals("mockUserId", assertionRequest?.userId)
|
||||
assertEquals("mockCipherId", assertionRequest?.cipherId)
|
||||
assertEquals("mockCredentialId", assertionRequest?.credentialId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should return null when build version is below 34`() {
|
||||
val intent = mockk<Intent>()
|
||||
|
||||
every { isBuildVersionBelow(34) } returns true
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
@@ -325,91 +143,41 @@ class Fido2IntentUtilsTest {
|
||||
assertNull(assertionRequest)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should return null when no passkey credential options are present in request`() {
|
||||
val intent = mockk<Intent>()
|
||||
|
||||
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
|
||||
credentialOptions = listOf(GetPasswordOption()),
|
||||
callingAppInfo = mockk(),
|
||||
)
|
||||
fun `getFido2AssertionRequestOrNull should return null when extras are not correctly set`() {
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockProviderGetCredentialRequest
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(any())
|
||||
} returns mockk()
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
|
||||
assertNull(assertionRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should return null when credential id is not in extras`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns null
|
||||
}
|
||||
val mockOption = GetPublicKeyCredentialOption(
|
||||
requestJson = "requestJson",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
allowedProviders = emptySet(),
|
||||
// Verify credential ID is required
|
||||
assertNull(
|
||||
mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns null
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
}
|
||||
.getFido2AssertionRequestOrNull(),
|
||||
)
|
||||
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
|
||||
credentialOptions = listOf(mockOption),
|
||||
callingAppInfo = mockk(),
|
||||
)
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockProviderGetCredentialRequest
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
|
||||
assertNull(assertionRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should return null when cipher id is not in extras`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns null
|
||||
}
|
||||
val mockOption = GetPublicKeyCredentialOption(
|
||||
requestJson = "requestJson",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
allowedProviders = emptySet(),
|
||||
// Verify cipher ID is required
|
||||
assertNull(
|
||||
mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns null
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
|
||||
}
|
||||
.getFido2AssertionRequestOrNull(),
|
||||
)
|
||||
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
|
||||
credentialOptions = listOf(mockOption),
|
||||
callingAppInfo = mockk(),
|
||||
)
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockProviderGetCredentialRequest
|
||||
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
assertNull(assertionRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2AssertionRequestOrNull should return null when user id is not in extras`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns null
|
||||
}
|
||||
val mockOption = GetPublicKeyCredentialOption(
|
||||
requestJson = "requestJson",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
allowedProviders = emptySet(),
|
||||
// Verify user ID is required
|
||||
assertNull(
|
||||
mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
|
||||
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns null
|
||||
}
|
||||
.getFido2AssertionRequestOrNull(),
|
||||
)
|
||||
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
|
||||
credentialOptions = listOf(mockOption),
|
||||
callingAppInfo = mockk(),
|
||||
)
|
||||
every {
|
||||
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||
} returns mockProviderGetCredentialRequest
|
||||
val assertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
assertNull(assertionRequest)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -418,87 +186,28 @@ class Fido2IntentUtilsTest {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra("user_id") } returns "mockUserId"
|
||||
}
|
||||
val mockOption = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData = bundleOf(),
|
||||
id = "mockId",
|
||||
requestJson = "mockRequestJson",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
)
|
||||
val mockCallingAppInfo = CallingAppInfo(
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
)
|
||||
|
||||
every { BeginGetCredentialRequest.asBundle(any()) } returns bundleOf()
|
||||
every {
|
||||
PendingIntentHandler.retrieveBeginGetCredentialRequest(intent)
|
||||
} returns mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
}
|
||||
} returns mockk()
|
||||
|
||||
val result = intent.getFido2GetCredentialsRequestOrNull()
|
||||
|
||||
assertEquals(
|
||||
Fido2GetCredentialsRequest(
|
||||
candidateQueryData = mockOption.candidateQueryData,
|
||||
id = mockOption.id,
|
||||
userId = "mockUserId",
|
||||
requestJson = mockOption.requestJson,
|
||||
clientDataHash = mockOption.clientDataHash,
|
||||
packageName = mockCallingAppInfo.packageName,
|
||||
signingInfo = mockCallingAppInfo.signingInfo,
|
||||
origin = mockCallingAppInfo.origin,
|
||||
),
|
||||
result,
|
||||
)
|
||||
assertNotNull(intent.getFido2GetCredentialsRequestOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getGido2GetCredentialsRequestOrNull should return null when build version is below 34`() {
|
||||
val intent = mockk<Intent>()
|
||||
every { isBuildVersionBelow(34) } returns true
|
||||
|
||||
val result = intent.getFido2GetCredentialsRequestOrNull()
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2GetCredentialsRequestOrNull should return null when retrieveBeginGetCredentialRequest is null`() {
|
||||
val intent = mockk<Intent> {
|
||||
every {
|
||||
getParcelableExtra(
|
||||
"android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST",
|
||||
BeginGetCredentialRequest::class.java,
|
||||
)
|
||||
} returns null
|
||||
}
|
||||
every { PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) } returns null
|
||||
val result = intent.getFido2GetCredentialsRequestOrNull()
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getFido2GetCredentialRequestOrNull should return null when no passkey credential options are present`() {
|
||||
val intent = mockk<Intent>()
|
||||
every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockk<BeginGetPasswordOption>())
|
||||
}
|
||||
val result = intent.getFido2GetCredentialsRequestOrNull()
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2GetCredentialRequestOrNull should return null when calling app info is null`() {
|
||||
val intent = mockk<Intent>()
|
||||
val mockOption = createMockBeginGetPublicKeyCredentialOption(number = 1)
|
||||
every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
every { callingAppInfo } returns null
|
||||
}
|
||||
every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns null
|
||||
val result = intent.getFido2GetCredentialsRequestOrNull()
|
||||
assertNull(result)
|
||||
}
|
||||
@@ -508,22 +217,8 @@ class Fido2IntentUtilsTest {
|
||||
val intent = mockk<Intent> {
|
||||
every { getStringExtra(EXTRA_KEY_USER_ID) } returns null
|
||||
}
|
||||
val mockOption = createMockBeginGetPublicKeyCredentialOption(number = 1)
|
||||
every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
every { callingAppInfo } returns mockk()
|
||||
}
|
||||
every { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) } returns mockk()
|
||||
val result = intent.getFido2GetCredentialsRequestOrNull()
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockBeginGetPublicKeyCredentialOption(
|
||||
number: Int,
|
||||
): BeginGetPublicKeyCredentialOption =
|
||||
BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData = bundleOf(),
|
||||
id = "mockId-$number",
|
||||
requestJson = "mockRequestJson-$number",
|
||||
clientDataHash = byteArrayOf(0),
|
||||
)
|
||||
+2
-6
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.core.os.bundleOf
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
@@ -148,11 +148,7 @@ class SpecialCircumstanceExtensionsTest {
|
||||
fun `toFido2RequestOrNull should return a non-null value for Fido2Save`() {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
assertEquals(
|
||||
fido2CreateCredentialRequest,
|
||||
|
||||
+3
-43
@@ -30,46 +30,6 @@ class CallingAppInfoExtensionsTest {
|
||||
unmockkStatic(Base64::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2RpOrNull should return null when origin is populated with invalid URI`() {
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns true
|
||||
every { origin } returns "invalidUri9685%^$^&(*"
|
||||
}
|
||||
|
||||
assertNull(mockCallingAppInfo.getFido2RpIdOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2RpOrNull should return origin when origin is populated`() {
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns true
|
||||
every { origin } returns "mockUri"
|
||||
}
|
||||
|
||||
assertEquals("mockUri", mockCallingAppInfo.getFido2RpIdOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2RpOrNull should return null when origin is null`() {
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns true
|
||||
every { origin } returns null
|
||||
}
|
||||
|
||||
assertNull(mockCallingAppInfo.getFido2RpIdOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFido2RpOrNull should return package name when origin is not populated`() {
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { isOriginPopulated() } returns false
|
||||
every { packageName } returns "mockPackageName"
|
||||
}
|
||||
|
||||
assertEquals("mockPackageName", mockCallingAppInfo.getFido2RpIdOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCallingAppApkFingerprint should return key hash`() {
|
||||
val mockMessageDigest = mockk<MessageDigest> {
|
||||
@@ -85,7 +45,7 @@ class CallingAppInfoExtensionsTest {
|
||||
val appInfo = mockk<CallingAppInfo> {
|
||||
every { packageName } returns "packageName"
|
||||
every { signingInfo } returns mockSigningInfo
|
||||
every { origin } returns null
|
||||
every { getOrigin(any()) } returns null
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_SIGNATURE_HASH,
|
||||
@@ -107,7 +67,7 @@ class CallingAppInfoExtensionsTest {
|
||||
val appInfo = mockk<CallingAppInfo> {
|
||||
every { packageName } returns "packageName"
|
||||
every { signingInfo } returns mockSigningInfo
|
||||
every { origin } returns null
|
||||
every { getOrigin(any()) } returns null
|
||||
}
|
||||
assertNull(appInfo.getSignatureFingerprintAsHexString())
|
||||
}
|
||||
@@ -164,7 +124,7 @@ class CallingAppInfoExtensionsTest {
|
||||
fun `validatePrivilegedApp should return PrivilegedAppNotAllowed when calling app is not present in allow list`() {
|
||||
val appInfo = mockk<CallingAppInfo> {
|
||||
every { packageName } returns "packageName"
|
||||
every { origin } returns "origin"
|
||||
every { getOrigin(any()) } returns "origin"
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
|
||||
+6
-6
@@ -22,10 +22,10 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
@@ -80,7 +80,7 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
||||
}
|
||||
private val fido2CompletionManager: Fido2CompletionManager = mockk {
|
||||
every { completeFido2Assertion(any()) } just runs
|
||||
every { completeFido2GetCredentialRequest(any()) } just runs
|
||||
every { completeFido2GetCredentialsRequest(any()) } just runs
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -136,8 +136,8 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
||||
),
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(
|
||||
result = Fido2GetCredentialsResult.Error(
|
||||
fido2CompletionManager.completeFido2GetCredentialsRequest(
|
||||
result = GetFido2CredentialsResult.Error(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
)
|
||||
@@ -150,7 +150,7 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
||||
mutableEventFlow.tryEmit(VaultUnlockEvent.Fido2CredentialAssertionError("".asText()))
|
||||
verify(exactly = 1) {
|
||||
fido2CompletionManager.completeFido2Assertion(
|
||||
result = Fido2CredentialAssertionResult.Error("".asText()),
|
||||
result = AssertFido2CredentialResult.Error("".asText()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-29
@@ -4,16 +4,18 @@ import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.credentials.exceptions.GetCredentialCancellationException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.Called
|
||||
import io.mockk.MockKVerificationScope
|
||||
@@ -56,7 +58,7 @@ class Fido2CompletionManagerTest {
|
||||
|
||||
@Test
|
||||
fun `completeFido2Registration should perform no operations`() {
|
||||
val mockRegistrationResult = mockk<Fido2RegisterCredentialResult>()
|
||||
val mockRegistrationResult = mockk<RegisterFido2CredentialResult>()
|
||||
fido2CompletionManager.completeFido2Registration(mockRegistrationResult)
|
||||
verify {
|
||||
mockRegistrationResult wasNot Called
|
||||
@@ -66,7 +68,7 @@ class Fido2CompletionManagerTest {
|
||||
|
||||
@Test
|
||||
fun `completeFido2Assertion should perform no operations`() {
|
||||
val mockAssertionResult = mockk<Fido2CredentialAssertionResult>()
|
||||
val mockAssertionResult = mockk<AssertFido2CredentialResult>()
|
||||
fido2CompletionManager.completeFido2Assertion(mockAssertionResult)
|
||||
verify {
|
||||
mockAssertionResult wasNot Called
|
||||
@@ -76,8 +78,8 @@ class Fido2CompletionManagerTest {
|
||||
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should perform no operations`() {
|
||||
val mockGetCredentialResult = mockk<Fido2GetCredentialsResult>()
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(mockGetCredentialResult)
|
||||
val mockGetCredentialResult = mockk<GetFido2CredentialsResult>()
|
||||
fido2CompletionManager.completeFido2GetCredentialsRequest(mockGetCredentialResult)
|
||||
verify {
|
||||
mockGetCredentialResult wasNot Called
|
||||
mockActivity wasNot Called
|
||||
@@ -117,7 +119,7 @@ class Fido2CompletionManagerTest {
|
||||
fun `completeFido2Registration should set CreateCredentialResponse, set activity result, then finish activity when result is Success`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2Registration(
|
||||
Fido2RegisterCredentialResult.Success(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
responseJson = "registrationResponse",
|
||||
),
|
||||
)
|
||||
@@ -131,9 +133,10 @@ class Fido2CompletionManagerTest {
|
||||
@Test
|
||||
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Error`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2Registration(Fido2RegisterCredentialResult.Error("".asText()))
|
||||
.completeFido2Registration(RegisterFido2CredentialResult.Error("".asText()))
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
mockActivity.resources
|
||||
PendingIntentHandler.setCreateCredentialException(any(), any())
|
||||
}
|
||||
}
|
||||
@@ -142,7 +145,7 @@ class Fido2CompletionManagerTest {
|
||||
@Test
|
||||
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Cancelled`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2Registration(Fido2RegisterCredentialResult.Cancelled)
|
||||
.completeFido2Registration(RegisterFido2CredentialResult.Cancelled)
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setCreateCredentialException(any(), any())
|
||||
@@ -153,7 +156,7 @@ class Fido2CompletionManagerTest {
|
||||
@Test
|
||||
fun `completeFido2Assertion should set GetCredentialResponse, set activity result, then finish activity when result is Success`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2Assertion(Fido2CredentialAssertionResult.Success("responseJson"))
|
||||
.completeFido2Assertion(AssertFido2CredentialResult.Success("responseJson"))
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setGetCredentialResponse(any(), any())
|
||||
@@ -164,10 +167,27 @@ class Fido2CompletionManagerTest {
|
||||
@Test
|
||||
fun `completeFido2Assertion should set GetCredentialException, set activity result, then finish activity when result is Error`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2Assertion(Fido2CredentialAssertionResult.Error("".asText()))
|
||||
.completeFido2Assertion(AssertFido2CredentialResult.Error("".asText()))
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setGetCredentialException(any(), any())
|
||||
mockActivity.resources
|
||||
PendingIntentHandler.setGetCredentialException(
|
||||
any(),
|
||||
any<GetCredentialUnknownException>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2Assertion should set GetCredentialException, set activity result, then finish activity when result is Cancelled`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2Assertion(AssertFido2CredentialResult.Cancelled)
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setGetCredentialException(
|
||||
any(),
|
||||
any<GetCredentialCancellationException>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,11 +195,11 @@ class Fido2CompletionManagerTest {
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set BeginGetCredentialResponse, set activity result, then finish activity when result is Success`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialsResult.Success(
|
||||
.completeFido2GetCredentialsRequest(
|
||||
GetFido2CredentialsResult.Success(
|
||||
userId = "mockUserId",
|
||||
options = mockk(),
|
||||
credentials = emptyList(),
|
||||
option = mockk(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -196,7 +216,6 @@ class Fido2CompletionManagerTest {
|
||||
|
||||
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
|
||||
val mockFido2AutofillView = createMockFido2CredentialAutofillView(number = 1)
|
||||
val mockFido2AutofillViewList = listOf(mockFido2AutofillView)
|
||||
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
@@ -214,11 +233,11 @@ class Fido2CompletionManagerTest {
|
||||
every { Icon.createWithResource(mockActivity, any()) } returns mockk<Icon>()
|
||||
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialsResult.Success(
|
||||
.completeFido2GetCredentialsRequest(
|
||||
GetFido2CredentialsResult.Success(
|
||||
userId = "mockUserId",
|
||||
options = mockk(),
|
||||
credentials = mockFido2AutofillViewList,
|
||||
credentials = listOf(mockFido2AutofillView),
|
||||
option = mockk(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -265,11 +284,11 @@ class Fido2CompletionManagerTest {
|
||||
every { Icon.createWithResource(mockActivity, any()) } returns mockk<Icon>()
|
||||
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialsResult.Success(
|
||||
.completeFido2GetCredentialsRequest(
|
||||
GetFido2CredentialsResult.Success(
|
||||
userId = "mockUserId",
|
||||
options = mockk(),
|
||||
credentials = mockFido2AutofillViewList,
|
||||
option = mockk(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -293,10 +312,8 @@ class Fido2CompletionManagerTest {
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialsResult.Error(
|
||||
"".asText(),
|
||||
),
|
||||
.completeFido2GetCredentialsRequest(
|
||||
GetFido2CredentialsResult.Error("".asText()),
|
||||
)
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
mockActivity.resources
|
||||
|
||||
+2
-6
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.core.os.bundleOf
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -664,11 +664,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
fun `when the active user has an unlocked vault but there is a Fido2Save special circumstance the nav state should be VaultUnlockedForFido2Save`() {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "activeUserId",
|
||||
requestJson = "{}",
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "mockOrigin",
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(fido2CreateCredentialRequest)
|
||||
|
||||
+3
-3
@@ -40,13 +40,13 @@ import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.UriMatchType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@@ -222,7 +222,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on CompleteFido2Create event should invoke Fido2CompletionManager`() {
|
||||
val result = Fido2RegisterCredentialResult.Success(
|
||||
val result = RegisterFido2CredentialResult.Success(
|
||||
responseJson = "mockRegistrationResponse",
|
||||
)
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Registration(result = result))
|
||||
|
||||
+193
-198
@@ -1,6 +1,10 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.DateTime
|
||||
@@ -8,6 +12,8 @@ import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CollectionView
|
||||
import com.bitwarden.vault.FolderView
|
||||
@@ -60,9 +66,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
|
||||
@@ -84,8 +89,10 @@ import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -187,6 +194,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
fun setup() {
|
||||
mockkStatic(CipherView::toViewState)
|
||||
mockkStatic(UUID::randomUUID)
|
||||
mockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
every { UUID.randomUUID().toString() } returns TEST_ID
|
||||
}
|
||||
|
||||
@@ -194,6 +202,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
fun tearDown() {
|
||||
unmockkStatic(CipherView::toViewState)
|
||||
unmockkStatic(UUID::randomUUID)
|
||||
unmockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -382,13 +391,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial add state should be correct when fido2 save`() = runTest {
|
||||
every {
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockk(relaxed = true)
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-1",
|
||||
requestJson = "mockRequestJson-1",
|
||||
packageName = "mockPackageName-1",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = null,
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
@@ -403,7 +411,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
commonContentViewState = fido2ContentState.common,
|
||||
typeContentViewState = fido2ContentState.type,
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = initState,
|
||||
@@ -870,11 +880,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId",
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
origin = null,
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
@@ -887,6 +893,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
@@ -894,6 +901,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
@@ -908,27 +916,29 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
)
|
||||
val mockCreateResult = Fido2RegisterCredentialResult.Success(
|
||||
responseJson = "mockRegistrationResponse",
|
||||
)
|
||||
val mockAttestationOptions = createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
|
||||
val mockCreatePublicKeyCredentialRequest =
|
||||
mockk<CreatePublicKeyCredentialRequest>(relaxed = true)
|
||||
setupFido2CreateRequest(
|
||||
mockCreatePublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
selectedCipherView = any(),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
} returns mockCreateResult
|
||||
} returns Fido2RegisterCredentialResult.Success("mockRegistrationResponse")
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(
|
||||
requestJson = fido2CreateCredentialRequest.requestJson,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
} returns mockAttestationOptions
|
||||
} returns UserVerificationRequirement.REQUIRED
|
||||
every { authRepository.activeUserId } returns "mockUserId"
|
||||
coEvery {
|
||||
vaultRepository.createCipherInOrganization(any(), any())
|
||||
} returns CreateCipherResult.Success
|
||||
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
@@ -949,11 +959,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
val mockUserId = "mockUserId"
|
||||
val fido2CreateCredentialRequest = Fido2CreateCredentialRequest(
|
||||
userId = mockUserId,
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
origin = null,
|
||||
isUserVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
@@ -966,6 +972,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
@@ -973,9 +980,34 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
val mockCreatePublicKeyCredentialRequest =
|
||||
mockk<CreatePublicKeyCredentialRequest>(relaxed = true)
|
||||
setupFido2CreateRequest(
|
||||
mockCreatePublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
} returns UserVerificationRequirement.DISCOURAGED
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
selectedCipherView = any(),
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(responseJson = "mockResponse")
|
||||
every { authRepository.activeUserId } returns mockUserId
|
||||
coEvery {
|
||||
vaultRepository.createCipherInOrganization(any(), any())
|
||||
} returns CreateCipherResult.Success
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
@@ -986,24 +1018,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
)
|
||||
val mockCreateResult = Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
val mockAttestationOptions = createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = mockUserId,
|
||||
selectedCipherView = any(),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
} returns mockCreateResult
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(
|
||||
requestJson = fido2CreateCredentialRequest.requestJson,
|
||||
)
|
||||
} returns mockAttestationOptions
|
||||
every { authRepository.activeUserId } returns mockUserId
|
||||
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
@@ -1016,14 +1030,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
assertEquals(stateWithName, stateFlow.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockCreateResult),
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
responseJson = "mockResponse",
|
||||
),
|
||||
),
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = mockUserId,
|
||||
selectedCipherView = any(),
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1042,6 +1061,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
@@ -1056,23 +1076,47 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
val mockCreatePublicKeyCredentialRequest =
|
||||
mockk<CreatePublicKeyCredentialRequest>(relaxed = true)
|
||||
setupFido2CreateRequest(
|
||||
mockCreatePublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
} returns UserVerificationRequirement.DISCOURAGED
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
selectedCipherView = any(),
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(responseJson = "mockResponse")
|
||||
every { authRepository.activeUserId } returns fido2CredentialRequest.userId
|
||||
every { fido2CredentialManager.isUserVerified } returns true
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = fido2CredentialRequest.userId,
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = any(),
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(responseJson = "mockResponse")
|
||||
coEvery {
|
||||
vaultRepository.createCipherInOrganization(any(), any())
|
||||
} returns CreateCipherResult.Success
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
coVerify {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = fido2CredentialRequest.userId,
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
selectedCipherView = any(),
|
||||
callingAppInfo = any(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1083,7 +1127,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should show fido2 error dialog when create options are null`() =
|
||||
fun `in add mode during fido2, SaveClick should show fido2 error dialog when request type is not supported`() =
|
||||
runTest {
|
||||
val fido2CredentialRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
@@ -1094,14 +1138,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
val mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest =
|
||||
mockk<ProviderCreateCredentialRequest>(relaxed = true) {
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
every { callingRequest } returns mockk<CreatePasswordRequest>(relaxed = true)
|
||||
}
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns null
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderCreateCredentialRequest
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
@@ -1118,7 +1167,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
message = R.string.passkey_operation_failed_because_the_request_is_unsupported
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
@@ -1138,20 +1187,21 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = any<CreatePublicKeyCredentialRequest>(),
|
||||
)
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
)
|
||||
} returns UserVerificationRequirement.PREFERRED
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
@@ -1183,20 +1233,22 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
request = any<CreatePublicKeyCredentialRequest>(),
|
||||
)
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
} returns UserVerificationRequirement.REQUIRED
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
@@ -1880,6 +1932,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
val mockFido2CredentialRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||
val stateWithName = createVaultAddItemState(
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
@@ -1892,12 +1945,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
"14:30 PM",
|
||||
),
|
||||
),
|
||||
fido2CreateCredentialRequest = mockFido2CredentialRequest,
|
||||
)
|
||||
val mockFido2CredentialRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockFido2CredentialRequest,
|
||||
)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
|
||||
every {
|
||||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
@@ -1937,6 +1993,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||
val mockFidoRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
@@ -1944,15 +2001,23 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
originalCipher = cipherView,
|
||||
notes = "mockNotes-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = mockFidoRequest,
|
||||
)
|
||||
val mockFidoRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo>(relaxed = true)
|
||||
val mockCreatePublicKeyCredentialRequest =
|
||||
mockk<CreatePublicKeyCredentialRequest>(relaxed = true)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockFidoRequest,
|
||||
)
|
||||
setupFido2CreateRequest(
|
||||
mockCallingAppInfo = mockCallingAppInfo,
|
||||
mockCreatePublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = mockFidoRequest.userId,
|
||||
fido2CreateCredentialRequest = mockFidoRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
selectedCipherView = any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
@@ -1987,7 +2052,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
fido2CredentialManager.isUserVerified
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = mockFidoRequest.userId,
|
||||
fido2CreateCredentialRequest = mockFidoRequest,
|
||||
createPublicKeyCredentialRequest = mockCreatePublicKeyCredentialRequest,
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
selectedCipherView = any(),
|
||||
)
|
||||
}
|
||||
@@ -2001,6 +2067,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
number = 1,
|
||||
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
|
||||
)
|
||||
val mockFidoRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
@@ -2016,11 +2083,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
notes = "mockNotes-1",
|
||||
),
|
||||
fido2CreateCredentialRequest = mockFidoRequest,
|
||||
)
|
||||
val mockFidoRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockFidoRequest,
|
||||
)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
|
||||
every {
|
||||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
@@ -2034,11 +2104,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
} returns stateWithName.viewState
|
||||
every { fido2CredentialManager.isUserVerified } returns false
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(any())
|
||||
} returns createMockPasskeyAttestationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
fido2CredentialManager.getUserVerificationRequirement(
|
||||
any<CreatePublicKeyCredentialRequest>(),
|
||||
)
|
||||
} returns UserVerificationRequirement.REQUIRED
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(cipherView = cipherView),
|
||||
@@ -2053,9 +2122,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.ConfirmOverwriteExistingPasskeyClick)
|
||||
|
||||
coVerify {
|
||||
verify {
|
||||
fido2CredentialManager.isUserVerified
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(mockFidoRequest.requestJson)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2144,7 +2212,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error(
|
||||
result = RegisterFido2CredentialResult.Error(
|
||||
R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
@@ -3845,7 +3913,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
@@ -4234,64 +4302,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is null`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(
|
||||
responseJson = "mockResponse",
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when Fido2Request is null`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
AutofillSaveItem.Login(
|
||||
username = "mockUsername",
|
||||
password = "mockPassword",
|
||||
uri = "mockUri",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(
|
||||
responseJson = "mockResponse",
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when activeUserId is null`() {
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when request is invalid`() {
|
||||
every { authRepository.activeUserId } returns null
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(createMockFido2CreateCredentialRequest(number = 1))
|
||||
@@ -4300,7 +4311,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
message = R.string.passkey_operation_failed_because_the_request_is_unsupported
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
@@ -4309,7 +4320,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential`() =
|
||||
fun `UserVerificationSuccess should set isUserVerified to true`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Success(
|
||||
@@ -4318,36 +4329,21 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockRequest,
|
||||
)
|
||||
setupFido2CreateRequest()
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
every { fido2CredentialManager.isUserVerified } returns true
|
||||
|
||||
every { fido2CredentialManager.isUserVerified } returns false
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
coVerify {
|
||||
verify {
|
||||
fido2CredentialManager.isUserVerified = true
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = any(),
|
||||
fido2CreateCredentialRequest = mockRequest,
|
||||
selectedCipherView = any(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.item_updated.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4356,7 +4352,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Error("".asText())
|
||||
val mockResult = Fido2RegisterCredentialResult.Error.InternalError
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockRequest,
|
||||
)
|
||||
@@ -4366,9 +4362,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
setupFido2CreateRequest()
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
mockResult,
|
||||
@@ -4382,7 +4381,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
R.string.passkey_registration_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
@@ -4399,12 +4403,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockRequest,
|
||||
)
|
||||
setupFido2CreateRequest()
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
@@ -4421,39 +4427,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Cancelled should emit CompleteFido2Registration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CreateCredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Cancelled
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = mockRequest,
|
||||
)
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
mockResult,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
responseJson = "mockResponse",
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
@@ -4474,6 +4452,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
bottomSheetState: VaultAddEditState.BottomSheetState? = null,
|
||||
totpData: TotpData? = null,
|
||||
shouldClearSpecialCircumstance: Boolean = true,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest? = null,
|
||||
): VaultAddEditState =
|
||||
VaultAddEditState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
@@ -4489,6 +4468,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
totpData = totpData,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldClearSpecialCircumstance = shouldClearSpecialCircumstance,
|
||||
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@@ -4828,6 +4808,21 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(action)
|
||||
assertEquals(expectedState, viewModel.stateFlow.value.viewState)
|
||||
}
|
||||
|
||||
private fun setupFido2CreateRequest(
|
||||
mockCallingAppInfo: CallingAppInfo = mockk(relaxed = true),
|
||||
mockCreatePublicKeyCredentialRequest: CreatePublicKeyCredentialRequest =
|
||||
mockk<CreatePublicKeyCredentialRequest>(relaxed = true),
|
||||
mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest =
|
||||
mockk<ProviderCreateCredentialRequest>(relaxed = true) {
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
every { callingRequest } returns mockCreatePublicKeyCredentialRequest
|
||||
},
|
||||
) {
|
||||
every {
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderCreateCredentialRequest
|
||||
}
|
||||
//endregion Helper functions
|
||||
}
|
||||
|
||||
|
||||
+17
-11
@@ -1,11 +1,15 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -18,17 +22,23 @@ class Fido2CredentialRequestExtensionsTest {
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(UUID::class)
|
||||
mockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(UUID::class)
|
||||
unmockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toDefaultAddTypeContent should return the correct content when calling app is not privileged`() {
|
||||
every { UUID.randomUUID().toString() } returns "uuid"
|
||||
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk(relaxed = true) {
|
||||
every { callingRequest.origin } returns null
|
||||
every { callingAppInfo.packageName } returns "mockPackageName-1"
|
||||
}
|
||||
assertEquals(
|
||||
VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(
|
||||
@@ -49,11 +59,7 @@ class Fido2CredentialRequestExtensionsTest {
|
||||
),
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-1",
|
||||
requestJson = "mockRequestJson-1",
|
||||
packageName = "mockPackageName-1",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = null,
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
.toDefaultAddTypeContent(
|
||||
attestationOptions = createMockPasskeyAttestationOptions(1),
|
||||
@@ -65,6 +71,10 @@ class Fido2CredentialRequestExtensionsTest {
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toDefaultAddTypeContent should return the correct content when calling app is privileged`() {
|
||||
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk(relaxed = true) {
|
||||
every { callingRequest.origin } returns "www.test.com"
|
||||
every { callingAppInfo.packageName } returns "mockPackageName-1"
|
||||
}
|
||||
every { UUID.randomUUID().toString() } returns "uuid"
|
||||
assertEquals(
|
||||
VaultAddEditState.ViewState.Content(
|
||||
@@ -86,11 +96,7 @@ class Fido2CredentialRequestExtensionsTest {
|
||||
),
|
||||
Fido2CreateCredentialRequest(
|
||||
userId = "mockUserId-1",
|
||||
requestJson = "mockRequestJson-1",
|
||||
packageName = "mockPackageName-1",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "www.test.com",
|
||||
isUserVerified = true,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
.toDefaultAddTypeContent(
|
||||
attestationOptions = createMockPasskeyAttestationOptions(number = 1),
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ fun createMockPasskeyAttestationOptions(
|
||||
number: Int,
|
||||
userVerificationRequirement: UserVerificationRequirement =
|
||||
UserVerificationRequirement.PREFERRED,
|
||||
relyingPartyId: String = "mockPublicKeyCredentialRpEntity-$number",
|
||||
) = PasskeyAttestationOptions(
|
||||
authenticatorSelection = PasskeyAttestationOptions
|
||||
.AuthenticatorSelectionCriteria(userVerification = userVerificationRequirement),
|
||||
@@ -32,7 +33,7 @@ fun createMockPasskeyAttestationOptions(
|
||||
),
|
||||
relyingParty = PasskeyAttestationOptions.PublicKeyCredentialRpEntity(
|
||||
name = "mockPublicKeyCredentialRpEntityName-$number",
|
||||
id = "mockPublicKeyCredentialRpEntity-$number",
|
||||
id = relyingPartyId,
|
||||
),
|
||||
user = PasskeyAttestationOptions.PublicKeyCredentialUserEntity(
|
||||
name = "mockPublicKeyCredentialUserEntityName-$number",
|
||||
|
||||
+10
-10
@@ -16,19 +16,19 @@ import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.AssertFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.GetFido2CredentialsResult
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
@@ -102,7 +102,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
private val fido2CompletionManager: Fido2CompletionManager = mockk {
|
||||
every { completeFido2Registration(any()) } just runs
|
||||
every { completeFido2Assertion(any()) } just runs
|
||||
every { completeFido2GetCredentialRequest(any()) } just runs
|
||||
every { completeFido2GetCredentialsRequest(any()) } just runs
|
||||
}
|
||||
private val biometricsManager: BiometricsManager = mockk()
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>()
|
||||
@@ -1905,7 +1905,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `CompleteFido2Registration event should call Fido2CompletionManager with result`() {
|
||||
val result = Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
val result = RegisterFido2CredentialResult.Success("mockResponse")
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Registration(result))
|
||||
verify {
|
||||
fido2CompletionManager.completeFido2Registration(result)
|
||||
@@ -1914,7 +1914,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `CompleteFido2Assertion event should call Fido2CompletionManager with result`() {
|
||||
val result = Fido2CredentialAssertionResult.Success("mockResponse")
|
||||
val result = AssertFido2CredentialResult.Success("mockResponse")
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Assertion(result))
|
||||
verify {
|
||||
fido2CompletionManager.completeFido2Assertion(result)
|
||||
@@ -1923,14 +1923,14 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `CompleteFido2GetCredentials event should call Fido2CompletionManager with result`() {
|
||||
val result = Fido2GetCredentialsResult.Success(
|
||||
val result = GetFido2CredentialsResult.Success(
|
||||
userId = "mockUserId",
|
||||
options = mockk(),
|
||||
option = mockk(),
|
||||
credentials = mockk(),
|
||||
)
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2GetCredentialsRequest(result))
|
||||
verify {
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(result)
|
||||
fido2CompletionManager.completeFido2GetCredentialsRequest(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+343
-380
File diff suppressed because it is too large.
Load diff
+16
-8
@@ -1,9 +1,11 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.net.Uri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -22,14 +24,15 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -50,6 +53,7 @@ class VaultItemListingDataExtensionsTest {
|
||||
fun tearDown() {
|
||||
unmockkStatic(Uri::class)
|
||||
unmockkStatic(CipherView::subtitle)
|
||||
unmockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -677,6 +681,7 @@ class VaultItemListingDataExtensionsTest {
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform an empty list of CipherViews into a NoItems ViewState with the appropriate data`() {
|
||||
mockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = listOf(),
|
||||
collectionViewList = listOf(),
|
||||
@@ -684,6 +689,13 @@ class VaultItemListingDataExtensionsTest {
|
||||
sendViewList = listOf(),
|
||||
)
|
||||
|
||||
every {
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockk(relaxed = true) {
|
||||
every { callingAppInfo.isOriginPopulated() } returns true
|
||||
every { callingRequest.origin } returns "www.test.com"
|
||||
}
|
||||
|
||||
// Trash
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.NoItems(
|
||||
@@ -874,12 +886,8 @@ class VaultItemListingDataExtensionsTest {
|
||||
isIconLoadingDisabled = false,
|
||||
autofillSelectionData = null,
|
||||
fido2CreationData = Fido2CreateCredentialRequest(
|
||||
userId = "",
|
||||
requestJson = "",
|
||||
packageName = "",
|
||||
signingInfo = SigningInfo(),
|
||||
origin = "https://www.test.com",
|
||||
isUserVerified = true,
|
||||
userId = "userId",
|
||||
requestData = bundleOf(),
|
||||
),
|
||||
fido2CredentialAutofillViews = null,
|
||||
totpData = null,
|
||||
|
||||
@@ -17,7 +17,7 @@ androidxComposeBom = "2025.03.01"
|
||||
androidxCore = "1.15.0"
|
||||
# Versions >= 1.5.0-alpha05 introduce breaking changes and bugs that are not suitable for
|
||||
# production. Do not update until https://issuetracker.google.com/issues/355141766 is resolved.
|
||||
androidxCredentials = "1.5.0-alpha04"
|
||||
androidxCredentials = "1.5.0"
|
||||
androidxHiltNavigationCompose = "1.2.0"
|
||||
androidxLifecycle = "2.8.7"
|
||||
androidxNavigation = "2.8.0"
|
||||
|
||||
Reference in new issue
Block a user