[PM-13070] Add userId to Fido2 GetCredentials and CredentialAssertion requests (#4003)

This commit is contained in:
Patrick Honkonen 2024-10-03 11:14:03 -04:00 committed by GitHub
parent 569ffc3583
commit e6eb626d85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 89 additions and 38 deletions

View file

@ -10,6 +10,7 @@ import kotlinx.parcelize.Parcelize
*/ */
@Parcelize @Parcelize
data class Fido2CredentialAssertionRequest( data class Fido2CredentialAssertionRequest(
val userId: String,
val cipherId: String?, val cipherId: String?,
val credentialId: String?, val credentialId: String?,
val requestJson: String, val requestJson: String,

View file

@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize
data class Fido2GetCredentialsRequest( data class Fido2GetCredentialsRequest(
val candidateQueryData: Bundle, val candidateQueryData: Bundle,
val id: String, val id: String,
val userId: String,
val requestJson: String, val requestJson: String,
val clientDataHash: ByteArray? = null, val clientDataHash: ByteArray? = null,
val packageName: String, val packageName: String,

View file

@ -10,11 +10,13 @@ sealed class Fido2GetCredentialsResult {
/** /**
* Indicates credentials were successfully queried. * Indicates credentials were successfully queried.
* *
* @param userId ID of the user whose credentials were queried.
* @param options Original request options provided by the relying party. * @param options Original request options provided by the relying party.
* @param credentials Collection of [Fido2CredentialAutofillView]s matching the original request * @param credentials Collection of [Fido2CredentialAutofillView]s matching the original request
* parameters. This may be an empty list if no matching values were found. * parameters. This may be an empty list if no matching values were found.
*/ */
data class Success( data class Success(
val userId: String,
val options: BeginGetPublicKeyCredentialOption, val options: BeginGetPublicKeyCredentialOption,
val credentials: List<Fido2CredentialAutofillView>, val credentials: List<Fido2CredentialAutofillView>,
) : Fido2GetCredentialsResult() ) : Fido2GetCredentialsResult()

View file

@ -161,6 +161,7 @@ class Fido2ProviderProcessorImpl(
title = context.getString(R.string.unlock), title = context.getString(R.string.unlock),
pendingIntent = intentManager.createFido2UnlockPendingIntent( pendingIntent = intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT, action = UNLOCK_ACCOUNT_INTENT,
userId = userState.activeUserId,
requestCode = requestCode.getAndIncrement(), requestCode = requestCode.getAndIncrement(),
), ),
) )
@ -209,13 +210,14 @@ class Fido2ProviderProcessorImpl(
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson) .getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
?.relyingPartyId ?.relyingPartyId
?: throw GetCredentialUnknownException("Invalid data.") ?: throw GetCredentialUnknownException("Invalid data.")
buildCredentialEntries(relyingPartyId, option) buildCredentialEntries(userId, relyingPartyId, option)
} else { } else {
throw GetCredentialUnsupportedException("Unsupported option.") throw GetCredentialUnsupportedException("Unsupported option.")
} }
} }
private suspend fun buildCredentialEntries( private suspend fun buildCredentialEntries(
userId: String,
relyingPartyId: String, relyingPartyId: String,
option: BeginGetPublicKeyCredentialOption, option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> { ): List<CredentialEntry> {
@ -236,12 +238,16 @@ class Fido2ProviderProcessorImpl(
result result
.fido2CredentialAutofillViews .fido2CredentialAutofillViews
.filter { it.rpId == relyingPartyId } .filter { it.rpId == relyingPartyId }
.toCredentialEntries(option) .toCredentialEntries(
userId = userId,
option = option,
)
} }
} }
} }
private fun List<Fido2CredentialAutofillView>.toCredentialEntries( private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
userId: String,
option: BeginGetPublicKeyCredentialOption, option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> = ): List<CredentialEntry> =
this this
@ -253,6 +259,7 @@ class Fido2ProviderProcessorImpl(
pendingIntent = intentManager pendingIntent = intentManager
.createFido2GetCredentialPendingIntent( .createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT, action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = it.credentialId.toString(), credentialId = it.credentialId.toString(),
cipherId = it.cipherId, cipherId = it.cipherId,
requestCode = requestCode.getAndIncrement(), requestCode = requestCode.getAndIncrement(),

View file

@ -64,7 +64,11 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID) val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
?: return null ?: return null
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
return Fido2CredentialAssertionRequest( return Fido2CredentialAssertionRequest(
userId = userId,
cipherId = cipherId, cipherId = cipherId,
credentialId = credentialId, credentialId = credentialId,
requestJson = option.requestJson, requestJson = option.requestJson,
@ -95,9 +99,13 @@ fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? {
.callingAppInfo .callingAppInfo
?: return null ?: return null
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
return Fido2GetCredentialsRequest( return Fido2GetCredentialsRequest(
candidateQueryData = option.candidateQueryData, candidateQueryData = option.candidateQueryData,
id = option.id, id = option.id,
userId = userId,
requestJson = option.requestJson, requestJson = option.requestJson,
clientDataHash = option.clientDataHash, clientDataHash = option.clientDataHash,
packageName = callingAppInfo.packageName, packageName = callingAppInfo.packageName,

View file

@ -104,6 +104,7 @@ class Fido2CompletionManagerImpl(
val pendingIntent = intentManager val pendingIntent = intentManager
.createFido2GetCredentialPendingIntent( .createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT, action = GET_PASSKEY_INTENT,
userId = result.userId,
credentialId = it.credentialId.toString(), credentialId = it.credentialId.toString(),
cipherId = it.cipherId, cipherId = it.cipherId,
requestCode = Random.nextInt(), requestCode = Random.nextInt(),

View file

@ -119,6 +119,7 @@ interface IntentManager {
*/ */
fun createFido2GetCredentialPendingIntent( fun createFido2GetCredentialPendingIntent(
action: String, action: String,
userId: String,
credentialId: String, credentialId: String,
cipherId: String, cipherId: String,
requestCode: Int, requestCode: Int,
@ -130,6 +131,7 @@ interface IntentManager {
*/ */
fun createFido2UnlockPendingIntent( fun createFido2UnlockPendingIntent(
action: String, action: String,
userId: String,
requestCode: Int, requestCode: Int,
): PendingIntent ): PendingIntent

View file

@ -258,12 +258,14 @@ class IntentManagerImpl(
override fun createFido2GetCredentialPendingIntent( override fun createFido2GetCredentialPendingIntent(
action: String, action: String,
userId: String,
credentialId: String, credentialId: String,
cipherId: String, cipherId: String,
requestCode: Int, requestCode: Int,
): PendingIntent { ): PendingIntent {
val intent = Intent(action) val intent = Intent(action)
.setPackage(context.packageName) .setPackage(context.packageName)
.putExtra(EXTRA_KEY_USER_ID, userId)
.putExtra(EXTRA_KEY_CREDENTIAL_ID, credentialId) .putExtra(EXTRA_KEY_CREDENTIAL_ID, credentialId)
.putExtra(EXTRA_KEY_CIPHER_ID, cipherId) .putExtra(EXTRA_KEY_CIPHER_ID, cipherId)
@ -277,9 +279,12 @@ class IntentManagerImpl(
override fun createFido2UnlockPendingIntent( override fun createFido2UnlockPendingIntent(
action: String, action: String,
userId: String,
requestCode: Int, requestCode: Int,
): PendingIntent { ): PendingIntent {
val intent = Intent(action).setPackage(context.packageName) val intent = Intent(action)
.setPackage(context.packageName)
.putExtra(EXTRA_KEY_USER_ID, userId)
return PendingIntent.getActivity( return PendingIntent.getActivity(
/* context = */ context, /* context = */ context,

View file

@ -1288,6 +1288,7 @@ class VaultItemListingViewModel @Inject constructor(
sendEvent( sendEvent(
VaultItemListingEvent.CompleteFido2GetCredentialsRequest( VaultItemListingEvent.CompleteFido2GetCredentialsRequest(
Fido2GetCredentialsResult.Success( Fido2GetCredentialsResult.Success(
userId = fido2GetCredentialsRequest.userId,
options = fido2GetCredentialsRequest.option, options = fido2GetCredentialsRequest.option,
credentials = vaultData credentials = vaultData
.data .data

View file

@ -4,6 +4,7 @@ import android.content.pm.SigningInfo
fun createMockFido2CredentialAssertionRequest(number: Int = 1): Fido2CredentialAssertionRequest = fun createMockFido2CredentialAssertionRequest(number: Int = 1): Fido2CredentialAssertionRequest =
Fido2CredentialAssertionRequest( Fido2CredentialAssertionRequest(
userId = "mockUserId-$number",
cipherId = "mockCipherId-$number", cipherId = "mockCipherId-$number",
credentialId = "mockCredentialId-$number", credentialId = "mockCredentialId-$number",
requestJson = "mockRequestJson-$number", requestJson = "mockRequestJson-$number",

View file

@ -10,6 +10,7 @@ fun createMockFido2GetCredentialsRequest(
): Fido2GetCredentialsRequest = Fido2GetCredentialsRequest( ): Fido2GetCredentialsRequest = Fido2GetCredentialsRequest(
candidateQueryData = Bundle(), candidateQueryData = Bundle(),
id = "mockId-$number", id = "mockId-$number",
userId = "mockUserId-$number",
requestJson = "requestJson-$number", requestJson = "requestJson-$number",
clientDataHash = null, clientDataHash = null,
packageName = "mockPackageName-$number", packageName = "mockPackageName-$number",

View file

@ -301,6 +301,7 @@ class Fido2ProviderProcessorTest {
every { every {
intentManager.createFido2UnlockPendingIntent( intentManager.createFido2UnlockPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT", action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
userId = "mockUserId-1",
requestCode = any(), requestCode = any(),
) )
} returns mockIntent } returns mockIntent
@ -317,6 +318,7 @@ class Fido2ProviderProcessorTest {
callback.onResult(any()) callback.onResult(any())
intentManager.createFido2UnlockPendingIntent( intentManager.createFido2UnlockPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT", action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT",
userId = "mockUserId-1",
requestCode = any(), requestCode = any(),
) )
} }
@ -463,6 +465,7 @@ class Fido2ProviderProcessorTest {
every { every {
intentManager.createFido2GetCredentialPendingIntent( intentManager.createFido2GetCredentialPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY", action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
userId = DEFAULT_USER_STATE.activeUserId,
credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(), credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
cipherId = mockFido2CredentialAutofillViews.first().cipherId, cipherId = mockFido2CredentialAutofillViews.first().cipherId,
requestCode = any(), requestCode = any(),

View file

@ -163,6 +163,7 @@ class Fido2IntentUtilsTest {
@Test @Test
fun `getFido2AssertionRequestOrNull should return Fido2AssertionRequest when present`() { fun `getFido2AssertionRequestOrNull should return Fido2AssertionRequest when present`() {
val intent = mockk<Intent> { val intent = mockk<Intent> {
every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId"
every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId" every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId"
every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId" every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId"
} }
@ -190,6 +191,7 @@ class Fido2IntentUtilsTest {
assertNotNull(assertionRequest) assertNotNull(assertionRequest)
assertEquals( assertEquals(
Fido2CredentialAssertionRequest( Fido2CredentialAssertionRequest(
userId = "mockUserId",
cipherId = "mockCipherId", cipherId = "mockCipherId",
credentialId = "mockCredentialId", credentialId = "mockCredentialId",
requestJson = mockOption.requestJson, requestJson = mockOption.requestJson,
@ -287,14 +289,38 @@ class Fido2IntentUtilsTest {
} returns mockProviderGetCredentialRequest } returns mockProviderGetCredentialRequest
val assertionRequest = intent.getFido2AssertionRequestOrNull() 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(),
)
val mockProviderGetCredentialRequest = ProviderGetCredentialRequest(
credentialOptions = listOf(mockOption),
callingAppInfo = mockk(),
)
every {
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
} returns mockProviderGetCredentialRequest
val assertionRequest = intent.getFido2AssertionRequestOrNull()
assertNull(assertionRequest) assertNull(assertionRequest)
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `getFido2GetCredentialsRequestOrNull should return Fido2GetCredentialRequest when present`() { fun `getFido2GetCredentialsRequestOrNull should return Fido2GetCredentialRequest when present`() {
val intent = mockk<Intent>() val intent = mockk<Intent> {
every { getStringExtra("user_id") } returns "mockUserId"
}
val mockOption = BeginGetPublicKeyCredentialOption( val mockOption = BeginGetPublicKeyCredentialOption(
candidateQueryData = bundleOf(), candidateQueryData = bundleOf(),
id = "mockId", id = "mockId",
@ -320,6 +346,7 @@ class Fido2IntentUtilsTest {
Fido2GetCredentialsRequest( Fido2GetCredentialsRequest(
candidateQueryData = mockOption.candidateQueryData, candidateQueryData = mockOption.candidateQueryData,
id = mockOption.id, id = mockOption.id,
userId = "mockUserId",
requestJson = mockOption.requestJson, requestJson = mockOption.requestJson,
clientDataHash = mockOption.clientDataHash, clientDataHash = mockOption.clientDataHash,
packageName = mockCallingAppInfo.packageName, packageName = mockCallingAppInfo.packageName,
@ -378,6 +405,20 @@ class Fido2IntentUtilsTest {
val result = intent.getFido2GetCredentialsRequestOrNull() val result = intent.getFido2GetCredentialsRequestOrNull()
assertNull(result) assertNull(result)
} }
@Test
fun `getFido2GetCredentialRequestOrNull should return null when user id is not in extras`() {
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()
}
val result = intent.getFido2GetCredentialsRequestOrNull()
assertNull(result)
}
} }
private fun createMockBeginGetPublicKeyCredentialOption( private fun createMockBeginGetPublicKeyCredentialOption(

View file

@ -6,15 +6,12 @@ import android.content.Intent
import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult 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.Fido2GetCredentialsResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult 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.autofill.fido2.processor.GET_PASSKEY_INTENT
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.Called import io.mockk.Called
import io.mockk.MockKVerificationScope import io.mockk.MockKVerificationScope
@ -175,6 +172,7 @@ class Fido2CompletionManagerTest {
fido2CompletionManager fido2CompletionManager
.completeFido2GetCredentialRequest( .completeFido2GetCredentialRequest(
Fido2GetCredentialsResult.Success( Fido2GetCredentialsResult.Success(
userId = "mockUserId",
options = mockk(), options = mockk(),
credentials = emptyList(), credentials = emptyList(),
), ),
@ -201,6 +199,7 @@ class Fido2CompletionManagerTest {
every { every {
mockIntentManager.createFido2GetCredentialPendingIntent( mockIntentManager.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT, action = GET_PASSKEY_INTENT,
userId = "mockUserId",
credentialId = mockFido2AutofillView.credentialId.toString(), credentialId = mockFido2AutofillView.credentialId.toString(),
cipherId = mockFido2AutofillView.cipherId, cipherId = mockFido2AutofillView.cipherId,
requestCode = any(), requestCode = any(),
@ -211,6 +210,7 @@ class Fido2CompletionManagerTest {
fido2CompletionManager fido2CompletionManager
.completeFido2GetCredentialRequest( .completeFido2GetCredentialRequest(
Fido2GetCredentialsResult.Success( Fido2GetCredentialsResult.Success(
userId = "mockUserId",
options = mockk(), options = mockk(),
credentials = mockFido2AutofillViewList, credentials = mockFido2AutofillViewList,
), ),
@ -249,6 +249,7 @@ class Fido2CompletionManagerTest {
every { every {
mockIntentManager.createFido2GetCredentialPendingIntent( mockIntentManager.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT, action = GET_PASSKEY_INTENT,
userId = "mockUserId",
credentialId = mockFido2AutofillView.credentialId.toString(), credentialId = mockFido2AutofillView.credentialId.toString(),
cipherId = mockFido2AutofillView.cipherId, cipherId = mockFido2AutofillView.cipherId,
requestCode = any(), requestCode = any(),
@ -259,6 +260,7 @@ class Fido2CompletionManagerTest {
fido2CompletionManager fido2CompletionManager
.completeFido2GetCredentialRequest( .completeFido2GetCredentialRequest(
Fido2GetCredentialsResult.Success( Fido2GetCredentialsResult.Success(
userId = "mockUserId",
options = mockk(), options = mockk(),
credentials = mockFido2AutofillViewList, credentials = mockFido2AutofillViewList,
), ),
@ -304,35 +306,5 @@ class Fido2CompletionManagerTest {
mockActivity.finish() mockActivity.finish()
} }
} }
private fun setupMockCompletionIntent(
mockFido2AutofillView1: Fido2CredentialAutofillView,
mockCredentialEntry1: PublicKeyCredentialEntry,
): Intent {
val mockIntent1 = mockk<Intent> {
every {
putExtra(
EXTRA_KEY_CIPHER_ID,
mockFido2AutofillView1.cipherId,
)
} returns this
every {
putExtra(
EXTRA_KEY_CREDENTIAL_ID,
mockFido2AutofillView1.credentialId.toString(),
)
} returns this
}
every {
anyConstructed<PublicKeyCredentialEntry.Builder>()
.build()
} returns mockCredentialEntry1
every { anyConstructed<Intent>().setPackage(any()) } returns mockIntent1
every {
PendingIntent.getActivity(mockActivity, any(), mockIntent1, any())
} returns mockk()
return mockIntent1
}
} }
} }

View file

@ -1794,7 +1794,11 @@ class VaultItemListingScreenTest : BaseComposeTest() {
@Test @Test
fun `CompleteFido2GetCredentials event should call Fido2CompletionManager with result`() { fun `CompleteFido2GetCredentials event should call Fido2CompletionManager with result`() {
val result = Fido2GetCredentialsResult.Success(mockk(), mockk()) val result = Fido2GetCredentialsResult.Success(
userId = "mockUserId",
options = mockk(),
credentials = mockk(),
)
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2GetCredentialsRequest(result)) mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2GetCredentialsRequest(result))
verify { verify {
fido2CompletionManager.completeFido2GetCredentialRequest(result) fido2CompletionManager.completeFido2GetCredentialRequest(result)

View file

@ -1517,6 +1517,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
candidateQueryData = mockk(), candidateQueryData = mockk(),
clientDataHash = byteArrayOf(0), clientDataHash = byteArrayOf(0),
id = "mockId", id = "mockId",
userId = "mockUserId",
) )
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =