-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ISO: Fix serialization of device signed items
- Loading branch information
Showing
8 changed files
with
360 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 0 additions & 120 deletions
120
vck/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceNameSpaces.kt
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
vck/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceSignedItemListSerializer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package at.asitplus.wallet.lib.iso | ||
|
||
import at.asitplus.wallet.lib.data.InstantStringSerializer | ||
import io.github.aakira.napier.Napier | ||
import kotlinx.datetime.Instant | ||
import kotlinx.datetime.LocalDate | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.builtins.ByteArraySerializer | ||
import kotlinx.serialization.builtins.serializer | ||
import kotlinx.serialization.descriptors.PrimitiveKind | ||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor | ||
import kotlinx.serialization.descriptors.SerialDescriptor | ||
import kotlinx.serialization.descriptors.mapSerialDescriptor | ||
import kotlinx.serialization.encoding.* | ||
|
||
/** | ||
* Serializes [DeviceSignedItemList.entries] as an "inline map", | ||
* having serialized instances of [DeviceSignedItem] as the values. | ||
*/ | ||
open class DeviceSignedItemListSerializer(private val namespace: String) : | ||
KSerializer<DeviceSignedItemList> { | ||
|
||
override val descriptor: SerialDescriptor = mapSerialDescriptor( | ||
PrimitiveSerialDescriptor("key", PrimitiveKind.STRING), | ||
PrimitiveSerialDescriptor("value", PrimitiveKind.STRING) | ||
) | ||
|
||
override fun serialize(encoder: Encoder, value: DeviceSignedItemList) { | ||
encoder.encodeStructure(descriptor) { | ||
var index = 0 | ||
value.entries.forEach { | ||
this.encodeStringElement(descriptor, index++, it.key) | ||
this.encodeAnything(it, index++) | ||
} | ||
} | ||
} | ||
|
||
|
||
private fun CompositeEncoder.encodeAnything(value: DeviceSignedItem, index: Int) { | ||
val elementValueSerializer = buildElementValueSerializer(namespace, value.value, value.key) | ||
val descriptor = mapSerialDescriptor( | ||
PrimitiveSerialDescriptor("key", PrimitiveKind.STRING), | ||
elementValueSerializer.descriptor | ||
) | ||
|
||
when (val it = value.value) { | ||
is String -> encodeStringElement(descriptor, index, it) | ||
is Int -> encodeIntElement(descriptor, index, it) | ||
is Long -> encodeLongElement(descriptor, index, it) | ||
is LocalDate -> encodeSerializableElement(descriptor, index, LocalDate.serializer(), it) | ||
is Instant -> encodeSerializableElement(descriptor, index, InstantStringSerializer(), it) | ||
is Boolean -> encodeBooleanElement(descriptor, index, it) | ||
is ByteArray -> encodeSerializableElement(descriptor, index, ByteArraySerializer(), it) | ||
else -> CborCredentialSerializer.encode(namespace, value.key, descriptor, index, this, it) | ||
} | ||
} | ||
|
||
private inline fun <reified T> buildElementValueSerializer( | ||
namespace: String, | ||
elementValue: T, | ||
elementIdentifier: String | ||
) = when (elementValue) { | ||
is String -> String.serializer() | ||
is Int -> Int.serializer() | ||
is Long -> Long.serializer() | ||
is LocalDate -> LocalDate.serializer() | ||
is Instant -> InstantStringSerializer() | ||
is Boolean -> Boolean.serializer() | ||
is ByteArray -> ByteArraySerializer() | ||
is Any -> CborCredentialSerializer.lookupSerializer(namespace, elementIdentifier) | ||
?: error("serializer not found for $elementIdentifier, with value $elementValue") | ||
|
||
else -> error("serializer not found for $elementIdentifier, with value $elementValue") | ||
} | ||
|
||
override fun deserialize(decoder: Decoder): DeviceSignedItemList { | ||
val entries = mutableListOf<DeviceSignedItem>() | ||
decoder.decodeStructure(descriptor) { | ||
lateinit var key: String | ||
var value: Any | ||
while (true) { | ||
val index = decodeElementIndex(descriptor) | ||
if (index == CompositeDecoder.DECODE_DONE) { | ||
break | ||
} else if (index % 2 == 0) { | ||
key = decodeStringElement(descriptor, index) | ||
} else if (index % 2 == 1) { | ||
value = decodeAnything(index, key) | ||
entries += DeviceSignedItem(key, value) | ||
} | ||
} | ||
} | ||
return DeviceSignedItemList(entries) | ||
} | ||
|
||
private fun CompositeDecoder.decodeAnything(index: Int, elementIdentifier: String?): Any { | ||
if (namespace.isBlank()) | ||
Napier.w("This decoder is not namespace-aware! Unspeakable things may happen…") | ||
|
||
// Tags are not read out here but skipped because `decodeElementIndex` is never called, so we cannot | ||
// discriminate technically, this should be a good thing though, because otherwise we'd consume more from the | ||
// input | ||
elementIdentifier?.let { | ||
CborCredentialSerializer.decode(descriptor, index, this, elementIdentifier, namespace) | ||
?.let { return it } | ||
?: Napier.v( | ||
"Falling back to defaults for namespace $namespace and elementIdentifier $elementIdentifier" | ||
) | ||
} | ||
|
||
// These are the ones that map to different CBOR data types, the rest don't, so if it is not registered, we'll | ||
// lose type information. No others must be added here, as they could consume data from the underlying bytes | ||
runCatching { return decodeStringElement(descriptor, index) } | ||
runCatching { return decodeLongElement(descriptor, index) } | ||
runCatching { return decodeDoubleElement(descriptor, index) } | ||
runCatching { return decodeBooleanElement(descriptor, index) } | ||
|
||
throw IllegalArgumentException("Could not decode value at $index") | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
vck/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceSignedList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package at.asitplus.wallet.lib.iso | ||
|
||
import at.asitplus.KmmResult.Companion.wrap | ||
import kotlinx.serialization.Contextual | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.decodeFromByteArray | ||
import kotlinx.serialization.encodeToByteArray | ||
|
||
/** | ||
* Convenience class to prevent | ||
* usage of the type `ByteStringWrapper<Map<String, Map<String, Any>>>` in [DeviceSigned.namespaces]. | ||
*/ | ||
@Serializable | ||
data class DeviceNameSpaces( | ||
@Serializable(with = NamespacedDeviceSignedItemListSerializer::class) | ||
val entries: Map<String, @Contextual DeviceSignedItemList> | ||
) { | ||
|
||
fun serialize() = vckCborSerializer.encodeToByteArray(this) | ||
|
||
companion object { | ||
fun deserialize(it: ByteArray) = kotlin.runCatching { | ||
vckCborSerializer.decodeFromByteArray<DeviceNameSpaces>(it) | ||
}.wrap() | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Convenience class with a custom serializer ([DeviceSignedItemListSerializer]) to prevent | ||
* usage of the type `Map<String, Map<String, Any>>` in [DeviceNameSpaces.entries]. | ||
*/ | ||
data class DeviceSignedItemList( | ||
val entries: List<DeviceSignedItem> | ||
) | ||
|
||
/** | ||
* Convenience class (getting serialized in [DeviceSignedItemListSerializer]) to prevent | ||
* usage of the type `List<Map<String, Any>>` in [DeviceSignedItemList.entries]. | ||
*/ | ||
data class DeviceSignedItem( | ||
val key: String, | ||
val value: Any, | ||
) { | ||
|
||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other == null || this::class != other::class) return false | ||
|
||
other as DeviceSignedItem | ||
|
||
if (key != other.key) return false | ||
if (value is ByteArray && other.value is ByteArray) return value.contentEquals(other.value) | ||
if (value is IntArray && other.value is IntArray) return value.contentEquals(other.value) | ||
if (value is BooleanArray && other.value is BooleanArray) return value.contentEquals(other.value) | ||
if (value is CharArray && other.value is CharArray) return value.contentEquals(other.value) | ||
if (value is ShortArray && other.value is ShortArray) return value.contentEquals(other.value) | ||
if (value is LongArray && other.value is LongArray) return value.contentEquals(other.value) | ||
if (value is FloatArray && other.value is FloatArray) return value.contentEquals(other.value) | ||
if (value is DoubleArray && other.value is DoubleArray) return value.contentEquals(other.value) | ||
return if (value is Array<*> && other.value is Array<*>) value.contentDeepEquals(other.value) | ||
else value == other.value | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = key.hashCode() | ||
result = 31 * result + value.hashCode() | ||
return result | ||
} | ||
} |
Oops, something went wrong.