BIT-329 Implement parsing and usage of kdf params (#112)

This commit is contained in:
Andrew Haisting 2023-10-13 14:05:55 -05:00 committed by Álison Fernandes
parent 57561d0ccd
commit 5ecb8fbb2c
6 changed files with 212 additions and 40 deletions

View file

@ -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,
)

View file

@ -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<PreLoginResponseJson> {
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,
)
}
}

View file

@ -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,

View file

@ -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,
)
}
}

View file

@ -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,
)

View file

@ -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),
)
}
}