This commit is contained in:
Adam Velebil 2024-08-26 08:24:57 +02:00
commit 9b1b2cc15a
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
4 changed files with 81 additions and 27 deletions

View File

@ -42,6 +42,7 @@ 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.getDeviceInfo
import com.yubico.authenticator.yubikit.withConnection
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
@ -105,6 +106,7 @@ class OathManager(
private var pendingAction: OathAction? = null
private var refreshJob: Job? = null
private var addToAny = false
private val updateDeviceInfo = AtomicBoolean(false)
override fun onPause() {
// cancel any pending actions, except for addToAny
@ -284,6 +286,10 @@ class OathManager(
logger.debug(
"Successfully read Oath session info (and credentials if unlocked) from connected key"
)
if (updateDeviceInfo.getAndSet(false)) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
} catch (e: Exception) {
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
logger.error("Failed to connect to CCID: ", e)
@ -362,7 +368,7 @@ class OathManager(
}
private suspend fun reset(): String =
useOathSession(OathActionDescription.Reset) {
useOathSession(OathActionDescription.Reset, updateDeviceInfo = true) {
// note, it is ok to reset locked session
it.reset()
keyManager.removeKey(it.deviceId)
@ -396,7 +402,11 @@ class OathManager(
currentPassword: String?,
newPassword: String,
): String =
useOathSession(OathActionDescription.SetPassword, unlock = false) { session ->
useOathSession(
OathActionDescription.SetPassword,
unlock = false,
updateDeviceInfo = true
) { session ->
if (session.isAccessKeySet) {
if (currentPassword == null) {
throw Exception("Must provide current password to be able to change it")
@ -648,22 +658,30 @@ class OathManager(
private suspend fun <T> useOathSession(
oathActionDescription: OathActionDescription,
unlock: Boolean = true,
updateDeviceInfo: Boolean = false,
action: (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, action) },
onUsb = { useOathSessionUsb(it, updateDeviceInfo, action) },
onNfc = { useOathSessionNfc(oathActionDescription, action) }
)
}
private suspend fun <T> useOathSessionUsb(
device: UsbYubiKeyDevice,
updateDeviceInfo: Boolean = false,
block: (YubiKitOathSession) -> T
): T = device.withConnection<SmartCardConnection, T> {
block(getOathSession(it))
}.also {
if (updateDeviceInfo) {
deviceManager.setDeviceInfo(getDeviceInfo(device))
}
}
private suspend fun <T> useOathSessionNfc(

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Yubico.
* Copyright (C) 2023,2024 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,9 +28,17 @@ import '../features.dart' as features;
import '../icon_provider/icon_pack_dialog.dart';
import '../keys.dart' as keys;
import '../models.dart';
import 'manage_password_dialog.dart';
import 'utils.dart';
bool oathShowActionNotifier(DeviceInfo? info) {
if (info == null) {
return false;
}
final (fipsCapable, fipsApproved) = info.getFipsStatus(Capability.oath);
return fipsCapable && !fipsApproved;
}
Widget oathBuildActions(
BuildContext context,
DevicePath devicePath,
@ -63,6 +71,10 @@ Widget oathBuildActions(
enabled = true;
}
final colors = Theme.of(context).buttonTheme.colorScheme ??
Theme.of(context).colorScheme;
final alertIcon = Icon(Symbols.warning_amber, color: colors.tertiary);
return Column(
children: [
ActionListSection(l10n.s_setup, children: [
@ -103,13 +115,10 @@ Widget oathBuildActions(
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password,
subtitle: l10n.l_password_protection,
icon: const Icon(Symbols.password),
trailing: fipsCapable && !fipsApproved ? alertIcon : null,
onTap: (context) {
Navigator.of(context).popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(devicePath, oathState),
);
managePassword(context, ref, devicePath, oathState);
}),
]),
],

View File

@ -167,7 +167,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
final hasFeature = ref.watch(featureProvider);
final hasActions = hasFeature(features.actions);
final searchText = searchController.text;
final deviceInfo =
ref.watch(currentDeviceDataProvider.select((s) => s.valueOrNull?.info));
Future<void> onFileDropped(File file) async {
final qrScanner = ref.read(qrScannerProvider);
if (qrScanner != null) {
@ -186,21 +187,36 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
if (numCreds == 0) {
return MessagePage(
actionsBuilder: (context, expanded) => [
if (!expanded)
ActionChip(
label: Text(l10n.s_add_account),
onPressed: () async {
await addOathAccount(
context,
ref,
widget.devicePath,
widget.oathState,
);
},
avatar: const Icon(Symbols.person_add_alt),
)
],
keyActionsBadge: oathShowActionNotifier(deviceInfo),
actionsBuilder: (context, expanded) {
final (fipsCapable, fipsApproved) =
deviceInfo?.getFipsStatus(Capability.oath) ?? (false, false);
return [
if (!expanded && (!fipsCapable || (fipsCapable && fipsApproved)))
ActionChip(
label: Text(l10n.s_add_account),
onPressed: () async {
await addOathAccount(
context,
ref,
widget.devicePath,
widget.oathState,
);
},
avatar: const Icon(Symbols.person_add_alt),
),
if (!expanded && fipsCapable && !fipsApproved)
ActionChip(
label: Text(l10n.s_set_password),
onPressed: () async {
await managePassword(
context, ref, widget.devicePath, widget.oathState);
},
avatar: const Icon(Symbols.person_add_alt),
)
];
},
title: l10n.s_accounts,
capabilities: const [Capability.oath],
key: keys.noAccountsView,
@ -225,6 +241,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
capabilities: const [Capability.oath],
centered: true,
delayedContent: true,
keyActionsBadge: oathShowActionNotifier(deviceInfo),
builder: (context, _) => const CircularProgressIndicator(),
);
}
@ -290,6 +307,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
alternativeTitle:
searchText != '' ? l10n.l_results_for(searchText) : null,
capabilities: const [Capability.oath],
keyActionsBadge: oathShowActionNotifier(deviceInfo),
keyActionsBuilder: hasActions
? (context) => oathBuildActions(
context,

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.
@ -35,6 +35,7 @@ import '../models.dart';
import 'add_account_dialog.dart';
import 'add_account_page.dart';
import 'add_multi_account_page.dart';
import 'manage_password_dialog.dart';
/// Calculates the available space for issuer and account name.
///
@ -178,3 +179,11 @@ Future<void> addOathAccount(BuildContext context, WidgetRef ref,
);
}
}
Future<void> managePassword(BuildContext context, WidgetRef ref,
DevicePath devicePath, OathState oathState) async {
await showBlurDialog(
context: context,
builder: (context) => ManagePasswordDialog(devicePath, oathState),
);
}