support retries on NFC failure

This commit is contained in:
Adam Velebil 2024-09-04 13:34:00 +02:00
parent 9cbe8c02f8
commit f98e34b5d0
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
21 changed files with 204 additions and 151 deletions

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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()
}
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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, _ ->

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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 =>

View File

@ -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');
}

View File

@ -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);

View File

@ -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,
)
]),

View 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,
);
}

View File

@ -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,

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -199,6 +199,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
),
textInputAction: TextInputAction.next,
focusNode: _issuerFocus,
autofocus: true,
onChanged: (value) {
setState(() {
_issuer = value.trim();