mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 03:08:50 +03:00
[PM-9409] Add FIDO 2 authentication to credential manager (#3629)
This commit is contained in:
parent
c09fe554bc
commit
b0f0c0f33b
9 changed files with 568 additions and 31 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
)
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue