From 1d65334072cb09dff4e33911f9e864dfafd86cd6 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Thu, 5 May 2022 13:56:08 +0200 Subject: [PATCH] use kotlinx-serialization with OATH Model --- android/app/build.gradle | 5 +- android/app/proguard-rules.pro | 43 ++++++- .../yubico/authenticator/oath/Conversion.kt | 16 +-- .../com/yubico/authenticator/oath/Model.kt | 115 +++++++++--------- .../yubico/authenticator/oath/OathManager.kt | 33 +++-- android/build.gradle | 4 +- lib/android/oath/command_providers.dart | 24 ++-- 7 files changed, 148 insertions(+), 92 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b6249244..f790f3a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,11 +13,13 @@ if (flutterRoot == null) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { + //noinspection GroovyUnusedAssignment flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { + //noinspection GroovyUnusedAssignment flutterVersionName = '1.0' } @@ -26,7 +28,7 @@ def authenticatorVersionName = "6.0.0-alpha.2" apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: "org.jetbrains.kotlin.plugin.serialization" +apply plugin: 'kotlinx-serialization' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { @@ -80,6 +82,7 @@ dependencies { api "com.yubico.yubikit:oath:$project.yubiKitVersion" api "com.yubico.yubikit:support:$project.yubiKitVersion" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' // Lifecycle diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 7d4b03f8..5ae68350 100755 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -5,4 +5,45 @@ # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html --dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings \ No newline at end of file +-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings + + +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + +# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`. +# If you have any, uncomment and replace classes with those containing named companion objects. +#-keepattributes InnerClasses # Needed for `getDeclaredClasses`. +#-if @kotlinx.serialization.Serializable class +#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions. +#com.example.myapplication.HasNamedCompanion2 +#{ +# static **$* *; +#} +#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. +# static <1>$$serializer INSTANCE; +#} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/Conversion.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/Conversion.kt index 75d22050..04c0aa8f 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/Conversion.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/Conversion.kt @@ -2,19 +2,7 @@ package com.yubico.authenticator.oath import com.yubico.yubikit.oath.Code import com.yubico.yubikit.oath.Credential -import com.yubico.yubikit.oath.OathSession import com.yubico.yubikit.oath.OathType -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive - -fun OathSession.toJson(remembered: Boolean) = JsonObject( - mapOf( - "deviceId" to JsonPrimitive(deviceId), - "hasKey" to JsonPrimitive(isAccessKeySet), - "remembered" to JsonPrimitive(remembered), - "locked" to JsonPrimitive(isLocked) - ) -) fun ByteArray.asString() = joinToString( separator = "" @@ -36,8 +24,8 @@ fun Credential.model(deviceId: String) = Model.Credential( fun Code.model() = Model.Code( value, - validFrom, - validUntil + validFrom / 1000, + validUntil / 1000 ) fun Map.model(deviceId: String): Map = diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/Model.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/Model.kt index 89b81709..455ef380 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/Model.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/Model.kt @@ -1,23 +1,48 @@ package com.yubico.authenticator.oath -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +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 class Model { + @Serializable + data class Session( + @SerialName("device_id") + val deviceId: String = "", + @SerialName("has_key") + val isAccessKeySet: Boolean = false, + @SerialName("remembered") + val isRemembered: Boolean = false, + @SerialName("locked") + val isLocked: Boolean = false, + @SerialName("keystore") + val keystoreState: String = "unknown" + ) + + @Serializable(with = OathTypeSerializer::class) enum class OathType(val value: Byte) { - TOTP(0x20), HOTP(0x10) + TOTP(0x20), + HOTP(0x10); } + @Serializable data class Credential( + @SerialName("device_id") val deviceId: String, val id: String, + @SerialName("oath_type") val oathType: OathType, val period: Int, val issuer: String? = null, + @SerialName("name") val accountName: String, + @SerialName("touch_required") val touchRequired: Boolean ) { override fun equals(other: Any?): Boolean = @@ -30,82 +55,62 @@ class Model { } } - data class Code( + @Serializable + class Code( val value: String? = null, + @SerialName("valid_from") val validFrom: Long, - val validUntil: Long + @SerialName("valid_to") + val validTo: Long ) + @Serializable data class CredentialWithCode( val credential: Credential, val code: Code? ) + object OathTypeSerializer : KSerializer { + override fun deserialize(decoder: Decoder): OathType = + when (decoder.decodeByte()) { + OathType.HOTP.value -> OathType.HOTP + else -> OathType.TOTP + } + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("OathType", PrimitiveKind.BYTE) + + override fun serialize(encoder: Encoder, value: OathType) { + encoder.encodeByte(value = value.value) + } + + } + companion object { fun Credential.isInteractive(): Boolean { return oathType == OathType.HOTP || (oathType == OathType.TOTP && touchRequired) } - fun Code.toJson() = JsonObject( - mapOf( - "value" to JsonPrimitive(value), - "valid_from" to JsonPrimitive(validFrom / 1000), - "valid_to" to JsonPrimitive(validUntil / 1000) - ) - ) - - fun Credential.toJson() = JsonObject( - mapOf( - "id" to JsonPrimitive(id), - "device_id" to JsonPrimitive(deviceId), - "issuer" to JsonPrimitive(issuer), - "name" to JsonPrimitive(accountName), - "oath_type" to JsonPrimitive(oathType.value), - "period" to JsonPrimitive(period), - "touch_required" to JsonPrimitive(touchRequired), - ) - ) - - fun Pair.toJson() = JsonObject( - mapOf( - "credential" to first.toJson(), - "code" to (second?.toJson() ?: JsonNull) - ) - ) - - fun CredentialWithCode.toJson() = JsonObject( - mapOf( - "credential" to credential.toJson(), - "code" to (code?.toJson() ?: JsonNull) - ) - ) - - fun Map.toJson() = JsonObject( - mapOf( - "entries" to JsonArray( - map { it.toPair().toJson() } - ) - ) - ) } - var deviceId: String = ""; private set + //var deviceId: String = ""; private set + var session = Session() var credentials = mutableMapOf(); private set // resets the model to initial values // used when a usb key has been disconnected fun reset() { this.credentials.clear() - this.deviceId = "" + this.session = Session() } fun update(deviceId: String, credentials: Map) { - if (this.deviceId != deviceId) { + if (this.session.deviceId != deviceId) { // device was changed, we use the new list this.credentials.clear() this.credentials.putAll(from = credentials) - this.deviceId = deviceId + this.session = Session(deviceId) } else { // update codes for non interactive keys @@ -126,7 +131,7 @@ class Model { } fun add(deviceId: String, credential: Credential, code: Code?): CredentialWithCode? { - if (this.deviceId != deviceId) { + if (this.session.deviceId != deviceId) { return null } @@ -140,7 +145,7 @@ class Model { oldCredential: Credential, newCredential: Credential ): Credential? { - if (this.deviceId != deviceId) { + if (this.session.deviceId != deviceId) { return null } @@ -162,7 +167,7 @@ class Model { } fun updateCode(deviceId: String, credential: Credential, code: Code?): Code? { - if (this.deviceId != deviceId) { + if (this.session.deviceId != deviceId) { return null } diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt index 9cf4ab5d..7b2f7e27 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/oath/OathManager.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.Observer import com.yubico.authenticator.* import com.yubico.authenticator.api.Pigeon.* import com.yubico.authenticator.data.device.toJson -import com.yubico.authenticator.oath.Model.Companion.toJson import com.yubico.authenticator.oath.keystore.ClearingMemProvider import com.yubico.authenticator.oath.keystore.KeyStoreProvider import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice @@ -19,6 +18,8 @@ import com.yubico.yubikit.oath.* import com.yubico.yubikit.support.DeviceUtil import io.flutter.plugin.common.BinaryMessenger import kotlinx.coroutines.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.net.URI import java.util.concurrent.Executors import kotlin.coroutines.resume @@ -33,6 +34,12 @@ class OathManager( private val dialogManager: DialogManager ) : OathApi { + // application specific Json settings + private val json = Json { + allowStructuredMapKeys = true + encodeDefaults = true + } + private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val coroutineScope = CoroutineScope(SupervisorJob() + _dispatcher) @@ -152,7 +159,7 @@ class OathManager( } if (response.isUnlocked == true) { _model.update(it.deviceId, calculateOathCodes(it).model(it.deviceId)) - codes = _model.credentials.toJson().toString() + codes = json.encodeToString(_model.credentials) } returnSuccess(result, response) } @@ -253,7 +260,7 @@ class OathManager( ) if (addedCred != null) { - val jsonResult = addedCred.toJson().toString() + val jsonResult = json.encodeToString(addedCred) returnSuccess(result, jsonResult) } else { // TODO - figure out better error handling here @@ -281,7 +288,8 @@ class OathManager( ) if (renamedCredential != null) { - val jsonResult = renamedCredential.toJson().toString() + val jsonResult = + json.encodeToString(renamedCredential) returnSuccess(result, jsonResult) } else { @@ -322,7 +330,7 @@ class OathManager( session.deviceId, calculateOathCodes(session).model(session.deviceId) ) - val resultJson = _model.credentials.toJson().toString() + val resultJson = json.encodeToString(_model.credentials) returnSuccess(result, resultJson) } } @@ -347,7 +355,7 @@ class OathManager( ) if (code != null) { - val resultJson = code.toJson().toString() + val resultJson = json.encodeToString(code) returnSuccess(result, resultJson) } else { @@ -426,9 +434,14 @@ class OathManager( tryToUnlockOathSession(oathSession) val isRemembered = _keyManager.isRemembered(oathSession.deviceId) - val oathSessionData = oathSession - .toJson(isRemembered) - .toString() + _model.session = Model.Session( + oathSession.deviceId, + oathSession.isAccessKeySet, + isRemembered, + oathSession.isLocked + ) + + val oathSessionData = json.encodeToString(_model.session) it.resume(oathSessionData) } } @@ -445,7 +458,7 @@ class OathManager( session.deviceId, calculateOathCodes(session).model(session.deviceId) ) - val resultJson = _model.credentials.toJson().toString() + val resultJson = json.encodeToString(_model.credentials) it.resume(resultJson) } } diff --git a/android/build.gradle b/android/build.gradle index 3e4450fc..b33acb73 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.6.20' repositories { google() mavenCentral() @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jetbrains.kotlin:kotlin-serialization:1.6.10" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } diff --git a/lib/android/oath/command_providers.dart b/lib/android/oath/command_providers.dart index 1cfabcd6..7d580350 100644 --- a/lib/android/oath/command_providers.dart +++ b/lib/android/oath/command_providers.dart @@ -14,11 +14,7 @@ class _StateProvider extends StateNotifier { void setFromString(String input) { var resultJson = jsonDecode(input); - state = OathState(resultJson['deviceId'], - hasKey: resultJson['hasKey'], - remembered: resultJson['remembered'], - locked: resultJson['locked'], - keystore: KeystoreState.unknown); + state = OathState.fromJson(resultJson); } } @@ -33,11 +29,21 @@ class _CredentialsProvider extends StateNotifier?> { void setFromString(String input) { var result = jsonDecode(input); + /// structure of data in the json object is: + /// [credential1, code1, credential2, code2, ...] + final List pairs = []; - for (var e in result['entries']) { - final credential = OathCredential.fromJson(e['credential']); - final code = e['code'] == null ? null : OathCode.fromJson(e['code']); - pairs.add(OathPair(credential, code)); + if (result is List) { + for (var index = 0; index < result.length / 2; index++) { + final credential = result[index * 2]; + final code = result[index * 2 + 1]; + pairs.add( + OathPair( + OathCredential.fromJson(credential), + code == null ? null : OathCode.fromJson(code), + ), + ); + } } state = pairs;