mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 09:25:58 +03:00
[PM-9410] Implement FIDO 2 Get Credentials completion (#3639)
Some checks failed
Crowdin Push / Crowdin Push (push) Waiting to run
Scan / Check PR run (push) Failing after 1s
Scan / Quality scan (push) Has been skipped
Scan / SAST scan (push) Has been skipped
Test / Check PR run (push) Failing after 0s
Test / Test (push) Has been skipped
Some checks failed
Crowdin Push / Crowdin Push (push) Waiting to run
Scan / Check PR run (push) Failing after 1s
Scan / Quality scan (push) Has been skipped
Scan / SAST scan (push) Has been skipped
Test / Check PR run (push) Failing after 0s
Test / Test (push) Has been skipped
This commit is contained in:
parent
0e90bbb905
commit
deb8f811e5
7 changed files with 283 additions and 5 deletions
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
|
||||
/**
|
||||
* Represents the result of a FIDO 2 Get Credentials request.
|
||||
*/
|
||||
sealed class Fido2GetCredentialResult {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
data class Success(
|
||||
val options: BeginGetPublicKeyCredentialOption,
|
||||
val credentials: List<Fido2CredentialAutofillView>,
|
||||
) : Fido2GetCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates an error was encountered when querying for matching credentials.
|
||||
*/
|
||||
data object Error : Fido2GetCredentialResult()
|
||||
}
|
|
@ -41,7 +41,7 @@ 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 GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
|
||||
const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT"
|
||||
|
||||
/**
|
||||
|
@ -225,6 +225,7 @@ class Fido2ProviderProcessorImpl(
|
|||
DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
throw GetCredentialUnknownException("Error decrypting credentials.")
|
||||
}
|
||||
|
||||
is DecryptFido2CredentialAutofillViewResult.Success -> {
|
||||
result
|
||||
.fido2CredentialAutofillViews
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.autofill.fido2.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
|
||||
/**
|
||||
|
@ -17,4 +18,9 @@ interface Fido2CompletionManager {
|
|||
* Complete the FIDO 2 credential assertion process with the provided [result].
|
||||
*/
|
||||
fun completeFido2Assertion(result: Fido2CredentialAssertionResult)
|
||||
|
||||
/**
|
||||
* Complete the FIDO 2 "Get credentials" process with the provided [result].
|
||||
*/
|
||||
fun completeFido2GetCredentialRequest(result: Fido2GetCredentialResult)
|
||||
}
|
||||
|
|
|
@ -10,9 +10,16 @@ import androidx.credentials.PublicKeyCredential
|
|||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CompletionManager] when the build version is
|
||||
|
@ -21,6 +28,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResu
|
|||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
class Fido2CompletionManagerImpl(
|
||||
private val activity: Activity,
|
||||
private val intentManager: IntentManager,
|
||||
) : Fido2CompletionManager {
|
||||
|
||||
override fun completeFido2Registration(result: Fido2RegisterCredentialResult) {
|
||||
|
@ -84,4 +92,53 @@ class Fido2CompletionManagerImpl(
|
|||
it.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialResult) {
|
||||
val resultIntent = Intent()
|
||||
val responseBuilder = BeginGetCredentialResponse.Builder()
|
||||
when (result) {
|
||||
is Fido2GetCredentialResult.Success -> {
|
||||
val entries = result
|
||||
.credentials
|
||||
.map {
|
||||
val pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = Random.nextInt(),
|
||||
)
|
||||
PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = activity,
|
||||
username = it.userNameForUi
|
||||
?: activity.getString(R.string.no_username),
|
||||
pendingIntent = pendingIntent,
|
||||
beginGetPublicKeyCredentialOption = result.options,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
PendingIntentHandler
|
||||
.setBeginGetCredentialResponse(
|
||||
resultIntent,
|
||||
responseBuilder
|
||||
.setCredentialEntries(entries)
|
||||
// Explicitly clear any pending authentication actions since we only
|
||||
// display results from the active account.
|
||||
.setAuthenticationActions(emptyList())
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
Fido2GetCredentialResult.Error,
|
||||
-> {
|
||||
PendingIntentHandler.setGetCredentialException(
|
||||
resultIntent,
|
||||
GetCredentialUnknownException(),
|
||||
)
|
||||
}
|
||||
}
|
||||
activity.setResult(Activity.RESULT_OK, resultIntent)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.autofill.fido2.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
|
||||
/**
|
||||
|
@ -11,4 +12,6 @@ object Fido2CompletionManagerUnsupportedApiImpl : Fido2CompletionManager {
|
|||
override fun completeFido2Registration(result: Fido2RegisterCredentialResult) = Unit
|
||||
|
||||
override fun completeFido2Assertion(result: Fido2CredentialAssertionResult) = Unit
|
||||
|
||||
override fun completeFido2GetCredentialRequest(result: Fido2GetCredentialResult) = Unit
|
||||
}
|
||||
|
|
|
@ -32,15 +32,16 @@ import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManagerImp
|
|||
@Composable
|
||||
fun LocalManagerProvider(content: @Composable () -> Unit) {
|
||||
val activity = LocalContext.current as Activity
|
||||
val fido2IntentManager: IntentManager = IntentManagerImpl(activity)
|
||||
val fido2CompletionManager =
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
Fido2CompletionManagerUnsupportedApiImpl
|
||||
} else {
|
||||
Fido2CompletionManagerImpl(activity)
|
||||
Fido2CompletionManagerImpl(activity, fido2IntentManager)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalPermissionsManager provides PermissionsManagerImpl(activity),
|
||||
LocalIntentManager provides IntentManagerImpl(activity),
|
||||
LocalIntentManager provides fido2IntentManager,
|
||||
LocalExitManager provides ExitManagerImpl(activity),
|
||||
LocalBiometricsManager provides BiometricsManagerImpl(activity),
|
||||
LocalNfcManager provides NfcManagerImpl(activity),
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
package com.x8bit.bitwarden.ui.autofill.fido2.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
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.Fido2GetCredentialResult
|
||||
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
|
||||
import io.mockk.Ordering
|
||||
|
@ -13,11 +24,16 @@ import io.mockk.just
|
|||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -25,6 +41,7 @@ import org.junit.jupiter.api.Test
|
|||
class Fido2CompletionManagerTest {
|
||||
|
||||
private val mockActivity = mockk<Activity> {
|
||||
every { packageName } returns "packageName"
|
||||
every { setResult(Activity.RESULT_OK, any()) } just runs
|
||||
every { finish() } just runs
|
||||
}
|
||||
|
@ -56,25 +73,41 @@ class Fido2CompletionManagerTest {
|
|||
mockActivity wasNot Called
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should perform no operations`() {
|
||||
val mockGetCredentialResult = mockk<Fido2GetCredentialResult>()
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(mockGetCredentialResult)
|
||||
verify {
|
||||
mockGetCredentialResult wasNot Called
|
||||
mockActivity wasNot Called
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class DefaultImplementation {
|
||||
|
||||
private val mockIntentManager = mockk<IntentManager>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
fido2CompletionManager = Fido2CompletionManagerImpl(mockActivity)
|
||||
fido2CompletionManager = Fido2CompletionManagerImpl(mockActivity, mockIntentManager)
|
||||
mockkConstructor(Intent::class)
|
||||
mockkObject(PendingIntentHandler.Companion)
|
||||
every {
|
||||
PendingIntentHandler.setCreateCredentialException(any(), any())
|
||||
} just runs
|
||||
every {
|
||||
PendingIntentHandler.setBeginGetCredentialResponse(any(), any())
|
||||
} just runs
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkConstructor(Intent::class)
|
||||
unmockkConstructor(Intent::class, PublicKeyCredentialEntry.Builder::class)
|
||||
unmockkObject(PendingIntentHandler.Companion)
|
||||
unmockkStatic(PendingIntent::class)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -136,6 +169,127 @@ class Fido2CompletionManagerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set BeginGetCredentialResponse, set activity result, then finish activity when result is Success`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialResult.Success(
|
||||
options = mockk(),
|
||||
credentials = emptyList(),
|
||||
),
|
||||
)
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setBeginGetCredentialResponse(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should create a PublicKeyCredentialEntry and clear authentication actions when result is Success`() {
|
||||
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
mockkStatic(PendingIntent::class)
|
||||
|
||||
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
|
||||
val mockFido2AutofillView = createMockFido2CredentialAutofillView(number = 1)
|
||||
val mockFido2AutofillViewList = listOf(mockFido2AutofillView)
|
||||
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockCredentialEntry
|
||||
every {
|
||||
mockIntentManager.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
credentialId = mockFido2AutofillView.credentialId.toString(),
|
||||
cipherId = mockFido2AutofillView.cipherId,
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockk()
|
||||
every { mockActivity.getString(any()) } returns "No username"
|
||||
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialResult.Success(
|
||||
options = mockk(),
|
||||
credentials = mockFido2AutofillViewList,
|
||||
),
|
||||
)
|
||||
|
||||
val responseSlot = slot<BeginGetCredentialResponse>()
|
||||
verify {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
PendingIntentHandler.setBeginGetCredentialResponse(
|
||||
intent = any(),
|
||||
response = capture(responseSlot),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(mockCredentialEntry),
|
||||
responseSlot.captured.credentialEntries,
|
||||
)
|
||||
|
||||
assertTrue(responseSlot.captured.authenticationActions.isEmpty())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set username to default value when userNameForUi is null`() {
|
||||
mockkConstructor(PublicKeyCredentialEntry.Builder::class)
|
||||
mockkStatic(PendingIntent::class)
|
||||
val mockCredentialEntry = mockk<PublicKeyCredentialEntry>()
|
||||
val mockFido2AutofillView = createMockFido2CredentialAutofillView(number = 1)
|
||||
.copy(userNameForUi = null)
|
||||
val mockFido2AutofillViewList = listOf(mockFido2AutofillView)
|
||||
|
||||
every {
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
} returns mockCredentialEntry
|
||||
every {
|
||||
mockIntentManager.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
credentialId = mockFido2AutofillView.credentialId.toString(),
|
||||
cipherId = mockFido2AutofillView.cipherId,
|
||||
requestCode = any(),
|
||||
)
|
||||
} returns mockk()
|
||||
every { mockActivity.getString(any()) } returns "No Username"
|
||||
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(
|
||||
Fido2GetCredentialResult.Success(
|
||||
options = mockk(),
|
||||
credentials = mockFido2AutofillViewList,
|
||||
),
|
||||
)
|
||||
|
||||
val responseSlot = slot<BeginGetCredentialResponse>()
|
||||
verify {
|
||||
mockActivity.getString(R.string.no_username)
|
||||
anyConstructed<PublicKeyCredentialEntry.Builder>().build()
|
||||
PendingIntentHandler.setBeginGetCredentialResponse(
|
||||
intent = any(),
|
||||
response = capture(responseSlot),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(mockCredentialEntry),
|
||||
responseSlot.captured.credentialEntries,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() {
|
||||
fido2CompletionManager
|
||||
.completeFido2GetCredentialRequest(Fido2GetCredentialResult.Error)
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setGetCredentialException(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to ensure the given [calls] are performed before setting the
|
||||
* [mockActivity] result and calling finish. This sequence is expected to be performed for
|
||||
|
@ -150,5 +304,35 @@ class Fido2CompletionManagerTest {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue