mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
use kotlinx-serialization with OATH Model
This commit is contained in:
parent
054cbb200a
commit
1d65334072
@ -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
|
||||
|
41
android/app/proguard-rules.pro
vendored
41
android/app/proguard-rules.pro
vendored
@ -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;
|
||||
#}
|
@ -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?> =
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user