mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 08:22:16 +03:00
separate yubikit and app's types
This commit is contained in:
parent
21085b5637
commit
26d0635c92
21
android/app/src/main/kotlin/com/yubico/authenticator/Util.kt
Normal file
21
android/app/src/main/kotlin/com/yubico/authenticator/Util.kt
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator
|
||||
|
||||
fun ByteArray.asString() = joinToString(
|
||||
separator = ""
|
||||
) { b -> "%02x".format(b) }
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import com.yubico.authenticator.device.Version
|
||||
import com.yubico.yubikit.oath.Code
|
||||
import com.yubico.yubikit.oath.Credential
|
||||
import com.yubico.yubikit.oath.OathSession
|
||||
import com.yubico.yubikit.oath.OathType
|
||||
|
||||
fun ByteArray.asString() = joinToString(
|
||||
separator = ""
|
||||
) { b -> "%02x".format(b) }
|
||||
|
||||
// convert yubikit types to Model types
|
||||
fun OathSession.model(isRemembered: Boolean) = Model.Session(
|
||||
deviceId,
|
||||
Version(
|
||||
version.major,
|
||||
version.minor,
|
||||
version.micro
|
||||
),
|
||||
isAccessKeySet,
|
||||
isRemembered,
|
||||
isLocked
|
||||
)
|
||||
|
||||
fun Credential.model(deviceId: String) = Model.Credential(
|
||||
deviceId = deviceId,
|
||||
id = id.asString(),
|
||||
oathType = when (oathType) {
|
||||
OathType.HOTP -> Model.OathType.HOTP
|
||||
else -> Model.OathType.TOTP
|
||||
},
|
||||
period = period,
|
||||
issuer = issuer,
|
||||
accountName = accountName,
|
||||
touchRequired = isTouchRequired
|
||||
)
|
||||
|
||||
fun Code.model() = Model.Code(
|
||||
value,
|
||||
validFrom / 1000,
|
||||
validUntil / 1000
|
||||
)
|
||||
|
||||
fun Map<Credential, Code?>.model(deviceId: String): Map<Model.Credential, Model.Code?> =
|
||||
map { (credential, code) ->
|
||||
Pair(
|
||||
credential.model(deviceId),
|
||||
code?.model()
|
||||
)
|
||||
}.toMap()
|
@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import com.yubico.authenticator.device.Version
|
||||
import kotlinx.serialization.*
|
||||
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("version")
|
||||
val version: Version,
|
||||
@SerialName("has_key")
|
||||
val isAccessKeySet: Boolean,
|
||||
@SerialName("remembered")
|
||||
val isRemembered: Boolean,
|
||||
@SerialName("locked")
|
||||
val isLocked: Boolean
|
||||
) {
|
||||
@SerialName("keystore")
|
||||
@Suppress("unused")
|
||||
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
|
||||
data 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -29,6 +29,17 @@ import com.yubico.authenticator.device.Config
|
||||
import com.yubico.authenticator.device.Info
|
||||
import com.yubico.authenticator.device.Version
|
||||
import com.yubico.authenticator.logging.Log
|
||||
import com.yubico.authenticator.oath.data.Code
|
||||
import com.yubico.authenticator.oath.data.CodeType
|
||||
import com.yubico.authenticator.oath.data.Credential
|
||||
import com.yubico.authenticator.oath.data.CredentialWithCode
|
||||
import com.yubico.authenticator.oath.data.Session
|
||||
import com.yubico.authenticator.oath.data.YubiKitCode
|
||||
import com.yubico.authenticator.oath.data.YubiKitCredential
|
||||
import com.yubico.authenticator.oath.data.YubiKitOathSession
|
||||
import com.yubico.authenticator.oath.data.YubiKitOathType
|
||||
import com.yubico.authenticator.oath.data.calculateSteamCode
|
||||
import com.yubico.authenticator.oath.data.isSteamCredential
|
||||
import com.yubico.authenticator.oath.keystore.ClearingMemProvider
|
||||
import com.yubico.authenticator.oath.keystore.KeyProvider
|
||||
import com.yubico.authenticator.oath.keystore.KeyStoreProvider
|
||||
@ -45,7 +56,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardProtocol
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.management.FormFactor
|
||||
import com.yubico.yubikit.oath.*
|
||||
import com.yubico.yubikit.oath.CredentialData
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@ -55,7 +66,7 @@ import java.net.URI
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
typealias OathAction = (Result<OathSession, Exception>) -> Unit
|
||||
typealias OathAction = (Result<YubiKitOathSession, Exception>) -> Unit
|
||||
|
||||
class OathManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
@ -145,11 +156,11 @@ class OathManager(
|
||||
}
|
||||
}
|
||||
|
||||
private val credentialObserver = Observer<List<Model.CredentialWithCode>?> { codes ->
|
||||
private val credentialObserver = Observer<List<CredentialWithCode>?> { codes ->
|
||||
refreshJob?.cancel()
|
||||
if (codes != null && appViewModel.connectedYubiKey.value != null) {
|
||||
val expirations = codes
|
||||
.filter { it.credential.oathType == Model.OathType.TOTP && !it.credential.touchRequired }
|
||||
.filter { it.credential.codeType == CodeType.TOTP && !it.credential.touchRequired }
|
||||
.mapNotNull { it.code?.validTo }
|
||||
if (expirations.isNotEmpty()) {
|
||||
val earliest = expirations.min() * 1000
|
||||
@ -231,23 +242,21 @@ class OathManager(
|
||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||
try {
|
||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||
val oath = OathSession(connection)
|
||||
tryToUnlockOathSession(oath)
|
||||
val session = YubiKitOathSession(connection)
|
||||
tryToUnlockOathSession(session)
|
||||
|
||||
val previousId = oathViewModel.sessionState.value?.deviceId
|
||||
if (oath.deviceId == previousId) {
|
||||
if (session.deviceId == previousId) {
|
||||
// Run any pending action
|
||||
pendingAction?.let { action ->
|
||||
action.invoke(Result.success(oath))
|
||||
action.invoke(Result.success(session))
|
||||
pendingAction = null
|
||||
}
|
||||
|
||||
// Refresh codes
|
||||
if (!oath.isLocked) {
|
||||
if (!session.isLocked) {
|
||||
try {
|
||||
oathViewModel.updateCredentials(
|
||||
calculateOathCodes(oath).model(oath.deviceId)
|
||||
)
|
||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "Failed to refresh codes", error.toString())
|
||||
}
|
||||
@ -259,11 +268,14 @@ class OathManager(
|
||||
}
|
||||
|
||||
// Update the OATH state
|
||||
oathViewModel.setSessionState(oath.model(keyManager.isRemembered(oath.deviceId)))
|
||||
if (!oath.isLocked) {
|
||||
oathViewModel.updateCredentials(
|
||||
calculateOathCodes(oath).model(oath.deviceId)
|
||||
oathViewModel.setSessionState(
|
||||
Session(
|
||||
session,
|
||||
keyManager.isRemembered(session.deviceId)
|
||||
)
|
||||
)
|
||||
if (!session.isLocked) {
|
||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||
}
|
||||
|
||||
// Awaiting an action for a different or no device?
|
||||
@ -272,7 +284,7 @@ class OathManager(
|
||||
if (addToAny) {
|
||||
// Special "add to any YubiKey" action, process
|
||||
addToAny = false
|
||||
action.invoke(Result.success(oath))
|
||||
action.invoke(Result.success(session))
|
||||
} else {
|
||||
// Awaiting an action for a different device? Fail it and stop processing.
|
||||
action.invoke(Result.failure(IllegalStateException("Wrong deviceId")))
|
||||
@ -280,7 +292,7 @@ class OathManager(
|
||||
}
|
||||
}
|
||||
|
||||
if (oath.version.isLessThan(4, 0, 0) && connection.transport == Transport.NFC) {
|
||||
if (session.version.isLessThan(4, 0, 0) && connection.transport == Transport.NFC) {
|
||||
// NEO over NFC, select OTP applet before reading info
|
||||
SmartCardProtocol(connection).select(OTP_AID)
|
||||
}
|
||||
@ -358,14 +370,14 @@ class OathManager(
|
||||
|
||||
val credential = session.putCredential(credentialData, requireTouch)
|
||||
val code =
|
||||
if (credentialData.oathType == OathType.TOTP && !requireTouch) {
|
||||
if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch) {
|
||||
// recalculate the code
|
||||
calculateCode(session, credential)
|
||||
} else null
|
||||
|
||||
val addedCred = oathViewModel.addCredential(
|
||||
credential.model(session.deviceId),
|
||||
code?.model()
|
||||
Credential(credential, session.deviceId),
|
||||
Code.from(code)
|
||||
)
|
||||
|
||||
Log.d(TAG, "Added cred $credential")
|
||||
@ -378,7 +390,7 @@ class OathManager(
|
||||
// note, it is ok to reset locked session
|
||||
it.reset()
|
||||
keyManager.removeKey(it.deviceId)
|
||||
oathViewModel.setSessionState(it.model(false))
|
||||
oathViewModel.setSessionState(Session(it, false))
|
||||
}
|
||||
return NULL
|
||||
}
|
||||
@ -391,8 +403,8 @@ class OathManager(
|
||||
val unlocked = tryToUnlockOathSession(it)
|
||||
val remembered = keyManager.isRemembered(it.deviceId)
|
||||
if (unlocked) {
|
||||
oathViewModel.setSessionState(it.model(remembered))
|
||||
oathViewModel.updateCredentials(calculateOathCodes(it).model(it.deviceId))
|
||||
oathViewModel.setSessionState(Session(it, remembered))
|
||||
oathViewModel.updateCredentials(calculateOathCodes(it))
|
||||
}
|
||||
|
||||
jsonSerializer.encodeToString(mapOf("unlocked" to unlocked, "remembered" to remembered))
|
||||
@ -415,7 +427,7 @@ class OathManager(
|
||||
val accessKey = session.deriveAccessKey(newPassword.toCharArray())
|
||||
session.setAccessKey(accessKey)
|
||||
keyManager.addKey(session.deviceId, accessKey, false)
|
||||
oathViewModel.setSessionState(session.model(false))
|
||||
oathViewModel.setSessionState(Session(session, false))
|
||||
Log.d(TAG, "Successfully set password")
|
||||
NULL
|
||||
}
|
||||
@ -427,7 +439,7 @@ class OathManager(
|
||||
if (session.unlock(currentPassword.toCharArray())) {
|
||||
session.deleteAccessKey()
|
||||
keyManager.removeKey(session.deviceId)
|
||||
oathViewModel.setSessionState(session.model(false))
|
||||
oathViewModel.setSessionState(Session(session, false))
|
||||
Log.d(TAG, "Successfully unset password")
|
||||
return@useOathSession NULL
|
||||
}
|
||||
@ -460,14 +472,14 @@ class OathManager(
|
||||
val credential = session.putCredential(credentialData, requireTouch)
|
||||
|
||||
val code =
|
||||
if (credentialData.oathType == OathType.TOTP && !requireTouch) {
|
||||
if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch) {
|
||||
// recalculate the code
|
||||
calculateCode(session, credential)
|
||||
} else null
|
||||
|
||||
val addedCred = oathViewModel.addCredential(
|
||||
credential.model(session.deviceId),
|
||||
code?.model()
|
||||
Credential(credential, session.deviceId),
|
||||
Code.from(code)
|
||||
)
|
||||
|
||||
jsonSerializer.encodeToString(addedCred)
|
||||
@ -477,9 +489,9 @@ class OathManager(
|
||||
useOathSession("Rename") { session ->
|
||||
val credential = getOathCredential(session, uri)
|
||||
val renamedCredential =
|
||||
session.renameCredential(credential, name, issuer).model(session.deviceId)
|
||||
Credential(session.renameCredential(credential, name, issuer), session.deviceId)
|
||||
oathViewModel.renameCredential(
|
||||
credential.model(session.deviceId),
|
||||
Credential(credential, session.deviceId),
|
||||
renamedCredential
|
||||
)
|
||||
|
||||
@ -490,7 +502,7 @@ class OathManager(
|
||||
useOathSession("Delete account") { session ->
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
session.deleteCredential(credential)
|
||||
oathViewModel.removeCredential(credential.model(session.deviceId))
|
||||
oathViewModel.removeCredential(Credential(credential, session.deviceId))
|
||||
NULL
|
||||
}
|
||||
|
||||
@ -498,13 +510,16 @@ class OathManager(
|
||||
appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice ->
|
||||
useOathSessionUsb(usbYubiKeyDevice) { session ->
|
||||
try {
|
||||
oathViewModel.updateCredentials(
|
||||
calculateOathCodes(session).model(session.deviceId)
|
||||
)
|
||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||
} catch (apduException: ApduException) {
|
||||
if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) {
|
||||
Log.d(TAG, "Handled oath credential refresh on locked session.")
|
||||
oathViewModel.setSessionState(session.model(keyManager.isRemembered(session.deviceId)))
|
||||
oathViewModel.setSessionState(
|
||||
Session(
|
||||
session,
|
||||
keyManager.isRemembered(session.deviceId)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Log.e(
|
||||
TAG,
|
||||
@ -520,9 +535,9 @@ class OathManager(
|
||||
useOathSession("Calculate") { session ->
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
|
||||
val code = calculateCode(session, credential).model()
|
||||
val code = Code.from(calculateCode(session, credential))
|
||||
oathViewModel.updateCode(
|
||||
credential.model(session.deviceId),
|
||||
Credential(credential, session.deviceId),
|
||||
code
|
||||
)
|
||||
Log.d(TAG, "Code calculated $code")
|
||||
@ -532,15 +547,15 @@ class OathManager(
|
||||
|
||||
/**
|
||||
* Returns Steam code or standard TOTP code based on the credential.
|
||||
* @param session OathSession which calculates the TOTP code
|
||||
* @param session YubiKitOathSession which calculates the TOTP code
|
||||
* @param credential
|
||||
*
|
||||
* @return calculated Code
|
||||
*/
|
||||
private fun calculateCode(
|
||||
session: OathSession,
|
||||
credential: Credential
|
||||
): Code {
|
||||
session: YubiKitOathSession,
|
||||
credential: YubiKitCredential
|
||||
): YubiKitCode {
|
||||
// Manual calculate, need to pad timer to avoid immediate expiration
|
||||
val timestamp = System.currentTimeMillis() + 10000
|
||||
try {
|
||||
@ -560,12 +575,12 @@ class OathManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to unlocks [OathSession] with [AccessKey] stored in [KeyManager]. On failure clears
|
||||
* Tries to unlocks [session] with access key stored in [KeyManager]. On failure clears
|
||||
* relevant access keys from [KeyManager]
|
||||
*
|
||||
* @return true if the session is not locked or it was successfully unlocked, false otherwise
|
||||
*/
|
||||
private fun tryToUnlockOathSession(session: OathSession): Boolean {
|
||||
private fun tryToUnlockOathSession(session: YubiKitOathSession): Boolean {
|
||||
if (!session.isLocked) {
|
||||
return true
|
||||
}
|
||||
@ -584,7 +599,7 @@ class OathManager(
|
||||
return false // the unlock did not work, session is locked
|
||||
}
|
||||
|
||||
private fun calculateOathCodes(session: OathSession): Map<Credential, Code> {
|
||||
private fun calculateOathCodes(session: YubiKitOathSession): Map<Credential, Code?> {
|
||||
val isUsbKey = appViewModel.connectedYubiKey.value != null
|
||||
var timestamp = System.currentTimeMillis()
|
||||
if (!isUsbKey) {
|
||||
@ -594,21 +609,21 @@ class OathManager(
|
||||
val bypassTouch = appPreferences.bypassTouchOnNfcTap && !isUsbKey
|
||||
return session.calculateCodes(timestamp).map { (credential, code) ->
|
||||
Pair(
|
||||
credential,
|
||||
if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) {
|
||||
Credential(credential, session.deviceId),
|
||||
Code.from(if (credential.isSteamCredential() && (!credential.isTouchRequired || bypassTouch)) {
|
||||
session.calculateSteamCode(credential, timestamp)
|
||||
} else if (credential.isTouchRequired && bypassTouch) {
|
||||
session.calculateCode(credential, timestamp)
|
||||
} else {
|
||||
code
|
||||
}
|
||||
})
|
||||
)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSession(
|
||||
title: String,
|
||||
action: (OathSession) -> T
|
||||
action: (YubiKitOathSession) -> T
|
||||
): T {
|
||||
return appViewModel.connectedYubiKey.value?.let {
|
||||
useOathSessionUsb(it, action)
|
||||
@ -617,16 +632,16 @@ class OathManager(
|
||||
|
||||
private suspend fun <T> useOathSessionUsb(
|
||||
device: UsbYubiKeyDevice,
|
||||
block: (OathSession) -> T
|
||||
block: (YubiKitOathSession) -> T
|
||||
): T = device.withConnection<SmartCardConnection, T> {
|
||||
val oath = OathSession(it)
|
||||
tryToUnlockOathSession(oath)
|
||||
block(oath)
|
||||
val session = YubiKitOathSession(it)
|
||||
tryToUnlockOathSession(session)
|
||||
block(session)
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSessionNfc(
|
||||
title: String,
|
||||
block: (OathSession) -> T
|
||||
block: (YubiKitOathSession) -> T
|
||||
): T {
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
@ -664,9 +679,9 @@ class OathManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOathCredential(oathSession: OathSession, credentialId: String) =
|
||||
private fun getOathCredential(session: YubiKitOathSession, credentialId: String) =
|
||||
// we need to use oathSession.calculateCodes() to get proper Credential.touchRequired value
|
||||
oathSession.calculateCodes().map { e -> e.key }.firstOrNull { credential ->
|
||||
session.calculateCodes().map { e -> e.key }.firstOrNull { credential ->
|
||||
(credential != null) && credential.id.asString() == credentialId
|
||||
} ?: throw Exception("Failed to find account")
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,12 +19,16 @@ package com.yubico.authenticator.oath
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.yubico.authenticator.oath.data.Code
|
||||
import com.yubico.authenticator.oath.data.Credential
|
||||
import com.yubico.authenticator.oath.data.CredentialWithCode
|
||||
import com.yubico.authenticator.oath.data.Session
|
||||
|
||||
class OathViewModel: ViewModel() {
|
||||
private val _sessionState = MutableLiveData<Model.Session?>()
|
||||
val sessionState: LiveData<Model.Session?> = _sessionState
|
||||
private val _sessionState = MutableLiveData<Session?>()
|
||||
val sessionState: LiveData<Session?> = _sessionState
|
||||
|
||||
fun setSessionState(sessionState: Model.Session?) {
|
||||
fun setSessionState(sessionState: Session?) {
|
||||
val oldDeviceId = _sessionState.value?.deviceId
|
||||
_sessionState.postValue(sessionState)
|
||||
if(oldDeviceId != sessionState?.deviceId) {
|
||||
@ -32,14 +36,14 @@ class OathViewModel: ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private val _credentials = MutableLiveData<List<Model.CredentialWithCode>?>()
|
||||
val credentials: LiveData<List<Model.CredentialWithCode>?> = _credentials
|
||||
private val _credentials = MutableLiveData<List<CredentialWithCode>?>()
|
||||
val credentials: LiveData<List<CredentialWithCode>?> = _credentials
|
||||
|
||||
fun updateCredentials(credentials: Map<Model.Credential, Model.Code?>): List<Model.CredentialWithCode> {
|
||||
fun updateCredentials(credentials: Map<Credential, Code?>): List<CredentialWithCode> {
|
||||
val existing = _credentials.value?.associate { it.credential to it.code } ?: mapOf()
|
||||
|
||||
val updated = credentials.map {
|
||||
Model.CredentialWithCode(it.key, it.value ?: existing[it.key])
|
||||
CredentialWithCode(it.key, it.value ?: existing[it.key])
|
||||
}
|
||||
|
||||
_credentials.postValue(updated)
|
||||
@ -47,36 +51,36 @@ class OathViewModel: ViewModel() {
|
||||
return updated
|
||||
}
|
||||
|
||||
fun addCredential(credential: Model.Credential, code: Model.Code?): Model.CredentialWithCode {
|
||||
fun addCredential(credential: Credential, code: Code?): CredentialWithCode {
|
||||
require(credential.deviceId == _sessionState.value?.deviceId) {
|
||||
"Cannot add credential for different deviceId"
|
||||
}
|
||||
return Model.CredentialWithCode(credential, code).also {
|
||||
return CredentialWithCode(credential, code).also {
|
||||
_credentials.postValue(_credentials.value?.plus(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCredential(
|
||||
oldCredential: Model.Credential,
|
||||
newCredential: Model.Credential
|
||||
oldCredential: Credential,
|
||||
newCredential: Credential
|
||||
) {
|
||||
val existing = _credentials.value!!
|
||||
val entry = existing.find { it.credential == oldCredential }!!
|
||||
require(entry.credential.deviceId == newCredential.deviceId) {
|
||||
"Cannot rename credential for different deviceId"
|
||||
}
|
||||
_credentials.postValue(existing.minus(entry).plus(Model.CredentialWithCode(newCredential, entry.code)))
|
||||
_credentials.postValue(existing.minus(entry).plus(CredentialWithCode(newCredential, entry.code)))
|
||||
}
|
||||
|
||||
fun removeCredential(credential: Model.Credential) {
|
||||
fun removeCredential(credential: Credential) {
|
||||
val existing = _credentials.value!!
|
||||
val entry = existing.find { it.credential == credential }!!
|
||||
_credentials.postValue(existing.minus(entry))
|
||||
}
|
||||
|
||||
fun updateCode(credential: Model.Credential, code: Model.Code?) {
|
||||
fun updateCode(credential: Credential, code: Code?) {
|
||||
val existing = _credentials.value!!
|
||||
val entry = existing.find { it.credential == credential }!!
|
||||
_credentials.postValue(existing.minus(entry).plus(Model.CredentialWithCode(credential, code)))
|
||||
_credentials.postValue(existing.minus(entry).plus(CredentialWithCode(credential, code)))
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias YubiKitCode = com.yubico.yubikit.oath.Code
|
||||
|
||||
@Serializable
|
||||
data class Code(
|
||||
val value: String? = null,
|
||||
@SerialName("valid_from")
|
||||
@Suppress("unused")
|
||||
val validFrom: Long,
|
||||
@SerialName("valid_to")
|
||||
@Suppress("unused")
|
||||
val validTo: Long
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun from(code: YubiKitCode?): Code? =
|
||||
code?.let {
|
||||
Code(
|
||||
it.value,
|
||||
it.validFrom / 1000,
|
||||
it.validUntil / 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath.data
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
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
|
||||
|
||||
|
||||
@Serializable(with = OathTypeSerializer::class)
|
||||
enum class CodeType(val value: Byte) {
|
||||
TOTP(0x20),
|
||||
HOTP(0x10);
|
||||
}
|
||||
|
||||
object OathTypeSerializer : KSerializer<CodeType> {
|
||||
override fun deserialize(decoder: Decoder): CodeType =
|
||||
when (decoder.decodeByte()) {
|
||||
CodeType.HOTP.value -> CodeType.HOTP
|
||||
CodeType.TOTP.value -> CodeType.TOTP
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("OathType", PrimitiveKind.BYTE)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: CodeType) {
|
||||
encoder.encodeByte(value = value.value)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath.data
|
||||
|
||||
import com.yubico.authenticator.asString
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias YubiKitCredential = com.yubico.yubikit.oath.Credential
|
||||
typealias YubiKitOathType = com.yubico.yubikit.oath.OathType
|
||||
|
||||
@Serializable
|
||||
data class Credential(
|
||||
@SerialName("device_id")
|
||||
val deviceId: String,
|
||||
val id: String,
|
||||
@SerialName("oath_type")
|
||||
val codeType: CodeType,
|
||||
val period: Int,
|
||||
val issuer: String? = null,
|
||||
@SerialName("name")
|
||||
val accountName: String,
|
||||
@SerialName("touch_required")
|
||||
val touchRequired: Boolean
|
||||
) {
|
||||
|
||||
constructor(credential: YubiKitCredential, deviceId: String) : this(
|
||||
deviceId = deviceId,
|
||||
id = credential.id.asString(),
|
||||
codeType = when (credential.oathType) {
|
||||
YubiKitOathType.HOTP -> CodeType.HOTP
|
||||
else -> CodeType.TOTP
|
||||
},
|
||||
period = credential.period,
|
||||
issuer = credential.issuer,
|
||||
accountName = credential.accountName,
|
||||
touchRequired = credential.isTouchRequired
|
||||
)
|
||||
|
||||
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
|
||||
data class CredentialWithCode(
|
||||
val credential: Credential,
|
||||
val code: Code?
|
||||
)
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath.data
|
||||
|
||||
import com.yubico.authenticator.device.Version
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias YubiKitOathSession = com.yubico.yubikit.oath.OathSession
|
||||
|
||||
@Serializable
|
||||
data class Session(
|
||||
@SerialName("device_id")
|
||||
val deviceId: String,
|
||||
@SerialName("version")
|
||||
val version: Version,
|
||||
@SerialName("has_key")
|
||||
val isAccessKeySet: Boolean,
|
||||
@SerialName("remembered")
|
||||
val isRemembered: Boolean,
|
||||
@SerialName("locked")
|
||||
val isLocked: Boolean
|
||||
) {
|
||||
@SerialName("keystore")
|
||||
@Suppress("unused")
|
||||
val keystoreState: String = "unknown"
|
||||
|
||||
constructor(oathSession: YubiKitOathSession, isRemembered: Boolean)
|
||||
: this(
|
||||
oathSession.deviceId,
|
||||
Version(
|
||||
oathSession.version.major,
|
||||
oathSession.version.minor,
|
||||
oathSession.version.micro
|
||||
),
|
||||
oathSession.isAccessKeySet,
|
||||
isRemembered,
|
||||
oathSession.isLocked
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,20 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.oath
|
||||
package com.yubico.authenticator.oath.data
|
||||
|
||||
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 java.nio.ByteBuffer
|
||||
import kotlin.experimental.and
|
||||
|
||||
/**
|
||||
* Returns true if this credential is considered to be Steam credential
|
||||
*/
|
||||
fun Credential.isSteamCredential(): Boolean =
|
||||
issuer == "Steam" && oathType == OathType.TOTP
|
||||
fun YubiKitCredential.isSteamCredential(): Boolean =
|
||||
issuer == "Steam" && oathType == YubiKitOathType.TOTP
|
||||
|
||||
/**
|
||||
* @return Code with value formatted for use with Steam
|
||||
@ -35,10 +31,10 @@ fun Credential.isSteamCredential(): Boolean =
|
||||
* @param timestamp the timestamp which is used for TOTP calculation
|
||||
* @throws IllegalArgumentException in case when the credential is not a Steam credential
|
||||
*/
|
||||
fun OathSession.calculateSteamCode(
|
||||
credential: Credential,
|
||||
fun YubiKitOathSession.calculateSteamCode(
|
||||
credential: YubiKitCredential,
|
||||
timestamp: Long,
|
||||
): Code {
|
||||
): YubiKitCode {
|
||||
val timeSlotMs = 30_000
|
||||
require(credential.isSteamCredential()) {
|
||||
"This is not steam credential"
|
||||
@ -46,7 +42,7 @@ fun OathSession.calculateSteamCode(
|
||||
|
||||
val currentTimeSlot = timestamp / timeSlotMs
|
||||
|
||||
return Code(
|
||||
return YubiKitCode(
|
||||
calculateResponse(credential.id, currentTimeSlot.toByteArray()).formatAsSteam(),
|
||||
currentTimeSlot * timeSlotMs,
|
||||
(currentTimeSlot + 1) * timeSlotMs
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,6 +22,9 @@ 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 com.yubico.authenticator.oath.data.Code
|
||||
import com.yubico.authenticator.oath.data.CodeType
|
||||
import com.yubico.authenticator.oath.data.Session
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
|
||||
@ -35,19 +38,21 @@ class ModelTest {
|
||||
private val viewModel = OathViewModel()
|
||||
|
||||
private fun connectDevice(deviceId: String) {
|
||||
viewModel.setSessionState(Model.Session(
|
||||
viewModel.setSessionState(
|
||||
Session(
|
||||
deviceId,
|
||||
Version(1, 2, 3),
|
||||
isAccessKeySet = false,
|
||||
isRemembered = false,
|
||||
isLocked = false
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses RFC 6238 values`() {
|
||||
assertEquals(0x10.toByte(), Model.OathType.HOTP.value)
|
||||
assertEquals(0x20.toByte(), Model.OathType.TOTP.value)
|
||||
assertEquals(0x10.toByte(), CodeType.HOTP.value)
|
||||
assertEquals(0x20.toByte(), CodeType.TOTP.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -178,7 +183,7 @@ class ModelTest {
|
||||
fun `update without code preserves existing value`() {
|
||||
val d = "device"
|
||||
val totp = totp(d, name = "totpCred")
|
||||
val totpCode: Model.Code? = null
|
||||
val totpCode: Code? = null
|
||||
|
||||
val hotp = hotp(d, name = "hotpCred")
|
||||
val hotpCode = code(value = "098765")
|
||||
@ -204,7 +209,7 @@ class ModelTest {
|
||||
fun `update preserves interactive totp credentials`() {
|
||||
val d = "device"
|
||||
val totp = totp(d, name = "totpCred", touchRequired = true)
|
||||
val totpCode: Model.Code? = null
|
||||
val totpCode: Code? = null
|
||||
|
||||
connectDevice(d)
|
||||
viewModel.updateCredentials(mapOf(totp to totpCode))
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,6 +16,10 @@
|
||||
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import com.yubico.authenticator.oath.data.Code
|
||||
import com.yubico.authenticator.oath.data.CodeType
|
||||
import com.yubico.authenticator.oath.data.Credential
|
||||
|
||||
object OathTestHelper {
|
||||
|
||||
// create a TOTP credential with default or custom parameters
|
||||
@ -27,7 +31,7 @@ object OathTestHelper {
|
||||
issuer: String? = nextIssuer(),
|
||||
touchRequired: Boolean = false,
|
||||
period: Int = 30
|
||||
) = cred(deviceId, name, issuer, Model.OathType.TOTP, touchRequired, period)
|
||||
) = cred(deviceId, name, issuer, CodeType.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
|
||||
@ -38,20 +42,20 @@ object OathTestHelper {
|
||||
issuer: String = nextIssuer(),
|
||||
touchRequired: Boolean = false,
|
||||
period: Int = 30
|
||||
) = cred(deviceId, name, issuer, Model.OathType.HOTP, touchRequired, period)
|
||||
) = cred(deviceId, name, issuer, CodeType.HOTP, touchRequired, period)
|
||||
|
||||
private fun cred(
|
||||
deviceId: String = nextDevice(),
|
||||
name: String = nextName(),
|
||||
issuer: String? = nextIssuer(),
|
||||
type: Model.OathType,
|
||||
type: CodeType,
|
||||
touchRequired: Boolean = false,
|
||||
period: Int = 30
|
||||
) =
|
||||
Model.Credential(
|
||||
Credential(
|
||||
deviceId = deviceId,
|
||||
id = """otpauth://${type.name}/${name}?secret=aabbaabbaabbaabb&issuer=${issuer}""",
|
||||
oathType = type,
|
||||
codeType = type,
|
||||
period = period,
|
||||
issuer = issuer,
|
||||
accountName = name,
|
||||
@ -62,9 +66,9 @@ object OathTestHelper {
|
||||
value: String = "111111",
|
||||
from: Long = 1000,
|
||||
to: Long = 2000
|
||||
) = Model.Code(value, from, to)
|
||||
) = Code(value, from, to)
|
||||
|
||||
fun emptyCredentials() = emptyMap<Model.Credential, Model.Code>()
|
||||
fun emptyCredentials() = emptyMap<Credential, Code>()
|
||||
|
||||
private var nameCounter = 0
|
||||
private fun nextName(): String {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,13 +21,21 @@ import com.yubico.authenticator.jsonSerializer
|
||||
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.*
|
||||
import org.junit.Assert.*
|
||||
import com.yubico.authenticator.oath.data.CredentialWithCode
|
||||
import com.yubico.authenticator.oath.data.Session
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class SerializationTest {
|
||||
|
||||
private val session = Model.Session(
|
||||
private val session = Session(
|
||||
"device",
|
||||
Version(1, 2, 3),
|
||||
isAccessKeySet = false,
|
||||
@ -49,7 +57,7 @@ class SerializationTest {
|
||||
|
||||
@Test
|
||||
fun `session json property names`() {
|
||||
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(session) as JsonObject
|
||||
val jsonObject: JsonObject = jsonSerializer.encodeToJsonElement(session) as JsonObject
|
||||
assertTrue(jsonObject.containsKey("device_id"))
|
||||
assertTrue(jsonObject.containsKey("has_key"))
|
||||
assertTrue(jsonObject.containsKey("remembered"))
|
||||
@ -59,7 +67,7 @@ class SerializationTest {
|
||||
|
||||
@Test
|
||||
fun `keystore is unknown`() {
|
||||
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(session) as JsonObject
|
||||
val jsonObject: JsonObject = jsonSerializer.encodeToJsonElement(session) as JsonObject
|
||||
assertEquals("unknown", jsonObject["keystore"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@ -75,7 +83,7 @@ class SerializationTest {
|
||||
fun `credential json property names`() {
|
||||
val c = totp()
|
||||
|
||||
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||
val jsonObject: JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||
|
||||
assertTrue(jsonObject.containsKey("device_id"))
|
||||
assertTrue(jsonObject.containsKey("id"))
|
||||
@ -98,7 +106,7 @@ class SerializationTest {
|
||||
fun `code json property names`() {
|
||||
val c = code()
|
||||
|
||||
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||
val jsonObject: JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||
|
||||
assertTrue(jsonObject.containsKey("value"))
|
||||
assertTrue(jsonObject.containsKey("valid_from"))
|
||||
@ -109,7 +117,7 @@ class SerializationTest {
|
||||
fun `code json content`() {
|
||||
val c = code(value = "001122", from = 1000, to = 2000)
|
||||
|
||||
val jsonObject : JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||
val jsonObject: JsonObject = jsonSerializer.encodeToJsonElement(c) as JsonObject
|
||||
|
||||
assertEquals(JsonPrimitive(1000), jsonObject["valid_from"])
|
||||
assertEquals(JsonPrimitive(2000), jsonObject["valid_to"])
|
||||
@ -119,7 +127,7 @@ class SerializationTest {
|
||||
@Test
|
||||
fun `credentials json type`() {
|
||||
val l = listOf(
|
||||
Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
|
||||
CredentialWithCode(totp(), code()), CredentialWithCode(hotp(), code()),
|
||||
)
|
||||
|
||||
val jsonElement = jsonSerializer.encodeToJsonElement(l)
|
||||
@ -128,12 +136,12 @@ class SerializationTest {
|
||||
|
||||
@Test
|
||||
fun `credentials json size`() {
|
||||
val l1 = listOf<Model.CredentialWithCode>()
|
||||
val l1 = listOf<CredentialWithCode>()
|
||||
val jsonElement1 = jsonSerializer.encodeToJsonElement(l1) as JsonArray
|
||||
assertEquals(0, jsonElement1.size)
|
||||
|
||||
val l2 = listOf(
|
||||
Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
|
||||
CredentialWithCode(totp(), code()), CredentialWithCode(hotp(), code()),
|
||||
)
|
||||
val jsonElement2 = jsonSerializer.encodeToJsonElement(l2) as JsonArray
|
||||
assertEquals(2, jsonElement2.size)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,9 +16,11 @@
|
||||
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import com.yubico.yubikit.oath.Credential
|
||||
import com.yubico.yubikit.oath.OathSession
|
||||
import com.yubico.yubikit.oath.OathType
|
||||
import com.yubico.authenticator.oath.data.YubiKitCredential
|
||||
import com.yubico.authenticator.oath.data.YubiKitOathSession
|
||||
import com.yubico.authenticator.oath.data.YubiKitOathType
|
||||
import com.yubico.authenticator.oath.data.calculateSteamCode
|
||||
import com.yubico.authenticator.oath.data.isSteamCredential
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.*
|
||||
@ -27,26 +29,26 @@ class SteamCredentialTest {
|
||||
|
||||
@Test
|
||||
fun `recognize Steam credential`() {
|
||||
val c = mock(Credential::class.java)
|
||||
`when`(c.oathType).thenReturn(OathType.TOTP)
|
||||
val c = mock(YubiKitCredential::class.java)
|
||||
`when`(c.oathType).thenReturn(YubiKitOathType.TOTP)
|
||||
`when`(c.issuer).thenReturn("Steam")
|
||||
Assert.assertTrue(c.isSteamCredential())
|
||||
|
||||
`when`(c.oathType).thenReturn(OathType.HOTP)
|
||||
`when`(c.oathType).thenReturn(YubiKitOathType.HOTP)
|
||||
`when`(c.issuer).thenReturn("Steam")
|
||||
Assert.assertFalse(c.isSteamCredential())
|
||||
|
||||
`when`(c.oathType).thenReturn(OathType.TOTP)
|
||||
`when`(c.oathType).thenReturn(YubiKitOathType.TOTP)
|
||||
`when`(c.issuer).thenReturn(null)
|
||||
Assert.assertFalse(c.isSteamCredential())
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `throw for non-Steam credential`() {
|
||||
val s = mock(OathSession::class.java)
|
||||
val s = mock(YubiKitOathSession::class.java)
|
||||
|
||||
val c = mock(Credential::class.java)
|
||||
`when`(c.oathType).thenReturn(OathType.HOTP)
|
||||
val c = mock(YubiKitCredential::class.java)
|
||||
`when`(c.oathType).thenReturn(YubiKitOathType.HOTP)
|
||||
`when`(c.issuer).thenReturn("Steam")
|
||||
|
||||
s.calculateSteamCode(c, 0)
|
||||
@ -98,7 +100,7 @@ class SteamCredentialTest {
|
||||
|
||||
// OathSession always calculating specific response
|
||||
private fun sessionWith(response: String) =
|
||||
mock(OathSession::class.java).also {
|
||||
mock(YubiKitOathSession::class.java).also {
|
||||
`when`(
|
||||
it.calculateResponse(
|
||||
isA(ByteArray::class.java),
|
||||
@ -108,8 +110,8 @@ class SteamCredentialTest {
|
||||
}
|
||||
|
||||
// valid Steam Credential mock
|
||||
private fun steamCredential() = mock(Credential::class.java).also {
|
||||
`when`(it.oathType).thenReturn(OathType.TOTP)
|
||||
private fun steamCredential() = mock(YubiKitCredential::class.java).also {
|
||||
`when`(it.oathType).thenReturn(YubiKitOathType.TOTP)
|
||||
`when`(it.issuer).thenReturn("Steam")
|
||||
`when`(it.id).thenReturn("id".toByteArray())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user