mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
Merge PR #930.
This commit is contained in:
commit
21085b5637
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user