Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b40e591
[PM-11884] Perform origin validation during FIDO 2 auth
SaintPatrck Sep 10, 2024
2e62f18
Merge branch 'main' into PM-11884/validate-origin-on-auth
SaintPatrck Sep 11, 2024
11b9259
Merge branch 'main' into PM-11884/validate-origin-on-auth
SaintPatrck Sep 12, 2024
69270ba
[PM-10678] Display all passkeys and accounts in OS prompt
SaintPatrck Aug 22, 2024
5f7e097
Merge branch 'PM-11884/validate-origin-on-auth' into PM-10678/passkeyโ€ฆ
SaintPatrck Sep 12, 2024
2d3f98c
Terminate FIDO 2 operation when error dialog is dismissed
SaintPatrck Sep 12, 2024
3cd9fcb
Add tests for Fido2ViewModel
SaintPatrck Sep 18, 2024
b472795
Merge remote-tracking branch 'origin/main' into PM-10678/passkey-ux-iโ€ฆ
SaintPatrck Sep 18, 2024
e9d979f
Update tests after merge
SaintPatrck Sep 19, 2024
b9e1507
Merge remote-tracking branch 'origin/main' into PM-10678/passkey-ux-iโ€ฆ
SaintPatrck Sep 19, 2024
a2d5acd
Merge remote-tracking branch 'origin/main' into PM-10678/passkey-ux-iโ€ฆ
SaintPatrck Sep 19, 2024
06523d6
Cache cipher name and fix tests
SaintPatrck Sep 19, 2024
cb76ee6
Fix Fido2ProviderProcessorTest
SaintPatrck Sep 19, 2024
8d32144
Fix Fido2CompletionManager tests
SaintPatrck Sep 19, 2024
a4fadd7
Update VaultUnlockScreenTest.kt
SaintPatrck Sep 19, 2024
434c043
Remove fade transition
SaintPatrck Sep 19, 2024
ca7ed02
Remove alternate accounts property from Fido2GetCredentialsResult
SaintPatrck Sep 19, 2024
7214e7d
Remove fallback cipher name
SaintPatrck Sep 19, 2024
f3526f8
Fix formatting
SaintPatrck Sep 19, 2024
9ead95f
Use `first` instead of `find`
SaintPatrck Sep 19, 2024
f3bcc72
Fix Fido2ViewModelTest
SaintPatrck Sep 19, 2024
3e8344c
Fix Fido2ProviderProcessorTest
SaintPatrck Sep 19, 2024
fe16091
Fix VaultUnlockViewModelTest
SaintPatrck Sep 19, 2024
f5b3a2c
Merge remote-tracking branch 'origin/main' into PM-10678/passkey-ux-iโ€ฆ
SaintPatrck Sep 23, 2024
3c616c8
Fix build failure
SaintPatrck Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ object Fido2ProviderModule {
@ApplicationContext context: Context,
authRepository: AuthRepository,
vaultRepository: VaultRepository,
fido2CredentialStore: Fido2CredentialStore,
fido2CredentialManager: Fido2CredentialManager,
dispatcherManager: DispatcherManager,
intentManager: IntentManager,
Expand All @@ -48,7 +47,6 @@ object Fido2ProviderModule {
context,
authRepository,
vaultRepository,
fido2CredentialStore,
fido2CredentialManager,
intentManager,
clock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class Fido2CredentialAssertionRequest(
val userId: String,
val cipherId: String?,
val credentialId: String?,
val requestJson: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
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.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.CallingAppInfo
import kotlinx.parcelize.Parcelize

/**
Expand All @@ -14,15 +12,10 @@ import kotlinx.parcelize.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?,
) : Parcelable {
val callingAppInfo: CallingAppInfo
get() = CallingAppInfo(packageName, signingInfo, origin)

val option: BeginGetPublicKeyCredentialOption
get() = BeginGetPublicKeyCredentialOption(
candidateQueryData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ sealed class Fido2GetCredentialsResult {
* Indicates credentials were successfully queried.
*
* @param options Original request options provided by the relying party.
* @param credentials Collection of [Fido2CredentialAutofillView]s matching the original request
* parameters. This may be an empty list if no matching values were found.
* @param credentials Map of Cipher Names and their [Fido2CredentialAutofillView]s matching the
* original request parameters. This may be an empty map if no matching values were found.
*/
data class Success(
val userId: String,
val options: BeginGetPublicKeyCredentialOption,
val credentials: List<Fido2CredentialAutofillView>,
val credentials: Map<String, Fido2CredentialAutofillView>,
) : Fido2GetCredentialsResult()

/**
* Indicates an error was encountered when querying for matching credentials.
*/
data object Error : Fido2GetCredentialsResult()

/**
* Indicates the user has cancelled credential discovery.
*/
data object Cancelled : Fido2GetCredentialsResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ 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
Expand All @@ -41,21 +40,21 @@ import kotlinx.coroutines.launch
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger

private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT"
private const val FIDO2_PACKAGE = "com.x8bit.bitwarden.fido2"
private const val CREATE_PASSKEY_INTENT = "$FIDO2_PACKAGE.ACTION_CREATE_PASSKEY"
const val GET_PASSKEY_INTENT = "$FIDO2_PACKAGE.ACTION_GET_PASSKEY"
const val UNLOCK_ACCOUNT_INTENT = "$FIDO2_PACKAGE.ACTION_UNLOCK_ACCOUNT"

/**
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
* processing.
*/
@Suppress("LongParameterList")
@Suppress("LongParameterList", "TooManyFunctions")
@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,
private val clock: Clock,
Expand Down Expand Up @@ -155,34 +154,27 @@ class Fido2ProviderProcessorImpl(
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,
)
val authenticationActions = userState.accounts
.toAuthenticationActions()

val switchAccountActions = userState.accounts
.toSwitchAccountActions(userState.activeUserId)

val credentialEntries =
if (userState.activeAccount.isVaultUnlocked) {
getMatchingFido2CredentialEntries(
userId = userState.activeUserId,
request = request,
)
} else {
emptyList()
}

callback.onResult(
BeginGetCredentialResponse(
authenticationActions = authenticationActions + switchAccountActions,
credentialEntries = credentialEntries,
),
)
Expand All @@ -191,11 +183,45 @@ class Fido2ProviderProcessorImpl(
}
}
cancellationSignal.setOnCancelListener {
if (getCredentialJob.isActive) {
getCredentialJob.cancel()
}
callback.onError(GetCredentialCancellationException())
getCredentialJob.cancel()
}
}

private fun List<UserState.Account>.toAuthenticationActions(): List<AuthenticationAction> =
this.filterNot { it.isVaultUnlocked }
.map { it.toAuthenticationAction() }

private fun UserState.Account.toAuthenticationAction(): AuthenticationAction =
AuthenticationAction(
title = context.getString(R.string.unlock_vault_for_x, name ?: email),
pendingIntent = intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = userId,
requestCode = requestCode.getAndIncrement(),
),
)

private fun List<UserState.Account>.toSwitchAccountActions(
activeUserId: String,
): List<AuthenticationAction> = this
.filter { it.isVaultUnlocked && it.userId != activeUserId }
.map { it.toSwitchAccountAction() }

private fun UserState.Account.toSwitchAccountAction(): AuthenticationAction =
AuthenticationAction
.Builder(
title = context.getString(R.string.check_x_for_passkeys, name ?: email),
pendingIntent = intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = userId,
requestCode = requestCode.getAndIncrement(),
),
)
.build()

@Throws(GetCredentialUnsupportedException::class)
private suspend fun getMatchingFido2CredentialEntries(
userId: String,
Expand All @@ -209,13 +235,14 @@ class Fido2ProviderProcessorImpl(
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
?.relyingPartyId
?: throw GetCredentialUnknownException("Invalid data.")
buildCredentialEntries(relyingPartyId, option)
buildCredentialEntries(userId, relyingPartyId, option)
} else {
throw GetCredentialUnsupportedException("Unsupported option.")
}
}

private suspend fun buildCredentialEntries(
userId: String,
relyingPartyId: String,
option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> {
Expand All @@ -233,34 +260,45 @@ class Fido2ProviderProcessorImpl(
}

is DecryptFido2CredentialAutofillViewResult.Success -> {
result
.fido2CredentialAutofillViews
val autoFillViews = result.fido2CredentialAutofillViews
.filter { it.rpId == relyingPartyId }
.toCredentialEntries(option)

cipherViews
.associate { cipherView ->
cipherView.name to autoFillViews.find { it.cipherId == cipherView.id }
}
.toCredentialEntries(userId, option)
}
}
}

private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
private fun Map<String, Fido2CredentialAutofillView?>.toCredentialEntries(
userId: String,
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()
}
): List<CredentialEntry> = mapNotNull {
// Skip this entry if the autofill view is null.
val autofillView = it.value
?: return@mapNotNull null
val username = autofillView.userNameForUi
?: context.getString(R.string.no_username)
val displayName = it.key
PublicKeyCredentialEntry
.Builder(
context = context,
username = username,
pendingIntent = intentManager
.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = autofillView.credentialId.toString(),
cipherId = autofillView.cipherId,
requestCode = requestCode.getAndIncrement(),
),
beginGetPublicKeyCredentialOption = option,
)
.setDisplayName(displayName)
.build()
}

override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
.firstNotNullOfOrNull { it as? GetPublicKeyCredentialOption }
?: return null

val userId = getStringExtra(EXTRA_KEY_USER_ID)
?: return null

val credentialId = getStringExtra(EXTRA_KEY_CREDENTIAL_ID)
?: return null

val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
?: return null

return Fido2CredentialAssertionRequest(
userId = userId,
cipherId = cipherId,
credentialId = credentialId,
requestJson = option.requestJson,
Expand All @@ -91,17 +95,14 @@ fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? {
.firstNotNullOfOrNull { it as? BeginGetPublicKeyCredentialOption }
?: return null

val callingAppInfo = systemRequest
.callingAppInfo
val userId = 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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class Fido2CredentialStoreImpl(
* Return all active ciphers that contain FIDO 2 credentials.
*/
override suspend fun allCredentials(): List<CipherView> {
vaultRepository.sync()
return vaultRepository.ciphersStateFlow.value.data
?.filter { it.isActiveWithFido2Credentials }
?: emptyList()
Expand All @@ -39,9 +38,6 @@ class Fido2CredentialStoreImpl(
*/
override suspend fun findCredentials(ids: List<ByteArray>?, ripId: String): List<CipherView> {
val userId = getActiveUserIdOrThrow()

vaultRepository.sync()

val ciphersWithFido2Credentials = vaultRepository.ciphersStateFlow.value.data
?.filter { it.isActiveWithFido2Credentials }
.orEmpty()
Expand Down
Loading