use kotlinx-serialization with OATH Model

This commit is contained in:
Adam Velebil 2022-05-05 13:56:08 +02:00
parent 054cbb200a
commit 1d65334072
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
7 changed files with 148 additions and 92 deletions

View File

@ -13,11 +13,13 @@ if (flutterRoot == null) {
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
//noinspection GroovyUnusedAssignment
flutterVersionCode = '1' flutterVersionCode = '1'
} }
def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) { if (flutterVersionName == null) {
//noinspection GroovyUnusedAssignment
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
@ -26,7 +28,7 @@ def authenticatorVersionName = "6.0.0-alpha.2"
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: "org.jetbrains.kotlin.plugin.serialization" apply plugin: 'kotlinx-serialization'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
@ -80,6 +82,7 @@ dependencies {
api "com.yubico.yubikit:oath:$project.yubiKitVersion" api "com.yubico.yubikit:oath:$project.yubiKitVersion"
api "com.yubico.yubikit:support:$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' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
// Lifecycle // Lifecycle

View File

@ -5,4 +5,45 @@
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings -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;
#}

View File

@ -2,19 +2,7 @@ package com.yubico.authenticator.oath
import com.yubico.yubikit.oath.Code import com.yubico.yubikit.oath.Code
import com.yubico.yubikit.oath.Credential import com.yubico.yubikit.oath.Credential
import com.yubico.yubikit.oath.OathSession
import com.yubico.yubikit.oath.OathType 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( fun ByteArray.asString() = joinToString(
separator = "" separator = ""
@ -36,8 +24,8 @@ fun Credential.model(deviceId: String) = Model.Credential(
fun Code.model() = Model.Code( fun Code.model() = Model.Code(
value, value,
validFrom, validFrom / 1000,
validUntil validUntil / 1000
) )
fun Map<Credential, Code?>.model(deviceId: String): Map<Model.Credential, Model.Code?> = fun Map<Credential, Code?>.model(deviceId: String): Map<Model.Credential, Model.Code?> =

View File

@ -1,23 +1,48 @@
package com.yubico.authenticator.oath package com.yubico.authenticator.oath
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.SerialName
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive 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 { 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) { enum class OathType(val value: Byte) {
TOTP(0x20), HOTP(0x10) TOTP(0x20),
HOTP(0x10);
} }
@Serializable
data class Credential( data class Credential(
@SerialName("device_id")
val deviceId: String, val deviceId: String,
val id: String, val id: String,
@SerialName("oath_type")
val oathType: OathType, val oathType: OathType,
val period: Int, val period: Int,
val issuer: String? = null, val issuer: String? = null,
@SerialName("name")
val accountName: String, val accountName: String,
@SerialName("touch_required")
val touchRequired: Boolean val touchRequired: Boolean
) { ) {
override fun equals(other: Any?): Boolean = override fun equals(other: Any?): Boolean =
@ -30,82 +55,62 @@ class Model {
} }
} }
data class Code( @Serializable
class Code(
val value: String? = null, val value: String? = null,
@SerialName("valid_from")
val validFrom: Long, val validFrom: Long,
val validUntil: Long @SerialName("valid_to")
val validTo: Long
) )
@Serializable
data class CredentialWithCode( data class CredentialWithCode(
val credential: Credential, val credential: Credential,
val code: Code? val code: Code?
) )
object OathTypeSerializer : KSerializer<OathType> {
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 { companion object {
fun Credential.isInteractive(): Boolean { fun Credential.isInteractive(): Boolean {
return oathType == OathType.HOTP || (oathType == OathType.TOTP && touchRequired) 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<Credential, Code?>.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<Credential, Code?>.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<Credential, Code?>(); private set var credentials = mutableMapOf<Credential, Code?>(); private set
// resets the model to initial values // resets the model to initial values
// used when a usb key has been disconnected // used when a usb key has been disconnected
fun reset() { fun reset() {
this.credentials.clear() this.credentials.clear()
this.deviceId = "" this.session = Session()
} }
fun update(deviceId: String, credentials: Map<Credential, Code?>) { fun update(deviceId: String, credentials: Map<Credential, Code?>) {
if (this.deviceId != deviceId) { if (this.session.deviceId != deviceId) {
// device was changed, we use the new list // device was changed, we use the new list
this.credentials.clear() this.credentials.clear()
this.credentials.putAll(from = credentials) this.credentials.putAll(from = credentials)
this.deviceId = deviceId this.session = Session(deviceId)
} else { } else {
// update codes for non interactive keys // update codes for non interactive keys
@ -126,7 +131,7 @@ class Model {
} }
fun add(deviceId: String, credential: Credential, code: Code?): CredentialWithCode? { fun add(deviceId: String, credential: Credential, code: Code?): CredentialWithCode? {
if (this.deviceId != deviceId) { if (this.session.deviceId != deviceId) {
return null return null
} }
@ -140,7 +145,7 @@ class Model {
oldCredential: Credential, oldCredential: Credential,
newCredential: Credential newCredential: Credential
): Credential? { ): Credential? {
if (this.deviceId != deviceId) { if (this.session.deviceId != deviceId) {
return null return null
} }
@ -162,7 +167,7 @@ class Model {
} }
fun updateCode(deviceId: String, credential: Credential, code: Code?): Code? { fun updateCode(deviceId: String, credential: Credential, code: Code?): Code? {
if (this.deviceId != deviceId) { if (this.session.deviceId != deviceId) {
return null return null
} }

View File

@ -7,7 +7,6 @@ import androidx.lifecycle.Observer
import com.yubico.authenticator.* import com.yubico.authenticator.*
import com.yubico.authenticator.api.Pigeon.* import com.yubico.authenticator.api.Pigeon.*
import com.yubico.authenticator.data.device.toJson 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.ClearingMemProvider
import com.yubico.authenticator.oath.keystore.KeyStoreProvider import com.yubico.authenticator.oath.keystore.KeyStoreProvider
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
@ -19,6 +18,8 @@ import com.yubico.yubikit.oath.*
import com.yubico.yubikit.support.DeviceUtil import com.yubico.yubikit.support.DeviceUtil
import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.BinaryMessenger
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.URI import java.net.URI
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -33,6 +34,12 @@ class OathManager(
private val dialogManager: DialogManager private val dialogManager: DialogManager
) : OathApi { ) : OathApi {
// application specific Json settings
private val json = Json {
allowStructuredMapKeys = true
encodeDefaults = true
}
private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val coroutineScope = CoroutineScope(SupervisorJob() + _dispatcher) private val coroutineScope = CoroutineScope(SupervisorJob() + _dispatcher)
@ -152,7 +159,7 @@ class OathManager(
} }
if (response.isUnlocked == true) { if (response.isUnlocked == true) {
_model.update(it.deviceId, calculateOathCodes(it).model(it.deviceId)) _model.update(it.deviceId, calculateOathCodes(it).model(it.deviceId))
codes = _model.credentials.toJson().toString() codes = json.encodeToString(_model.credentials)
} }
returnSuccess(result, response) returnSuccess(result, response)
} }
@ -253,7 +260,7 @@ class OathManager(
) )
if (addedCred != null) { if (addedCred != null) {
val jsonResult = addedCred.toJson().toString() val jsonResult = json.encodeToString(addedCred)
returnSuccess(result, jsonResult) returnSuccess(result, jsonResult)
} else { } else {
// TODO - figure out better error handling here // TODO - figure out better error handling here
@ -281,7 +288,8 @@ class OathManager(
) )
if (renamedCredential != null) { if (renamedCredential != null) {
val jsonResult = renamedCredential.toJson().toString() val jsonResult =
json.encodeToString(renamedCredential)
returnSuccess(result, jsonResult) returnSuccess(result, jsonResult)
} else { } else {
@ -322,7 +330,7 @@ class OathManager(
session.deviceId, session.deviceId,
calculateOathCodes(session).model(session.deviceId) calculateOathCodes(session).model(session.deviceId)
) )
val resultJson = _model.credentials.toJson().toString() val resultJson = json.encodeToString(_model.credentials)
returnSuccess(result, resultJson) returnSuccess(result, resultJson)
} }
} }
@ -347,7 +355,7 @@ class OathManager(
) )
if (code != null) { if (code != null) {
val resultJson = code.toJson().toString() val resultJson = json.encodeToString(code)
returnSuccess(result, resultJson) returnSuccess(result, resultJson)
} else { } else {
@ -426,9 +434,14 @@ class OathManager(
tryToUnlockOathSession(oathSession) tryToUnlockOathSession(oathSession)
val isRemembered = _keyManager.isRemembered(oathSession.deviceId) val isRemembered = _keyManager.isRemembered(oathSession.deviceId)
val oathSessionData = oathSession _model.session = Model.Session(
.toJson(isRemembered) oathSession.deviceId,
.toString() oathSession.isAccessKeySet,
isRemembered,
oathSession.isLocked
)
val oathSessionData = json.encodeToString(_model.session)
it.resume(oathSessionData) it.resume(oathSessionData)
} }
} }
@ -445,7 +458,7 @@ class OathManager(
session.deviceId, session.deviceId,
calculateOathCodes(session).model(session.deviceId) calculateOathCodes(session).model(session.deviceId)
) )
val resultJson = _model.credentials.toJson().toString() val resultJson = json.encodeToString(_model.credentials)
it.resume(resultJson) it.resume(resultJson)
} }
} }

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.6.20'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -8,7 +8,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.3' classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 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"
} }
} }

View File

@ -14,11 +14,7 @@ class _StateProvider extends StateNotifier<OathState?> {
void setFromString(String input) { void setFromString(String input) {
var resultJson = jsonDecode(input); var resultJson = jsonDecode(input);
state = OathState(resultJson['deviceId'], state = OathState.fromJson(resultJson);
hasKey: resultJson['hasKey'],
remembered: resultJson['remembered'],
locked: resultJson['locked'],
keystore: KeystoreState.unknown);
} }
} }
@ -33,11 +29,21 @@ class _CredentialsProvider extends StateNotifier<List<OathPair>?> {
void setFromString(String input) { void setFromString(String input) {
var result = jsonDecode(input); var result = jsonDecode(input);
/// structure of data in the json object is:
/// [credential1, code1, credential2, code2, ...]
final List<OathPair> pairs = []; final List<OathPair> pairs = [];
for (var e in result['entries']) { if (result is List<dynamic>) {
final credential = OathCredential.fromJson(e['credential']); for (var index = 0; index < result.length / 2; index++) {
final code = e['code'] == null ? null : OathCode.fromJson(e['code']); final credential = result[index * 2];
pairs.add(OathPair(credential, code)); final code = result[index * 2 + 1];
pairs.add(
OathPair(
OathCredential.fromJson(credential),
code == null ? null : OathCode.fromJson(code),
),
);
}
} }
state = pairs; state = pairs;