This commit is contained in:
Adam Velebil 2023-01-26 17:49:13 +01:00
commit 21085b5637
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
11 changed files with 304 additions and 207 deletions

View File

@ -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,7 @@
package com.yubico.authenticator
import android.annotation.TargetApi
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
@ -34,12 +35,8 @@ object ClipboardUtil {
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(toClipboard, toClipboard)
clipData.apply {
if (SdkVersion.ge(Build.VERSION_CODES.TIRAMISU)) {
description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, isSensitive)
}
}
compatUtil.from(Build.VERSION_CODES.TIRAMISU) {
updateExtrasTiramisu(clipData, isSensitive)
}
clipboardManager.setPrimaryClip(clipData)
@ -49,4 +46,10 @@ object ClipboardUtil {
}
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
private fun updateExtrasTiramisu(clipData: ClipData, isSensitive: Boolean) {
clipData.description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, isSensitive)
}
}
}

View File

@ -0,0 +1,107 @@
/*
* 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
import android.os.Build
/**
* Utility class for handling Android SDK compatibility in a testable way.
*
* Replaces runtime check with simple methods. The following code
* ```
* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { doFromM() } else { doUntilM() }
* ```
* can be rewritten as
* ```
* val compatUtil = CompatUtil(Build.VERSION.SDK_INT)
* compatUtil.from(Build.VERSION_CODES.M) {
* doFromM()
* }.otherwise {
* doUntilM()
* }
* ```
*
* @param sdkVersion the version this instance uses for compatibility checking. The release app
* uses `Build.VERSION.SDK_INT`, tests use appropriate other values.
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
class CompatUtil(private val sdkVersion: Int) {
/**
* Wrapper class holding values computed by [CompatUtil]
*/
class CompatValue<T> private constructor() {
companion object {
fun <T> of(value: T) = CompatValue(value)
fun <T> empty() = CompatValue<T>()
}
private var value: T? = null
private var hasValue: Boolean = false
private constructor(value: T) : this() {
this.value = value
hasValue = true
}
/**
* @return unwrapped value or result of [block]
*/
@Suppress("UNCHECKED_CAST")
fun otherwise(block: () -> T): T =
if (hasValue) {
if (value == null) {
null as T
} else {
value!!
}
} else {
block()
}
/**
* @return unwrapped value or [value]
*/
fun otherwise(value: T): T = otherwise { value }
}
/**
* Execute [block] only on devices running lower sdk version than [version]
*
* @return wrapped value
*/
fun <T> until(version: Int, block: () -> T): CompatValue<T> =
`when`({ sdkVersion < version }, block)
/**
* Execute [block] only on devices running higher or equal sdk version than [version]
*
* @return wrapped value
*/
fun <T> from(version: Int, block: () -> T): CompatValue<T> =
`when`({ sdkVersion >= version }, block)
/**
* Execute [block] only when predicate [p] holds
*
* @return wrapped value
*/
private fun <T> `when`(p: () -> Boolean, block: () -> T): CompatValue<T> = when {
p() -> CompatValue.of(block())
else -> CompatValue.empty()
}
}
val compatUtil = CompatUtil(Build.VERSION.SDK_INT)

View File

@ -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.
@ -58,7 +58,7 @@ class NdefActivity : Activity() {
val otpSlotContent = parseOtpFromIntent()
ClipboardUtil.setPrimaryClip(this, otpSlotContent.content, true)
if (SdkVersion.lt(Build.VERSION_CODES.TIRAMISU)) {
compatUtil.until(Build.VERSION_CODES.TIRAMISU) {
showToast(
when (otpSlotContent.type) {
OtpType.Otp -> R.string.otp_success_set_otp_to_clipboard

View File

@ -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,23 +16,42 @@
package com.yubico.authenticator
import android.annotation.TargetApi
import android.content.Intent
import android.os.Build
import android.os.Parcelable
inline fun <reified T : Parcelable> Intent.parcelableExtra(name: String): T? =
if (SdkVersion.ge(Build.VERSION_CODES.TIRAMISU)) {
getParcelableExtra(name, T::class.java)
} else {
compatUtil.from(Build.VERSION_CODES.TIRAMISU) {
CompatHelper.getParcelableExtraT(this, name, T::class.java)
}.otherwise(
@Suppress("deprecation") getParcelableExtra(name) as? T
}
)
inline fun <reified T : Parcelable> Intent.parcelableArrayExtra(name: String): Array<out T>? =
if (SdkVersion.ge(Build.VERSION_CODES.TIRAMISU)) {
getParcelableArrayExtra(name, T::class.java)
} else {
compatUtil.from(Build.VERSION_CODES.TIRAMISU) {
CompatHelper.getParcelableArrayExtraT(this, name, T::class.java)
}.otherwise(
@Suppress("deprecation")
getParcelableArrayExtra(name)
?.filterIsInstance<T>()
?.toTypedArray()
)
class CompatHelper {
companion object {
@TargetApi(Build.VERSION_CODES.TIRAMISU)
inline fun <reified T : Parcelable> getParcelableExtraT(
intent: Intent,
name: String,
clazz: Class<T>
) = intent.getParcelableExtra(name, clazz)
@TargetApi(Build.VERSION_CODES.TIRAMISU)
inline fun <reified T : Parcelable> getParcelableArrayExtraT(
intent: Intent,
name: String,
clazz: Class<T>
): Array<T>? = intent.getParcelableArrayExtra(name, clazz)
}
}

View File

@ -1,28 +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
import android.os.Build
object SdkVersion {
fun ge(other: Int): Boolean {
return Build.VERSION.SDK_INT >= other
}
fun lt(other: Int): Boolean {
return !ge(other)
}
}

View File

@ -16,6 +16,7 @@
package com.yubico.authenticator.oath
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import androidx.lifecycle.DefaultLifecycleObserver
@ -29,6 +30,7 @@ import com.yubico.authenticator.device.Info
import com.yubico.authenticator.device.Version
import com.yubico.authenticator.logging.Log
import com.yubico.authenticator.oath.keystore.ClearingMemProvider
import com.yubico.authenticator.oath.keystore.KeyProvider
import com.yubico.authenticator.oath.keystore.KeyStoreProvider
import com.yubico.authenticator.oath.keystore.SharedPrefProvider
import com.yubico.authenticator.yubikit.getDeviceInfo
@ -77,14 +79,17 @@ class OathManager(
private val memoryKeyProvider = ClearingMemProvider()
private val keyManager by lazy {
KeyManager(
if (SdkVersion.ge(Build.VERSION_CODES.M)) {
KeyStoreProvider()
} else {
compatUtil.from(Build.VERSION_CODES.M) {
createKeyStoreProviderM()
}.otherwise(
SharedPrefProvider(lifecycleOwner as Context)
}, memoryKeyProvider
), memoryKeyProvider
)
}
@TargetApi(Build.VERSION_CODES.M)
private fun createKeyStoreProviderM(): KeyProvider = KeyStoreProvider()
private var pendingAction: OathAction? = null
private var refreshJob: Job? = null
private var addToAny = false
@ -182,10 +187,12 @@ class OathManager(
args["password"] as String,
args["remember"] as Boolean
)
"setPassword" -> setPassword(
args["current"] as String?,
args["password"] as String
)
"unsetPassword" -> unsetPassword(args["current"] as String)
"forgetPassword" -> forgetPassword()
"calculate" -> calculate(args["credentialId"] as String)
@ -193,16 +200,19 @@ class OathManager(
args["uri"] as String,
args["requireTouch"] as Boolean
)
"renameAccount" -> renameAccount(
args["credentialId"] as String,
args["name"] as String,
args["issuer"] as String?
)
"deleteAccount" -> deleteAccount(args["credentialId"] as String)
"addAccountToAny" -> addAccountToAny(
args["uri"] as String,
args["requireTouch"] as Boolean
)
else -> throw NotImplementedError()
}
}

View File

@ -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,10 @@
package com.yubico.authenticator.oath.keystore
import android.annotation.TargetApi
import android.os.Build
import android.security.keystore.KeyProperties
import com.yubico.authenticator.SdkVersion
import com.yubico.authenticator.compatUtil
import com.yubico.yubikit.oath.AccessKey
interface KeyProvider {
@ -31,8 +32,9 @@ interface KeyProvider {
fun getAlias(deviceId: String) = "$deviceId,0"
val KEY_ALGORITHM_HMAC_SHA1 = if (SdkVersion.ge(Build.VERSION_CODES.M)) {
KeyProperties.KEY_ALGORITHM_HMAC_SHA1
} else {
"HmacSHA1"
}
val KEY_ALGORITHM_HMAC_SHA1 = compatUtil.from(Build.VERSION_CODES.M) {
getHmacSha1AlgorithmNameM()
}.otherwise("HmacSHA1")
@TargetApi(Build.VERSION_CODES.M)
fun getHmacSha1AlgorithmNameM() = KeyProperties.KEY_ALGORITHM_HMAC_SHA1

View File

@ -19,6 +19,7 @@ package com.yubico.authenticator.yubikit
import com.yubico.authenticator.device.Info
import com.yubico.authenticator.logging.Log
import com.yubico.authenticator.oath.OathManager
import com.yubico.authenticator.compatUtil
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
import com.yubico.yubikit.core.YubiKeyDevice
@ -43,7 +44,7 @@ suspend fun getDeviceInfo(device: YubiKeyDevice): Info {
device.withConnection<FidoConnection, DeviceInfo> { DeviceUtil.readInfo(it, pid) }
}.recoverCatching { t ->
Log.d(OathManager.TAG, "FIDO connection not available: ${t.message}")
return SkyHelper.getDeviceInfo(device)
return SkyHelper(compatUtil).getDeviceInfo(device)
}.getOrElse {
Log.e(OathManager.TAG, "Failed to recognize device: ${it.message}")
throw it

View File

@ -16,8 +16,9 @@
package com.yubico.authenticator.yubikit
import android.annotation.TargetApi
import android.os.Build
import com.yubico.authenticator.SdkVersion
import com.yubico.authenticator.CompatUtil
import com.yubico.authenticator.device.Capabilities
import com.yubico.authenticator.device.Config
import com.yubico.authenticator.device.Info
@ -29,7 +30,7 @@ import com.yubico.yubikit.management.DeviceInfo
import com.yubico.yubikit.management.FormFactor
import java.util.regex.Pattern
class SkyHelper {
class SkyHelper(private val compatUtil: CompatUtil) {
companion object {
private val VERSION_0 = Version(0, 0, 0)
private val VERSION_3 = Version(3, 0, 0)
@ -38,87 +39,95 @@ class SkyHelper {
private val USB_VERSION_STRING_PATTERN: Pattern =
Pattern.compile("\\b(\\d{1,3})\\.(\\d)(\\d+)\\b")
/**
* Retrieves a [DeviceInfo] from USB Security YubiKey (SKY).
*
* Should be only used as last resort when all other DeviceInfo queries failed because
* the returned information might not be accurate.
*
* @param device YubiKeyDevice to get DeviceInfo for. Should be USB and SKY device
* @return [DeviceInfo] instance initialized with information from USB descriptors.
* @throws IllegalArgumentException if [device] is not instance of [UsbYubiKeyDevice] or
* if the USB device has wrong PID
*/
fun getDeviceInfo(device: YubiKeyDevice): Info {
require(device is UsbYubiKeyDevice)
val pid = device.pid
require(pid in listOf(UsbPid.YK4_FIDO, UsbPid.SKY_FIDO, UsbPid.NEO_FIDO))
val usbVersion = validateVersionForPid(getVersionFromUsbDescriptor(device), pid)
// build DeviceInfo containing only USB product name and USB version
// we assume this is a Security Key based on the USB PID
return Info(
config = Config(null, null, null, Capabilities(usb = 0)),
serialNumber = null,
version = com.yubico.authenticator.device.Version(usbVersion),
formFactor = FormFactor.UNKNOWN.value,
isLocked = false,
isSky = true,
isFips = false,
name = (device.usbDevice.productName ?: "Yubico Security Key"),
isNfc = false,
usbPid = pid.value,
supportedCapabilities = Capabilities(usb = 0)
)
}
// try to convert USB version to YubiKey version
private fun getVersionFromUsbDescriptor(device: UsbYubiKeyDevice): Version {
if (SdkVersion.ge(Build.VERSION_CODES.M)) {
val version = device.usbDevice.version
val match = USB_VERSION_STRING_PATTERN.matcher(version)
if (match.find()) {
val major = match.group(1)?.toByte() ?: 0
val minor = match.group(2)?.toByte() ?: 0
val patch = match.group(3)?.toByte() ?: 0
return Version(major, minor, patch)
}
}
return VERSION_0
}
/**
* Check whether usbVersion is in expected range defined by UsbPid
*
* @return original version or [Version(0,0,0)] indicating invalid/unknown version
*/
private fun validateVersionForPid(usbVersion: Version, pid: UsbPid): Version {
if ((pid == UsbPid.NEO_FIDO && usbVersion.inRange(VERSION_3, VERSION_4)) ||
(pid == UsbPid.SKY_FIDO && usbVersion.isAtLeast(VERSION_3)) ||
(pid == UsbPid.YK4_FIDO && usbVersion.isAtLeast(VERSION_4))
) {
return usbVersion
}
return VERSION_0
}
/** Check if this version is at least v1 and less than v2
* @return true if this is in range [v1,v2)
*/
private fun Version.inRange(v1: Version, v2: Version): Boolean {
return this >= v1 && this < v2
}
/** Check if this version is at least v
* @return true if this >= v
*/
private fun Version.isAtLeast(v: Version): Boolean {
return this >= v
}
}
/**
* Retrieves a [DeviceInfo] from USB Security YubiKey (SKY).
*
* Should be only used as last resort when all other DeviceInfo queries failed because
* the returned information might not be accurate.
*
* @param device YubiKeyDevice to get DeviceInfo for. Should be USB and SKY device
* @return [DeviceInfo] instance initialized with information from USB descriptors.
* @throws IllegalArgumentException if [device] is not instance of [UsbYubiKeyDevice] or
* if the USB device has wrong PID
*/
fun getDeviceInfo(device: YubiKeyDevice): Info {
require(device is UsbYubiKeyDevice)
val pid = device.pid
require(pid in listOf(UsbPid.YK4_FIDO, UsbPid.SKY_FIDO, UsbPid.NEO_FIDO))
val usbVersion = validateVersionForPid(getVersionFromUsbDescriptor(device), pid)
// build DeviceInfo containing only USB product name and USB version
// we assume this is a Security Key based on the USB PID
return Info(
config = Config(null, null, null, Capabilities(usb = 0)),
serialNumber = null,
version = com.yubico.authenticator.device.Version(usbVersion),
formFactor = FormFactor.UNKNOWN.value,
isLocked = false,
isSky = true,
isFips = false,
name = (device.usbDevice.productName ?: "Yubico Security Key"),
isNfc = false,
usbPid = pid.value,
supportedCapabilities = Capabilities(usb = 0)
)
}
// try to convert USB version to YubiKey version
private fun getVersionFromUsbDescriptor(device: UsbYubiKeyDevice): Version =
compatUtil.from(Build.VERSION_CODES.M) {
getVersionFromUsbDescriptorM(device)
}.otherwise(
VERSION_0
)
@TargetApi(Build.VERSION_CODES.M)
private fun getVersionFromUsbDescriptorM(device: UsbYubiKeyDevice): Version {
val version = device.usbDevice.version
val match = USB_VERSION_STRING_PATTERN.matcher(version)
if (match.find()) {
val major = match.group(1)?.toByte() ?: 0
val minor = match.group(2)?.toByte() ?: 0
val patch = match.group(3)?.toByte() ?: 0
return Version(major, minor, patch)
}
return VERSION_0
}
/**
* Check whether usbVersion is in expected range defined by UsbPid
*
* @return original version or [Version(0,0,0)] indicating invalid/unknown version
*/
private fun validateVersionForPid(usbVersion: Version, pid: UsbPid): Version {
if ((pid == UsbPid.NEO_FIDO && usbVersion.inRange(VERSION_3, VERSION_4)) ||
(pid == UsbPid.SKY_FIDO && usbVersion.isAtLeast(VERSION_3)) ||
(pid == UsbPid.YK4_FIDO && usbVersion.isAtLeast(VERSION_4))
) {
return usbVersion
}
return VERSION_0
}
/** Check if this version is at least v1 and less than v2
* @return true if this is in range [v1,v2)
*/
private fun Version.inRange(v1: Version, v2: Version): Boolean {
return this >= v1 && this < v2
}
/** Check if this version is at least v
* @return true if this >= v
*/
private fun Version.isAtLeast(v: Version): Boolean {
return this >= v
}
}

View File

@ -1,33 +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
import android.os.Build
import java.lang.reflect.Field
import java.lang.reflect.Modifier
object TestUtil {
fun mockSdkInt(sdkInt: Int) {
val versionField = Build.VERSION::class.java.getField("SDK_INT")
versionField.isAccessible = true
Field::class.java.getDeclaredField("modifiers").apply {
isAccessible = true
setInt(versionField, versionField.modifiers and Modifier.FINAL.inv())
}
versionField.set(null, sdkInt)
}
}

View File

@ -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.
@ -17,8 +17,7 @@
package com.yubico.authenticator.yubikit
import android.hardware.usb.UsbDevice
import com.yubico.authenticator.SdkVersion
import com.yubico.authenticator.TestUtil
import com.yubico.authenticator.CompatUtil
import com.yubico.authenticator.device.Version
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
@ -32,13 +31,17 @@ class SkyHelperTest {
@Test
fun `passing NfcYubiKeyDevice will throw`() {
val skyHelper = SkyHelper(CompatUtil(33))
assertThrows(IllegalArgumentException::class.java) {
SkyHelper.getDeviceInfo(mock(NfcYubiKeyDevice::class.java))
skyHelper.getDeviceInfo(mock(NfcYubiKeyDevice::class.java))
}
}
@Test
fun `supports three specific UsbPids`() {
val skyHelper = SkyHelper(CompatUtil(33))
for (pid in UsbPid.values()) {
val ykDevice = getUsbYubiKeyDeviceMock().also {
`when`(it.pid).thenReturn(pid)
@ -46,11 +49,11 @@ class SkyHelperTest {
if (pid in listOf(UsbPid.YK4_FIDO, UsbPid.SKY_FIDO, UsbPid.NEO_FIDO)) {
// these will not throw
assertNotNull(SkyHelper.getDeviceInfo(ykDevice))
assertNotNull(skyHelper.getDeviceInfo(ykDevice))
} else {
// all other will throw
assertThrows(IllegalArgumentException::class.java) {
SkyHelper.getDeviceInfo(ykDevice)
skyHelper.getDeviceInfo(ykDevice)
}
}
}
@ -59,36 +62,36 @@ class SkyHelperTest {
@Test
fun `handles NEO_FIDO versions`() {
TestUtil.mockSdkInt(23)
val skyHelper = SkyHelper(CompatUtil(23))
val ykDevice = getUsbYubiKeyDeviceMock().also {
`when`(it.pid).thenReturn(UsbPid.NEO_FIDO)
}
`when`(ykDevice.usbDevice.version).thenReturn("3.00")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(3, 0, 0), it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("3.47")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(3, 4, 7), it.version)
}
// lower than 3 should return 0.0.0
`when`(ykDevice.usbDevice.version).thenReturn("2.10")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
// greater or equal 4.0.0 should return 0.0.0
`when`(ykDevice.usbDevice.version).thenReturn("4.00")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.37")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
}
@ -96,35 +99,35 @@ class SkyHelperTest {
@Test
fun `handles SKY_FIDO versions`() {
TestUtil.mockSdkInt(23)
val skyHelper = SkyHelper(CompatUtil(23))
val ykDevice = getUsbYubiKeyDeviceMock().also {
`when`(it.pid).thenReturn(UsbPid.SKY_FIDO)
}
`when`(ykDevice.usbDevice.version).thenReturn("3.00")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(3, 0, 0), it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("3.47")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(3, 4, 7), it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.00")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(4, 0, 0), it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.37")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(4, 3, 7), it.version)
}
// lower than 3 should return 0.0.0
`when`(ykDevice.usbDevice.version).thenReturn("2.10")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
@ -133,25 +136,25 @@ class SkyHelperTest {
@Test
fun `handles YK4_FIDO versions`() {
TestUtil.mockSdkInt(23)
val skyHelper = SkyHelper(CompatUtil(23))
val ykDevice = getUsbYubiKeyDeviceMock().also {
`when`(it.pid).thenReturn(UsbPid.YK4_FIDO)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.00")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(4, 0, 0), it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.37")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(Version(4, 3, 7), it.version)
}
// lower than 4 should return 0.0.0
`when`(ykDevice.usbDevice.version).thenReturn("3.47")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
}
@ -161,14 +164,14 @@ class SkyHelperTest {
// below API 23, there is no UsbDevice.version
// therefore we expect deviceInfo to have VERSION_0
// for every FIDO key
TestUtil.mockSdkInt(22)
val skyHelper = SkyHelper(CompatUtil(22))
val neoFidoDevice = getUsbYubiKeyDeviceMock().also {
`when`(it.pid).thenReturn(UsbPid.NEO_FIDO)
}
`when`(neoFidoDevice.usbDevice.version).thenReturn("3.47") // valid NEO_FIDO version
SkyHelper.getDeviceInfo(neoFidoDevice).also {
skyHelper.getDeviceInfo(neoFidoDevice).also {
assertEquals(VERSION_0, it.version)
}
@ -177,7 +180,7 @@ class SkyHelperTest {
}
`when`(skyFidoDevice.usbDevice.version).thenReturn("3.47") // valid SKY_FIDO version
SkyHelper.getDeviceInfo(skyFidoDevice).also {
skyHelper.getDeviceInfo(skyFidoDevice).also {
assertEquals(VERSION_0, it.version)
}
@ -186,43 +189,45 @@ class SkyHelperTest {
}
`when`(yk4FidoDevice.usbDevice.version).thenReturn("4.37") // valid YK4_FIDO version
SkyHelper.getDeviceInfo(yk4FidoDevice).also {
skyHelper.getDeviceInfo(yk4FidoDevice).also {
assertEquals(VERSION_0, it.version)
}
}
@Test
fun `returns VERSION_0 for invalid input`() {
val skyHelper = SkyHelper(CompatUtil(33))
val ykDevice = getUsbYubiKeyDeviceMock().also {
`when`(it.pid).thenReturn(UsbPid.SKY_FIDO)
}
`when`(ykDevice.usbDevice.version).thenReturn("")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("yubico")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.0")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
`when`(ykDevice.usbDevice.version).thenReturn("4.0.0")
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(VERSION_0, it.version)
}
@ -230,12 +235,14 @@ class SkyHelperTest {
@Test
fun `returns default product name`() {
val skyHelper = SkyHelper(CompatUtil(33))
val ykDevice = getUsbYubiKeyDeviceMock()
`when`(ykDevice.pid).thenReturn(UsbPid.SKY_FIDO)
`when`(ykDevice.usbDevice.version).thenReturn("5.50")
`when`(ykDevice.usbDevice.productName).thenReturn(null)
SkyHelper.getDeviceInfo(ykDevice).also {
skyHelper.getDeviceInfo(ykDevice).also {
assertEquals(it.name, "Yubico Security Key")
}
}