diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequest.kt index 8de5a7b39..3c0e840a7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequest.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequest.kt @@ -10,6 +10,7 @@ import kotlinx.parcelize.Parcelize */ @Parcelize data class Fido2CredentialAssertionRequest( + val userId: String, val cipherId: String?, val credentialId: String?, val requestJson: String, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt index f0f54f59e..b32e6fb6b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequest.kt @@ -14,6 +14,7 @@ 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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt index 81bcd2ad4..0751f83be 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsResult.kt @@ -10,11 +10,13 @@ sealed class Fido2GetCredentialsResult { /** * 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 credentials Collection of [Fido2CredentialAutofillView]s matching the original request * parameters. This may be an empty list if no matching values were found. */ data class Success( + val userId: String, val options: BeginGetPublicKeyCredentialOption, val credentials: List, ) : Fido2GetCredentialsResult() diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt index 341b7c91a..b3c27c024 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorImpl.kt @@ -161,6 +161,7 @@ class Fido2ProviderProcessorImpl( title = context.getString(R.string.unlock), pendingIntent = intentManager.createFido2UnlockPendingIntent( action = UNLOCK_ACCOUNT_INTENT, + userId = userState.activeUserId, requestCode = requestCode.getAndIncrement(), ), ) @@ -209,13 +210,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 { @@ -236,12 +238,16 @@ class Fido2ProviderProcessorImpl( result .fido2CredentialAutofillViews .filter { it.rpId == relyingPartyId } - .toCredentialEntries(option) + .toCredentialEntries( + userId = userId, + option = option, + ) } } } private fun List.toCredentialEntries( + userId: String, option: BeginGetPublicKeyCredentialOption, ): List = this @@ -253,6 +259,7 @@ class Fido2ProviderProcessorImpl( pendingIntent = intentManager .createFido2GetCredentialPendingIntent( action = GET_PASSKEY_INTENT, + userId = userId, credentialId = it.credentialId.toString(), cipherId = it.cipherId, requestCode = requestCode.getAndIncrement(), diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt index 92788931b..4de40444e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtils.kt @@ -64,7 +64,11 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? { val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID) ?: return null + val userId: String = getStringExtra(EXTRA_KEY_USER_ID) + ?: return null + return Fido2CredentialAssertionRequest( + userId = userId, cipherId = cipherId, credentialId = credentialId, requestJson = option.requestJson, @@ -95,9 +99,13 @@ fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? { .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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt index 1b262c6eb..ed5cb41b1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerImpl.kt @@ -104,6 +104,7 @@ class Fido2CompletionManagerImpl( val pendingIntent = intentManager .createFido2GetCredentialPendingIntent( action = GET_PASSKEY_INTENT, + userId = result.userId, credentialId = it.credentialId.toString(), cipherId = it.cipherId, requestCode = Random.nextInt(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt index 8c841a2da..ff00823ca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManager.kt @@ -119,6 +119,7 @@ interface IntentManager { */ fun createFido2GetCredentialPendingIntent( action: String, + userId: String, credentialId: String, cipherId: String, requestCode: Int, @@ -130,6 +131,7 @@ interface IntentManager { */ fun createFido2UnlockPendingIntent( action: String, + userId: String, requestCode: Int, ): PendingIntent diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt index 9d797e085..e53088032 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/intent/IntentManagerImpl.kt @@ -258,12 +258,14 @@ class IntentManagerImpl( override fun createFido2GetCredentialPendingIntent( action: String, + userId: String, credentialId: String, cipherId: String, requestCode: Int, ): PendingIntent { val intent = Intent(action) .setPackage(context.packageName) + .putExtra(EXTRA_KEY_USER_ID, userId) .putExtra(EXTRA_KEY_CREDENTIAL_ID, credentialId) .putExtra(EXTRA_KEY_CIPHER_ID, cipherId) @@ -277,9 +279,12 @@ class IntentManagerImpl( override fun createFido2UnlockPendingIntent( action: String, + userId: String, requestCode: Int, ): PendingIntent { - val intent = Intent(action).setPackage(context.packageName) + val intent = Intent(action) + .setPackage(context.packageName) + .putExtra(EXTRA_KEY_USER_ID, userId) return PendingIntent.getActivity( /* context = */ context, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 68cb80ce4..9d0ddeea2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -1288,6 +1288,7 @@ class VaultItemListingViewModel @Inject constructor( sendEvent( VaultItemListingEvent.CompleteFido2GetCredentialsRequest( Fido2GetCredentialsResult.Success( + userId = fido2GetCredentialsRequest.userId, options = fido2GetCredentialsRequest.option, credentials = vaultData .data diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequestUtil.kt index 1826f9d43..f5e562b32 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2CredentialAssertionRequestUtil.kt @@ -4,6 +4,7 @@ import android.content.pm.SigningInfo fun createMockFido2CredentialAssertionRequest(number: Int = 1): Fido2CredentialAssertionRequest = Fido2CredentialAssertionRequest( + userId = "mockUserId-$number", cipherId = "mockCipherId-$number", credentialId = "mockCredentialId-$number", requestJson = "mockRequestJson-$number", diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequestUtil.kt index d3971efde..91ccd8d0c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/model/Fido2GetCredentialsRequestUtil.kt @@ -10,6 +10,7 @@ fun createMockFido2GetCredentialsRequest( ): Fido2GetCredentialsRequest = Fido2GetCredentialsRequest( candidateQueryData = Bundle(), id = "mockId-$number", + userId = "mockUserId-$number", requestJson = "requestJson-$number", clientDataHash = null, packageName = "mockPackageName-$number", diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt index 2e7ac68a4..6086e7f3e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt @@ -301,6 +301,7 @@ class Fido2ProviderProcessorTest { every { intentManager.createFido2UnlockPendingIntent( action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT", + userId = "mockUserId-1", requestCode = any(), ) } returns mockIntent @@ -317,6 +318,7 @@ class Fido2ProviderProcessorTest { callback.onResult(any()) intentManager.createFido2UnlockPendingIntent( action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT", + userId = "mockUserId-1", requestCode = any(), ) } @@ -463,6 +465,7 @@ class Fido2ProviderProcessorTest { every { intentManager.createFido2GetCredentialPendingIntent( action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY", + userId = DEFAULT_USER_STATE.activeUserId, credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(), cipherId = mockFido2CredentialAutofillViews.first().cipherId, requestCode = any(), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt index 9b1f40304..51894708f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/util/Fido2IntentUtilsTest.kt @@ -163,6 +163,7 @@ class Fido2IntentUtilsTest { @Test fun `getFido2AssertionRequestOrNull should return Fido2AssertionRequest when present`() { val intent = mockk { + every { getStringExtra(EXTRA_KEY_USER_ID) } returns "mockUserId" every { getStringExtra(EXTRA_KEY_CIPHER_ID) } returns "mockCipherId" every { getStringExtra(EXTRA_KEY_CREDENTIAL_ID) } returns "mockCredentialId" } @@ -190,6 +191,7 @@ class Fido2IntentUtilsTest { assertNotNull(assertionRequest) assertEquals( Fido2CredentialAssertionRequest( + userId = "mockUserId", cipherId = "mockCipherId", credentialId = "mockCredentialId", requestJson = mockOption.requestJson, @@ -287,14 +289,38 @@ class Fido2IntentUtilsTest { } returns mockProviderGetCredentialRequest val assertionRequest = intent.getFido2AssertionRequestOrNull() + assertNull(assertionRequest) + } + @Test + fun `getFido2AssertionRequestOrNull should return null when user id is not in extras`() { + val intent = mockk { + 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) } @Suppress("MaxLineLength") @Test fun `getFido2GetCredentialsRequestOrNull should return Fido2GetCredentialRequest when present`() { - val intent = mockk() + val intent = mockk { + every { getStringExtra("user_id") } returns "mockUserId" + } val mockOption = BeginGetPublicKeyCredentialOption( candidateQueryData = bundleOf(), id = "mockId", @@ -320,6 +346,7 @@ class Fido2IntentUtilsTest { Fido2GetCredentialsRequest( candidateQueryData = mockOption.candidateQueryData, id = mockOption.id, + userId = "mockUserId", requestJson = mockOption.requestJson, clientDataHash = mockOption.clientDataHash, packageName = mockCallingAppInfo.packageName, @@ -378,6 +405,20 @@ class Fido2IntentUtilsTest { val result = intent.getFido2GetCredentialsRequestOrNull() assertNull(result) } + + @Test + fun `getFido2GetCredentialRequestOrNull should return null when user id is not in extras`() { + val intent = mockk { + 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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt index 39f267da9..3cb038735 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt @@ -6,15 +6,12 @@ import android.content.Intent import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PublicKeyCredentialEntry -import com.bitwarden.fido.Fido2CredentialAutofillView 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.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 io.mockk.Called import io.mockk.MockKVerificationScope @@ -175,6 +172,7 @@ class Fido2CompletionManagerTest { fido2CompletionManager .completeFido2GetCredentialRequest( Fido2GetCredentialsResult.Success( + userId = "mockUserId", options = mockk(), credentials = emptyList(), ), @@ -201,6 +199,7 @@ class Fido2CompletionManagerTest { every { mockIntentManager.createFido2GetCredentialPendingIntent( action = GET_PASSKEY_INTENT, + userId = "mockUserId", credentialId = mockFido2AutofillView.credentialId.toString(), cipherId = mockFido2AutofillView.cipherId, requestCode = any(), @@ -211,6 +210,7 @@ class Fido2CompletionManagerTest { fido2CompletionManager .completeFido2GetCredentialRequest( Fido2GetCredentialsResult.Success( + userId = "mockUserId", options = mockk(), credentials = mockFido2AutofillViewList, ), @@ -249,6 +249,7 @@ class Fido2CompletionManagerTest { every { mockIntentManager.createFido2GetCredentialPendingIntent( action = GET_PASSKEY_INTENT, + userId = "mockUserId", credentialId = mockFido2AutofillView.credentialId.toString(), cipherId = mockFido2AutofillView.cipherId, requestCode = any(), @@ -259,6 +260,7 @@ class Fido2CompletionManagerTest { fido2CompletionManager .completeFido2GetCredentialRequest( Fido2GetCredentialsResult.Success( + userId = "mockUserId", options = mockk(), credentials = mockFido2AutofillViewList, ), @@ -304,35 +306,5 @@ class Fido2CompletionManagerTest { mockActivity.finish() } } - - private fun setupMockCompletionIntent( - mockFido2AutofillView1: Fido2CredentialAutofillView, - mockCredentialEntry1: PublicKeyCredentialEntry, - ): Intent { - val mockIntent1 = mockk { - every { - putExtra( - EXTRA_KEY_CIPHER_ID, - mockFido2AutofillView1.cipherId, - ) - } returns this - every { - putExtra( - EXTRA_KEY_CREDENTIAL_ID, - mockFido2AutofillView1.credentialId.toString(), - ) - } returns this - } - - every { - anyConstructed() - .build() - } returns mockCredentialEntry1 - every { anyConstructed().setPackage(any()) } returns mockIntent1 - every { - PendingIntent.getActivity(mockActivity, any(), mockIntent1, any()) - } returns mockk() - return mockIntent1 - } } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index e93269fe9..d104a945a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -1794,7 +1794,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { @Test 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)) verify { fido2CompletionManager.completeFido2GetCredentialRequest(result) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 6f23ee654..a4373cdfa 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -1517,6 +1517,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { candidateQueryData = mockk(), clientDataHash = byteArrayOf(0), id = "mockId", + userId = "mockUserId", ) specialCircumstanceManager.specialCircumstance =