This commit is contained in:
Adam Velebil 2024-09-13 08:52:15 +02:00
commit 5a24f57e0b
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
12 changed files with 179 additions and 140 deletions

View File

@ -28,7 +28,7 @@ abstract class AppContextManager {
open fun onPause() {}
open fun onError() {}
open fun onError(e: Exception) {}
}
class ContextDisposedException : Exception()

View File

@ -79,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
@ -310,17 +311,14 @@ class MainActivity : FlutterFragmentActivity() {
}
private suspend fun processYubiKey(device: YubiKeyDevice) {
val deviceInfo = getDeviceInfo(device)
if (deviceInfo == null) {
deviceManager.setDeviceInfo(null)
return
}
val deviceInfo = try {
if (device is NfcYubiKeyDevice) {
appMethodChannel.nfcStateChanged(NfcState.ONGOING)
}
val deviceInfo = getDeviceInfo(device)
deviceManager.scpKeyParams = null
// If NFC and FIPS check for SCP11b key
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
@ -340,13 +338,28 @@ class MainActivity : FlutterFragmentActivity() {
}
}
} catch (e: Exception) {
logger.debug("Exception while getting scp keys: ", e)
contextManager?.onError()
logger.error("Exception when reading SCP key information: ", e)
// we throw IO exception to unify handling failures as we don't want
// th clear device info
throw IOException("Failure getting SCP keys")
}
}
deviceInfo
} catch (e: Exception) {
logger.debug("Exception while getting device info and scp keys: ", e)
contextManager?.onError(e)
if (device is NfcYubiKeyDevice) {
appMethodChannel.nfcStateChanged(NfcState.FAILURE)
}
null
// do not clear deviceInfo on IOExceptions,
// this allows for retries of failed actions
if (e !is IOException) {
logger.debug("Resetting device info")
deviceManager.setDeviceInfo(null)
}
return
}
// this YubiKey provides SCP11b key but the phone cannot perform AESCMAC

View File

@ -42,14 +42,6 @@ class FidoConnectionHelper(private val deviceManager: DeviceManager) {
return requestHandled
}
fun failPending(e: Exception) {
pendingAction?.let { action ->
logger.error("Failing pending action with {}", e.message)
action.invoke(Result.failure(e))
pendingAction = null
}
}
fun cancelPending() {
pendingAction?.let { action ->
action.invoke(Result.failure(CancellationException()))
@ -80,7 +72,7 @@ class FidoConnectionHelper(private val deviceManager: DeviceManager) {
block(YubiKitFidoSession(it))
}.also {
if (updateDeviceInfo) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
}
}

View File

@ -174,9 +174,9 @@ class FidoManager(
}
}
override fun onError() {
super.onError()
logger.debug("Cancel any pending action because of upstream error")
override fun onError(e: Exception) {
super.onError(e)
logger.error("Cancelling pending action. Cause: ", e)
connectionHelper.cancelPending()
}
@ -204,13 +204,12 @@ class FidoManager(
}
if (updateDeviceInfo.getAndSet(false)) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
}
} catch (e: Exception) {
// something went wrong, try to get DeviceInfo from any available connection type
logger.error("Failure when processing YubiKey: ", e)
connectionHelper.failPending(e)
logger.error("Cancelling pending action. Cause: ", e)
connectionHelper.cancelPending()
if (e !is IOException) {
// we don't clear the session on IOExceptions so that the session is ready for
@ -240,7 +239,7 @@ class FidoManager(
currentSession
)
val sameDevice = currentSession == previousSession
val sameDevice = currentSession.sameDevice(previousSession)
if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) {
requestHandled = connectionHelper.invokePending(fidoSession)

View File

@ -41,6 +41,19 @@ data class Options(
infoData.getOptionsBoolean("ep"),
)
fun sameDevice(other: Options) : Boolean {
if (this === other) return true
if (clientPin != other.clientPin) return false
if (credMgmt != other.credMgmt) return false
if (credentialMgmtPreview != other.credentialMgmtPreview) return false
if (bioEnroll != other.bioEnroll) return false
// alwaysUv may differ
// ep may differ
return true
}
companion object {
private fun InfoData.getOptionsBoolean(
key: String
@ -67,6 +80,21 @@ data class SessionInfo(
infoData.remainingDiscoverableCredentials
)
// this is a more permissive comparison, which does not take in an account properties,
// which might change by using the FIDO authenticator
fun sameDevice(other: SessionInfo?): Boolean {
if (other == null) return false
if (this === other) return true
if (!options.sameDevice(other.options)) return false
if (!aaguid.contentEquals(other.aaguid)) return false
// minPinLength may differ
// forcePinChange may differ
// remainingDiscoverableCredentials may differ
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@ -110,9 +110,9 @@ class OathManager(
private val updateDeviceInfo = AtomicBoolean(false)
private var deviceInfoTimer: TimerTask? = null
override fun onError() {
super.onError()
logger.debug("Cancel any pending action because of upstream error")
override fun onError(e: Exception) {
super.onError(e)
logger.error("Cancelling pending action in onError. Cause: ", e)
pendingAction?.let { action ->
action.invoke(Result.failure(CancellationException()))
pendingAction = null
@ -343,16 +343,16 @@ class OathManager(
)
if (updateDeviceInfo.getAndSet(false)) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
}
} catch (e: Exception) {
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
logger.error("Exception during SmartCard connection/OATH session creation: ", e)
// Remove any pending action
// Cancel any pending action
pendingAction?.let { action ->
logger.error("Failing pending action with {}", e.message)
action.invoke(Result.failure(e))
logger.error("Cancelling pending action. Cause: ", e)
action.invoke(Result.failure(CancellationException()))
pendingAction = null
}
@ -782,7 +782,7 @@ class OathManager(
block(getOathSession(it))
}.also {
if (updateDeviceInfo) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
deviceManager.setDeviceInfo(runCatching { getDeviceInfo(device) }.getOrNull())
}
}

View File

@ -47,17 +47,17 @@ class DeviceInfoHelper {
private val restrictedNfcBytes =
byteArrayOf(0x00, 0x1F, 0xD1.toByte(), 0x01, 0x1b, 0x55, 0x04) + uri
suspend fun getDeviceInfo(device: YubiKeyDevice): Info? {
suspend fun getDeviceInfo(device: YubiKeyDevice): Info {
SessionVersionOverride.set(null)
var deviceInfo = readDeviceInfo(device)
if (deviceInfo?.version?.major == 0.toByte()) {
if (deviceInfo.version.major == 0.toByte()) {
SessionVersionOverride.set(Version(5, 7, 2))
deviceInfo = readDeviceInfo(device)
}
return deviceInfo
}
private suspend fun readDeviceInfo(device: YubiKeyDevice): Info? {
private suspend fun readDeviceInfo(device: YubiKeyDevice): Info {
val pid = (device as? UsbYubiKeyDevice)?.pid
val deviceInfo = runCatching {
@ -106,8 +106,8 @@ class DeviceInfoHelper {
}
} catch (e: Exception) {
// no smart card connectivity
logger.error("Failure getting device info", e)
return null
logger.error("Failure getting device info: ", e)
throw e
}
}

View File

@ -365,9 +365,8 @@ class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
var decodedException = pe.decode();
if (decodedException is CancellationException) {
_log.debug('User cancelled delete credential FIDO operation');
} else {
throw decodedException;
}
throw decodedException;
}
}
}

View File

@ -60,7 +60,8 @@ class MainPage extends ConsumerWidget {
final prevSerial =
prev?.hasValue == true ? prev?.value?.info.serial : null;
if ((serial != null && serial == prevSerial) ||
(next.hasValue && (prev != null && prev.isLoading))) {
(next.hasValue && (prev != null && prev.isLoading)) ||
next.isLoading) {
return;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,6 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../widgets/responsive_dialog.dart';
import '../models.dart';
import '../state.dart';
@ -57,6 +58,7 @@ class DeleteCredentialDialog extends ConsumerWidget {
actions: [
TextButton(
onPressed: () async {
try {
await ref
.read(credentialProvider(devicePath).notifier)
.deleteCredential(credential);
@ -66,6 +68,9 @@ class DeleteCredentialDialog extends ConsumerWidget {
showMessage(context, l10n.s_passkey_deleted);
},
);
} on CancellationException catch (_) {
// ignored
}
},
child: Text(l10n.s_delete),
),

View File

@ -22,6 +22,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../management/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_field.dart';
@ -71,6 +72,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
_submit() async {
_removeFocus();
try {
final result = await ref
.read(oathStateProvider(widget.path).notifier)
.setPassword(_currentPasswordController.text, _newPassword);
@ -83,12 +85,16 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
}
} else {
_currentPasswordController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentPasswordController.text.length);
baseOffset: 0,
extentOffset: _currentPasswordController.text.length);
_currentPasswordFocus.requestFocus();
setState(() {
_currentIsWrong = true;
});
}
} on CancellationException catch (_) {
// ignored
}
}
@override

View File

@ -73,43 +73,9 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
oathType: credential.oathType,
period: credential.period,
existing: existing,
rename: (issuer, name) async {
final withContext = ref.read(withContextProvider);
try {
// Rename credentials
final renamed = await ref
rename: (issuer, name) async => await ref
.read(credentialListProvider(devicePath).notifier)
.renameAccount(credential, issuer, name);
// Update favorite
ref
.read(favoritesProvider.notifier)
.renameCredential(credential.id, renamed.id);
await withContext((context) async => showMessage(
context, AppLocalizations.of(context)!.s_account_renamed));
return renamed;
} on CancellationException catch (_) {
// ignored
} catch (e) {
_log.error('Failed to rename account', e);
final String errorMessage;
// TODO: Make this cleaner than importing desktop specific RpcError.
if (e is RpcError) {
errorMessage = e.message;
} else {
errorMessage = e.toString();
}
await withContext((context) async => showMessage(
context,
AppLocalizations.of(context)!
.l_rename_account_failed(errorMessage),
duration: const Duration(seconds: 4),
));
return null;
}
},
);
.renameAccount(credential, issuer, name));
}
}
@ -138,9 +104,39 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
_issuerFocus.unfocus();
_nameFocus.unfocus();
final nav = Navigator.of(context);
final withContext = ref.read(withContextProvider);
try {
// Rename credentials
final renamed =
await widget.rename(_issuer.isNotEmpty ? _issuer : null, _name);
// Update favorite
ref
.read(favoritesProvider.notifier)
.renameCredential(renamed.id, renamed.id);
await withContext((context) async => showMessage(
context, AppLocalizations.of(context)!.s_account_renamed));
nav.pop(renamed);
} on CancellationException catch (_) {
// ignored
} catch (e) {
_log.error('Failed to rename account', e);
final String errorMessage;
// TODO: Make this cleaner than importing desktop specific RpcError.
if (e is RpcError) {
errorMessage = e.message;
} else {
errorMessage = e.toString();
}
await withContext((context) async => showMessage(
context,
AppLocalizations.of(context)!.l_rename_account_failed(errorMessage),
duration: const Duration(seconds: 4),
));
}
}
@override