[PM-9409] Add FIDO 2 authentication to credential manager (#3629)

This commit is contained in:
Patrick Honkonen 2024-07-25 15:46:26 -04:00 committed by GitHub
parent c09fe554bc
commit b0f0c0f33b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 568 additions and 31 deletions

View file

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.data.autofill.fido2.manager
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
@ -53,6 +55,15 @@ interface Fido2CredentialManager {
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult
/**
* Authenticate a FIDO credential against a cipher in the users vault.
*/
suspend fun authenticateFido2Credential(
userId: String,
request: Fido2CredentialAssertionRequest,
selectedCipherView: CipherView,
): Fido2CredentialAssertionResult
/**
* Whether or not the user has authentication attempts remaining.
*/

View file

@ -6,6 +6,8 @@ import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
@ -20,8 +22,10 @@ import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -31,6 +35,7 @@ private const val ALLOW_LIST_FILE_NAME = "fido2_privileged_allow_list.json"
/**
* Primary implementation of [Fido2CredentialManager].
*/
@Suppress("TooManyFunctions")
class Fido2CredentialManagerImpl(
private val assetManager: AssetManager,
private val digitalAssetLinkService: DigitalAssetLinkService,
@ -118,6 +123,37 @@ class Fido2CredentialManagerImpl(
null
}
override suspend fun authenticateFido2Credential(
userId: String,
request: Fido2CredentialAssertionRequest,
selectedCipherView: CipherView,
): Fido2CredentialAssertionResult {
val callingAppInfo = request.callingAppInfo
val clientData = request.clientDataHash
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
return vaultSdkSource
.authenticateFido2Credential(
request = AuthenticateFido2CredentialRequest(
userId = userId,
origin = callingAppInfo.origin
?: callingAppInfo.getAppOrigin(),
requestJson = """{"publicKey": ${request.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
isUserVerificationSupported = true,
),
fido2CredentialStore = this,
)
.map { it.toAndroidFido2PublicKeyCredential() }
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
onFailure = { Fido2CredentialAssertionResult.Error },
)
}
private suspend fun validateCallingApplicationAssetLinks(
fido2CredentialRequest: Fido2CredentialRequest,
): Fido2ValidateOriginResult {

View file

@ -14,9 +14,4 @@ sealed class Fido2CredentialAssertionResult {
* Indicates there was an error and the assertion was not successful.
*/
data object Error : Fido2CredentialAssertionResult()
/**
* Indicates assertion was cancelled by the user.
*/
data object Cancelled : Fido2CredentialAssertionResult()
}

View file

@ -0,0 +1,57 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Models a FIDO 2 public key credential.
*/
@Serializable
data class Fido2PublicKeyCredential(
@SerialName("id")
val id: String,
@SerialName("rawId")
val rawId: String,
@SerialName("type")
val type: String,
@SerialName("authenticatorAttachment")
val authenticatorAttachment: String?,
@SerialName("response")
val response: Fido2AssertionResponse,
@SerialName("clientExtensionResults")
val clientExtensionResults: ClientExtensionResults,
) {
/**
* Models a FIDO 2 public key assertion response.
*/
@Serializable
data class Fido2AssertionResponse(
@SerialName("clientDataJSON")
val clientDataJson: String?,
@SerialName("authenticatorData")
val authenticatorData: String,
@SerialName("signature")
val signature: String,
@SerialName("userHandle")
val userHandle: String?,
)
/**
* Models FIDO 2 credential properties provided by a client.
*/
@Serializable
data class ClientExtensionResults(
@SerialName("credProps")
val credentialProperties: CredentialProperties?,
) {
/**
* Models the FIDO 2 credential properties provided by a client.
*/
@Serializable
data class CredentialProperties(
@SerialName("rk")
val residentKey: Boolean?,
)
}
}

View file

@ -0,0 +1,38 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.util
import android.util.Base64
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2PublicKeyCredential
/**
* Converts the Bitwarden SDK response to a [Fido2PublicKeyCredential] that can be serialized into
* the expected system JSON.
*/
fun PublicKeyCredentialAuthenticatorAssertionResponse.toAndroidFido2PublicKeyCredential() =
Fido2PublicKeyCredential(
id = id,
rawId = rawId.base64EncodeForFido2Response(),
type = this.ty,
authenticatorAttachment = authenticatorAttachment,
response = Fido2PublicKeyCredential.Fido2AssertionResponse(
clientDataJson = response.clientDataJson.base64EncodeForFido2Response(),
authenticatorData = response.authenticatorData.base64EncodeForFido2Response(),
signature = response.signature.base64EncodeForFido2Response(),
userHandle = response.userHandle.base64EncodeForFido2Response(),
),
clientExtensionResults = Fido2PublicKeyCredential.ClientExtensionResults(
credentialProperties = clientExtensionResults.credProps?.let { credProps ->
Fido2PublicKeyCredential
.ClientExtensionResults
.CredentialProperties(
residentKey = credProps.rk ?: true,
)
},
),
)
private fun ByteArray.base64EncodeForFido2Response(): String =
Base64.encodeToString(
this,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
)

View file

@ -9,7 +9,6 @@ import androidx.credentials.GetCredentialResponse
import androidx.credentials.PublicKeyCredential
import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
@ -63,14 +62,6 @@ class Fido2CompletionManagerImpl(
activity.also {
val intent = Intent()
when (result) {
Fido2CredentialAssertionResult.Cancelled -> {
PendingIntentHandler
.setGetCredentialException(
intent = intent,
exception = GetCredentialCancellationException(),
)
}
Fido2CredentialAssertionResult.Error -> {
PendingIntentHandler
.setGetCredentialException(

View file

@ -5,23 +5,30 @@ import android.content.pm.SigningInfo
import android.util.Base64
import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2PublicKeyCredential
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPublicKeyAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
import io.mockk.coEvery
@ -33,6 +40,7 @@ import io.mockk.slot
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
@ -44,6 +52,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.security.MessageDigest
@Suppress("LargeClass")
class Fido2CredentialManagerTest {
private lateinit var fido2CredentialManager: Fido2CredentialManager
@ -108,6 +117,9 @@ class Fido2CredentialManagerTest {
@AfterEach
fun tearDown() {
unmockkStatic(MessageDigest::class, Base64::class)
unmockkStatic(
PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential,
)
}
@Test
@ -342,10 +354,6 @@ class Fido2CredentialManagerTest {
@Test
fun `registerFido2Credential should construct ClientData DefaultWithCustomHash when callingAppInfo origin is populated`() =
runTest {
val mockSigningInfo = mockk<SigningInfo> {
every { apkContentsSigners } returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
every { hasMultipleSigners() } returns false
}
val mockFido2CreateCredentialRequest = createMockFido2CredentialRequest(
number = 1,
origin = "origin",
@ -575,6 +583,282 @@ class Fido2CredentialManagerTest {
fido2CredentialManager.authenticationAttempts = 6
assertFalse(fido2CredentialManager.hasAuthenticationAttemptsRemaining())
}
@Suppress("MaxLineLength")
@Test
fun `authenticateFido2Credential should construct ClientData DefaultWithCustomHash when clientDataHash is not null`() =
runTest {
every { Base64.encodeToString(any(), any()) } returns ""
val mockCipherView = createMockCipherView(number = 1)
val mockRequest = createMockFido2CredentialAssertionRequest(number = 1)
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
number = 1,
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
mockRequest = mockRequest,
mockCipherView = mockCipherView,
)
val mockSdkResponse =
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
coEvery {
mockVaultSdkSource.authenticateFido2Credential(
request = mockSdkRequest,
fido2CredentialStore = any(),
)
} returns mockSdkResponse.asSuccess()
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockRequest,
selectedCipherView = createMockCipherView(number = 1),
)
coVerify {
mockVaultSdkSource.authenticateFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
)
}
assertEquals(
AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = "mockOrigin-1",
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
),
requestCaptureSlot.captured,
)
}
@Suppress("MaxLineLength")
@Test
fun `authenticateFido2Credential should construct ClientData DefaultWithExtraData when clientDataHash is null`() =
runTest {
every {
mockSigningInfo.apkContentsSigners
} returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
every {
mockSigningInfo.hasMultipleSigners()
} returns false
val mockCipherView = createMockCipherView(number = 1)
val mockRequest = Fido2CredentialAssertionRequest(
cipherId = "mockCipherId",
credentialId = "mockCredentialId",
requestJson = "requestJson",
clientDataHash = null,
packageName = "mockPackageName",
signingInfo = mockSigningInfo,
origin = "mockOrigin",
)
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
val mockSdkRequest = AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = mockRequest.origin!!,
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = ClientData.DefaultWithExtraData(
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
),
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
)
val mockSdkResponse =
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
coEvery {
mockVaultSdkSource.authenticateFido2Credential(
request = mockSdkRequest,
fido2CredentialStore = any(),
)
} returns mockSdkResponse.asSuccess()
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockRequest,
selectedCipherView = createMockCipherView(number = 1),
)
coVerify {
mockVaultSdkSource.authenticateFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
)
}
assertEquals(
AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = mockRequest.origin!!,
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = ClientData.DefaultWithExtraData(
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
),
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
),
requestCaptureSlot.captured,
)
}
@Test
fun `authenticateFido2Credential should use apk key hash for privileged apps`() = runTest {
every {
mockSigningInfo.apkContentsSigners
} returns arrayOf(Signature(DEFAULT_APP_SIGNATURE))
every {
mockSigningInfo.hasMultipleSigners()
} returns false
val mockCipherView = createMockCipherView(number = 1)
val mockRequest = createMockFido2CredentialAssertionRequest(
number = 1,
clientDataHash = null,
signingInfo = mockSigningInfo,
origin = null,
)
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
number = 1,
clientData = ClientData.DefaultWithExtraData(
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
),
mockRequest = mockRequest,
mockCipherView = mockCipherView,
origin = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
)
val mockSdkResponse =
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true)
every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
coEvery {
mockVaultSdkSource.authenticateFido2Credential(
request = mockSdkRequest,
fido2CredentialStore = any(),
)
} returns mockSdkResponse.asSuccess()
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockRequest,
selectedCipherView = createMockCipherView(number = 1),
)
coVerify {
mockVaultSdkSource.authenticateFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
)
}
assertEquals(
AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = ClientData.DefaultWithExtraData(
androidPackageName = "android:apk-key-hash:$DEFAULT_APP_SIGNATURE",
),
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
),
requestCaptureSlot.captured,
)
}
@Suppress("MaxLineLength")
@Test
fun `authenticateFidoCredential should convert SDK response to AndroidFido2PublicKeyCredential, deserialize the response to JSON, and return Success with response JSON`() =
runTest {
mockkStatic(PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential)
every { Base64.encodeToString(any(), any()) } returns ""
val mockCipherView = createMockCipherView(number = 1)
val mockRequest = createMockFido2CredentialAssertionRequest(number = 1)
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
number = 1,
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
mockRequest = mockRequest,
mockCipherView = mockCipherView,
)
val mockPublicKeyCredential = mockk<Fido2PublicKeyCredential>()
val mockSdkResponse =
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true) {
every { toAndroidFido2PublicKeyCredential() } returns mockPublicKeyCredential
}
coEvery {
mockVaultSdkSource.authenticateFido2Credential(
request = mockSdkRequest,
fido2CredentialStore = any(),
)
} returns mockSdkResponse.asSuccess()
every { json.encodeToString(mockPublicKeyCredential) } returns "mockResponseJson"
val authResult = fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockRequest,
selectedCipherView = createMockCipherView(number = 1),
)
coVerify {
mockVaultSdkSource.authenticateFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
)
mockSdkResponse.toAndroidFido2PublicKeyCredential()
json.encodeToString(mockPublicKeyCredential)
}
assertEquals(
Fido2CredentialAssertionResult.Success("mockResponseJson"),
authResult,
)
}
@Suppress("MaxLineLength")
@Test
fun `authenticateFido2Credential should return Error when response cannot be serialized`() =
runTest {
mockkStatic(PublicKeyCredentialAuthenticatorAssertionResponse::toAndroidFido2PublicKeyCredential)
every { Base64.encodeToString(any(), any()) } returns ""
val mockCipherView = createMockCipherView(number = 1)
val mockRequest = createMockFido2CredentialAssertionRequest(number = 1)
val requestCaptureSlot = slot<AuthenticateFido2CredentialRequest>()
val mockSdkRequest = createAuthenticateFido2CredentialRequest(
number = 1,
clientData = ClientData.DefaultWithCustomHash(mockRequest.clientDataHash!!),
mockRequest = mockRequest,
mockCipherView = mockCipherView,
)
val mockPublicKeyCredential = mockk<Fido2PublicKeyCredential>()
val mockSdkResponse =
mockk<PublicKeyCredentialAuthenticatorAssertionResponse>(relaxed = true) {
every { toAndroidFido2PublicKeyCredential() } returns mockPublicKeyCredential
}
coEvery {
mockVaultSdkSource.authenticateFido2Credential(
request = mockSdkRequest,
fido2CredentialStore = any(),
)
} returns mockSdkResponse.asSuccess()
every { json.encodeToString(mockPublicKeyCredential) } throws SerializationException()
val authResult = fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockRequest,
selectedCipherView = createMockCipherView(number = 1),
)
coVerify {
mockVaultSdkSource.authenticateFido2Credential(
request = capture(requestCaptureSlot),
fido2CredentialStore = any(),
)
mockSdkResponse.toAndroidFido2PublicKeyCredential()
json.encodeToString(mockPublicKeyCredential)
}
assertEquals(
Fido2CredentialAssertionResult.Error,
authResult,
)
}
}
private const val DEFAULT_APP_SIGNATURE = "0987654321ABCDEF"
@ -637,3 +921,34 @@ private const val MISSING_PACKAGE_ALLOW_LIST = """
]
}
"""
private fun createMockFido2CredentialAssertionRequest(
number: Int,
clientDataHash: ByteArray? = byteArrayOf(0),
signingInfo: SigningInfo = SigningInfo(),
origin: String? = "mockOrigin-$number",
) = Fido2CredentialAssertionRequest(
cipherId = "mockCipherId-$number",
credentialId = "mockCredentialId-$number",
requestJson = "requestJson-$number",
clientDataHash = clientDataHash,
packageName = "mockPackageName-$number",
signingInfo = signingInfo,
origin = origin,
)
private fun createAuthenticateFido2CredentialRequest(
number: Int,
clientData: ClientData,
mockRequest: Fido2CredentialAssertionRequest =
createMockFido2CredentialAssertionRequest(number),
mockCipherView: CipherView = createMockCipherView(number),
origin: String = mockRequest.origin!!,
) = AuthenticateFido2CredentialRequest(
userId = "activeUserId",
origin = origin,
requestJson = """{"publicKey": ${mockRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = mockCipherView,
isUserVerificationSupported = true,
)

View file

@ -0,0 +1,105 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.util
import android.util.Base64
import com.bitwarden.fido.AuthenticatorAssertionResponse
import com.bitwarden.fido.ClientExtensionResults
import com.bitwarden.fido.CredPropsResult
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
import com.bitwarden.fido.SelectedCredential
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialView
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class PublicKeyCredentialAuthenticatorAssertionResponseExtensionsTest {
@BeforeEach
fun setUp() {
mockkStatic(Base64::class)
every { Base64.encodeToString(any(), any()) } returns ""
}
@AfterEach
fun tearDown() {
unmockkStatic(Base64::class)
}
@Test
fun `authenticatorAttachment should be null when SDK value is null`() {
val mockSdkResponse = createMockSdkAssertionResponse(number = 1)
val result = mockSdkResponse.toAndroidFido2PublicKeyCredential()
assertNull(result.authenticatorAttachment)
}
@Test
fun `authenticatorAttachment should be populated when SDK value is non-null`() {
val mockSdkResponse = createMockSdkAssertionResponse(
number = 1,
authenticatorAttachment = "mockAuthenticatorAttachment",
)
val result = mockSdkResponse.toAndroidFido2PublicKeyCredential()
assertNotNull(result.authenticatorAttachment)
}
@Test
fun `credentialProperties should be null when SDK value is null`() {
val mockSdkResponse = createMockSdkAssertionResponse(number = 1)
val result = mockSdkResponse.toAndroidFido2PublicKeyCredential()
assertNull(result.clientExtensionResults.credentialProperties)
}
@Test
fun `credentialProperties should be populated when SDK value is non-null`() {
val mockSdkResponse = createMockSdkAssertionResponse(
number = 1,
credProps = CredPropsResult(
rk = true,
authenticatorDisplayName = null,
),
)
val result = mockSdkResponse.toAndroidFido2PublicKeyCredential()
assertNotNull(result.clientExtensionResults.credentialProperties)
}
@Test
fun `residentKey defaults to true when SDK value is null`() {
val mockSdkResponse = createMockSdkAssertionResponse(
number = 1,
credProps = CredPropsResult(
rk = null,
authenticatorDisplayName = null,
),
)
val result = mockSdkResponse.toAndroidFido2PublicKeyCredential()
assertTrue(result.clientExtensionResults.credentialProperties?.residentKey!!)
}
}
private fun createMockSdkAssertionResponse(
number: Int,
authenticatorAttachment: String? = null,
credProps: CredPropsResult? = null,
) = PublicKeyCredentialAuthenticatorAssertionResponse(
id = "mockId-$number",
rawId = byteArrayOf(0),
ty = "mockTy-$number",
authenticatorAttachment = authenticatorAttachment,
clientExtensionResults = ClientExtensionResults(credProps = credProps),
response = AuthenticatorAssertionResponse(
clientDataJson = byteArrayOf(0),
authenticatorData = byteArrayOf(0),
signature = byteArrayOf(0),
userHandle = byteArrayOf(0),
),
selectedCredential = SelectedCredential(
cipher = createMockCipherView(number = 1),
credential = createMockFido2CredentialView(number = 1),
),
)

View file

@ -105,7 +105,7 @@ class Fido2CompletionManagerTest {
@Suppress("MaxLineLength")
@Test
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Canclled`() {
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Cancelled`() {
fido2CompletionManager
.completeFido2Registration(Fido2RegisterCredentialResult.Cancelled)
@ -136,17 +136,6 @@ class Fido2CompletionManagerTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `completeFido2Assertion should set cancellation exception, set activity result, then finish activity when result is Cancelled`() {
fido2CompletionManager
.completeFido2Assertion(Fido2CredentialAssertionResult.Cancelled)
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