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')
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

View File

@ -6,3 +6,44 @@
# http://developer.android.com/guide/developing/tools/proguard.html
-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.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<Credential, Code?>.model(deviceId: String): Map<Model.Credential, Model.Code?> =

View File

@ -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<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 {
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<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
// 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<Credential, Code?>) {
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
}

View File

@ -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)
}
}

View File

@ -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"
}
}

View File

@ -14,11 +14,7 @@ class _StateProvider extends StateNotifier<OathState?> {
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<List<OathPair>?> {
void setFromString(String input) {
var result = jsonDecode(input);
/// structure of data in the json object is:
/// [credential1, code1, credential2, code2, ...]
final List<OathPair> 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<dynamic>) {
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;