mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-25 11:12:10 +03:00
Merge PR #114.
This commit is contained in:
commit
a9c94e13ef
@ -13,20 +13,22 @@ 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'
|
||||||
}
|
}
|
||||||
|
|
||||||
def authenticatorVersionCode = 59900
|
def authenticatorVersionCode = 59900
|
||||||
def authenticatorVersionName = "6.0.0-alpha.2"
|
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,11 +82,12 @@ 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
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
|
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||||
|
|
||||||
|
43
android/app/proguard-rules.pro
vendored
43
android/app/proguard-rules.pro
vendored
@ -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;
|
||||||
|
#}
|
@ -2,56 +2,36 @@ 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 kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonNull
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
|
||||||
|
|
||||||
fun OathSession.toJson(remembered: Boolean) = JsonObject(
|
fun ByteArray.asString() = joinToString(
|
||||||
mapOf(
|
|
||||||
"deviceId" to JsonPrimitive(deviceId),
|
|
||||||
"hasKey" to JsonPrimitive(isAccessKeySet),
|
|
||||||
"remembered" to JsonPrimitive(remembered),
|
|
||||||
"locked" to JsonPrimitive(isLocked)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Code.toJson() = JsonObject(
|
|
||||||
mapOf(
|
|
||||||
"value" to JsonPrimitive(value),
|
|
||||||
"valid_from" to JsonPrimitive(validFrom / 1000),
|
|
||||||
"valid_to" to JsonPrimitive(validUntil / 1000)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Credential.idAsString() = id.joinToString(
|
|
||||||
separator = ""
|
separator = ""
|
||||||
) { b -> "%02x".format(b) }
|
) { b -> "%02x".format(b) }
|
||||||
|
|
||||||
fun Credential.toJson(deviceId: String) = JsonObject(
|
// convert yubikit types to Model types
|
||||||
mapOf(
|
fun Credential.model(deviceId: String) = Model.Credential(
|
||||||
"id" to JsonPrimitive(idAsString()),
|
deviceId = deviceId,
|
||||||
"device_id" to JsonPrimitive(deviceId),
|
id = id.asString(),
|
||||||
"issuer" to JsonPrimitive(issuer),
|
oathType = when (oathType) {
|
||||||
"name" to JsonPrimitive(accountName),
|
OathType.HOTP -> Model.OathType.HOTP
|
||||||
"oath_type" to JsonPrimitive(oathType.value),
|
else -> Model.OathType.TOTP
|
||||||
"period" to JsonPrimitive(period),
|
},
|
||||||
"touch_required" to JsonPrimitive(isTouchRequired),
|
period = period,
|
||||||
)
|
issuer = issuer,
|
||||||
|
accountName = accountName,
|
||||||
|
touchRequired = isTouchRequired
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Map<Credential, Code?>.toJson(deviceId: String) = JsonObject(
|
fun Code.model() = Model.Code(
|
||||||
mapOf(
|
value,
|
||||||
"entries" to JsonArray(
|
validFrom / 1000,
|
||||||
map { it.toPair().toJson(deviceId) }
|
validUntil / 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Map<Credential, Code?>.model(deviceId: String): Map<Model.Credential, Model.Code?> =
|
||||||
|
map { (credential, code) ->
|
||||||
|
Pair(
|
||||||
|
credential.model(deviceId),
|
||||||
|
code?.model()
|
||||||
)
|
)
|
||||||
)
|
}.toMap()
|
||||||
)
|
|
||||||
|
|
||||||
fun Pair<Credential, Code?>.toJson(deviceId: String) = JsonObject(
|
|
||||||
mapOf(
|
|
||||||
"credential" to first.toJson(deviceId),
|
|
||||||
"code" to (second?.toJson() ?: JsonNull)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.yubico.authenticator.oath
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
val jsonSerializer = Json {
|
||||||
|
// creates properties for default values
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
package com.yubico.authenticator.oath
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
fun Model.Credential.isInteractive(): Boolean {
|
||||||
|
return oathType == Model.OathType.HOTP || (oathType == Model.OathType.TOTP && touchRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 =
|
||||||
|
(other is Credential) && id == other.id && deviceId == other.deviceId
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = deviceId.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Code(
|
||||||
|
val value: String? = null,
|
||||||
|
@SerialName("valid_from")
|
||||||
|
@Suppress("unused")
|
||||||
|
val validFrom: Long,
|
||||||
|
@SerialName("valid_to")
|
||||||
|
@Suppress("unused")
|
||||||
|
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
|
||||||
|
OathType.TOTP.value -> OathType.TOTP
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor("OathType", PrimitiveKind.BYTE)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: OathType) {
|
||||||
|
encoder.encodeByte(value = value.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _credentials = mutableMapOf<Credential, Code?>(); private set
|
||||||
|
|
||||||
|
var session = Session()
|
||||||
|
val credentials: List<CredentialWithCode>
|
||||||
|
get() = _credentials.map {
|
||||||
|
CredentialWithCode(it.key, it.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resets the model to initial values
|
||||||
|
// used when a usb key has been disconnected
|
||||||
|
fun reset() {
|
||||||
|
this._credentials.clear()
|
||||||
|
this.session = Session()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(deviceId: String, credentials: Map<Credential, Code?>) {
|
||||||
|
if (this.session.deviceId != deviceId) {
|
||||||
|
// device was changed, we use the new list
|
||||||
|
this._credentials.clear()
|
||||||
|
this._credentials.putAll(from = credentials)
|
||||||
|
this.session = Session(deviceId)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// update codes for non interactive keys
|
||||||
|
for ((credential, code) in credentials) {
|
||||||
|
if (!credential.isInteractive()) {
|
||||||
|
this._credentials[credential] = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove obsolete credentials
|
||||||
|
this._credentials.filter { entry ->
|
||||||
|
// get only keys which are not present in the input map
|
||||||
|
!credentials.contains(entry.key)
|
||||||
|
}.forEach(action = {
|
||||||
|
this._credentials.remove(it.key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(deviceId: String, credential: Credential, code: Code?): CredentialWithCode? {
|
||||||
|
if (this.session.deviceId != deviceId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
_credentials[credential] = code
|
||||||
|
|
||||||
|
return CredentialWithCode(credential, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rename(
|
||||||
|
deviceId: String,
|
||||||
|
oldCredential: Credential,
|
||||||
|
newCredential: Credential
|
||||||
|
): Credential? {
|
||||||
|
if (this.session.deviceId != deviceId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldCredential.deviceId != newCredential.deviceId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_credentials.contains(oldCredential)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// preserve code
|
||||||
|
val code = _credentials[oldCredential]
|
||||||
|
|
||||||
|
_credentials.remove(oldCredential)
|
||||||
|
_credentials[newCredential] = code
|
||||||
|
|
||||||
|
return newCredential
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCode(deviceId: String, credential: Credential, code: Code?): Code? {
|
||||||
|
if (this.session.deviceId != deviceId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_credentials.contains(credential)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
_credentials[credential] = code
|
||||||
|
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ 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 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
|
||||||
@ -45,6 +46,8 @@ class OathManager(
|
|||||||
private val _pendingYubiKeyAction = MutableLiveData<YubiKeyAction?>()
|
private val _pendingYubiKeyAction = MutableLiveData<YubiKeyAction?>()
|
||||||
private val pendingYubiKeyAction: LiveData<YubiKeyAction?> = _pendingYubiKeyAction
|
private val pendingYubiKeyAction: LiveData<YubiKeyAction?> = _pendingYubiKeyAction
|
||||||
|
|
||||||
|
private val _model = Model()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
OathApi.setup(messenger, this)
|
OathApi.setup(messenger, this)
|
||||||
|
|
||||||
@ -112,6 +115,7 @@ class OathManager(
|
|||||||
_memoryKeyProvider.clearAll()
|
_memoryKeyProvider.clearAll()
|
||||||
_pendingYubiKeyAction.postValue(null)
|
_pendingYubiKeyAction.postValue(null)
|
||||||
_fManagementApi.updateDeviceInfo("") {}
|
_fManagementApi.updateDeviceInfo("") {}
|
||||||
|
_model.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,9 +151,8 @@ class OathManager(
|
|||||||
isRemembered = _keyManager.isRemembered(it.deviceId)
|
isRemembered = _keyManager.isRemembered(it.deviceId)
|
||||||
}
|
}
|
||||||
if (response.isUnlocked == true) {
|
if (response.isUnlocked == true) {
|
||||||
codes = calculateOathCodes(it)
|
_model.update(it.deviceId, calculateOathCodes(it).model(it.deviceId))
|
||||||
.toJson(it.deviceId)
|
codes = jsonSerializer.encodeToString(_model.credentials)
|
||||||
.toString()
|
|
||||||
}
|
}
|
||||||
returnSuccess(result, response)
|
returnSuccess(result, response)
|
||||||
}
|
}
|
||||||
@ -243,11 +246,19 @@ class OathManager(
|
|||||||
calculateCode(session, credential, System.currentTimeMillis())
|
calculateCode(session, credential, System.currentTimeMillis())
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
val jsonResult = Pair<Credential, Code?>(credential, code)
|
val addedCred = _model.add(
|
||||||
.toJson(session.deviceId)
|
session.deviceId,
|
||||||
.toString()
|
credential.model(session.deviceId),
|
||||||
|
code?.model()
|
||||||
|
)
|
||||||
|
|
||||||
returnSuccess(result, jsonResult)
|
if (addedCred != null) {
|
||||||
|
val jsonResult = jsonSerializer.encodeToString(addedCred)
|
||||||
|
returnSuccess(result, jsonResult)
|
||||||
|
} else {
|
||||||
|
// TODO - figure out better error handling here
|
||||||
|
returnError(result, java.lang.IllegalStateException())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (cause: Throwable) {
|
} catch (cause: Throwable) {
|
||||||
@ -263,12 +274,21 @@ class OathManager(
|
|||||||
withUnlockedSession(session) {
|
withUnlockedSession(session) {
|
||||||
val credential = getOathCredential(session, uri)
|
val credential = getOathCredential(session, uri)
|
||||||
|
|
||||||
val jsonResult =
|
val renamedCredential = _model.rename(
|
||||||
session.renameCredential(credential, name, issuer)
|
it.deviceId,
|
||||||
.toJson(session.deviceId)
|
credential.model(it.deviceId),
|
||||||
.toString()
|
session.renameCredential(credential, name, issuer).model(it.deviceId)
|
||||||
|
)
|
||||||
|
|
||||||
returnSuccess(result, jsonResult)
|
if (renamedCredential != null) {
|
||||||
|
val jsonResult =
|
||||||
|
jsonSerializer.encodeToString(renamedCredential)
|
||||||
|
|
||||||
|
returnSuccess(result, jsonResult)
|
||||||
|
} else {
|
||||||
|
// TODO - figure out better error handling here
|
||||||
|
returnError(result, java.lang.IllegalStateException())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (cause: Throwable) {
|
} catch (cause: Throwable) {
|
||||||
@ -298,9 +318,12 @@ class OathManager(
|
|||||||
|
|
||||||
useOathSession("Refresh codes", false) {
|
useOathSession("Refresh codes", false) {
|
||||||
withUnlockedSession(it) { session ->
|
withUnlockedSession(it) { session ->
|
||||||
val resultJson = calculateOathCodes(session)
|
|
||||||
.toJson(session.deviceId)
|
_model.update(
|
||||||
.toString()
|
session.deviceId,
|
||||||
|
calculateOathCodes(session).model(session.deviceId)
|
||||||
|
)
|
||||||
|
val resultJson = jsonSerializer.encodeToString(_model.credentials)
|
||||||
returnSuccess(result, resultJson)
|
returnSuccess(result, resultJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -318,12 +341,20 @@ class OathManager(
|
|||||||
|
|
||||||
val credential = getOathCredential(session, uri)
|
val credential = getOathCredential(session, uri)
|
||||||
|
|
||||||
val resultJson =
|
val code = _model.updateCode(
|
||||||
calculateCode(session, credential, System.currentTimeMillis())
|
session.deviceId,
|
||||||
.toJson()
|
credential.model(session.deviceId),
|
||||||
.toString()
|
calculateCode(session, credential, System.currentTimeMillis()).model()
|
||||||
|
)
|
||||||
|
|
||||||
returnSuccess(result, resultJson)
|
if (code != null) {
|
||||||
|
val resultJson = jsonSerializer.encodeToString(code)
|
||||||
|
|
||||||
|
returnSuccess(result, resultJson)
|
||||||
|
} else {
|
||||||
|
// TODO - figure out better error handling here
|
||||||
|
returnError(result, java.lang.IllegalStateException())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (cause: Throwable) {
|
} catch (cause: Throwable) {
|
||||||
@ -396,9 +427,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 = jsonSerializer.encodeToString(_model.session)
|
||||||
it.resume(oathSessionData)
|
it.resume(oathSessionData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -411,9 +447,11 @@ class OathManager(
|
|||||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||||
val session = OathSession(result.value)
|
val session = OathSession(result.value)
|
||||||
if (tryToUnlockOathSession(session)) {
|
if (tryToUnlockOathSession(session)) {
|
||||||
val resultJson = calculateOathCodes(session)
|
_model.update(
|
||||||
.toJson(session.deviceId)
|
session.deviceId,
|
||||||
.toString()
|
calculateOathCodes(session).model(session.deviceId)
|
||||||
|
)
|
||||||
|
val resultJson = jsonSerializer.encodeToString(_model.credentials)
|
||||||
it.resume(resultJson)
|
it.resume(resultJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -503,7 +541,7 @@ class OathManager(
|
|||||||
|
|
||||||
private fun getOathCredential(oathSession: OathSession, credentialId: String) =
|
private fun getOathCredential(oathSession: OathSession, credentialId: String) =
|
||||||
oathSession.credentials.firstOrNull { credential ->
|
oathSession.credentials.firstOrNull { credential ->
|
||||||
(credential != null) && credential.idAsString() == credentialId
|
(credential != null) && credential.id.asString() == credentialId
|
||||||
} ?: throw Exception("Failed to find account to delete")
|
} ?: throw Exception("Failed to find account to delete")
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,316 @@
|
|||||||
|
package com.yubico.authenticator.oath
|
||||||
|
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.code
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.emptyCredentials
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.hotp
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.totp
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ModelTest {
|
||||||
|
|
||||||
|
private val model = Model()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `uses RFC 6238 values`() {
|
||||||
|
assertEquals(0x10.toByte(), Model.OathType.HOTP.value)
|
||||||
|
assertEquals(0x20.toByte(), Model.OathType.TOTP.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `hotp is interactive`() {
|
||||||
|
assertTrue(hotp().isInteractive())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `totp with touch is interactive`() {
|
||||||
|
assertTrue(totp(touchRequired = true).isInteractive())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `totp without touch is not interactive`() {
|
||||||
|
assertFalse(totp(touchRequired = false).isInteractive())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `has no credentials after initialization`() {
|
||||||
|
assertTrue(model.credentials.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updates empty model`() {
|
||||||
|
|
||||||
|
val d = "device1"
|
||||||
|
val m = mapOf(totp(d) to code())
|
||||||
|
model.update(d, m)
|
||||||
|
|
||||||
|
assertEquals(1, model.credentials.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `tracks deviceId`() {
|
||||||
|
val noDevice = ""
|
||||||
|
val device1 = "d1"
|
||||||
|
val device2 = "d2"
|
||||||
|
assertEquals(noDevice, model.session.deviceId)
|
||||||
|
model.update(device1, emptyCredentials())
|
||||||
|
assertEquals(device1, model.session.deviceId)
|
||||||
|
model.update(device2, emptyCredentials())
|
||||||
|
assertEquals(device2, model.session.deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `replaces credentials on device change`() {
|
||||||
|
val d1 = "device1"
|
||||||
|
val m1 = mapOf(
|
||||||
|
totp(d1) to code(),
|
||||||
|
totp(d1) to code()
|
||||||
|
)
|
||||||
|
model.update(d1, m1)
|
||||||
|
|
||||||
|
val d2 = "device2"
|
||||||
|
val m2 = emptyCredentials()
|
||||||
|
model.update(d2, m2)
|
||||||
|
|
||||||
|
assertTrue(model.credentials.isEmpty())
|
||||||
|
|
||||||
|
model.update(d1, m1)
|
||||||
|
assertEquals(2, model.credentials.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `preserves credentials on update`() {
|
||||||
|
|
||||||
|
val d1 = "device1"
|
||||||
|
|
||||||
|
val cred1 = totp(d1, name = "cred1")
|
||||||
|
val cred2 = totp(d1, name = "cred2")
|
||||||
|
val cred3 = totp(d1, name = "cred3")
|
||||||
|
|
||||||
|
val m1 = mapOf(
|
||||||
|
cred1 to code(),
|
||||||
|
cred2 to code()
|
||||||
|
)
|
||||||
|
model.update(d1, m1)
|
||||||
|
|
||||||
|
// one more credential was added
|
||||||
|
val m2 = mapOf(
|
||||||
|
cred2 to code(),
|
||||||
|
cred3 to code(),
|
||||||
|
cred1 to code()
|
||||||
|
)
|
||||||
|
|
||||||
|
model.update(d1, m2)
|
||||||
|
|
||||||
|
assertEquals("device1", model.session.deviceId)
|
||||||
|
assertEquals(3, model.credentials.size)
|
||||||
|
assertTrue(model.credentials.find { it.credential == cred1 } != null)
|
||||||
|
assertTrue(model.credentials.find { it.credential == cred2 } != null)
|
||||||
|
assertTrue(model.credentials.find { it.credential == cred3 } != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updates credential codes`() {
|
||||||
|
val cred = totp(name = "cred1")
|
||||||
|
val code = code(value = "123456")
|
||||||
|
val m1 = mapOf(cred to code)
|
||||||
|
model.update(cred.deviceId, m1)
|
||||||
|
|
||||||
|
assertTrue(model.credentials.find { it.code == code } != null)
|
||||||
|
|
||||||
|
val updatedCode = code(value = "121212")
|
||||||
|
val m2 = mapOf(cred to updatedCode)
|
||||||
|
model.update(cred.deviceId, m2)
|
||||||
|
|
||||||
|
assertTrue(model.credentials.find { it.code == updatedCode } != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update preserves non-interactive codes`() {
|
||||||
|
val d = "device"
|
||||||
|
val totp = totp(d, name = "totpCred")
|
||||||
|
val totpCode: Model.Code? = null
|
||||||
|
|
||||||
|
val hotp = hotp(d, name = "hotpCred")
|
||||||
|
val hotpCode: Model.Code? = null
|
||||||
|
|
||||||
|
val m1 = mapOf(hotp to hotpCode, totp to totpCode)
|
||||||
|
model.update(d, m1)
|
||||||
|
|
||||||
|
assertTrue(model.credentials.find { it.code == hotpCode } != null)
|
||||||
|
|
||||||
|
val updatedTotpCode = code(value = "121212")
|
||||||
|
val updatedHotpCode = code(value = "098765")
|
||||||
|
val m2 = mapOf(hotp to updatedHotpCode, totp to updatedTotpCode)
|
||||||
|
model.update(d, m2)
|
||||||
|
|
||||||
|
assertTrue(model.credentials.find { it.code == updatedTotpCode } != null)
|
||||||
|
assertTrue(model.credentials.find { it.code == hotpCode } != null)
|
||||||
|
assertFalse(model.credentials.find { it.code == updatedHotpCode } != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update preserves interactive totp credentials`() {
|
||||||
|
val d = "device"
|
||||||
|
val totp = totp(d, name = "totpCred", touchRequired = true)
|
||||||
|
val totpCode: Model.Code? = null
|
||||||
|
|
||||||
|
model.update(d, mapOf(totp to totpCode))
|
||||||
|
|
||||||
|
// simulate touch
|
||||||
|
val newCode = model.updateCode(d, totp, code(value = "00000"))
|
||||||
|
assertNotNull(newCode)
|
||||||
|
|
||||||
|
// update with same values
|
||||||
|
model.update(d, mapOf(totp to newCode))
|
||||||
|
|
||||||
|
assertEquals(1, model.credentials.size)
|
||||||
|
assertEquals("00000", model.credentials.find { it.credential == totp }?.code?.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `adds new credentials`() {
|
||||||
|
val d = "Device"
|
||||||
|
val t1 = totp()
|
||||||
|
val c1 = code()
|
||||||
|
model.update(d, mapOf(t1 to c1))
|
||||||
|
|
||||||
|
val t2 = totp()
|
||||||
|
val c2 = code()
|
||||||
|
val t3 = totp()
|
||||||
|
val c3 = code()
|
||||||
|
model.update(d, mapOf(t3 to c3, t2 to c2, t1 to c1))
|
||||||
|
|
||||||
|
// t3 and t2 are added to credentials
|
||||||
|
assertEquals(3, model.credentials.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `removes non-existing credentials`() {
|
||||||
|
val d = "Device"
|
||||||
|
val t1 = totp()
|
||||||
|
val c1 = code()
|
||||||
|
val t2 = totp()
|
||||||
|
val c2 = code()
|
||||||
|
val t3 = totp()
|
||||||
|
val c3 = code()
|
||||||
|
|
||||||
|
model.update(d, mapOf(t3 to c3, t1 to c1, t2 to c2))
|
||||||
|
assertEquals(3, model.credentials.size)
|
||||||
|
|
||||||
|
model.update(d, mapOf(t1 to c1))
|
||||||
|
|
||||||
|
// only t1 is part of credentials
|
||||||
|
assertEquals(1, model.credentials.size)
|
||||||
|
assertTrue(model.credentials.find { it.credential == t1 } != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `adds one credential with code to empty`() {
|
||||||
|
val d = "device"
|
||||||
|
model.update(d, mapOf(totp() to code()))
|
||||||
|
|
||||||
|
assertEquals(1, model.credentials.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `does not add one credential with code to not initialized model`() {
|
||||||
|
val d = "device"
|
||||||
|
model.add(d, totp(), code())
|
||||||
|
|
||||||
|
assertEquals(0, model.credentials.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `adds credential only to correct device`() {
|
||||||
|
val d1 = "device1"
|
||||||
|
val d2 = "device2"
|
||||||
|
model.update(d1, mapOf(totp() to code()))
|
||||||
|
|
||||||
|
// cannot add to this model
|
||||||
|
assertNull(model.add(d2, totp(), code()))
|
||||||
|
|
||||||
|
// can add to this model
|
||||||
|
assertNotNull(model.add(d1, totp(), code()))
|
||||||
|
|
||||||
|
assertEquals(2, model.credentials.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `renames only on correct device`() {
|
||||||
|
val d1 = "device1"
|
||||||
|
val d2 = "device2"
|
||||||
|
val toRename = totp(d1, name = "oldName", issuer = "oldIssuer")
|
||||||
|
val code1 = code()
|
||||||
|
|
||||||
|
model.update(d1, mapOf(toRename to code1))
|
||||||
|
|
||||||
|
val renamedForD2 = totp(d2, name = "newName", issuer = "newIssuer")
|
||||||
|
assertNull(model.rename(d1, toRename, renamedForD2))
|
||||||
|
|
||||||
|
val renamedForD1 = totp(d1, name = "newName", issuer = "newIssuer")
|
||||||
|
// trying to rename on wrong device
|
||||||
|
assertNull(model.rename(d2, toRename, renamedForD2))
|
||||||
|
|
||||||
|
|
||||||
|
// rename success
|
||||||
|
val renamed = model.rename(d1, toRename, renamedForD1)
|
||||||
|
assertNotNull(renamed)
|
||||||
|
|
||||||
|
// the name and issuer are correct
|
||||||
|
assertEquals("newName", renamed?.accountName)
|
||||||
|
assertEquals("newIssuer", renamed?.issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `renames issuer`() {
|
||||||
|
val d = "device1"
|
||||||
|
val toRename = totp(d, name = "oldName", issuer = "oldIssuer")
|
||||||
|
val code1 = code()
|
||||||
|
|
||||||
|
model.update(d, mapOf(toRename to code1))
|
||||||
|
|
||||||
|
val nullIssuer = totp(d, name = "newName", issuer = null)
|
||||||
|
val renamed = model.rename(d, toRename, nullIssuer)
|
||||||
|
|
||||||
|
assertNull(renamed!!.issuer)
|
||||||
|
|
||||||
|
val nonNullIssuer = totp(d, name = "newName", issuer = "valueHere")
|
||||||
|
val renamed2 = model.rename(d, renamed, nonNullIssuer)
|
||||||
|
|
||||||
|
assertNotNull(renamed2!!.issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updates code`() {
|
||||||
|
val d1 = "d1"
|
||||||
|
val d2 = "d2"
|
||||||
|
val totpD1 = totp(d1, name = "sameName", issuer = "sameIssuer")
|
||||||
|
val totpD2 = totp(d2, name = "sameName", issuer = "sameIssuer")
|
||||||
|
val code1 = code(value = "12345")
|
||||||
|
val code2 = code(value = "00000")
|
||||||
|
|
||||||
|
model.update(d1, mapOf(totpD1 to code1))
|
||||||
|
|
||||||
|
// cant update on different device
|
||||||
|
assertNull(model.updateCode(d2, totpD1, code()))
|
||||||
|
|
||||||
|
// cant update for credential from different device
|
||||||
|
assertNull(model.updateCode(d1, totpD2, code()))
|
||||||
|
|
||||||
|
// updates correctly to new code
|
||||||
|
val newCode = model.updateCode(d1, totpD1, code2)
|
||||||
|
assertNotNull(newCode)
|
||||||
|
assertEquals("00000", newCode!!.value!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `removes data on reset`() {
|
||||||
|
model.update("device", mapOf(totp() to code()))
|
||||||
|
model.reset()
|
||||||
|
|
||||||
|
assertEquals("", model.session.deviceId)
|
||||||
|
assertTrue(model.credentials.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.yubico.authenticator.oath
|
||||||
|
|
||||||
|
object OathTestHelper {
|
||||||
|
|
||||||
|
// create a TOTP credential with default or custom parameters
|
||||||
|
// if not specified, default values for deviceId, name and issuer will use a unique value
|
||||||
|
// which is incremented on every call to this function
|
||||||
|
fun totp(
|
||||||
|
deviceId: String = nextDevice(),
|
||||||
|
name: String = nextName(),
|
||||||
|
issuer: String? = nextIssuer(),
|
||||||
|
touchRequired: Boolean = false,
|
||||||
|
period: Int = 30
|
||||||
|
) = cred(deviceId, name, issuer, Model.OathType.TOTP, touchRequired, period)
|
||||||
|
|
||||||
|
// create a HOTP credential with default or custom parameters
|
||||||
|
// if not specified, default values for deviceId, name and issuer will use a unique value
|
||||||
|
// which is incremented on every call to this function
|
||||||
|
fun hotp(
|
||||||
|
deviceId: String = nextDevice(),
|
||||||
|
name: String = nextName(),
|
||||||
|
issuer: String = nextIssuer(),
|
||||||
|
touchRequired: Boolean = false,
|
||||||
|
period: Int = 30
|
||||||
|
) = cred(deviceId, name, issuer, Model.OathType.HOTP, touchRequired, period)
|
||||||
|
|
||||||
|
private fun cred(
|
||||||
|
deviceId: String = nextDevice(),
|
||||||
|
name: String = nextName(),
|
||||||
|
issuer: String? = nextIssuer(),
|
||||||
|
type: Model.OathType,
|
||||||
|
touchRequired: Boolean = false,
|
||||||
|
period: Int = 30
|
||||||
|
) =
|
||||||
|
Model.Credential(
|
||||||
|
deviceId = deviceId,
|
||||||
|
id = """otpauth://${type.name}/${name}?secret=aabbaabbaabbaabb&issuer=${issuer}""",
|
||||||
|
oathType = type,
|
||||||
|
period = period,
|
||||||
|
issuer = issuer,
|
||||||
|
accountName = name,
|
||||||
|
touchRequired = touchRequired
|
||||||
|
)
|
||||||
|
// create a Code with default or custom parameters
|
||||||
|
fun code(
|
||||||
|
value: String = "111111",
|
||||||
|
from: Long = 1000,
|
||||||
|
to: Long = 2000
|
||||||
|
) = Model.Code(value, from, to)
|
||||||
|
|
||||||
|
fun emptyCredentials() = emptyMap<Model.Credential, Model.Code>()
|
||||||
|
|
||||||
|
private var nameCounter = 0
|
||||||
|
private fun nextName(): String {
|
||||||
|
return "name${nameCounter++}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var issuerCounter = 0
|
||||||
|
private fun nextIssuer(): String {
|
||||||
|
return "issuer${issuerCounter++}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var deviceCounter = 0
|
||||||
|
private fun nextDevice(): String {
|
||||||
|
return "deviceId${deviceCounter++}"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package com.yubico.authenticator.oath
|
||||||
|
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.code
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.hotp
|
||||||
|
import com.yubico.authenticator.oath.OathTestHelper.totp
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SerializationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `serialization settings`() {
|
||||||
|
assertTrue(jsonSerializer.configuration.encodeDefaults)
|
||||||
|
assertFalse(jsonSerializer.configuration.allowStructuredMapKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `session json type`() {
|
||||||
|
val s = Model.Session()
|
||||||
|
|
||||||
|
val jsonElement = jsonSerializer.encodeToJsonElement(s)
|
||||||
|
assertTrue(jsonElement is JsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `session json property names`() {
|
||||||
|
val s = Model.Session()
|
||||||
|
|
||||||
|
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(s) as JsonObject
|
||||||
|
assertTrue(jsonObject.containsKey("device_id"))
|
||||||
|
assertTrue(jsonObject.containsKey("has_key"))
|
||||||
|
assertTrue(jsonObject.containsKey("remembered"))
|
||||||
|
assertTrue(jsonObject.containsKey("locked"))
|
||||||
|
assertTrue(jsonObject.containsKey("keystore"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `credential json type`() {
|
||||||
|
val c = totp()
|
||||||
|
|
||||||
|
val jsonElement = jsonSerializer.encodeToJsonElement(c)
|
||||||
|
assertTrue(jsonElement is JsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `credential json property names`() {
|
||||||
|
val c = totp()
|
||||||
|
|
||||||
|
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||||
|
|
||||||
|
assertTrue(jsonObject.containsKey("device_id"))
|
||||||
|
assertTrue(jsonObject.containsKey("id"))
|
||||||
|
assertTrue(jsonObject.containsKey("oath_type"))
|
||||||
|
assertTrue(jsonObject.containsKey("period"))
|
||||||
|
assertTrue(jsonObject.containsKey("issuer"))
|
||||||
|
assertTrue(jsonObject.containsKey("name"))
|
||||||
|
assertTrue(jsonObject.containsKey("touch_required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `code json type`() {
|
||||||
|
val c = code()
|
||||||
|
|
||||||
|
val jsonElement = jsonSerializer.encodeToJsonElement(c)
|
||||||
|
assertTrue(jsonElement is JsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `code json property names`() {
|
||||||
|
val c = code()
|
||||||
|
|
||||||
|
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||||
|
|
||||||
|
assertTrue(jsonObject.containsKey("value"))
|
||||||
|
assertTrue(jsonObject.containsKey("valid_from"))
|
||||||
|
assertTrue(jsonObject.containsKey("valid_to"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `code json content`() {
|
||||||
|
val c = code(value = "001122", from = 1000, to = 2000)
|
||||||
|
|
||||||
|
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||||
|
|
||||||
|
assertEquals(JsonPrimitive(1000), jsonObject["valid_from"])
|
||||||
|
assertEquals(JsonPrimitive(2000), jsonObject["valid_to"])
|
||||||
|
assertEquals(JsonPrimitive("001122"), jsonObject["value"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `credentials json type`() {
|
||||||
|
val l = listOf(
|
||||||
|
Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
|
||||||
|
)
|
||||||
|
|
||||||
|
val jsonElement = jsonSerializer.encodeToJsonElement(l)
|
||||||
|
assertTrue(jsonElement is JsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `credentials json size`() {
|
||||||
|
val l1 = listOf<Model.CredentialWithCode>()
|
||||||
|
val jsonElement1 = jsonSerializer.encodeToJsonElement(l1) as JsonArray
|
||||||
|
assertEquals(0, jsonElement1.size)
|
||||||
|
|
||||||
|
val l2 = listOf(
|
||||||
|
Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
|
||||||
|
)
|
||||||
|
val jsonElement2 = jsonSerializer.encodeToJsonElement(l2) as JsonArray
|
||||||
|
assertEquals(2, jsonElement2.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,13 +29,10 @@ class _CredentialsProvider extends StateNotifier<List<OathPair>?> {
|
|||||||
void setFromString(String input) {
|
void setFromString(String input) {
|
||||||
var result = jsonDecode(input);
|
var result = jsonDecode(input);
|
||||||
|
|
||||||
final List<OathPair> pairs = [];
|
if (result is List) {
|
||||||
for (var e in result['entries']) {
|
state = result.map((e) => OathPair.fromJson(e)).toList();
|
||||||
final credential = OathCredential.fromJson(e['credential']);
|
} else {
|
||||||
final code = e['code'] == null ? null : OathCode.fromJson(e['code']);
|
state = [];
|
||||||
pairs.add(OathPair(credential, code));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state = pairs;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,9 @@ class OathCode with _$OathCode {
|
|||||||
@freezed
|
@freezed
|
||||||
class OathPair with _$OathPair {
|
class OathPair with _$OathPair {
|
||||||
factory OathPair(OathCredential credential, OathCode? code) = _OathPair;
|
factory OathPair(OathCredential credential, OathCode? code) = _OathPair;
|
||||||
|
|
||||||
|
factory OathPair.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OathPairFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -474,6 +474,10 @@ abstract class _OathCode implements OathCode {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OathPair _$OathPairFromJson(Map<String, dynamic> json) {
|
||||||
|
return _OathPair.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$OathPairTearOff {
|
class _$OathPairTearOff {
|
||||||
const _$OathPairTearOff();
|
const _$OathPairTearOff();
|
||||||
@ -484,6 +488,10 @@ class _$OathPairTearOff {
|
|||||||
code,
|
code,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OathPair fromJson(Map<String, Object?> json) {
|
||||||
|
return OathPair.fromJson(json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -494,6 +502,7 @@ mixin _$OathPair {
|
|||||||
OathCredential get credential => throw _privateConstructorUsedError;
|
OathCredential get credential => throw _privateConstructorUsedError;
|
||||||
OathCode? get code => throw _privateConstructorUsedError;
|
OathCode? get code => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$OathPairCopyWith<OathPair> get copyWith =>
|
$OathPairCopyWith<OathPair> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@ -594,10 +603,13 @@ class __$OathPairCopyWithImpl<$Res> extends _$OathPairCopyWithImpl<$Res>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
class _$_OathPair implements _OathPair {
|
class _$_OathPair implements _OathPair {
|
||||||
_$_OathPair(this.credential, this.code);
|
_$_OathPair(this.credential, this.code);
|
||||||
|
|
||||||
|
factory _$_OathPair.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$_OathPairFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final OathCredential credential;
|
final OathCredential credential;
|
||||||
@override
|
@override
|
||||||
@ -628,11 +640,18 @@ class _$_OathPair implements _OathPair {
|
|||||||
@override
|
@override
|
||||||
_$OathPairCopyWith<_OathPair> get copyWith =>
|
_$OathPairCopyWith<_OathPair> get copyWith =>
|
||||||
__$OathPairCopyWithImpl<_OathPair>(this, _$identity);
|
__$OathPairCopyWithImpl<_OathPair>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$_OathPairToJson(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _OathPair implements OathPair {
|
abstract class _OathPair implements OathPair {
|
||||||
factory _OathPair(OathCredential credential, OathCode? code) = _$_OathPair;
|
factory _OathPair(OathCredential credential, OathCode? code) = _$_OathPair;
|
||||||
|
|
||||||
|
factory _OathPair.fromJson(Map<String, dynamic> json) = _$_OathPair.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
OathCredential get credential;
|
OathCredential get credential;
|
||||||
@override
|
@override
|
||||||
|
@ -46,6 +46,19 @@ Map<String, dynamic> _$$_OathCodeToJson(_$_OathCode instance) =>
|
|||||||
'valid_to': instance.validTo,
|
'valid_to': instance.validTo,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$_OathPair _$$_OathPairFromJson(Map<String, dynamic> json) => _$_OathPair(
|
||||||
|
OathCredential.fromJson(json['credential'] as Map<String, dynamic>),
|
||||||
|
json['code'] == null
|
||||||
|
? null
|
||||||
|
: OathCode.fromJson(json['code'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$_OathPairToJson(_$_OathPair instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'credential': instance.credential,
|
||||||
|
'code': instance.code,
|
||||||
|
};
|
||||||
|
|
||||||
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
|
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
|
||||||
json['device_id'] as String,
|
json['device_id'] as String,
|
||||||
hasKey: json['has_key'] as bool,
|
hasKey: json['has_key'] as bool,
|
||||||
|
Loading…
Reference in New Issue
Block a user