mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 20:40:00 +03:00
BIT-329 Implement parsing and usage of kdf params (#112)
This commit is contained in:
parent
57561d0ccd
commit
5ecb8fbb2c
6 changed files with 212 additions and 40 deletions
|
@ -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,
|
||||||
|
)
|
|
@ -1,20 +1,96 @@
|
||||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.Serializable
|
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.
|
* Response body for pre login.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable(PreLoginResponseSerializer::class)
|
||||||
data class PreLoginResponseJson(
|
data class PreLoginResponseJson(
|
||||||
// TODO parse this property as an enum (BIT-329)
|
val kdfParams: KdfParams,
|
||||||
@SerialName("kdf")
|
) {
|
||||||
val kdf: Int,
|
|
||||||
@SerialName("kdfIterations")
|
/**
|
||||||
val kdfIterations: UInt,
|
* Models different kdf types.
|
||||||
@SerialName("kdfMemory")
|
*
|
||||||
val kdfMemory: Int? = null,
|
* See https://bitwarden.com/help/kdf-algorithms/.
|
||||||
@SerialName("kdfParallelism")
|
*/
|
||||||
val kdfParallelism: Int? = null,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.x8bit.bitwarden.data.auth.repository
|
package com.x8bit.bitwarden.data.auth.repository
|
||||||
|
|
||||||
import com.bitwarden.core.Kdf
|
|
||||||
import com.bitwarden.sdk.Client
|
import com.bitwarden.sdk.Client
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
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.AccountsService
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
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.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.datasource.network.interceptor.AuthTokenInterceptor
|
||||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -55,13 +55,12 @@ class AuthRepositoryImpl @Inject constructor(
|
||||||
): LoginResult = accountsService
|
): LoginResult = accountsService
|
||||||
.preLogin(email = email)
|
.preLogin(email = email)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
// TODO: Use KDF enum from pre login correctly (BIT-329)
|
|
||||||
val passwordHash = bitwardenSdkClient
|
val passwordHash = bitwardenSdkClient
|
||||||
.auth()
|
.auth()
|
||||||
.hashPassword(
|
.hashPassword(
|
||||||
email = email,
|
email = email,
|
||||||
password = password,
|
password = password,
|
||||||
kdfParams = Kdf.Pbkdf2(it.kdfIterations),
|
kdfParams = it.kdfParams.toSdkParams(),
|
||||||
)
|
)
|
||||||
identityService.getToken(
|
identityService.getToken(
|
||||||
email = email,
|
email = email,
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,29 +15,85 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||||
private val service = AccountsServiceImpl(accountsApi)
|
private val service = AccountsServiceImpl(accountsApi)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `preLogin should call API`() = runTest {
|
fun `preLogin with unknown kdf type be failure`() = runTest {
|
||||||
val response = MockResponse().setBody(PRE_LOGIN_RESPONSE_JSON)
|
val json = """
|
||||||
|
{
|
||||||
|
"kdf": 2,
|
||||||
|
"kdfIterations": 1,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
val response = MockResponse().setBody(json)
|
||||||
server.enqueue(response)
|
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 {
|
companion object {
|
||||||
private const val EMAIL = "email"
|
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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.x8bit.bitwarden.data.auth.repository
|
package com.x8bit.bitwarden.data.auth.repository
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.bitwarden.core.Kdf
|
|
||||||
import com.bitwarden.sdk.Client
|
import com.bitwarden.sdk.Client
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
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.AccountsService
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
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.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.datasource.network.interceptor.AuthTokenInterceptor
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -35,7 +35,7 @@ class AuthRepositoryTest {
|
||||||
auth().hashPassword(
|
auth().hashPassword(
|
||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
password = PASSWORD,
|
password = PASSWORD,
|
||||||
kdfParams = Kdf.Pbkdf2(iterations = PRE_LOGIN_SUCCESS.kdfIterations),
|
kdfParams = PRE_LOGIN_SUCCESS.kdfParams.toSdkParams(),
|
||||||
)
|
)
|
||||||
} returns PASSWORD_HASH
|
} returns PASSWORD_HASH
|
||||||
}
|
}
|
||||||
|
@ -209,10 +209,7 @@ class AuthRepositoryTest {
|
||||||
private const val ACCESS_TOKEN = "accessToken"
|
private const val ACCESS_TOKEN = "accessToken"
|
||||||
private const val CAPTCHA_KEY = "captcha"
|
private const val CAPTCHA_KEY = "captcha"
|
||||||
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
|
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
|
||||||
kdf = 1,
|
kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u),
|
||||||
kdfIterations = 1u,
|
|
||||||
kdfMemory = null,
|
|
||||||
kdfParallelism = null,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue