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 index c7cb44ab2..9e868924b 100644 --- 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 @@ -11,7 +11,7 @@ import kotlinx.serialization.Serializable @Serializable data class InternalPreLoginResponseJson( @SerialName("kdf") - val kdfType: Int, + val kdfType: KdfTypeJson, @SerialName("kdfIterations") val kdfIterations: UInt, @SerialName("kdfMemory") diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/KdfTypeJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/KdfTypeJson.kt new file mode 100644 index 000000000..31e2bbd5a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/KdfTypeJson.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents different key derivation functions (KDFs). + */ +@Serializable(KdfTypeSerializer::class) +enum class KdfTypeJson { + @SerialName("1") + ARGON2_ID, + + @SerialName("0") + PBKDF2_SHA256, +} + +private class KdfTypeSerializer : + BaseEnumeratedIntSerializer(KdfTypeJson.values()) 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 20f458cb5..638eec0dc 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 @@ -3,9 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.model import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseSurrogateSerializer import kotlinx.serialization.Serializable -private const val KDF_TYPE_ARGON2_ID = 1 -private const val KDF_TYPE_PBKDF2_SHA256 = 0 - /** * Response body for pre login. */ @@ -21,6 +18,11 @@ data class PreLoginResponseJson( */ sealed class KdfParams { + /** + * The associated [KdfTypeJson]. + */ + abstract val kdfTypeJson: KdfTypeJson + /** * Models params for the Argon2id algorithm. */ @@ -28,12 +30,20 @@ data class PreLoginResponseJson( val iterations: UInt, val memory: UInt, val parallelism: UInt, - ) : KdfParams() + ) : KdfParams() { + override val kdfTypeJson: KdfTypeJson + get() = KdfTypeJson.ARGON2_ID + } /** * Models params for the PBKDF2 algorithm. */ - data class Pbkdf2(val iterations: UInt) : KdfParams() + data class Pbkdf2( + val iterations: UInt, + ) : KdfParams() { + override val kdfTypeJson: KdfTypeJson + get() = KdfTypeJson.PBKDF2_SHA256 + } } } @@ -45,13 +55,13 @@ private class PreLoginResponseSerializer : override fun InternalPreLoginResponseJson.toExternalType(): PreLoginResponseJson = PreLoginResponseJson( kdfParams = when (this.kdfType) { - KDF_TYPE_PBKDF2_SHA256 -> { + KdfTypeJson.PBKDF2_SHA256 -> { PreLoginResponseJson.KdfParams.Pbkdf2( iterations = this.kdfIterations, ) } - KDF_TYPE_ARGON2_ID -> { + KdfTypeJson.ARGON2_ID -> { @Suppress("UnsafeCallOnNullableType") PreLoginResponseJson.KdfParams.Argon2ID( iterations = this.kdfIterations, @@ -59,10 +69,6 @@ private class PreLoginResponseSerializer : parallelism = this.kdfParallelism!!, ) } - - else -> throw IllegalStateException( - "Unable to parse KDF params for unknown kdfType: ${this.kdfType}", - ) }, ) @@ -70,7 +76,7 @@ private class PreLoginResponseSerializer : when (val params = this.kdfParams) { is PreLoginResponseJson.KdfParams.Argon2ID -> { InternalPreLoginResponseJson( - kdfType = KDF_TYPE_ARGON2_ID, + kdfType = params.kdfTypeJson, kdfIterations = params.iterations, kdfMemory = params.memory, kdfParallelism = params.parallelism, @@ -79,7 +85,7 @@ private class PreLoginResponseSerializer : is PreLoginResponseJson.KdfParams.Pbkdf2 -> { InternalPreLoginResponseJson( - kdfType = KDF_TYPE_PBKDF2_SHA256, + kdfType = params.kdfTypeJson, kdfIterations = params.iterations, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseEnumeratedIntSerializer.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseEnumeratedIntSerializer.kt new file mode 100644 index 000000000..0216594e7 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseEnumeratedIntSerializer.kt @@ -0,0 +1,41 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.serializer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * Base [KSerializer] for mapping an [Enum] with possible values given by [values] to/from integer + * values, which should be specified using [SerialName]. + */ +@Suppress("UnnecessaryAbstractClass") +abstract class BaseEnumeratedIntSerializer>( + private val values: Array, +) : KSerializer { + + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor( + serialName = this::class.java.simpleName, + kind = PrimitiveKind.INT, + ) + + override fun deserialize(decoder: Decoder): T { + val decodedValue = decoder.decodeInt().toString() + return values.first { it.serialNameAnnotation?.value == decodedValue } + } + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeInt( + requireNotNull( + value.serialNameAnnotation?.value?.toInt(), + ), + ) + } + + private val Enum<*>.serialNameAnnotation: SerialName? + get() = javaClass.getDeclaredField(name).getAnnotation(SerialName::class.java) +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseEnumeratedIntSerializerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseEnumeratedIntSerializerTest.kt new file mode 100644 index 000000000..c516729eb --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/serializer/BaseEnumeratedIntSerializerTest.kt @@ -0,0 +1,50 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.serializer + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class BaseEnumeratedIntSerializerTest { + private val json = Json + + @Test + fun `properly deserializes integers to enums`() { + assertEquals( + TestEnum.CASE_1, + json.decodeFromString( + """ + 1 + """, + ), + ) + } + + @Test + fun `properly serializes enums back to integers`() { + assertEquals( + json.parseToJsonElement( + """ + 1 + """, + ), + json.encodeToJsonElement( + TestEnum.CASE_1, + ), + ) + } +} + +@Serializable(TestEnumSerializer::class) +private enum class TestEnum { + @SerialName("1") + CASE_1, + + @SerialName("2") + CASE_2, +} + +private class TestEnumSerializer : + BaseEnumeratedIntSerializer(values = TestEnum.values())