Merge branch main into adamve/nfc_activity_widget

This commit is contained in:
Adam Velebil 2024-09-04 15:50:10 +02:00
commit 3873ec4514
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
14 changed files with 105 additions and 28 deletions

View File

@ -18,6 +18,7 @@ package com.yubico.authenticator.fido
import com.yubico.authenticator.device.DeviceManager
import com.yubico.authenticator.fido.data.YubiKitFidoSession
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
import com.yubico.yubikit.core.fido.FidoConnection
@ -43,9 +44,13 @@ class FidoConnectionHelper(private val deviceManager: DeviceManager) {
}
}
suspend fun <T> useSession(block: (YubiKitFidoSession) -> T): T {
suspend fun <T> useSession(
updateDeviceInfo: Boolean = false,
block: (YubiKitFidoSession) -> T
): T {
FidoManager.updateDeviceInfo.set(updateDeviceInfo)
return deviceManager.withKey(
onUsb = { useSessionUsb(it, block) },
onUsb = { useSessionUsb(it, updateDeviceInfo, block) },
onNfc = { useSessionNfc(block) },
onDialogCancelled = {
pendingAction?.invoke(Result.failure(CancellationException()))
@ -56,9 +61,14 @@ class FidoConnectionHelper(private val deviceManager: DeviceManager) {
suspend fun <T> useSessionUsb(
device: UsbYubiKeyDevice,
updateDeviceInfo: Boolean = false,
block: (YubiKitFidoSession) -> T
): T = device.withConnection<FidoConnection, T> {
block(YubiKitFidoSession(it))
}.also {
if (updateDeviceInfo) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
}
suspend fun <T> useSessionNfc(

View File

@ -31,6 +31,7 @@ import com.yubico.authenticator.fido.data.Session
import com.yubico.authenticator.fido.data.SessionInfo
import com.yubico.authenticator.fido.data.YubiKitFidoSession
import com.yubico.authenticator.setHandler
import com.yubico.authenticator.yubikit.DeviceInfoHelper.Companion.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.core.YubiKeyConnection
@ -62,6 +63,7 @@ import org.slf4j.LoggerFactory
import java.io.IOException
import java.util.Arrays
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
typealias FidoAction = (Result<YubiKitFidoSession, Exception>) -> Unit
@ -82,6 +84,7 @@ class FidoManager(
}
companion object {
val updateDeviceInfo = AtomicBoolean(false)
fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol {
val pinUvAuthProtocols = infoData.pinUvAuthProtocols
val pinSupported = infoData.options["clientPin"] != null
@ -124,6 +127,8 @@ class FidoManager(
pinStore
)
init {
pinRetries = null
@ -177,6 +182,7 @@ class FidoManager(
fidoChannel.setMethodCallHandler(null)
fidoViewModel.clearSessionState()
fidoViewModel.updateCredentials(null)
connectionHelper.cancelPending()
coroutineScope.cancel()
}
@ -191,6 +197,10 @@ class FidoManager(
processYubiKey(connection, device)
}
}
if (updateDeviceInfo.getAndSet(false)) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
} catch (e: Exception) {
// something went wrong, try to get DeviceInfo from any available connection type
logger.error("Failure when processing YubiKey: ", e)
@ -383,7 +393,7 @@ class FidoManager(
}
private suspend fun setPin(pin: CharArray?, newPin: CharArray): String =
connectionHelper.useSession { fidoSession ->
connectionHelper.useSession(updateDeviceInfo = true) { fidoSession ->
try {
val clientPin =
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))

View File

@ -167,8 +167,9 @@ class FidoResetHelper(
coroutineScope.launch(Dispatchers.Main) {
fidoViewModel.updateResetState(FidoResetState.Touch)
logger.debug("Waiting for touch")
deviceManager.withKey { usbYubiKeyDevice ->
connectionHelper.useSessionUsb(usbYubiKeyDevice) { fidoSession ->
deviceManager.withKey {
usbYubiKeyDevice ->
connectionHelper.useSessionUsb(usbYubiKeyDevice, updateDeviceInfo = true) { fidoSession ->
resetCommandState = CommandState()
try {
if (cancelReset) {
@ -219,6 +220,7 @@ class FidoResetHelper(
}
fidoViewModel.updateResetState(FidoResetState.Touch)
try {
FidoManager.updateDeviceInfo.set(true)
connectionHelper.useSessionNfc { fidoSession ->
doReset(fidoSession)
continuation.resume(Unit)

View File

@ -209,6 +209,7 @@ class OathManager(
oathChannel.setMethodCallHandler(null)
oathViewModel.clearSession()
oathViewModel.updateCredentials(mapOf())
pendingAction?.invoke(Result.failure(Exception()))
coroutineScope.cancel()
}

View File

@ -35,7 +35,9 @@ import '../../exception/no_data_exception.dart';
import '../../exception/platform_exception_decoder.dart';
import '../../oath/models.dart';
import '../../oath/state.dart';
import '../../widgets/toast.dart';
import '../method_channel_notifier.dart';
import '../tap_request_dialog.dart';
final _log = Logger('android.oath.state');
@ -146,27 +148,45 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
}
}
// Converts Platform exception during Add Account operation
// Returns CancellationException for situations we don't want to show a Toast
Exception _decodeAddAccountException(PlatformException platformException) {
final decodedException = platformException.decode();
Exception handlePlatformException(
Ref ref, PlatformException platformException) {
final decoded = platformException.decode();
final l10n = ref.read(l10nProvider);
final withContext = ref.read(withContextProvider);
// Auth required, the app will show Unlock dialog
if (decodedException is ApduException && decodedException.sw == 0x6982) {
_log.error('Add account failed: Auth required');
return CancellationException();
toast(String message, {bool popStack = false}) =>
withContext((context) async {
ref.read(androidDialogProvider.notifier).closeDialog();
if (popStack) {
Navigator.of(context).popUntil((route) {
return route.isFirst;
});
}
showToast(context, message, duration: const Duration(seconds: 4));
});
switch (decoded) {
case ApduException apduException:
if (apduException.sw == 0x6985) {
// pop stack to show the OATH view with "Set password"
toast(l10n.l_add_account_password_required, popStack: true);
return CancellationException();
}
if (apduException.sw == 0x6982) {
toast(l10n.l_add_account_unlock_required);
return CancellationException();
}
case PlatformException pe:
if (pe.code == 'JobCancellationException') {
// pop stack to show FIDO view
toast(l10n.l_add_account_func_missing, popStack: true);
return CancellationException();
} else if (pe.code == 'IllegalArgumentException') {
toast(l10n.l_add_account_already_exists);
return CancellationException();
}
}
// Thrown in native code when the account already exists on the YubiKey
// The entry dialog will show an error message and that is why we convert
// this to CancellationException to avoid showing a Toast
if (platformException.code == 'IllegalArgumentException') {
_log.error('Add account failed: Account already exists');
return CancellationException();
}
// original exception
return decodedException;
return decoded;
}
final addCredentialToAnyProvider =
@ -179,7 +199,7 @@ final addCredentialToAnyProvider =
var result = jsonDecode(resultString);
return OathCredential.fromJson(result['credential']);
} on PlatformException catch (pe) {
throw _decodeAddAccountException(pe);
throw handlePlatformException(ref, pe);
}
});
@ -290,7 +310,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
var result = jsonDecode(resultString);
return OathCredential.fromJson(result['credential']);
} on PlatformException catch (pe) {
throw _decodeAddAccountException(pe);
throw handlePlatformException(_ref, pe);
}
}

View File

@ -134,7 +134,7 @@ class _DialogProvider extends Notifier<int> {
break;
case 'close':
notifier.sendCommand(hideNfcView);
closeDialog();
break;
default:
@ -147,6 +147,10 @@ class _DialogProvider extends Notifier<int> {
return 0;
}
void closeDialog() {
ref.read(nfcEventCommandNotifier.notifier).sendCommand(hideNfcView);
}
void cancelDialog() async {
explicitAction = false;
await _channel.invokeMethod('cancel');

View File

@ -639,6 +639,8 @@ class _AppPageState extends ConsumerState<AppPage> {
},
),
),
iconTheme: IconThemeData(
color: Theme.of(context).colorScheme.onSurfaceVariant),
scrolledUnderElevation: 0.0,
leadingWidth: hasRail ? 84 : null,
backgroundColor: Theme.of(context).colorScheme.surface,
@ -730,7 +732,10 @@ class _AppPageState extends ConsumerState<AppPage> {
.read(_detailViewVisibilityProvider.notifier)
.toggleExpanded();
},
icon: const Icon(Symbols.more_vert),
icon: const Icon(
Symbols.more_vert,
weight: 600.0,
),
iconSize: 24,
tooltip: showDetailView ? l10n.s_hide_menu : l10n.s_show_menu,
padding: const EdgeInsets.all(12),

View File

@ -69,6 +69,7 @@ class MainPage extends ConsumerWidget {
'oath_add_account',
'oath_icon_pack_dialog',
'android_qr_scanner_view',
'android_alert_dialog'
].contains(route.settings.name);
});
});

View File

@ -431,6 +431,10 @@
"message": {}
}
},
"l_add_account_password_required": null,
"l_add_account_unlock_required": null,
"l_add_account_already_exists": null,
"l_add_account_func_missing": null,
"l_account_name_required": "Ihr Konto muss einen Namen haben",
"l_name_already_exists": "Für diesen Aussteller existiert dieser Name bereits",
"l_account_already_exists": "Dieses Konto existiert bereits auf dem YubiKey",

View File

@ -431,6 +431,10 @@
"message": {}
}
},
"l_add_account_password_required": "Password required",
"l_add_account_unlock_required": "Unlock required",
"l_add_account_already_exists": "Account already exists",
"l_add_account_func_missing": "Functionality missing or disabled",
"l_account_name_required": "Your account must have a name",
"l_name_already_exists": "This name already exists for the issuer",
"l_account_already_exists": "This account already exists on the YubiKey",

View File

@ -431,6 +431,10 @@
"message": {}
}
},
"l_add_account_password_required": null,
"l_add_account_unlock_required": null,
"l_add_account_already_exists": null,
"l_add_account_func_missing": null,
"l_account_name_required": "Votre compte doit avoir un nom",
"l_name_already_exists": "Ce nom existe déjà pour l'émetteur",
"l_account_already_exists": "Ce compte existe déjà sur la YubiKey",

View File

@ -431,6 +431,10 @@
"message": {}
}
},
"l_add_account_password_required": null,
"l_add_account_unlock_required": null,
"l_add_account_already_exists": null,
"l_add_account_func_missing": null,
"l_account_name_required": "アカウントには名前が必要です",
"l_name_already_exists": "この名前は発行者にすでに存在します",
"l_account_already_exists": "このアカウントはYubiKeyにすでに存在します",

View File

@ -431,6 +431,10 @@
"message": {}
}
},
"l_add_account_password_required": null,
"l_add_account_unlock_required": null,
"l_add_account_already_exists": null,
"l_add_account_func_missing": null,
"l_account_name_required": "Twoje konto musi mieć nazwę",
"l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy",
"l_account_already_exists": "To konto już istnieje w YubiKey",

View File

@ -431,6 +431,10 @@
"message": {}
}
},
"l_add_account_password_required": null,
"l_add_account_unlock_required": null,
"l_add_account_already_exists": null,
"l_add_account_func_missing": null,
"l_account_name_required": "Tài khoản của bạn phải có tên",
"l_name_already_exists": "Tên này đã tồn tại cho nhà phát hành",
"l_account_already_exists": "Tài khoản này đã tồn tại trên YubiKey",