[PM-8137] Respond to SDK user verification callbacks implicitly (#3448)

This commit is contained in:
Patrick Honkonen 2024-07-12 10:29:34 -04:00 committed by GitHub
parent 27747b6cb9
commit c6d05b4631
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 188 additions and 258 deletions

View file

@ -2,8 +2,6 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.fido.ClientData
import com.bitwarden.sdk.CheckUserResult
import com.bitwarden.sdk.CipherViewWrapper
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
@ -60,27 +58,25 @@ class Fido2CredentialManagerImpl(
val origin = fido2CredentialRequest.origin
?: fido2CredentialRequest.callingAppInfo.getAppOrigin()
return vaultSdkSource.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = origin,
requestJson = """{"publicKey": ${fido2CredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
isUserVerificationSupported = true,
),
fido2CredentialStore = this,
// TODO: [PM-8137] Determine if user verification is supported
checkUser = { _, _ -> CheckUserResult(true, true) },
checkUserAndPickCredential = { _, _ -> CipherViewWrapper(selectedCipherView) },
)
return vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = origin,
requestJson = """{"publicKey": ${fido2CredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
// User verification is handled prior to engaging the SDK. We always respond
// `true` so that the SDK does not fail if the relying party requests UV.
isUserVerificationSupported = true,
),
fido2CredentialStore = this,
)
.map { it.toAndroidAttestationResponse() }
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = {
Fido2RegisterCredentialResult.Error
},
onFailure = { Fido2RegisterCredentialResult.Error },
)
}

View file

@ -8,15 +8,10 @@ import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.UpdatePasswordResponse
import com.bitwarden.crypto.TrustDeviceResponse
import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.CheckUserOptions
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAttestationResponse
import com.bitwarden.sdk.CheckUserResult
import com.bitwarden.sdk.CipherViewWrapper
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.sdk.UiHint
import com.bitwarden.send.Send
import com.bitwarden.send.SendView
import com.bitwarden.vault.Attachment
@ -27,12 +22,12 @@ import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.Collection
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.Fido2CredentialNewView
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView
import com.bitwarden.vault.PasswordHistory
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.TotpResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import java.io.File
@ -420,53 +415,23 @@ interface VaultSdkSource {
/**
* Register a new FIDO 2 credential to a cipher.
*
* @param checkUser Receives [CheckUserOptions] and [UiHint] indicating what interactions and
* prompts must be presented to the user during registration. A [CheckUserResult] is expected
* when interactions are completed.
* @param checkUserAndPickCredential Receives [CheckUserOptions] indicating user
* verification requirements and a [Fido2CredentialNewView] representing the newly registered
* credential. A [CipherViewWrapper] containing the selectedCipherView updated with the
* [Fido2CredentialNewView] is expected in response.
*
* @return Result of the FIDO 2 credential registration. If successful, a
* [PublicKeyCredentialAuthenticatorAttestationResponse] is provided.
*/
suspend fun registerFido2Credential(
request: RegisterFido2CredentialRequest,
fido2CredentialStore: Fido2CredentialStore,
checkUser: suspend (CheckUserOptions, UiHint?) -> CheckUserResult,
checkUserAndPickCredential: suspend (
options: CheckUserOptions,
newCredential: Fido2CredentialNewView,
) -> CipherViewWrapper,
): Result<PublicKeyCredentialAuthenticatorAttestationResponse>
/**
* Authenticate a user with a FIDO 2 credential.
*
* @param userId Active user's ID.
* @param origin Origin of the relying party request.
* @param requestJson JSON provided by the relying party.
* @param clientData Client metadata about the relying party or calling application.
* @param isVerificationSupported Whether user verification can be performed on this device.
* @param checkUser Receives [CheckUserOptions] and [UiHint] indicating what interactions and
* prompts must be presented to the user for registration to complete. A [CheckUserResult] is
* expected when interactions are completed.
* @param pickCredentialForAuthentication Receives a collection of [CipherView]s that can be
* chosen to perform authentication with.
*
* @return Result of the FIDO 2 credential registration. If successful, a
* [PublicKeyCredentialAuthenticatorAttestationResponse] is provided.
*/
@Suppress("LongParameterList")
suspend fun authenticateFido2Credential(
userId: String,
origin: String,
requestJson: String,
clientData: ClientData,
isVerificationSupported: Boolean,
checkUser: suspend (CheckUserOptions, UiHint?) -> CheckUserResult,
pickCredentialForAuthentication: suspend (List<CipherView>) -> CipherViewWrapper,
request: AuthenticateFido2CredentialRequest,
fido2CredentialStore: Fido2CredentialStore,
): Result<PublicKeyCredentialAuthenticatorAssertionResponse>

View file

@ -7,18 +7,13 @@ import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.UpdatePasswordResponse
import com.bitwarden.crypto.TrustDeviceResponse
import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.CheckUserOptions
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAttestationResponse
import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.CheckUserResult
import com.bitwarden.sdk.CipherViewWrapper
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientVault
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.sdk.UiHint
import com.bitwarden.send.Send
import com.bitwarden.send.SendView
import com.bitwarden.vault.Attachment
@ -28,14 +23,13 @@ import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.Collection
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.Fido2CredentialNewView
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView
import com.bitwarden.vault.PasswordHistory
import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.TotpResponse
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.util.asFailure
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.InitializeCryptoResult
@ -452,11 +446,6 @@ class VaultSdkSourceImpl(
override suspend fun registerFido2Credential(
request: RegisterFido2CredentialRequest,
fido2CredentialStore: Fido2CredentialStore,
checkUser: suspend (CheckUserOptions, UiHint?) -> CheckUserResult,
checkUserAndPickCredential: suspend (
options: CheckUserOptions,
newCredential: Fido2CredentialNewView,
) -> CipherViewWrapper,
): Result<PublicKeyCredentialAuthenticatorAttestationResponse> = runCatching {
callbackFlow {
try {
@ -465,9 +454,8 @@ class VaultSdkSourceImpl(
.fido2()
.client(
userInterface = Fido2CredentialRegistrationUserInterfaceImpl(
selectedCipherView = request.selectedCipherView,
isVerificationSupported = request.isUserVerificationSupported,
checkUser = checkUser,
checkUserAndPickCredentialForCreation = checkUserAndPickCredential,
),
credentialStore = fido2CredentialStore,
)
@ -480,10 +468,9 @@ class VaultSdkSourceImpl(
)
send(result)
} catch (e: BitwardenException) {
e.asFailure()
} finally {
close()
} catch (e: BitwardenException) {
close(e)
}
awaitClose()
}
@ -492,40 +479,32 @@ class VaultSdkSourceImpl(
@Suppress("MaxLineLength")
override suspend fun authenticateFido2Credential(
userId: String,
origin: String,
requestJson: String,
clientData: ClientData,
isVerificationSupported: Boolean,
checkUser: suspend (CheckUserOptions, UiHint?) -> CheckUserResult,
pickCredentialForAuthentication: suspend (List<CipherView>) -> CipherViewWrapper,
request: AuthenticateFido2CredentialRequest,
fido2CredentialStore: Fido2CredentialStore,
): Result<PublicKeyCredentialAuthenticatorAssertionResponse> = runCatching {
callbackFlow {
try {
val client = getClient(userId)
val client = getClient(request.userId)
.platform()
.fido2()
.client(
userInterface = Fido2CredentialAuthenticationUserInterfaceImpl(
isVerificationSupported = isVerificationSupported,
checkUser = checkUser,
pickCredentialForAuthentication = pickCredentialForAuthentication,
selectedCipherView = request.selectedCipherView,
isVerificationSupported = request.isUserVerificationSupported,
),
credentialStore = fido2CredentialStore,
)
val result = client.authenticate(
origin = origin,
request = requestJson,
clientData = clientData,
origin = request.origin,
request = request.requestJson,
clientData = request.clientData,
)
send(result)
} catch (e: BitwardenException) {
e.asFailure()
} finally {
close()
} catch (e: BitwardenException) {
close(e)
}
awaitClose()

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.fido.ClientData
import com.bitwarden.vault.CipherView
/**
* Models a FIDO 2 authentication request to the Bitwarden SDK.
*
* @param userId User whom the credential is being authenticated for.
* @param origin Origin of the Relying Party. This can either be a Relying Party's URL or their
* application fingerprint.
* @param requestJson Authentication request JSON received from the OS.
* @param clientData Metadata containing either privileged application certificate hash or Android
* package name of the Relying Party.
* @param selectedCipherView [CipherView] containing the FIDO 2 credential being authenticated.
* @param isUserVerificationSupported Whether the device or application are capable of performing
* user verification.
*/
data class AuthenticateFido2CredentialRequest(
val userId: String,
val origin: String,
val requestJson: String,
val clientData: ClientData,
val selectedCipherView: CipherView,
val isUserVerificationSupported: Boolean,
)

View file

@ -15,14 +15,13 @@ import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
*/
@OmitFromCoverage
class Fido2CredentialAuthenticationUserInterfaceImpl(
private val selectedCipherView: CipherView,
private val isVerificationSupported: Boolean,
private val checkUser: suspend (CheckUserOptions, UiHint?) -> CheckUserResult,
private val pickCredentialForAuthentication: suspend (List<CipherView>) -> CipherViewWrapper,
) : Fido2UserInterface {
override suspend fun checkUser(
options: CheckUserOptions,
hint: UiHint,
): CheckUserResult = checkUser.invoke(options, hint)
): CheckUserResult = CheckUserResult(true, true)
override suspend fun checkUserAndPickCredentialForCreation(
options: CheckUserOptions,
@ -33,5 +32,5 @@ class Fido2CredentialAuthenticationUserInterfaceImpl(
override suspend fun pickCredentialForAuthentication(
availableCredentials: List<CipherView>,
): CipherViewWrapper = pickCredentialForAuthentication.invoke(availableCredentials)
): CipherViewWrapper = CipherViewWrapper(selectedCipherView)
}

View file

@ -15,23 +15,22 @@ import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
*/
@OmitFromCoverage
class Fido2CredentialRegistrationUserInterfaceImpl(
private val selectedCipherView: CipherView,
private val isVerificationSupported: Boolean,
private val checkUser: suspend (CheckUserOptions, UiHint?) -> CheckUserResult,
private val checkUserAndPickCredentialForCreation: suspend (
options: CheckUserOptions,
newCredential: Fido2CredentialNewView,
) -> CipherViewWrapper,
) : Fido2UserInterface {
override suspend fun checkUser(
options: CheckUserOptions,
hint: UiHint,
): CheckUserResult = checkUser.invoke(options, hint)
): CheckUserResult = CheckUserResult(true, true)
override suspend fun checkUserAndPickCredentialForCreation(
options: CheckUserOptions,
newCredential: Fido2CredentialNewView,
): CheckUserAndPickCredentialForCreationResult = throw IllegalStateException()
): CheckUserAndPickCredentialForCreationResult = CheckUserAndPickCredentialForCreationResult(
cipher = CipherViewWrapper(selectedCipherView),
checkUserResult = CheckUserResult(true, true),
)
override suspend fun isVerificationEnabled(): Boolean = isVerificationSupported

View file

@ -1,11 +1,8 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.data.vault.datasource.sdk.util
import android.util.Base64
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAttestationResponse
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/**
* Converts the SDK attestation response to a [Fido2AttestationResponse] that can be serialized into
@ -28,11 +25,11 @@ fun PublicKeyCredentialAuthenticatorAttestationResponse.toAndroidAttestationResp
clientExtensionResults = clientExtensionResults
.credProps
?.rk
?.let {
?.let { residentKey ->
Fido2AttestationResponse.ClientExtensionResults(
credentialProperties = Fido2AttestationResponse
.ClientExtensionResults
.CredentialProperties(residentKey = it),
.CredentialProperties(residentKey = residentKey),
)
},
authenticatorAttachment = authenticatorAttachment,

View file

@ -322,8 +322,6 @@ class Fido2CredentialManagerTest {
mockVaultSdkSource.registerFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
checkUser = any(),
checkUserAndPickCredential = any(),
)
} coAnswers {
mockRegistrationResponse
@ -362,8 +360,6 @@ class Fido2CredentialManagerTest {
mockVaultSdkSource.registerFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
checkUser = any(),
checkUserAndPickCredential = any(),
)
} coAnswers {
mockRegistrationResponse
@ -399,8 +395,6 @@ class Fido2CredentialManagerTest {
mockVaultSdkSource.registerFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
checkUser = any(),
checkUserAndPickCredential = any(),
)
} coAnswers {
mockRegistrationResponse
@ -441,8 +435,6 @@ class Fido2CredentialManagerTest {
mockVaultSdkSource.registerFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
checkUser = any(),
checkUserAndPickCredential = any(),
)
} coAnswers {
mockRegistrationResponse
@ -515,8 +507,6 @@ class Fido2CredentialManagerTest {
mockVaultSdkSource.registerFido2Credential(
request = any(),
fido2CredentialStore = any(),
checkUser = any(),
checkUserAndPickCredential = any(),
)
} coAnswers {
mockRegistrationResponse

View file

@ -7,14 +7,11 @@ import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.UpdatePasswordResponse
import com.bitwarden.crypto.TrustDeviceResponse
import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.CheckUserOptions
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAttestationResponse
import com.bitwarden.fido.Verification
import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.CheckUserResult
import com.bitwarden.sdk.CipherViewWrapper
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientAuth
import com.bitwarden.sdk.ClientCiphers
@ -27,8 +24,6 @@ import com.bitwarden.sdk.ClientPlatform
import com.bitwarden.sdk.ClientSends
import com.bitwarden.sdk.ClientVault
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.sdk.Fido2UserInterface
import com.bitwarden.sdk.UiHint
import com.bitwarden.send.Send
import com.bitwarden.send.SendView
import com.bitwarden.vault.Attachment
@ -46,6 +41,7 @@ import com.bitwarden.vault.TotpResponse
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.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
@ -58,11 +54,11 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.security.MessageDigest
@ -1016,26 +1012,12 @@ class VaultSdkSourceTest {
every { digest(any()) } returns DEFAULT_SIGNATURE.toByteArray()
}
val mockCipherView = createMockCipherView(1)
val mockAttestation = mockk<PublicKeyCredentialAuthenticatorAttestationResponse>()
coEvery { fido2.register(any(), any(), any()) } returns mockAttestation
val result = vaultSdkSource.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = "mockUserId",
origin = "www.bitwarden.com",
requestJson = "requestJson",
clientData = ClientData.DefaultWithCustomHash(
hash = DEFAULT_SIGNATURE.toByteArray(),
),
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
),
checkUser = { _, _ -> CheckUserResult(true, true) },
checkUserAndPickCredential = { _, _ ->
CipherViewWrapper(mockCipherView)
},
DEFAULT_FIDO_2_REGISTER_CREDENTIAL_REQUEST,
fido2CredentialStore = mockFido2CredentialStore,
)
@ -1047,58 +1029,23 @@ class VaultSdkSourceTest {
}
@Test
fun `registerFido2Credential should invoke checkUser when called by the SDK`() = runTest {
val checkUserResult = CheckUserResult(true, true)
val checkUserOptionsSlot = slot<CheckUserOptions>()
val uiHintSlot = slot<UiHint>()
val mockUserInterface = mockk<Fido2UserInterface> {
fun `registerFido2Credential should return Failure when BitwardenException is thrown`() =
runTest {
coEvery {
checkUser(
capture(checkUserOptionsSlot),
capture(uiHintSlot),
fido2.register(
any(),
any(),
any(),
)
} returns checkUserResult
}
} throws BitwardenException.E("mockException")
val mockCipherView = createMockCipherView(number = 1)
val mockAttestation = mockk<PublicKeyCredentialAuthenticatorAttestationResponse>()
val mockCheckUserOptions = CheckUserOptions(true, Verification.REQUIRED)
coEvery { fido2.register(any(), any(), any()) } coAnswers {
mockUserInterface.checkUser(
mockCheckUserOptions,
UiHint.InformNoCredentialsFound,
val result = vaultSdkSource.registerFido2Credential(
DEFAULT_FIDO_2_REGISTER_CREDENTIAL_REQUEST,
fido2CredentialStore = mockFido2CredentialStore,
)
mockAttestation
assertTrue(result.isFailure)
}
vaultSdkSource.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = "mockUserId",
origin = "www.bitwarden.com",
requestJson = "requestJson",
clientData = ClientData.DefaultWithCustomHash(
hash = DEFAULT_SIGNATURE.toByteArray(),
),
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
),
checkUser = { _, _ -> checkUserResult },
checkUserAndPickCredential = { _, _ -> CipherViewWrapper(mockCipherView) },
fido2CredentialStore = mockFido2CredentialStore,
)
coVerify { mockUserInterface.checkUser(any(), any()) }
assertEquals(
mockCheckUserOptions,
checkUserOptionsSlot.captured,
)
assertEquals(
UiHint.InformNoCredentialsFound,
uiHintSlot.captured,
)
}
@Suppress("MaxLineLength")
@Test
@ -1109,21 +1056,15 @@ class VaultSdkSourceTest {
every { digest(any()) } returns DEFAULT_SIGNATURE.toByteArray()
}
val mockCipherView = createMockCipherView(1)
val mockAssertion = mockk<PublicKeyCredentialAuthenticatorAssertionResponse>()
coEvery { fido2.authenticate(any(), any(), any()) } returns mockAssertion
val result = vaultSdkSource.authenticateFido2Credential(
userId = "mockUserId",
origin = "www.bitwarden.com",
requestJson = "requestJson",
clientData = ClientData.DefaultWithCustomHash(DEFAULT_SIGNATURE.toByteArray()),
isVerificationSupported = true,
checkUser = { _, _ -> CheckUserResult(true, true) },
pickCredentialForAuthentication = { CipherViewWrapper(mockCipherView) },
fido2CredentialStore = mockFido2CredentialStore,
)
val result = vaultSdkSource
.authenticateFido2Credential(
DEFAULT_FIDO_2_AUTH_REQUEST,
fido2CredentialStore = mockFido2CredentialStore,
)
assertEquals(
mockAssertion.asSuccess(),
@ -1133,53 +1074,88 @@ class VaultSdkSourceTest {
}
@Test
fun `authenticateFido2Credential should invoke checkUser when called by the SDK`() = runTest {
val checkUserResult = CheckUserResult(true, true)
val checkUserOptionsSlot = slot<CheckUserOptions>()
val uiHintSlot = slot<UiHint>()
val mockUserInterface = mockk<Fido2UserInterface> {
fun `authenticateFido2Credential should return Failure when BitwardenException is thrown`() =
runTest {
coEvery {
checkUser(
capture(checkUserOptionsSlot),
capture(uiHintSlot),
fido2.authenticate(
any(),
any(),
any(),
)
} returns checkUserResult
} throws BitwardenException.E("mockException")
val result = vaultSdkSource
.authenticateFido2Credential(
DEFAULT_FIDO_2_AUTH_REQUEST,
fido2CredentialStore = mockFido2CredentialStore,
)
assertTrue(result.isFailure)
}
@Test
fun `decryptFido2CredentialAutofillViews should return results when successful`() = runTest {
val mockCipherView = createMockCipherView(number = 1)
val mockAssertion = mockk<PublicKeyCredentialAuthenticatorAssertionResponse>()
val mockCheckUserOptions = CheckUserOptions(true, Verification.REQUIRED)
coEvery { fido2.authenticate(any(), any(), any()) } coAnswers {
mockUserInterface.checkUser(
mockCheckUserOptions,
UiHint.InformNoCredentialsFound,
)
mockAssertion
}
vaultSdkSource.authenticateFido2Credential(
val mockAutofillView = Fido2CredentialAutofillView(
credentialId = byteArrayOf(0),
cipherId = "mockCipherId",
rpId = "mockRpId",
userNameForUi = "mockUserNameForUi",
userHandle = "mockUserHandle".toByteArray(),
)
val autofillViews = listOf(mockAutofillView)
coEvery {
clientFido2.decryptFido2AutofillCredentials(mockCipherView)
} returns autofillViews
val result = vaultSdkSource.decryptFido2CredentialAutofillViews(
userId = "mockUserId",
origin = "www.bitwarden.com",
requestJson = "requestJson",
clientData = ClientData.DefaultWithCustomHash(DEFAULT_SIGNATURE.toByteArray()),
isVerificationSupported = true,
checkUser = { _, _ -> checkUserResult },
pickCredentialForAuthentication = { CipherViewWrapper(mockCipherView) },
fido2CredentialStore = mockFido2CredentialStore,
)
coVerify { mockUserInterface.checkUser(any(), any()) }
assertEquals(
mockCheckUserOptions,
checkUserOptionsSlot.captured,
cipherViews = arrayOf(mockCipherView),
)
assertEquals(
UiHint.InformNoCredentialsFound,
uiHintSlot.captured,
autofillViews.asSuccess(),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `decryptFido2CredentialAutofillViews should return Failure when Bitwarden exception is thrown`() =
runTest {
val mockCipherView = createMockCipherView(number = 1)
coEvery {
clientFido2.decryptFido2AutofillCredentials(mockCipherView)
} throws BitwardenException.E("mockException")
val result = vaultSdkSource.decryptFido2CredentialAutofillViews(
userId = "mockUserId",
cipherViews = arrayOf(mockCipherView),
)
assertTrue(result.isFailure)
}
}
private const val DEFAULT_SIGNATURE = "0987654321ABCDEF"
private val DEFAULT_FIDO_2_REGISTER_CREDENTIAL_REQUEST = RegisterFido2CredentialRequest(
userId = "mockUserId",
origin = "www.bitwarden.com",
requestJson = "requestJson",
clientData = ClientData.DefaultWithCustomHash(
DEFAULT_SIGNATURE.toByteArray(),
),
isUserVerificationSupported = true,
selectedCipherView = createMockCipherView(number = 1),
)
val DEFAULT_FIDO_2_AUTH_REQUEST = AuthenticateFido2CredentialRequest(
userId = "mockUserId",
origin = "www.bitwarden.com",
requestJson = "requestJson",
clientData = ClientData.DefaultWithCustomHash(
DEFAULT_SIGNATURE.toByteArray(),
),
isUserVerificationSupported = true,
selectedCipherView = createMockCipherView(number = 1),
)

View file

@ -6,31 +6,34 @@ import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKe
* Returns a mock FIDO 2 [PublicKeyCredentialCreationOptions] object to simulate a credential
* creation request.
*/
fun createMockPublicKeyCredentialCreationOptions(number: Int) =
PublicKeyCredentialCreationOptions(
authenticatorSelection = PublicKeyCredentialCreationOptions
.AuthenticatorSelectionCriteria(),
challenge = "mockPublicKeyCredentialCreationOptionsChallenge-$number",
excludeCredentials = listOf(
PublicKeyCredentialCreationOptions.PublicKeyCredentialDescriptor(
type = "mockPublicKeyCredentialDescriptorType-$number",
id = "mockPublicKeyCredentialDescriptorId-$number",
transports = listOf("mockPublicKeyCredentialDescriptorTransports-$number"),
),
@Suppress("MaxLineLength")
fun createMockPublicKeyCredentialCreationOptions(
number: Int,
userVerificationRequirement: PublicKeyCredentialCreationOptions.AuthenticatorSelectionCriteria.UserVerificationRequirement? = null,
) = PublicKeyCredentialCreationOptions(
authenticatorSelection = PublicKeyCredentialCreationOptions
.AuthenticatorSelectionCriteria(userVerification = userVerificationRequirement),
challenge = "mockPublicKeyCredentialCreationOptionsChallenge-$number",
excludeCredentials = listOf(
PublicKeyCredentialCreationOptions.PublicKeyCredentialDescriptor(
type = "mockPublicKeyCredentialDescriptorType-$number",
id = "mockPublicKeyCredentialDescriptorId-$number",
transports = listOf("mockPublicKeyCredentialDescriptorTransports-$number"),
),
pubKeyCredParams = listOf(
PublicKeyCredentialCreationOptions.PublicKeyCredentialParameters(
type = "PublicKeyCredentialParametersType-$number",
alg = number.toLong(),
),
),
pubKeyCredParams = listOf(
PublicKeyCredentialCreationOptions.PublicKeyCredentialParameters(
type = "PublicKeyCredentialParametersType-$number",
alg = number.toLong(),
),
relyingParty = PublicKeyCredentialCreationOptions.PublicKeyCredentialRpEntity(
name = "mockPublicKeyCredentialRpEntityName-$number",
id = "mockPublicKeyCredentialRpEntity-$number",
),
user = PublicKeyCredentialCreationOptions.PublicKeyCredentialUserEntity(
name = "mockPublicKeyCredentialUserEntityName-$number",
id = "mockPublicKeyCredentialUserEntityId-$number",
displayName = "mockPublicKeyCredentialUserEntityDisplayName-$number",
),
)
),
relyingParty = PublicKeyCredentialCreationOptions.PublicKeyCredentialRpEntity(
name = "mockPublicKeyCredentialRpEntityName-$number",
id = "mockPublicKeyCredentialRpEntity-$number",
),
user = PublicKeyCredentialCreationOptions.PublicKeyCredentialUserEntity(
name = "mockPublicKeyCredentialUserEntityName-$number",
id = "mockPublicKeyCredentialUserEntityId-$number",
displayName = "mockPublicKeyCredentialUserEntityDisplayName-$number",
),
)