support for FIDO, conditional messages

This commit is contained in:
Adam Velebil 2024-08-31 10:45:33 +02:00
parent afaab491b8
commit 34f78d2518
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
16 changed files with 102 additions and 63 deletions

View File

@ -352,6 +352,9 @@ 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)
@ -389,11 +392,11 @@ class MainActivity : FlutterFragmentActivity() {
messenger = flutterEngine.dartExecutor.binaryMessenger
flutterLog = FlutterLog(messenger)
deviceManager = DeviceManager(this, viewModel)
appMethodChannel = AppMethodChannel(messenger)
deviceManager = DeviceManager(this, viewModel,appMethodChannel)
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
dialogManager = DialogManager(messenger, this.lifecycleScope)
appPreferences = AppPreferences(this)
appMethodChannel = AppMethodChannel(messenger)
appLinkMethodChannel = AppLinkMethodChannel(messenger)
managementHandler = ManagementHandler(messenger, deviceManager, dialogManager)
@ -441,7 +444,6 @@ class MainActivity : FlutterFragmentActivity() {
oathViewModel,
dialogManager,
appPreferences,
appMethodChannel,
nfcActivityListener
)

View File

@ -20,8 +20,10 @@ import androidx.collection.ArraySet
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.yubico.authenticator.MainActivity
import com.yubico.authenticator.MainViewModel
import com.yubico.authenticator.OperationContext
import com.yubico.authenticator.yubikit.NfcActivityState
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
import com.yubico.yubikit.core.YubiKeyDevice
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
@ -41,7 +43,8 @@ interface DeviceListener {
class DeviceManager(
private val lifecycleOwner: LifecycleOwner,
private val appViewModel: MainViewModel
private val appViewModel: MainViewModel,
private val appMethodChannel: MainActivity.AppMethodChannel
) {
var clearDeviceInfoOnDisconnect: Boolean = true
@ -179,8 +182,19 @@ class DeviceManager(
onUsb(it)
}
suspend fun <T> withKey(onNfc: suspend () -> T, onUsb: suspend (UsbYubiKeyDevice) -> T) =
suspend fun <T> withKey(
onNfc: suspend () -> com.yubico.yubikit.core.util.Result<T, Throwable>,
onUsb: suspend (UsbYubiKeyDevice) -> T
): T =
appViewModel.connectedYubiKey.value?.let {
onUsb(it)
} ?: onNfc()
} ?: try {
onNfc().value.also {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
}
} catch (e: Throwable) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
throw e
}
}

View File

@ -60,7 +60,7 @@ class FidoConnectionHelper(
block(YubiKitFidoSession(it))
}
suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): T {
suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): Result<T, Throwable> {
try {
val result = suspendCoroutine { outer ->
pendingAction = {
@ -74,11 +74,11 @@ class FidoConnectionHelper(
pendingAction = null
}
}
return result
return Result.success(result!!)
} catch (cancelled: CancellationException) {
throw cancelled
return Result.failure(cancelled)
} catch (error: Throwable) {
throw error
return Result.failure(error)
} finally {
dialogManager.closeDialog()
}

View File

@ -35,12 +35,9 @@ class ManagementConnectionHelper(
) {
private var action: ManagementAction? = null
suspend fun <T> useSession(
actionDescription: ManagementActionDescription,
action: (YubiKitManagementSession) -> T
): T {
suspend fun <T> useSession(action: (YubiKitManagementSession) -> T): T {
return deviceManager.withKey(
onNfc = { useSessionNfc(actionDescription, action) },
onNfc = { useSessionNfc(action) },
onUsb = { useSessionUsb(it, action) })
}
@ -51,28 +48,25 @@ class ManagementConnectionHelper(
block(YubiKitManagementSession(it))
}
private suspend fun <T> useSessionNfc(
actionDescription: ManagementActionDescription,
block: (YubiKitManagementSession) -> T
): T {
private suspend fun <T> useSessionNfc(block: (YubiKitManagementSession) -> T): Result<T, Throwable> {
try {
val result = suspendCoroutine { outer ->
val result = suspendCoroutine<T> { outer ->
action = {
outer.resumeWith(runCatching {
block.invoke(it.value)
})
}
dialogManager.showDialog {
logger.debug("Cancelled Dialog {}", actionDescription.name)
logger.debug("Cancelled Dialog")
action?.invoke(Result.failure(CancellationException()))
action = null
}
}
return result
return Result.success(result!!)
} catch (cancelled: CancellationException) {
throw cancelled
return Result.failure(cancelled)
} catch (error: Throwable) {
throw error
return Result.failure(error)
} finally {
dialogManager.closeDialog()
}

View File

@ -27,15 +27,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
const val dialogDescriptionManagementIndex = 300
enum class ManagementActionDescription(private val value: Int) {
DeviceReset(0), ActionFailure(1);
val id: Int
get() = value + dialogDescriptionManagementIndex
}
class ManagementHandler(
messenger: BinaryMessenger,
deviceManager: DeviceManager,
@ -58,7 +49,7 @@ class ManagementHandler(
}
private suspend fun deviceReset(): String =
connectionHelper.useSession(ManagementActionDescription.DeviceReset) { managementSession ->
connectionHelper.useSession { managementSession ->
managementSession.deviceReset()
NULL
}

View File

@ -80,7 +80,6 @@ class OathManager(
private val oathViewModel: OathViewModel,
private val dialogManager: DialogManager,
private val appPreferences: AppPreferences,
private val appMethodChannel: MainActivity.AppMethodChannel,
private val nfcActivityListener: NfcActivityListener
) : AppContextManager(), DeviceListener {
@ -221,10 +220,6 @@ class OathManager(
override suspend fun processYubiKey(device: YubiKeyDevice) {
try {
if (device is NfcYubiKeyDevice) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_STARTED)
}
device.withConnection<SmartCardConnection, Unit> { connection ->
val session = getOathSession(connection)
val previousId = oathViewModel.currentSession()?.deviceId
@ -310,7 +305,6 @@ class OathManager(
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
} catch (e: Exception) {
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
logger.error("Failed to connect to CCID: ", e)
// Clear any cached OATH state
@ -346,7 +340,7 @@ class OathManager(
logger.debug("Added cred {}", credential)
jsonSerializer.encodeToString(addedCred)
}
}.value
}
private suspend fun addAccountsToAny(
@ -725,7 +719,7 @@ class OathManager(
private suspend fun <T> useOathSessionNfc(
block: (YubiKitOathSession) -> T
): T {
): Result<T, Throwable> {
var firstShow = true
while (true) { // loop until success or cancel
try {
@ -749,14 +743,12 @@ class OathManager(
// here the coroutine is suspended and waits till pendingAction is
// invoked - the pending action result will resume this coroutine
}
nfcActivityListener.onChange(NfcActivityState.PROCESSING_FINISHED)
return result
return Result.success(result!!)
} catch (cancelled: CancellationException) {
throw cancelled
return Result.failure(cancelled)
} catch (e: Exception) {
logger.error("Exception during action: ", e)
nfcActivityListener.onChange(NfcActivityState.PROCESSING_INTERRUPTED)
throw e
return Result.failure(e)
}
} // while
}

View File

@ -109,8 +109,13 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
try {
await oath.setPassword(current, password);
return true;
} on PlatformException catch (e) {
_log.debug('Calling set password failed with exception: $e');
} on PlatformException catch (pe) {
final decoded = pe.decode();
if (decoded is CancellationException) {
_log.debug('Set password cancelled');
throw decoded;
}
_log.debug('Calling set password failed with exception: $pe');
return false;
}
}
@ -120,8 +125,13 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
try {
await oath.unsetPassword(current);
return true;
} on PlatformException catch (e) {
_log.debug('Calling unset password failed with exception: $e');
} on PlatformException catch (pe) {
final decoded = pe.decode();
if (decoded is CancellationException) {
_log.debug('Unset password cancelled');
throw decoded;
}
_log.debug('Calling unset password failed with exception: $pe');
return false;
}
}

View File

@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/message.dart';
import '../app/state.dart';
import 'state.dart';
import 'views/nfc/models.dart';
@ -80,7 +81,9 @@ class _DialogProvider extends Notifier<int> {
case NfcActivity.processingFinished:
explicitAction = false; // next action might not be explicit
processingTimer?.cancel();
if (properties.showSuccess ?? false) {
final showSuccess = properties.showSuccess ?? false;
allowMessages = !showSuccess;
if (showSuccess) {
notifier.sendCommand(
updateNfcView(NfcActivityClosingCountdownWidgetView(
closeInSec: 5,

View File

@ -97,9 +97,7 @@ class _NfcActivityClosingCountdownWidgetViewState
}
void hideNow() {
debugPrint('XXX closing because have to!');
ref.read(nfcEventCommandNotifier.notifier).sendCommand(
NfcEventCommand(event: const NfcHideViewEvent(timeoutMs: 0)));
ref.read(nfcEventCommandNotifier.notifier).sendCommand(hideNfcView);
}
}
@ -123,10 +121,10 @@ class _NfcViewNotifier extends Notifier<NfcView> {
bool? showSuccess,
bool? showCloseButton}) {
state = state.copyWith(
operationSuccess: operationSuccess,
operationFailure: operationFailure,
showSuccess: showSuccess,
showCloseButton: showCloseButton);
operationSuccess: operationSuccess ?? state.operationSuccess,
operationFailure: operationFailure ?? state.operationFailure,
showSuccess: showSuccess ?? state.showSuccess,
showCloseButton: showCloseButton ?? state.showCloseButton);
}
}

View File

@ -21,12 +21,17 @@ import 'package:flutter/material.dart';
import '../widgets/toast.dart';
var allowMessages = true;
void Function() showMessage(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) =>
showToast(context, message, duration: duration);
}) {
return allowMessages
? showToast(context, message, duration: duration)
: () {};
}
Future<T?> showBlurDialog<T>({
required BuildContext context,

View File

@ -442,6 +442,12 @@
"s_rename_account": "Konto umbenennen",
"l_rename_account_desc": "Bearbeiten Sie den Aussteller/Namen des Kontos",
"s_account_renamed": "Konto umbenannt",
"l_rename_account_failed": null,
"@l_rename_account_failed": {
"placeholders": {
"message": {}
}
},
"p_rename_will_change_account_displayed": "Das ändert die Anzeige dieses Kontos in der Liste.",
"s_delete_account": "Konto löschen",
"l_delete_account_desc": "Löschen Sie das Konto von Ihrem YubiKey",

View File

@ -442,6 +442,12 @@
"s_rename_account": "Rename account",
"l_rename_account_desc": "Edit the issuer/name of the account",
"s_account_renamed": "Account renamed",
"l_rename_account_failed": "Failed renaming account: {message}",
"@l_rename_account_failed": {
"placeholders": {
"message": {}
}
},
"p_rename_will_change_account_displayed": "This will change how the account is displayed in the list.",
"s_delete_account": "Delete account",
"l_delete_account_desc": "Remove the account from your YubiKey",

View File

@ -442,6 +442,12 @@
"s_rename_account": "Renommer compte",
"l_rename_account_desc": "Modifier émetteur/nom du compte",
"s_account_renamed": "Compte renommé",
"l_rename_account_failed": null,
"@l_rename_account_failed": {
"placeholders": {
"message": {}
}
},
"p_rename_will_change_account_displayed": "Cela modifiera l'affichage du compte dans la liste.",
"s_delete_account": "Supprimer compte",
"l_delete_account_desc": "Supprimer le compte de votre YubiKey",

View File

@ -442,6 +442,12 @@
"s_rename_account": "アカウント名を変更",
"l_rename_account_desc": "アカウントの発行者/名前を編集",
"s_account_renamed": "アカウントの名前が変更されました",
"l_rename_account_failed": null,
"@l_rename_account_failed": {
"placeholders": {
"message": {}
}
},
"p_rename_will_change_account_displayed": "これにより、リスト内のアカウントの表示が変更されます。",
"s_delete_account": "アカウントを削除",
"l_delete_account_desc": "YubiKeyからアカウントを削除",

View File

@ -442,6 +442,12 @@
"s_rename_account": "Zmień nazwę konta",
"l_rename_account_desc": "Edytuj wydawcę/nazwę konta",
"s_account_renamed": "Zmieniono nazwę konta",
"l_rename_account_failed": null,
"@l_rename_account_failed": {
"placeholders": {
"message": {}
}
},
"p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.",
"s_delete_account": "Usuń konto",
"l_delete_account_desc": "Usuń konto z klucza YubiKey",

View File

@ -92,7 +92,7 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
} on CancellationException catch (_) {
// ignored
} catch (e) {
_log.error('Failed to add account', e);
_log.error('Failed to rename account', e);
final String errorMessage;
// TODO: Make this cleaner than importing desktop specific RpcError.
if (e is RpcError) {
@ -103,7 +103,7 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
await withContext((context) async => showMessage(
context,
AppLocalizations.of(context)!
.l_account_add_failed(errorMessage),
.l_rename_account_failed(errorMessage),
duration: const Duration(seconds: 4),
));
return null;