mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-9408: Show bottom sheet with passkey options (#3444)
This commit is contained in:
parent
0e44b21361
commit
62154f5261
23 changed files with 784 additions and 18 deletions
|
@ -9,6 +9,8 @@
|
|||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorI
|
|||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -35,12 +36,18 @@ object Fido2ProviderModule {
|
|||
fun provideCredentialProviderProcessor(
|
||||
@ApplicationContext context: Context,
|
||||
authRepository: AuthRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
context,
|
||||
authRepository,
|
||||
vaultRepository,
|
||||
fido2CredentialStore,
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.bitwarden.vault.CipherView
|
|||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -36,6 +37,13 @@ interface Fido2CredentialManager {
|
|||
requestJson: String,
|
||||
): PasskeyAttestationOptions?
|
||||
|
||||
/**
|
||||
* Attempt to extract FIDO 2 passkey assertion options from the system [requestJson], or null.
|
||||
*/
|
||||
fun getPasskeyAssertionOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PasskeyAssertionOptions?
|
||||
|
||||
/**
|
||||
* Register a new FIDO 2 credential to a users vault.
|
||||
*/
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.Digita
|
|||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
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.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
@ -106,6 +107,17 @@ class Fido2CredentialManagerImpl(
|
|||
null
|
||||
}
|
||||
|
||||
override fun getPasskeyAssertionOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PasskeyAssertionOptions? =
|
||||
try {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
): Fido2ValidateOriginResult {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
/**
|
||||
* Models the request options for a passkey request, based off the spec found at:
|
||||
* https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
|
||||
*/
|
||||
@Serializable
|
||||
data class PasskeyAssertionOptions(
|
||||
@SerialName("challenge") val challenge: String,
|
||||
@SerialName("allowCredentials") val allowCredentials: List<PublicKeyCredentialDescriptor>?,
|
||||
@SerialName("rpId") val relyingPartyId: String?,
|
||||
@SerialName("userVerification") val userVerification: String?,
|
||||
)
|
|
@ -90,19 +90,6 @@ data class PasskeyAttestationOptions(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents details about a credential provided in the creation options.
|
||||
*/
|
||||
@Serializable
|
||||
data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents parameters for a credential in the creation options.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents details about a credential provided in the creation options.
|
||||
*/
|
||||
@Serializable
|
||||
data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>,
|
||||
)
|
|
@ -10,34 +10,50 @@ import androidx.credentials.exceptions.ClearCredentialUnsupportedException
|
|||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialCancellationException
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialUnsupportedException
|
||||
import androidx.credentials.provider.AuthenticationAction
|
||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
|
||||
private const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
|
||||
const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT"
|
||||
|
||||
/**
|
||||
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
|
||||
* processing.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
class Fido2ProviderProcessorImpl(
|
||||
private val context: Context,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : Fido2ProviderProcessor {
|
||||
|
@ -124,10 +140,106 @@ class Fido2ProviderProcessorImpl(
|
|||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
callback.onError(GetCredentialUnsupportedException())
|
||||
// If the user is not logged in, return an error.
|
||||
val userState = authRepository.userStateFlow.value
|
||||
if (userState == null) {
|
||||
callback.onError(GetCredentialUnknownException("Active user is required."))
|
||||
return
|
||||
}
|
||||
|
||||
// Return an unlock action if the current account is locked.
|
||||
if (!userState.activeAccount.isVaultUnlocked) {
|
||||
val authenticationAction = AuthenticationAction(
|
||||
title = context.getString(R.string.unlock),
|
||||
pendingIntent = intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
|
||||
callback.onResult(
|
||||
BeginGetCredentialResponse(
|
||||
authenticationActions = listOf(authenticationAction),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, find all matching credentials from the current vault.
|
||||
val getCredentialJob = scope.launch {
|
||||
try {
|
||||
val credentialEntries = getMatchingFido2CredentialEntries(
|
||||
userId = userState.activeUserId,
|
||||
request = request,
|
||||
)
|
||||
|
||||
callback.onResult(
|
||||
BeginGetCredentialResponse(
|
||||
credentialEntries = credentialEntries,
|
||||
),
|
||||
)
|
||||
} catch (e: GetCredentialException) {
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
cancellationSignal.setOnCancelListener {
|
||||
callback.onError(GetCredentialCancellationException())
|
||||
getCredentialJob.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws
|
||||
private suspend fun getMatchingFido2CredentialEntries(
|
||||
userId: String,
|
||||
request: BeginGetCredentialRequest,
|
||||
): List<CredentialEntry> =
|
||||
request
|
||||
.beginGetCredentialOptions
|
||||
.flatMap { option ->
|
||||
if (option is BeginGetPublicKeyCredentialOption) {
|
||||
val relyingPartyId = fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
|
||||
?.relyingPartyId
|
||||
?: throw GetCredentialUnknownException("Invalid data.")
|
||||
|
||||
vaultRepository
|
||||
.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { it.toCredentialEntries(option) },
|
||||
onFailure = {
|
||||
throw GetCredentialUnknownException("Error decrypting credentials.")
|
||||
},
|
||||
)
|
||||
} else {
|
||||
throw GetCredentialUnsupportedException("Unsupported option.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
): List<CredentialEntry> =
|
||||
this
|
||||
.map {
|
||||
PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = it.userNameForUi ?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun processClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
|
|
|
@ -446,4 +446,15 @@ interface VaultSdkSource {
|
|||
userId: String,
|
||||
vararg cipherViews: CipherView,
|
||||
): Result<List<Fido2CredentialAutofillView>>
|
||||
|
||||
/**
|
||||
* Silently discovers FIDO 2 credentials for a given [userId] and [relyingPartyId].
|
||||
*
|
||||
* @return A list of FIDO 2 credentials.
|
||||
*/
|
||||
suspend fun silentlyDiscoverCredentials(
|
||||
userId: String,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
relyingPartyId: String,
|
||||
): Result<List<Fido2CredentialAutofillView>>
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialAuthenticationUserInterfaceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialRegistrationUserInterfaceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialSearchUserInterfaceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
|
@ -524,6 +525,21 @@ class VaultSdkSourceImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun silentlyDiscoverCredentials(
|
||||
userId: String,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
relyingPartyId: String,
|
||||
): Result<List<Fido2CredentialAutofillView>> = runCatching {
|
||||
getClient(userId)
|
||||
.platform()
|
||||
.fido2()
|
||||
.authenticator(
|
||||
userInterface = Fido2CredentialSearchUserInterfaceImpl(),
|
||||
credentialStore = fido2CredentialStore,
|
||||
)
|
||||
.silentlyDiscoverCredentials(relyingPartyId)
|
||||
}
|
||||
|
||||
private suspend fun getClient(
|
||||
userId: String,
|
||||
): Client = sdkClientManager.getOrCreateClient(userId = userId)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.fido.CheckUserOptions
|
||||
import com.bitwarden.sdk.CheckUserAndPickCredentialForCreationResult
|
||||
import com.bitwarden.sdk.CheckUserResult
|
||||
import com.bitwarden.sdk.CipherViewWrapper
|
||||
import com.bitwarden.sdk.Fido2UserInterface
|
||||
import com.bitwarden.sdk.UiHint
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.Fido2CredentialNewView
|
||||
|
||||
/**
|
||||
* Implementation of [Fido2UserInterface] for searching for matching FIDO 2 credentials.
|
||||
*/
|
||||
class Fido2CredentialSearchUserInterfaceImpl : Fido2UserInterface {
|
||||
override suspend fun checkUser(
|
||||
options: CheckUserOptions,
|
||||
hint: UiHint,
|
||||
): CheckUserResult =
|
||||
CheckUserResult(
|
||||
userPresent = true,
|
||||
userVerified = true,
|
||||
)
|
||||
|
||||
override suspend fun checkUserAndPickCredentialForCreation(
|
||||
options: CheckUserOptions,
|
||||
newCredential: Fido2CredentialNewView,
|
||||
): CheckUserAndPickCredentialForCreationResult = throw IllegalStateException()
|
||||
|
||||
// Always return true for this property because any problems with verification should
|
||||
// be handled downstream where the app can actually offer verification methods.
|
||||
override suspend fun isVerificationEnabled(): Boolean = true
|
||||
|
||||
override suspend fun pickCredentialForAuthentication(
|
||||
availableCredentials: List<CipherView>,
|
||||
): CipherViewWrapper = throw IllegalStateException()
|
||||
}
|
|
@ -4,6 +4,8 @@ import android.net.Uri
|
|||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
@ -152,6 +154,15 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
|||
cipherViewList: List<CipherView>,
|
||||
): DecryptFido2CredentialAutofillViewResult
|
||||
|
||||
/**
|
||||
* Silently discovers FIDO 2 credentials for a given [userId] and [relyingPartyId].
|
||||
*/
|
||||
suspend fun silentlyDiscoverCredentials(
|
||||
userId: String,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
relyingPartyId: String,
|
||||
): Result<List<Fido2CredentialAutofillView>>
|
||||
|
||||
/**
|
||||
* Emits the totp code result flow to listeners.
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,8 @@ import com.bitwarden.core.InitOrgCryptoRequest
|
|||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.Send
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
|
@ -538,6 +540,18 @@ class VaultRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun silentlyDiscoverCredentials(
|
||||
userId: String,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
relyingPartyId: String,
|
||||
): Result<List<Fido2CredentialAutofillView>> =
|
||||
vaultSdkSource
|
||||
.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
|
||||
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
|
||||
}
|
||||
|
|
|
@ -91,6 +91,26 @@ interface IntentManager {
|
|||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing
|
||||
* [androidx.credentials.provider.CredentialEntry] instances for FIDO 2 credential filling.
|
||||
*/
|
||||
fun createFido2GetCredentialPendingIntent(
|
||||
action: String,
|
||||
credentialId: String,
|
||||
cipherId: String,
|
||||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing
|
||||
* [androidx.credentials.provider.AuthenticationAction] instances for FIDO 2 credential filling.
|
||||
*/
|
||||
fun createFido2UnlockPendingIntent(
|
||||
action: String,
|
||||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Represents file information.
|
||||
*/
|
||||
|
|
|
@ -51,6 +51,20 @@ private const val TEMP_CAMERA_IMAGE_DIR: String = "camera_temp"
|
|||
*/
|
||||
const val EXTRA_KEY_USER_ID: String = "user_id"
|
||||
|
||||
/**
|
||||
* Key for the credential id included in FIDO 2 provider "get entries".
|
||||
*
|
||||
* @see IntentManager.createFido2GetCredentialPendingIntent
|
||||
*/
|
||||
const val EXTRA_KEY_CREDENTIAL_ID: String = "credential_id"
|
||||
|
||||
/**
|
||||
* Key for the cipher id included in FIDO 2 provider "get entries".
|
||||
*
|
||||
* @see IntentManager.createFido2GetCredentialPendingIntent
|
||||
*/
|
||||
const val EXTRA_KEY_CIPHER_ID: String = "cipher_id"
|
||||
|
||||
/**
|
||||
* The default implementation of the [IntentManager] for simplifying the handling of Android
|
||||
* Intents within a given context.
|
||||
|
@ -210,6 +224,39 @@ class IntentManagerImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun createFido2GetCredentialPendingIntent(
|
||||
action: String,
|
||||
credentialId: String,
|
||||
cipherId: String,
|
||||
requestCode: Int,
|
||||
): PendingIntent {
|
||||
val intent = Intent(action)
|
||||
.setPackage(context.packageName)
|
||||
.putExtra(EXTRA_KEY_CREDENTIAL_ID, credentialId)
|
||||
.putExtra(EXTRA_KEY_CIPHER_ID, cipherId)
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
/* context = */ context,
|
||||
/* requestCode = */ requestCode,
|
||||
/* intent = */ intent,
|
||||
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun createFido2UnlockPendingIntent(
|
||||
action: String,
|
||||
requestCode: Int,
|
||||
): PendingIntent {
|
||||
val intent = Intent(action).setPackage(context.packageName)
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
/* context = */ context,
|
||||
/* requestCode = */ requestCode,
|
||||
/* intent = */ intent,
|
||||
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCameraFileData(): IntentManager.FileData {
|
||||
val tmpDir = File(context.filesDir, TEMP_CAMERA_IMAGE_DIR)
|
||||
val file = File(tmpDir, TEMP_CAMERA_IMAGE_NAME)
|
||||
|
|
|
@ -148,6 +148,7 @@
|
|||
<string name="no_favorites">There are no favorites in your vault.</string>
|
||||
<string name="no_items">There are no items in your vault.</string>
|
||||
<string name="no_items_tap">There are no items in your vault for this website/app. Tap to add one.</string>
|
||||
<string name="no_username">No Username</string>
|
||||
<string name="no_username_password_configured">This login does not have a username or password configured.</string>
|
||||
<string name="ok_got_it">Ok, got it!</string>
|
||||
<string name="option_defaults">Option defaults are set from the main Bitwarden app\'s password generator tool.</string>
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
|||
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.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
@ -21,6 +22,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPublicKeyAttestationResponse
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
@ -57,6 +59,9 @@ class Fido2CredentialManagerTest {
|
|||
every {
|
||||
decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} returns createMockPasskeyAttestationOptions(number = 1)
|
||||
every {
|
||||
decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} returns createMockPasskeyAssertionOptions(number = 1)
|
||||
}
|
||||
private val mockPrivilegedCallingAppInfo = mockk<CallingAppInfo> {
|
||||
every { packageName } returns "com.x8bit.bitwarden"
|
||||
|
@ -277,7 +282,7 @@ class Fido2CredentialManagerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getPasskeyCreateOptionsOrNull should return null when deserialization fails`() =
|
||||
fun `getPasskeyAttestationOptionsOrNull should return null when deserialization fails`() =
|
||||
runTest {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
|
@ -291,7 +296,7 @@ class Fido2CredentialManagerTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPasskeyCreateOptionsOrNull should return null when IllegalArgumentException is thrown`() {
|
||||
fun `getPasskeyAttestationOptionsOrNull should return null when IllegalArgumentException is thrown`() {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAttestationOptions>(any())
|
||||
} throws IllegalArgumentException()
|
||||
|
@ -299,6 +304,39 @@ class Fido2CredentialManagerTest {
|
|||
assertNull(fido2CredentialManager.getPasskeyAttestationOptionsOrNull(requestJson = ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasskeyAssertionOptionsOrNull should return options when deserialized`() = runTest {
|
||||
assertEquals(
|
||||
createMockPasskeyAssertionOptions(number = 1),
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
|
||||
requestJson = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasskeyAssertionOptionsOrNull should return null when deserialization fails`() =
|
||||
runTest {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} throws SerializationException()
|
||||
assertNull(
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
|
||||
requestJson = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPasskeyAssertionOptionsOrNull should return null when IllegalArgumentException is thrown`() {
|
||||
every {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(any())
|
||||
} throws IllegalArgumentException()
|
||||
|
||||
assertNull(fido2CredentialManager.getPasskeyAssertionOptionsOrNull(requestJson = ""))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `registerFido2Credential should construct ClientData DefaultWithCustomHash when callingAppInfo origin is populated`() =
|
||||
|
|
|
@ -7,24 +7,49 @@ import android.os.CancellationSignal
|
|||
import android.os.OutcomeReceiver
|
||||
import androidx.credentials.exceptions.CreateCredentialException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialUnsupportedException
|
||||
import androidx.credentials.provider.AuthenticationAction
|
||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -35,19 +60,35 @@ class Fido2ProviderProcessorTest {
|
|||
|
||||
private val context: Context = mockk()
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val mutableCiphersStateFlow = MutableStateFlow<DataState<List<CipherView>>>(
|
||||
DataState.Loaded(emptyList()),
|
||||
)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { activeUserId } returns "mockActiveUserId"
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { ciphersStateFlow } returns mutableCiphersStateFlow
|
||||
}
|
||||
private val passkeyAssertionOptions = createMockPasskeyAssertionOptions(number = 1)
|
||||
private val fido2CredentialManager: Fido2CredentialManager = mockk {
|
||||
every { getPasskeyAssertionOptionsOrNull(any()) } returns passkeyAssertionOptions
|
||||
}
|
||||
private val fido2CredentialStore: Fido2CredentialStore = mockk()
|
||||
private val intentManager: IntentManager = mockk()
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
private val cancellationSignal: CancellationSignal = mockk()
|
||||
|
||||
private val json = PlatformNetworkModule.providesJson()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
fido2Processor = Fido2ProviderProcessorImpl(
|
||||
context,
|
||||
authRepository,
|
||||
vaultRepository,
|
||||
fido2CredentialStore,
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
@ -207,6 +248,238 @@ class Fido2ProviderProcessorTest {
|
|||
assertEquals(mockAccount.email, captureSlot.captured.createEntries[index].accountName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with error when user state is null`() {
|
||||
val request: BeginGetCredentialRequest = mockk()
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
verify(exactly = 1) { callback.onError(any()) }
|
||||
|
||||
verify(exactly = 0) { callback.onResult(any()) }
|
||||
|
||||
assert(captureSlot.captured is GetCredentialUnknownException)
|
||||
assertEquals("Active user is required.", captureSlot.captured.errorMessage)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with authentication action when vault is locked`() {
|
||||
val request: BeginGetCredentialRequest = mockk()
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<BeginGetCredentialResponse>()
|
||||
val mockIntent: PendingIntent = mockk()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
.copy(
|
||||
accounts = listOf(
|
||||
DEFAULT_USER_STATE
|
||||
.accounts
|
||||
.first { it.userId == "mockUserId-1" }
|
||||
.copy(isVaultUnlocked = false),
|
||||
),
|
||||
)
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onResult(capture(captureSlot)) } just runs
|
||||
every { context.getString(any()) } returns "mockTitle"
|
||||
every {
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockIntent
|
||||
|
||||
val expected = AuthenticationAction(
|
||||
title = "mockTitle",
|
||||
pendingIntent = mockIntent,
|
||||
)
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
verify(exactly = 0) { callback.onError(any()) }
|
||||
verify(exactly = 1) {
|
||||
callback.onResult(any())
|
||||
intentManager.createFido2UnlockPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
|
||||
requestCode = any(),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
expected.title,
|
||||
captureSlot.captured.authenticationActions.first().title,
|
||||
)
|
||||
assertEquals(
|
||||
expected.pendingIntent,
|
||||
captureSlot.captured.authenticationActions.first().pendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with error when option is not BeginGetPublicKeyCredentialOption`() {
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockk<BeginGetPasswordOption>())
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
verify(exactly = 1) { callback.onError(any()) }
|
||||
verify(exactly = 0) { callback.onResult(any()) }
|
||||
|
||||
assert(captureSlot.captured is GetCredentialUnsupportedException)
|
||||
assertEquals("Unsupported option.", captureSlot.captured.errorMessage)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with error when option does not contain valid request json`() {
|
||||
val mockOption = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData = Bundle(),
|
||||
id = "",
|
||||
requestJson = json.encodeToString(passkeyAssertionOptions),
|
||||
)
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
}
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(any())
|
||||
} returns null
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
verify(exactly = 1) { callback.onError(any()) }
|
||||
verify(exactly = 0) { callback.onResult(any()) }
|
||||
|
||||
assert(captureSlot.captured is GetCredentialUnknownException)
|
||||
assertEquals("Invalid data.", captureSlot.captured.errorMessage)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with error when discovering passkey fails`() {
|
||||
val mockOption = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData = Bundle(),
|
||||
id = "",
|
||||
requestJson = json.encodeToString(passkeyAssertionOptions),
|
||||
)
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<GetCredentialException>()
|
||||
val mockCipherViews = listOf(createMockCipherView(number = 1))
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onError(capture(captureSlot)) } just runs
|
||||
coEvery {
|
||||
vaultRepository.silentlyDiscoverCredentials(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = "mockRelyingPartyId-1",
|
||||
)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
verify(exactly = 1) { callback.onError(any()) }
|
||||
verify(exactly = 0) { callback.onResult(any()) }
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.silentlyDiscoverCredentials(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = "mockRelyingPartyId-1",
|
||||
)
|
||||
}
|
||||
|
||||
assert(captureSlot.captured is GetCredentialUnknownException)
|
||||
assertEquals("Error decrypting credentials.", captureSlot.captured.errorMessage)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processGetCredentialRequest should invoke callback with filtered and discovered passkeys`() {
|
||||
val mockOption = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData = Bundle(),
|
||||
id = "",
|
||||
requestJson = json.encodeToString(passkeyAssertionOptions),
|
||||
)
|
||||
val request: BeginGetCredentialRequest = mockk {
|
||||
every { beginGetCredentialOptions } returns listOf(mockOption)
|
||||
}
|
||||
val callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException> = mockk()
|
||||
val captureSlot = slot<BeginGetCredentialResponse>()
|
||||
val mockCipherViews = listOf(createMockCipherView(number = 1))
|
||||
val mockFido2CredentialAutofillViews = listOf(
|
||||
createMockFido2CredentialAutofillView(number = 1),
|
||||
)
|
||||
val mockIntent: PendingIntent = mockk()
|
||||
val mockPublicKeyCredentialEntry: PublicKeyCredentialEntry = mockk()
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
|
||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||
every { callback.onResult(capture(captureSlot)) } just runs
|
||||
coEvery {
|
||||
vaultRepository.silentlyDiscoverCredentials(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = "mockRelyingPartyId-1",
|
||||
)
|
||||
} returns mockFido2CredentialAutofillViews.asSuccess()
|
||||
every {
|
||||
intentManager.createFido2GetCredentialPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
|
||||
credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
|
||||
cipherId = mockFido2CredentialAutofillViews.first().cipherId,
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockIntent
|
||||
|
||||
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockPublicKeyCredentialEntry
|
||||
|
||||
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
|
||||
|
||||
verify(exactly = 0) { callback.onError(any()) }
|
||||
verify(exactly = 1) {
|
||||
callback.onResult(any())
|
||||
intentManager.createFido2GetCredentialPendingIntent(
|
||||
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
|
||||
credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
|
||||
cipherId = mockFido2CredentialAutofillViews.first().cipherId,
|
||||
requestCode = any(),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.silentlyDiscoverCredentials(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = "mockRelyingPartyId-1",
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(1, captureSlot.captured.credentialEntries.size)
|
||||
assertEquals(mockPublicKeyCredentialEntry, captureSlot.captured.credentialEntries.first())
|
||||
|
||||
unmockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.bitwarden.sdk.ClientCiphers
|
|||
import com.bitwarden.sdk.ClientCrypto
|
||||
import com.bitwarden.sdk.ClientExporters
|
||||
import com.bitwarden.sdk.ClientFido2
|
||||
import com.bitwarden.sdk.ClientFido2Authenticator
|
||||
import com.bitwarden.sdk.ClientFido2Client
|
||||
import com.bitwarden.sdk.ClientPasswordHistory
|
||||
import com.bitwarden.sdk.ClientPlatform
|
||||
|
@ -42,6 +43,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
|||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialSearchUserInterfaceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
|
@ -1134,6 +1136,67 @@ class VaultSdkSourceTest {
|
|||
cipherViews = arrayOf(mockCipherView),
|
||||
)
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `silentlyDiscoverCredentials should return results when successful`() = runTest {
|
||||
val userId = "userId"
|
||||
val fido2CredentialStore: Fido2CredentialStore = mockk()
|
||||
val relyingPartyId = "relyingPartyId"
|
||||
val mockAutofillView = Fido2CredentialAutofillView(
|
||||
credentialId = byteArrayOf(0),
|
||||
cipherId = "mockCipherId",
|
||||
rpId = "mockRpId",
|
||||
userNameForUi = "mockUserNameForUi",
|
||||
userHandle = "mockUserHandle".toByteArray(),
|
||||
)
|
||||
val autofillViews = listOf(mockAutofillView)
|
||||
|
||||
val authenticator: ClientFido2Authenticator = mockk {
|
||||
coEvery { silentlyDiscoverCredentials(relyingPartyId) } returns autofillViews
|
||||
}
|
||||
every {
|
||||
clientFido2.authenticator(
|
||||
userInterface = any(),
|
||||
credentialStore = fido2CredentialStore,
|
||||
)
|
||||
} returns authenticator
|
||||
|
||||
val result = vaultSdkSource.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
autofillViews.asSuccess(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `silentlyDiscoverCredentials should return Failure when Bitwarden exception is thrown`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val fido2CredentialStore: Fido2CredentialStore = mockk()
|
||||
val relyingPartyId = "relyingPartyId"
|
||||
|
||||
coEvery {
|
||||
clientFido2
|
||||
.authenticator(
|
||||
userInterface = Fido2CredentialSearchUserInterfaceImpl(),
|
||||
credentialStore = fido2CredentialStore,
|
||||
)
|
||||
.silentlyDiscoverCredentials(relyingPartyId)
|
||||
} throws BitwardenException.E("mockException")
|
||||
|
||||
val result = vaultSdkSource.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.vault.Fido2CredentialView
|
||||
import java.time.Instant
|
||||
|
||||
|
@ -21,3 +22,16 @@ fun createMockFido2CredentialView(number: Int): Fido2CredentialView = Fido2Crede
|
|||
discoverable = "mockDiscoverable-$number",
|
||||
creationDate = Instant.now(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a [createMockFido2CredentialAutofillView] instance for testing.
|
||||
*/
|
||||
fun createMockFido2CredentialAutofillView(
|
||||
number: Int,
|
||||
): Fido2CredentialAutofillView = Fido2CredentialAutofillView(
|
||||
credentialId = "mockCredentialId-$number".toByteArray(),
|
||||
cipherId = "mockCipherId-$number",
|
||||
rpId = "mockRpId-$number",
|
||||
userNameForUi = "mockUserNameForUi-$number",
|
||||
userHandle = "mockUserHandle-$number".toByteArray(),
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.bitwarden.core.InitOrgCryptoRequest
|
|||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
@ -4256,6 +4257,42 @@ class VaultRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `silentlyDiscoverCredentials should return result`() = runTest {
|
||||
val userId = "userId"
|
||||
val fido2CredentialStore: Fido2CredentialStore = mockk()
|
||||
val relyingPartyId = "relyingPartyId"
|
||||
val expected: Result<List<Fido2CredentialAutofillView>> = mockk()
|
||||
coEvery {
|
||||
vaultSdkSource.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
} returns expected
|
||||
|
||||
turbineScope {
|
||||
val result = vaultRepository.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
coVerify {
|
||||
vaultSdkSource.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PublicKeyCredentialDescriptor
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
|
||||
/**
|
||||
* Returns a mock FIDO 2 [PasskeyAssertionOptions] object to simulate a credential
|
||||
* creation request.
|
||||
*/
|
||||
fun createMockPasskeyAssertionOptions(
|
||||
number: Int,
|
||||
) = PasskeyAssertionOptions(
|
||||
challenge = "mockChallenge-$number",
|
||||
allowCredentials = listOf(
|
||||
PublicKeyCredentialDescriptor(
|
||||
type = "mockPublicKeyCredentialDescriptorType-$number",
|
||||
id = "mockPublicKeyCredentialDescriptorId-$number",
|
||||
transports = listOf("mockPublicKeyCredentialDescriptorTransports-$number"),
|
||||
),
|
||||
),
|
||||
relyingPartyId = "mockRelyingPartyId-$number",
|
||||
userVerification = "mockUserVerification-$number",
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PublicKeyCredentialDescriptor
|
||||
|
||||
/**
|
||||
* Returns a mock FIDO 2 [PasskeyAttestationOptions] object to simulate a credential
|
||||
|
@ -15,7 +16,7 @@ fun createMockPasskeyAttestationOptions(
|
|||
.AuthenticatorSelectionCriteria(userVerification = userVerificationRequirement),
|
||||
challenge = "mockPublicKeyCredentialCreationOptionsChallenge-$number",
|
||||
excludeCredentials = listOf(
|
||||
PasskeyAttestationOptions.PublicKeyCredentialDescriptor(
|
||||
PublicKeyCredentialDescriptor(
|
||||
type = "mockPublicKeyCredentialDescriptorType-$number",
|
||||
id = "mockPublicKeyCredentialDescriptorId-$number",
|
||||
transports = listOf("mockPublicKeyCredentialDescriptorTransports-$number"),
|
Loading…
Reference in a new issue