From 5ecb8fbb2cd012d999dfe421b0bc94c1d5dc0c80 Mon Sep 17 00:00:00 2001 From: Andrew Haisting <142518658+ahaisting-livefront@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:05:55 -0500 Subject: [PATCH] BIT-329 Implement parsing and usage of kdf params (#112) --- .../model/InternalPreLoginResponseJson.kt | 21 ++++ .../network/model/PreLoginResponseJson.kt | 100 +++++++++++++++--- .../auth/repository/AuthRepositoryImpl.kt | 5 +- .../data/auth/util/KdfParamsExtensions.kt | 23 ++++ .../network/service/AccountsServiceTest.kt | 94 ++++++++++++---- .../auth/repository/AuthRepositoryTest.kt | 9 +- 6 files changed, 212 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/InternalPreLoginResponseJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsExtensions.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/InternalPreLoginResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/InternalPreLoginResponseJson.kt new file mode 100644 index 000000000..c7cb44ab2 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/InternalPreLoginResponseJson.kt @@ -0,0 +1,21 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Response body for pre login. This internal model is only used for as a surrogate serializer. + * + * See [PreLoginResponseJson] for exposed model. + */ +@Serializable +data class InternalPreLoginResponseJson( + @SerialName("kdf") + val kdfType: Int, + @SerialName("kdfIterations") + val kdfIterations: UInt, + @SerialName("kdfMemory") + val kdfMemory: UInt? = null, + @SerialName("kdfParallelism") + val kdfParallelism: UInt? = null, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt index 5c040bf41..8b2b3b44d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt @@ -1,20 +1,96 @@ package com.x8bit.bitwarden.data.auth.datasource.network.model -import kotlinx.serialization.SerialName +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +private const val KDF_TYPE_ARGON2_ID = 1 +private const val KDF_TYPE_PBKDF2_SHA256 = 0 /** * Response body for pre login. */ -@Serializable +@Serializable(PreLoginResponseSerializer::class) data class PreLoginResponseJson( - // TODO parse this property as an enum (BIT-329) - @SerialName("kdf") - val kdf: Int, - @SerialName("kdfIterations") - val kdfIterations: UInt, - @SerialName("kdfMemory") - val kdfMemory: Int? = null, - @SerialName("kdfParallelism") - val kdfParallelism: Int? = null, -) + val kdfParams: KdfParams, +) { + + /** + * Models different kdf types. + * + * See https://bitwarden.com/help/kdf-algorithms/. + */ + sealed class KdfParams { + + /** + * Models params for the Argon2id algorithm. + */ + data class Argon2ID( + val iterations: UInt, + val memory: UInt, + val parallelism: UInt, + ) : KdfParams() + + /** + * Models params for the PBKDF2 algorithm. + */ + data class Pbkdf2(val iterations: UInt) : KdfParams() + } +} + +private class PreLoginResponseSerializer : KSerializer { + + private val surrogateSerializer = InternalPreLoginResponseJson.serializer() + + override val descriptor: SerialDescriptor = surrogateSerializer.descriptor + + override fun deserialize(decoder: Decoder): PreLoginResponseJson { + val surrogate = decoder.decodeSerializableValue(surrogateSerializer) + val kdfParams = when (surrogate.kdfType) { + KDF_TYPE_PBKDF2_SHA256 -> { + PreLoginResponseJson.KdfParams.Pbkdf2( + iterations = surrogate.kdfIterations, + ) + } + + KDF_TYPE_ARGON2_ID -> { + PreLoginResponseJson.KdfParams.Argon2ID( + iterations = surrogate.kdfIterations, + memory = surrogate.kdfMemory!!, + parallelism = surrogate.kdfParallelism!!, + ) + } + + else -> throw IllegalStateException( + "Unable to parse KDF params for unknown kdfType: ${surrogate.kdfType}", + ) + } + return PreLoginResponseJson(kdfParams = kdfParams) + } + + override fun serialize(encoder: Encoder, value: PreLoginResponseJson) { + val surrogate = when (val params = value.kdfParams) { + is PreLoginResponseJson.KdfParams.Argon2ID -> { + InternalPreLoginResponseJson( + kdfType = KDF_TYPE_ARGON2_ID, + kdfIterations = params.iterations, + kdfMemory = params.memory, + kdfParallelism = params.parallelism, + ) + } + + is PreLoginResponseJson.KdfParams.Pbkdf2 -> { + InternalPreLoginResponseJson( + kdfType = KDF_TYPE_PBKDF2_SHA256, + kdfIterations = params.iterations, + ) + } + } + encoder.encodeSerializableValue( + surrogateSerializer, + surrogate, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 4f0161f47..b7fee8c75 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.data.auth.repository -import com.bitwarden.core.Kdf import com.bitwarden.sdk.Client import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState @@ -11,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult +import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.util.flatMap import kotlinx.coroutines.flow.Flow @@ -55,13 +55,12 @@ class AuthRepositoryImpl @Inject constructor( ): LoginResult = accountsService .preLogin(email = email) .flatMap { - // TODO: Use KDF enum from pre login correctly (BIT-329) val passwordHash = bitwardenSdkClient .auth() .hashPassword( email = email, password = password, - kdfParams = Kdf.Pbkdf2(it.kdfIterations), + kdfParams = it.kdfParams.toSdkParams(), ) identityService.getToken( email = email, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsExtensions.kt new file mode 100644 index 000000000..4c46256dc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsExtensions.kt @@ -0,0 +1,23 @@ +package com.x8bit.bitwarden.data.auth.util + +import com.bitwarden.core.Kdf +import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson + +/** + * Convert [PreLoginResponseJson.KdfParams] to [Kdf] params for use with Bitwarden SDK. + */ +fun PreLoginResponseJson.KdfParams.toSdkParams(): Kdf = when (this) { + is PreLoginResponseJson.KdfParams.Argon2ID -> { + Kdf.Argon2id( + iterations = this.iterations, + memory = this.memory, + parallelism = this.parallelism, + ) + } + + is PreLoginResponseJson.KdfParams.Pbkdf2 -> { + Kdf.Pbkdf2( + iterations = this.iterations, + ) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt index 5bd1204eb..04eff473c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceTest.kt @@ -15,29 +15,85 @@ class AccountsServiceTest : BaseServiceTest() { private val service = AccountsServiceImpl(accountsApi) @Test - fun `preLogin should call API`() = runTest { - val response = MockResponse().setBody(PRE_LOGIN_RESPONSE_JSON) + fun `preLogin with unknown kdf type be failure`() = runTest { + val json = """ + { + "kdf": 2, + "kdfIterations": 1, + } + """ + val response = MockResponse().setBody(json) server.enqueue(response) - assertEquals(Result.success(PRE_LOGIN_RESPONSE), service.preLogin(EMAIL)) + assert(service.preLogin(EMAIL).isFailure) + } + + @Test + fun `preLogin Argon2 without memory property should be failure`() = runTest { + val json = """ + { + "kdf": 1, + "kdfIterations": 1, + "kdfParallelism": 1 + } + """ + val response = MockResponse().setBody(json) + server.enqueue(response) + assert(service.preLogin(EMAIL).isFailure) + } + + @Test + fun `preLogin Argon2 without parallelism property should be failure`() = runTest { + val json = """ + { + "kdf": 1, + "kdfIterations": 1, + "kdfMemory": 1 + } + """ + val response = MockResponse().setBody(json) + server.enqueue(response) + assert(service.preLogin(EMAIL).isFailure) + } + + @Test + fun `preLogin Argon2 should be success`() = runTest { + val json = """ + { + "kdf": 1, + "kdfIterations": 1, + "kdfMemory": 1, + "kdfParallelism": 1 + } + """ + val expectedResponse = PreLoginResponseJson( + kdfParams = PreLoginResponseJson.KdfParams.Argon2ID( + iterations = 1u, + memory = 1u, + parallelism = 1u, + ), + ) + val response = MockResponse().setBody(json) + server.enqueue(response) + assertEquals(Result.success(expectedResponse), service.preLogin(EMAIL)) + } + + @Test + fun `preLogin Pbkdf2 should be success`() = runTest { + val json = """ + { + "kdf": 0, + "kdfIterations": 1 + } + """ + val expectedResponse = PreLoginResponseJson( + kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(1u), + ) + val response = MockResponse().setBody(json) + server.enqueue(response) + assertEquals(Result.success(expectedResponse), service.preLogin(EMAIL)) } companion object { private const val EMAIL = "email" } } - -private const val PRE_LOGIN_RESPONSE_JSON = """ -{ - "kdf": 1, - "kdfIterations": 1, - "kdfMemory": 1, - "kdfParallelism": 1 -} -""" - -private val PRE_LOGIN_RESPONSE = PreLoginResponseJson( - kdf = 1, - kdfIterations = 1u, - kdfMemory = 1, - kdfParallelism = 1, -) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 2fe003c49..d53290a8a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.auth.repository import app.cash.turbine.test -import com.bitwarden.core.Kdf import com.bitwarden.sdk.Client import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState @@ -11,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJs import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult +import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import io.mockk.clearMocks import io.mockk.coEvery @@ -35,7 +35,7 @@ class AuthRepositoryTest { auth().hashPassword( email = EMAIL, password = PASSWORD, - kdfParams = Kdf.Pbkdf2(iterations = PRE_LOGIN_SUCCESS.kdfIterations), + kdfParams = PRE_LOGIN_SUCCESS.kdfParams.toSdkParams(), ) } returns PASSWORD_HASH } @@ -209,10 +209,7 @@ class AuthRepositoryTest { private const val ACCESS_TOKEN = "accessToken" private const val CAPTCHA_KEY = "captcha" private val PRE_LOGIN_SUCCESS = PreLoginResponseJson( - kdf = 1, - kdfIterations = 1u, - kdfMemory = null, - kdfParallelism = null, + kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u), ) } }