mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
support retries on NFC failure
This commit is contained in:
parent
9cbe8c02f8
commit
f98e34b5d0
@ -21,7 +21,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
@ -80,6 +79,7 @@ import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.concurrent.Executors
|
||||
import javax.crypto.Mac
|
||||
@ -318,10 +318,14 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
// If NFC and FIPS check for SCP11b key
|
||||
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
||||
logger.debug("Checking for usable SCP11b key...")
|
||||
deviceManager.scpKeyParams =
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_STARTED)
|
||||
}
|
||||
|
||||
val scpKeyParams : ScpKeyParams? = try {
|
||||
// If NFC and FIPS check for SCP11b key
|
||||
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
||||
logger.debug("Checking for usable SCP11b key...")
|
||||
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
|
||||
val scp = SecurityDomainSession(connection)
|
||||
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
|
||||
@ -335,15 +339,22 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
logger.debug("Found SCP11b key: {}", keyRef)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
logger.debug("Exception while getting scp keys: ", e)
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
// this YubiKey provides SCP11b key but the phone cannot perform AESCMAC
|
||||
if (deviceManager.scpKeyParams != null && !supportsScp11b) {
|
||||
if (scpKeyParams != null && !supportsScp11b) {
|
||||
deviceManager.setDeviceInfo(noScp11bNfcSupport)
|
||||
return
|
||||
}
|
||||
|
||||
deviceManager.setDeviceInfo(deviceInfo)
|
||||
deviceManager.setDeviceInfo(deviceInfo, scpKeyParams)
|
||||
val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo)
|
||||
logger.debug("Connected key supports: {}", supportedContexts)
|
||||
if (!supportedContexts.contains(viewModel.appContext.value)) {
|
||||
@ -362,9 +373,6 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
contextManager?.let {
|
||||
try {
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_STARTED)
|
||||
}
|
||||
it.processYubiKey(device)
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
|
||||
@ -372,10 +380,12 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
} catch (e: IOException) {
|
||||
logger.debug("Caught IOException during YubiKey processing: ", e)
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
logger.error("Error processing YubiKey in AppContextManager", e)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,12 +413,13 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
flutterLog = FlutterLog(messenger)
|
||||
appMethodChannel = AppMethodChannel(messenger)
|
||||
deviceManager = DeviceManager(this, viewModel,appMethodChannel)
|
||||
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
||||
deviceManager = DeviceManager(this, viewModel,appMethodChannel, dialogManager)
|
||||
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
||||
|
||||
appPreferences = AppPreferences(this)
|
||||
appLinkMethodChannel = AppLinkMethodChannel(messenger)
|
||||
managementHandler = ManagementHandler(messenger, deviceManager, dialogManager)
|
||||
managementHandler = ManagementHandler(messenger, deviceManager)
|
||||
|
||||
nfcActivityListener.appMethodChannel = appMethodChannel
|
||||
|
||||
@ -453,8 +464,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
deviceManager,
|
||||
oathViewModel,
|
||||
dialogManager,
|
||||
appPreferences,
|
||||
nfcActivityListener
|
||||
appPreferences
|
||||
)
|
||||
|
||||
OperationContext.FidoFingerprints,
|
||||
@ -462,9 +472,10 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
messenger,
|
||||
this,
|
||||
deviceManager,
|
||||
appMethodChannel,
|
||||
dialogManager,
|
||||
fidoViewModel,
|
||||
viewModel,
|
||||
dialogManager
|
||||
viewModel
|
||||
)
|
||||
|
||||
else -> null
|
||||
|
@ -20,6 +20,7 @@ import androidx.collection.ArraySet
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.MainActivity
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.OperationContext
|
||||
@ -28,7 +29,10 @@ import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
|
||||
import com.yubico.yubikit.management.Capability
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface DeviceListener {
|
||||
// a USB device is connected
|
||||
@ -44,7 +48,8 @@ interface DeviceListener {
|
||||
class DeviceManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val appViewModel: MainViewModel,
|
||||
private val appMethodChannel: MainActivity.AppMethodChannel
|
||||
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||
private val dialogManager: DialogManager
|
||||
) {
|
||||
var clearDeviceInfoOnDisconnect: Boolean = true
|
||||
|
||||
@ -168,9 +173,9 @@ class DeviceManager(
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
}
|
||||
|
||||
fun setDeviceInfo(deviceInfo: Info?) {
|
||||
fun setDeviceInfo(deviceInfo: Info?, scpKeyParams: ScpKeyParams? = null) {
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
scpKeyParams = null
|
||||
this.scpKeyParams = scpKeyParams
|
||||
}
|
||||
|
||||
fun isUsbKeyConnected(): Boolean {
|
||||
@ -183,18 +188,35 @@ class DeviceManager(
|
||||
}
|
||||
|
||||
suspend fun <T> withKey(
|
||||
onUsb: suspend (UsbYubiKeyDevice) -> T,
|
||||
onNfc: suspend () -> com.yubico.yubikit.core.util.Result<T, Throwable>,
|
||||
onUsb: suspend (UsbYubiKeyDevice) -> T
|
||||
onDialogCancelled: () -> Unit
|
||||
): T =
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
onUsb(it)
|
||||
} ?: try {
|
||||
onNfc().value.also {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
throw e
|
||||
} ?: onNfcWithRetries(onNfc, onDialogCancelled)
|
||||
|
||||
private suspend fun <T> onNfcWithRetries(
|
||||
onNfc: suspend () -> com.yubico.yubikit.core.util.Result<T, Throwable>,
|
||||
onDialogCancelled: () -> Unit) : T {
|
||||
|
||||
dialogManager.showDialog {
|
||||
logger.debug("Cancelled dialog")
|
||||
onDialogCancelled.invoke()
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return onNfc.invoke().value
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
}
|
||||
|
||||
logger.debug("NFC action failed, asking to try again")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
@ -27,10 +26,7 @@ import org.slf4j.LoggerFactory
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class FidoConnectionHelper(
|
||||
private val deviceManager: DeviceManager,
|
||||
private val dialogManager: DialogManager
|
||||
) {
|
||||
class FidoConnectionHelper(private val deviceManager: DeviceManager) {
|
||||
private var pendingAction: FidoAction? = null
|
||||
|
||||
fun invokePending(fidoSession: YubiKitFidoSession) {
|
||||
@ -47,10 +43,15 @@ class FidoConnectionHelper(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> useSession(action: (YubiKitFidoSession) -> T): T {
|
||||
suspend fun <T> useSession(block: (YubiKitFidoSession) -> T): T {
|
||||
return deviceManager.withKey(
|
||||
onNfc = { useSessionNfc(action) },
|
||||
onUsb = { useSessionUsb(it, action) })
|
||||
onUsb = { useSessionUsb(it, block) },
|
||||
onNfc = { useSessionNfc(block) },
|
||||
onDialogCancelled = {
|
||||
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||
pendingAction = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> useSessionUsb(
|
||||
@ -60,7 +61,9 @@ class FidoConnectionHelper(
|
||||
block(YubiKitFidoSession(it))
|
||||
}
|
||||
|
||||
suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): Result<T, Throwable> {
|
||||
suspend fun <T> useSessionNfc(
|
||||
block: (YubiKitFidoSession) -> T
|
||||
): Result<T, Throwable> {
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
pendingAction = {
|
||||
@ -68,19 +71,13 @@ class FidoConnectionHelper(
|
||||
block.invoke(it.value)
|
||||
})
|
||||
}
|
||||
dialogManager.showDialog {
|
||||
logger.debug("Cancelled dialog")
|
||||
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||
pendingAction = null
|
||||
}
|
||||
}
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
return Result.failure(cancelled)
|
||||
} catch (error: Throwable) {
|
||||
logger.error("Exception during action: ", error)
|
||||
return Result.failure(error)
|
||||
} finally {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ package com.yubico.authenticator.fido
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.yubico.authenticator.AppContextManager
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.MainActivity
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.asString
|
||||
@ -68,9 +69,10 @@ class FidoManager(
|
||||
messenger: BinaryMessenger,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||
private val dialogManager: DialogManager,
|
||||
private val fidoViewModel: FidoViewModel,
|
||||
mainViewModel: MainViewModel,
|
||||
dialogManager: DialogManager,
|
||||
mainViewModel: MainViewModel
|
||||
) : AppContextManager(), DeviceListener {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@ -97,7 +99,7 @@ class FidoManager(
|
||||
}
|
||||
}
|
||||
|
||||
private val connectionHelper = FidoConnectionHelper(deviceManager, dialogManager)
|
||||
private val connectionHelper = FidoConnectionHelper(deviceManager)
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
@ -114,6 +116,8 @@ class FidoManager(
|
||||
FidoResetHelper(
|
||||
lifecycleOwner,
|
||||
deviceManager,
|
||||
appMethodChannel,
|
||||
dialogManager,
|
||||
fidoViewModel,
|
||||
mainViewModel,
|
||||
connectionHelper,
|
||||
@ -194,7 +198,6 @@ class FidoManager(
|
||||
// Clear any cached FIDO state
|
||||
fidoViewModel.clearSessionState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processYubiKey(connection: YubiKeyConnection, device: YubiKeyDevice) {
|
||||
@ -578,7 +581,7 @@ class FidoManager(
|
||||
}
|
||||
else -> throw ctapException
|
||||
}
|
||||
} catch (io: IOException) {
|
||||
} catch (_: IOException) {
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
|
@ -18,11 +18,14 @@ package com.yubico.authenticator.fido
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.MainActivity
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.Session
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
import com.yubico.authenticator.yubikit.NfcActivityState
|
||||
import com.yubico.yubikit.core.application.CommandState
|
||||
import com.yubico.yubikit.core.fido.CtapException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -68,6 +71,8 @@ fun createCaptureErrorEvent(code: Int) : FidoRegisterFpCaptureErrorEvent {
|
||||
class FidoResetHelper(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||
private val dialogManager: DialogManager,
|
||||
private val fidoViewModel: FidoViewModel,
|
||||
private val mainViewModel: MainViewModel,
|
||||
private val connectionHelper: FidoConnectionHelper,
|
||||
@ -106,7 +111,7 @@ class FidoResetHelper(
|
||||
resetOverNfc()
|
||||
}
|
||||
logger.info("FIDO reset complete")
|
||||
} catch (e: CancellationException) {
|
||||
} catch (_: CancellationException) {
|
||||
logger.debug("FIDO reset cancelled")
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -209,15 +214,19 @@ class FidoResetHelper(
|
||||
|
||||
private suspend fun resetOverNfc() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch {
|
||||
dialogManager.showDialog {
|
||||
|
||||
}
|
||||
fidoViewModel.updateResetState(FidoResetState.Touch)
|
||||
try {
|
||||
connectionHelper.useSessionNfc { fidoSession ->
|
||||
doReset(fidoSession)
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}.value
|
||||
} catch (e: Throwable) {
|
||||
// on NFC, clean device info in this situation
|
||||
mainViewModel.setDeviceInfo(null)
|
||||
logger.error("Failure during FIDO reset:", e)
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,11 @@
|
||||
|
||||
package com.yubico.authenticator.management
|
||||
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@ -30,16 +28,19 @@ typealias YubiKitManagementSession = com.yubico.yubikit.management.ManagementSes
|
||||
typealias ManagementAction = (Result<YubiKitManagementSession, Exception>) -> Unit
|
||||
|
||||
class ManagementConnectionHelper(
|
||||
private val deviceManager: DeviceManager,
|
||||
private val dialogManager: DialogManager
|
||||
private val deviceManager: DeviceManager
|
||||
) {
|
||||
private var action: ManagementAction? = null
|
||||
|
||||
suspend fun <T> useSession(action: (YubiKitManagementSession) -> T): T {
|
||||
return deviceManager.withKey(
|
||||
onNfc = { useSessionNfc(action) },
|
||||
onUsb = { useSessionUsb(it, action) })
|
||||
}
|
||||
suspend fun <T> useSession(block: (YubiKitManagementSession) -> T): T =
|
||||
deviceManager.withKey(
|
||||
onUsb = { useSessionUsb(it, block) },
|
||||
onNfc = { useSessionNfc(block) },
|
||||
onDialogCancelled = {
|
||||
action?.invoke(Result.failure(CancellationException()))
|
||||
action = null
|
||||
},
|
||||
)
|
||||
|
||||
private suspend fun <T> useSessionUsb(
|
||||
device: UsbYubiKeyDevice,
|
||||
@ -48,7 +49,8 @@ class ManagementConnectionHelper(
|
||||
block(YubiKitManagementSession(it))
|
||||
}
|
||||
|
||||
private suspend fun <T> useSessionNfc(block: (YubiKitManagementSession) -> T): Result<T, Throwable> {
|
||||
private suspend fun <T> useSessionNfc(
|
||||
block: (YubiKitManagementSession) -> T): Result<T, Throwable> {
|
||||
try {
|
||||
val result = suspendCoroutine<T> { outer ->
|
||||
action = {
|
||||
@ -56,23 +58,12 @@ class ManagementConnectionHelper(
|
||||
block.invoke(it.value)
|
||||
})
|
||||
}
|
||||
dialogManager.showDialog {
|
||||
logger.debug("Cancelled Dialog")
|
||||
action?.invoke(Result.failure(CancellationException()))
|
||||
action = null
|
||||
}
|
||||
}
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
return Result.failure(cancelled)
|
||||
} catch (error: Throwable) {
|
||||
return Result.failure(error)
|
||||
} finally {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(ManagementConnectionHelper::class.java)
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
|
||||
package com.yubico.authenticator.management
|
||||
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.setHandler
|
||||
@ -29,14 +28,13 @@ import java.util.concurrent.Executors
|
||||
|
||||
class ManagementHandler(
|
||||
messenger: BinaryMessenger,
|
||||
deviceManager: DeviceManager,
|
||||
dialogManager: DialogManager
|
||||
deviceManager: DeviceManager
|
||||
) {
|
||||
private val channel = MethodChannel(messenger, "android.management.methods")
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private val connectionHelper = ManagementConnectionHelper(deviceManager, dialogManager)
|
||||
private val connectionHelper = ManagementConnectionHelper(deviceManager)
|
||||
|
||||
init {
|
||||
channel.setHandler(coroutineScope) { method, _ ->
|
||||
|
@ -42,8 +42,6 @@ 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.NfcActivityListener
|
||||
import com.yubico.authenticator.yubikit.NfcActivityState
|
||||
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
@ -58,7 +56,6 @@ import com.yubico.yubikit.core.smartcard.SmartCardProtocol
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.management.Capability
|
||||
import com.yubico.yubikit.oath.CredentialData
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.*
|
||||
@ -66,10 +63,10 @@ import kotlinx.serialization.encodeToString
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias OathAction = (Result<YubiKitOathSession, Exception>) -> Unit
|
||||
|
||||
@ -79,8 +76,7 @@ class OathManager(
|
||||
private val deviceManager: DeviceManager,
|
||||
private val oathViewModel: OathViewModel,
|
||||
private val dialogManager: DialogManager,
|
||||
private val appPreferences: AppPreferences,
|
||||
private val nfcActivityListener: NfcActivityListener
|
||||
private val appPreferences: AppPreferences
|
||||
) : AppContextManager(), DeviceListener {
|
||||
|
||||
companion object {
|
||||
@ -216,8 +212,6 @@ class OathManager(
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
var showProcessingTimerTask: TimerTask? = null
|
||||
|
||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||
try {
|
||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||
@ -227,8 +221,8 @@ class OathManager(
|
||||
// Either run a pending action, or just refresh codes
|
||||
if (pendingAction != null) {
|
||||
pendingAction?.let { action ->
|
||||
action.invoke(Result.success(session))
|
||||
pendingAction = null
|
||||
action.invoke(Result.success(session))
|
||||
}
|
||||
} else {
|
||||
// Refresh codes
|
||||
@ -268,7 +262,6 @@ class OathManager(
|
||||
} else {
|
||||
// Awaiting an action for a different device? Fail it and stop processing.
|
||||
action.invoke(Result.failure(IllegalStateException("Wrong deviceId")))
|
||||
showProcessingTimerTask?.cancel()
|
||||
return@withConnection
|
||||
}
|
||||
}
|
||||
@ -289,14 +282,12 @@ class OathManager(
|
||||
supportedCapabilities = oathCapabilities
|
||||
)
|
||||
)
|
||||
showProcessingTimerTask?.cancel()
|
||||
return@withConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showProcessingTimerTask?.cancel()
|
||||
logger.debug(
|
||||
"Successfully read Oath session info (and credentials if unlocked) from connected key"
|
||||
)
|
||||
@ -320,7 +311,7 @@ class OathManager(
|
||||
val credentialData: CredentialData =
|
||||
CredentialData.parseUri(URI.create(uri))
|
||||
addToAny = true
|
||||
return useOathSessionNfc { session ->
|
||||
return useSessionNfc { session ->
|
||||
// We need to check for duplicates here since we haven't yet read the credentials
|
||||
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
|
||||
throw IllegalArgumentException()
|
||||
@ -499,12 +490,6 @@ class OathManager(
|
||||
renamed
|
||||
)
|
||||
|
||||
// // simulate long taking op
|
||||
// val renamedCredential = credential
|
||||
// logger.debug("simulate error")
|
||||
// Thread.sleep(3000)
|
||||
// throw IOException("Test exception")
|
||||
|
||||
jsonSerializer.encodeToString(renamed)
|
||||
}
|
||||
|
||||
@ -527,7 +512,7 @@ class OathManager(
|
||||
|
||||
deviceManager.withKey { usbYubiKeyDevice ->
|
||||
try {
|
||||
useOathSessionUsb(usbYubiKeyDevice) { session ->
|
||||
useSessionUsb(usbYubiKeyDevice) { session ->
|
||||
try {
|
||||
oathViewModel.updateCredentials(calculateOathCodes(session))
|
||||
} catch (apduException: ApduException) {
|
||||
@ -653,7 +638,6 @@ class OathManager(
|
||||
return session
|
||||
}
|
||||
|
||||
|
||||
private fun calculateOathCodes(session: YubiKitOathSession): Map<Credential, Code?> {
|
||||
val isUsbKey = deviceManager.isUsbKeyConnected()
|
||||
var timestamp = System.currentTimeMillis()
|
||||
@ -693,19 +677,23 @@ class OathManager(
|
||||
private suspend fun <T> useOathSession(
|
||||
unlock: Boolean = true,
|
||||
updateDeviceInfo: Boolean = false,
|
||||
action: (YubiKitOathSession) -> T
|
||||
block: (YubiKitOathSession) -> T
|
||||
): T {
|
||||
// callers can decide whether the session should be unlocked first
|
||||
unlockOnConnect.set(unlock)
|
||||
// callers can request whether device info should be updated after session operation
|
||||
this@OathManager.updateDeviceInfo.set(updateDeviceInfo)
|
||||
return deviceManager.withKey(
|
||||
onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) },
|
||||
onNfc = { useOathSessionNfc(action) }
|
||||
onUsb = { useSessionUsb(it, updateDeviceInfo, block) },
|
||||
onNfc = { useSessionNfc(block) },
|
||||
onDialogCancelled = {
|
||||
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||
pendingAction = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSessionUsb(
|
||||
private suspend fun <T> useSessionUsb(
|
||||
device: UsbYubiKeyDevice,
|
||||
updateDeviceInfo: Boolean = false,
|
||||
block: (YubiKitOathSession) -> T
|
||||
@ -717,40 +705,27 @@ class OathManager(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSessionNfc(
|
||||
block: (YubiKitOathSession) -> T
|
||||
private suspend fun <T> useSessionNfc(
|
||||
block: (YubiKitOathSession) -> T,
|
||||
): Result<T, Throwable> {
|
||||
var firstShow = true
|
||||
while (true) { // loop until success or cancel
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
pendingAction = {
|
||||
outer.resumeWith(runCatching {
|
||||
val session = it.value // this can throw CancellationException
|
||||
nfcActivityListener.onChange(NfcActivityState.PROCESSING_STARTED)
|
||||
block.invoke(session)
|
||||
})
|
||||
}
|
||||
|
||||
if (firstShow) {
|
||||
dialogManager.showDialog {
|
||||
logger.debug("Cancelled dialog")
|
||||
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||
pendingAction = null
|
||||
}
|
||||
firstShow = false
|
||||
}
|
||||
// here the coroutine is suspended and waits till pendingAction is
|
||||
// invoked - the pending action result will resume this coroutine
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
pendingAction = {
|
||||
outer.resumeWith(runCatching {
|
||||
block.invoke(it.value)
|
||||
})
|
||||
}
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
return Result.failure(cancelled)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Exception during action: ", e)
|
||||
return Result.failure(e)
|
||||
|
||||
// here the coroutine is suspended and waits till pendingAction is
|
||||
// invoked - the pending action result will resume this coroutine
|
||||
}
|
||||
} // while
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
return Result.failure(cancelled)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Exception during action: ", e)
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected(device: YubiKeyDevice) {
|
||||
|
@ -23,9 +23,13 @@ import kotlin.coroutines.suspendCoroutine
|
||||
suspend inline fun <reified C : YubiKeyConnection, T> YubiKeyDevice.withConnection(
|
||||
crossinline block: (C) -> T
|
||||
): T = suspendCoroutine { continuation ->
|
||||
requestConnection(C::class.java) {
|
||||
continuation.resumeWith(runCatching {
|
||||
block(it.value)
|
||||
})
|
||||
try {
|
||||
requestConnection(C::class.java) {
|
||||
continuation.resumeWith(runCatching {
|
||||
block(it.value)
|
||||
})
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
@ -413,7 +413,7 @@ class _FidoMethodChannelNotifier extends MethodChannelNotifier {
|
||||
'callArgs': {'pin': pin},
|
||||
'operationSuccess': l10n.s_nfc_unlock_success,
|
||||
'operationFailure': l10n.s_nfc_unlock_failure,
|
||||
'showSuccess': true
|
||||
'showSuccess': false
|
||||
});
|
||||
|
||||
Future<dynamic> enableEnterpriseAttestation() async =>
|
||||
|
@ -16,19 +16,22 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../app/logging.dart';
|
||||
import '../app/message.dart';
|
||||
import '../app/state.dart';
|
||||
import 'state.dart';
|
||||
import 'views/nfc/models.dart';
|
||||
import 'views/nfc/nfc_activity_overlay.dart';
|
||||
import 'views/nfc/nfc_content_widget.dart';
|
||||
import 'views/nfc/nfc_failure_icon.dart';
|
||||
import 'views/nfc/nfc_progress_bar.dart';
|
||||
import 'views/nfc/nfc_success_icon.dart';
|
||||
|
||||
final _log = Logger('android.tap_request_dialog');
|
||||
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
|
||||
|
||||
final androidDialogProvider =
|
||||
@ -99,17 +102,19 @@ class _DialogProvider extends Notifier<int> {
|
||||
}
|
||||
break;
|
||||
case NfcActivity.processingInterrupted:
|
||||
explicitAction = false; // next action might not be explicit
|
||||
processingTimer?.cancel();
|
||||
viewNotifier.setDialogProperties(showCloseButton: true);
|
||||
notifier.sendCommand(updateNfcView(NfcContentWidget(
|
||||
title: properties.operationFailure,
|
||||
icon: const NfcIconProgressBar(false),
|
||||
subtitle: l10n.s_nfc_scan_again,
|
||||
icon: const NfcIconFailure(),
|
||||
)));
|
||||
break;
|
||||
case NfcActivity.notActive:
|
||||
debugPrint('Received not handled notActive');
|
||||
_log.debug('Received not handled notActive');
|
||||
break;
|
||||
case NfcActivity.ready:
|
||||
debugPrint('Received not handled ready');
|
||||
_log.debug('Received not handled ready');
|
||||
}
|
||||
});
|
||||
|
||||
@ -143,7 +148,6 @@ class _DialogProvider extends Notifier<int> {
|
||||
}
|
||||
|
||||
void cancelDialog() async {
|
||||
debugPrint('Cancelled dialog');
|
||||
explicitAction = false;
|
||||
await _channel.invokeMethod('cancel');
|
||||
}
|
||||
|
@ -16,11 +16,15 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../app/logging.dart';
|
||||
import '../../tap_request_dialog.dart';
|
||||
import 'models.dart';
|
||||
import 'nfc_activity_overlay.dart';
|
||||
|
||||
final _log = Logger('android.nfc_activity_command_listener');
|
||||
|
||||
final nfcEventCommandListener =
|
||||
Provider<_NfcEventCommandListener>((ref) => _NfcEventCommandListener(ref));
|
||||
|
||||
@ -34,8 +38,7 @@ class _NfcEventCommandListener {
|
||||
listener?.close();
|
||||
listener = _ref.listen(nfcEventCommandNotifier.select((c) => c.event),
|
||||
(previous, action) {
|
||||
debugPrint(
|
||||
'XXX Change in command for Overlay: $previous -> $action in context: $context');
|
||||
_log.debug('Change in command for Overlay: $previous -> $action');
|
||||
switch (action) {
|
||||
case (NfcShowViewEvent a):
|
||||
_show(context, a.child);
|
||||
|
@ -144,14 +144,14 @@ class NfcBottomSheet extends ConsumerWidget {
|
||||
Stack(fit: StackFit.passthrough, children: [
|
||||
if (showCloseButton)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Symbols.close, fill: 1, size: 24)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 40, 0, 0),
|
||||
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
|
||||
child: widget,
|
||||
)
|
||||
]),
|
||||
|
29
lib/android/views/nfc/nfc_failure_icon.dart
Normal file
29
lib/android/views/nfc/nfc_failure_icon.dart
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class NfcIconFailure extends StatelessWidget {
|
||||
const NfcIconFailure({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Icon(
|
||||
Symbols.close,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
}
|
@ -944,6 +944,7 @@
|
||||
"s_nfc_ready_to_scan": null,
|
||||
"s_nfc_accessing_yubikey": null,
|
||||
"s_nfc_scan_yubikey": null,
|
||||
"s_nfc_scan_again": null,
|
||||
|
||||
"c_nfc_unlock": null,
|
||||
"s_nfc_unlock_processing": null,
|
||||
|
@ -937,13 +937,14 @@
|
||||
}
|
||||
},
|
||||
|
||||
"l_nfc_read_key_failure": "Failed to read YubiKey, try again",
|
||||
"l_nfc_read_key_failure": "Failed to scan YubiKey",
|
||||
|
||||
"s_nfc_remove_key": "You can remove YubiKey",
|
||||
|
||||
"s_nfc_ready_to_scan": "Ready to scan",
|
||||
"s_nfc_accessing_yubikey": "Accessing YubiKey",
|
||||
"s_nfc_scan_yubikey": "Scan your YubiKey",
|
||||
"s_nfc_scan_again": "Scan again",
|
||||
|
||||
"c_nfc_unlock": "unlock",
|
||||
"s_nfc_unlock_processing": "Unlocking",
|
||||
|
@ -944,6 +944,7 @@
|
||||
"s_nfc_ready_to_scan": null,
|
||||
"s_nfc_accessing_yubikey": null,
|
||||
"s_nfc_scan_yubikey": null,
|
||||
"s_nfc_scan_again": null,
|
||||
|
||||
"c_nfc_unlock": null,
|
||||
"s_nfc_unlock_processing": null,
|
||||
|
@ -944,6 +944,7 @@
|
||||
"s_nfc_ready_to_scan": null,
|
||||
"s_nfc_accessing_yubikey": null,
|
||||
"s_nfc_scan_yubikey": null,
|
||||
"s_nfc_scan_again": null,
|
||||
|
||||
"c_nfc_unlock": null,
|
||||
"s_nfc_unlock_processing": null,
|
||||
|
@ -944,6 +944,7 @@
|
||||
"s_nfc_ready_to_scan": null,
|
||||
"s_nfc_accessing_yubikey": null,
|
||||
"s_nfc_scan_yubikey": null,
|
||||
"s_nfc_scan_again": null,
|
||||
|
||||
"c_nfc_unlock": null,
|
||||
"s_nfc_unlock_processing": null,
|
||||
|
@ -944,6 +944,7 @@
|
||||
"s_nfc_ready_to_scan": null,
|
||||
"s_nfc_accessing_yubikey": null,
|
||||
"s_nfc_scan_yubikey": null,
|
||||
"s_nfc_scan_again": null,
|
||||
|
||||
"c_nfc_unlock": null,
|
||||
"s_nfc_unlock_processing": null,
|
||||
|
@ -199,6 +199,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
focusNode: _issuerFocus,
|
||||
autofocus: true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_issuer = value.trim();
|
||||
|
Loading…
Reference in New Issue
Block a user