This commit is contained in:
Dain Nilsson 2024-03-11 16:41:18 +01:00
commit 52c613a14c
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
19 changed files with 130 additions and 142 deletions

View File

@ -64,8 +64,8 @@ Future<Widget> initialize() async {
oathStateProvider.overrideWithProvider(androidOathStateProvider.call),
credentialListProvider
.overrideWithProvider(androidCredentialListProvider.call),
currentAppProvider.overrideWith((ref) => AndroidSubPageNotifier(
ref.watch(supportedAppsProvider), ref.watch(prefProvider))),
currentSectionProvider.overrideWith((ref) => AndroidSectionNotifier(
ref.watch(supportedSectionsProvider), ref.watch(prefProvider))),
managementStateProvider.overrideWithProvider(androidManagementState.call),
currentDeviceProvider.overrideWith(
() => AndroidCurrentDeviceNotifier(),

View File

@ -90,18 +90,18 @@ final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
}
});
class AndroidSubPageNotifier extends CurrentAppNotifier {
AndroidSubPageNotifier(super.supportedApps, super.prefs) {
class AndroidSectionNotifier extends CurrentSectionNotifier {
AndroidSectionNotifier(super._supportedSections, super.prefs) {
_handleSubPage(state);
}
@override
void setCurrentApp(Application app) {
super.setCurrentApp(app);
_handleSubPage(app);
void setCurrentSection(Section section) {
super.setCurrentSection(section);
_handleSubPage(section);
}
void _handleSubPage(Application subPage) async {
void _handleSubPage(Section subPage) async {
await _contextChannel.invokeMethod('setContext', {'index': subPage.index});
}
}

View File

@ -16,11 +16,12 @@
import '../core/state.dart';
final home = root.feature('home');
final oath = root.feature('oath');
final fido = root.feature('fido');
final piv = root.feature('piv');
final otp = root.feature('otp');
final management = root.feature('management');
final home = root.feature('home');
final fingerprints = fido.feature('fingerprints');

View File

@ -31,43 +31,32 @@ const _listEquality = ListEquality();
enum Availability { enabled, disabled, unsupported }
enum Application {
enum Section {
home(),
accounts([Capability.oath]),
webauthn([Capability.u2f]),
securityKey([Capability.u2f]),
fingerprints([Capability.fido2]),
passkeys([Capability.fido2]),
certificates([Capability.piv]),
slots([Capability.otp]),
management();
slots([Capability.otp]);
final List<Capability> capabilities;
const Application([this.capabilities = const []]);
const Section([this.capabilities = const []]);
String getDisplayName(AppLocalizations l10n) => switch (this) {
Application.home => l10n.s_home,
Application.accounts => l10n.s_accounts,
Application.webauthn => l10n.s_webauthn,
Application.fingerprints => l10n.s_fingerprints,
Application.passkeys => l10n.s_passkeys,
Application.certificates => l10n.s_certificates,
Application.slots => l10n.s_slots,
_ => name.substring(0, 1).toUpperCase() + name.substring(1),
Section.home => l10n.s_home,
Section.accounts => l10n.s_accounts,
Section.securityKey => l10n.s_security_key,
Section.fingerprints => l10n.s_fingerprints,
Section.passkeys => l10n.s_passkeys,
Section.certificates => l10n.s_certificates,
Section.slots => l10n.s_slots,
};
Availability getAvailability(YubiKeyData data) {
if (this == Application.management) {
final version = data.info.version;
final available = (version.major > 4 || // YK5 and up
(version.major == 4 && version.minor >= 1) || // YK4.1 and up
version.major == 3); // NEO
// Management can't be disabled
return available ? Availability.enabled : Availability.unsupported;
}
// TODO: Require credman for passkeys?
if (this == Application.fingerprints) {
if (this == Section.fingerprints) {
if (!const {FormFactor.usbABio, FormFactor.usbCBio}
.contains(data.info.formFactor)) {
return Availability.unsupported;
@ -79,8 +68,8 @@ enum Application {
final int enabled =
data.info.config.enabledCapabilities[data.node.transport] ?? 0;
// Don't show WebAuthn if we have FIDO2
if (this == Application.webauthn &&
// Don't show securityKey if we have FIDO2
if (this == Section.securityKey &&
Capability.fido2.value & supported != 0) {
return Availability.unsupported;
}

View File

@ -38,23 +38,24 @@ const officialLocales = [
Locale('en', ''),
];
extension on Application {
extension on Section {
Feature get _feature => switch (this) {
Application.home => features.home,
Application.accounts => features.oath,
Application.webauthn => features.fido,
Application.passkeys => features.fido,
Application.fingerprints => features.fingerprints,
Application.slots => features.otp,
Application.certificates => features.piv,
Application.management => features.management,
Section.home => features.home,
Section.accounts => features.oath,
Section.securityKey => features.fido,
Section.passkeys => features.fido,
Section.fingerprints => features.fingerprints,
Section.slots => features.otp,
Section.certificates => features.piv,
};
}
final supportedAppsProvider = Provider<List<Application>>(
final supportedSectionsProvider = Provider<List<Section>>(
(ref) {
final hasFeature = ref.watch(featureProvider);
return Application.values.where((app) => hasFeature(app._feature)).toList();
return Section.values
.where((section) => hasFeature(section._feature))
.toList();
},
);
@ -201,62 +202,62 @@ abstract class CurrentDeviceNotifier extends Notifier<DeviceNode?> {
setCurrentDevice(DeviceNode? device);
}
final currentAppProvider =
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
final notifier = CurrentAppNotifier(
ref.watch(supportedAppsProvider), ref.watch(prefProvider));
final currentSectionProvider =
StateNotifierProvider<CurrentSectionNotifier, Section>((ref) {
final notifier = CurrentSectionNotifier(
ref.watch(supportedSectionsProvider), ref.watch(prefProvider));
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
}, fireImmediately: true);
return notifier;
});
class CurrentAppNotifier extends StateNotifier<Application> {
final List<Application> _supportedApps;
static const String _key = 'APP_STATE_LAST_APP';
class CurrentSectionNotifier extends StateNotifier<Section> {
final List<Section> _supportedSections;
static const String _key = 'APP_STATE_LAST_SECTION';
final SharedPreferences _prefs;
CurrentAppNotifier(this._supportedApps, this._prefs)
: super(_fromName(_prefs.getString(_key), _supportedApps));
CurrentSectionNotifier(this._supportedSections, this._prefs)
: super(_fromName(_prefs.getString(_key), _supportedSections));
void setCurrentApp(Application app) {
state = app;
_prefs.setString(_key, app.name);
void setCurrentSection(Section section) {
state = section;
_prefs.setString(_key, section.name);
}
void _notifyDeviceChanged(YubiKeyData? data) {
if (data == null) {
state = _supportedApps.first;
state = _supportedSections.first;
return;
}
String? lastAppName = _prefs.getString(_key);
if (lastAppName != null && lastAppName != state.name) {
// Try switching to saved app
state = Application.values.firstWhere((app) => app.name == lastAppName);
state = Section.values.firstWhere((app) => app.name == lastAppName);
}
if (state == Application.passkeys &&
if (state == Section.passkeys &&
state.getAvailability(data) != Availability.enabled) {
state = Application.webauthn;
state = Section.securityKey;
}
if (state == Application.webauthn &&
if (state == Section.securityKey &&
state.getAvailability(data) != Availability.enabled) {
state = Application.passkeys;
state = Section.passkeys;
}
if (state.getAvailability(data) != Availability.unsupported) {
// Keep current app
return;
}
state = _supportedApps.firstWhere(
state = _supportedSections.firstWhere(
(app) => app.getAvailability(data) == Availability.enabled,
orElse: () => _supportedApps.first,
orElse: () => _supportedSections.first,
);
}
static Application _fromName(String? name, List<Application> supportedApps) =>
supportedApps.firstWhere((element) => element.name == name,
orElse: () => supportedApps.first);
static Section _fromName(String? name, List<Section> supportedSections) =>
supportedSections.firstWhere((element) => element.name == name,
orElse: () => supportedSections.first);
}
abstract class QrScanner {

View File

@ -64,9 +64,9 @@ class AppFailurePage extends ConsumerWidget {
case 'fido':
if (Platform.isWindows &&
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
final currentApp = ref.read(currentAppProvider);
title = currentApp.getDisplayName(l10n);
capabilities = currentApp.capabilities;
final currentSection = ref.read(currentSectionProvider);
title = currentSection.getDisplayName(l10n);
capabilities = currentSection.capabilities;
header = l10n.l_admin_privileges_required;
message = l10n.p_webauthn_elevated_permissions_required;
centered = false;

View File

@ -39,9 +39,9 @@ class DeviceErrorScreen extends ConsumerWidget {
if (pid.usbInterfaces == UsbInterface.fido.value) {
if (Platform.isWindows &&
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
final currentApp = ref.read(currentAppProvider);
final currentSection = ref.read(currentSectionProvider);
return HomeMessagePage(
capabilities: currentApp.capabilities,
capabilities: currentSection.capabilities,
header: l10n.l_admin_privileges_required,
message: l10n.p_elevated_permissions_required,
actionsBuilder: (context, expanded) => [

View File

@ -118,8 +118,8 @@ class MainPage extends ConsumerWidget {
} else {
return ref.watch(currentDeviceDataProvider).when(
data: (data) {
final app = ref.watch(currentAppProvider);
final capabilities = app.capabilities;
final section = ref.watch(currentSectionProvider);
final capabilities = section.capabilities;
if (data.info.supportedCapabilities.isEmpty &&
data.name == 'Unrecognized device') {
return HomeMessagePage(
@ -131,19 +131,20 @@ class MainPage extends ConsumerWidget {
),
header: l10n.s_yk_not_recognized,
);
} else if (app.getAvailability(data) ==
} else if (section.getAvailability(data) ==
Availability.unsupported) {
return MessagePage(
title: app.getDisplayName(l10n),
title: section.getDisplayName(l10n),
capabilities: capabilities,
header: l10n.s_app_not_supported,
message: l10n.l_app_not_supported_on_yk(capabilities
.map((c) => c.getDisplayName(l10n))
.join(',')),
);
} else if (app.getAvailability(data) != Availability.enabled) {
} else if (section.getAvailability(data) !=
Availability.enabled) {
return MessagePage(
title: app.getDisplayName(l10n),
title: section.getDisplayName(l10n),
capabilities: capabilities,
header: l10n.s_app_disabled,
message: l10n.l_app_disabled_desc(capabilities
@ -166,18 +167,14 @@ class MainPage extends ConsumerWidget {
);
}
return switch (app) {
Application.home => HomeScreen(data),
Application.accounts => OathScreen(data.node.path),
Application.webauthn => const WebAuthnScreen(),
Application.passkeys => PasskeysScreen(data),
Application.fingerprints => FingerprintsScreen(data),
Application.certificates => PivScreen(data.node.path),
Application.slots => OtpScreen(data.node.path),
_ => MessagePage(
header: l10n.s_app_not_supported,
message: l10n.l_app_not_supported_desc,
),
return switch (section) {
Section.home => HomeScreen(data),
Section.accounts => OathScreen(data.node.path),
Section.securityKey => const WebAuthnScreen(),
Section.passkeys => PasskeysScreen(data),
Section.fingerprints => FingerprintsScreen(data),
Section.certificates => PivScreen(data.node.path),
Section.slots => OtpScreen(data.node.path),
};
},
loading: () => DeviceErrorScreen(deviceNode),

View File

@ -86,27 +86,25 @@ class NavigationItem extends StatelessWidget {
}
}
extension on Application {
extension on Section {
IconData get _icon => switch (this) {
Application.accounts => Symbols.supervisor_account,
Application.webauthn => Symbols.security_key,
Application.passkeys => Symbols.passkey,
Application.fingerprints => Symbols.fingerprint,
Application.slots => Symbols.touch_app,
Application.certificates => Symbols.approval,
Application.management => Symbols.construction,
Application.home => Symbols.home
Section.home => Symbols.home,
Section.accounts => Symbols.supervisor_account,
Section.securityKey => Symbols.security_key,
Section.passkeys => Symbols.passkey,
Section.fingerprints => Symbols.fingerprint,
Section.slots => Symbols.touch_app,
Section.certificates => Symbols.badge,
};
Key get _key => switch (this) {
Application.accounts => oathAppDrawer,
Application.webauthn => u2fAppDrawer,
Application.passkeys => fidoPasskeysAppDrawer,
Application.fingerprints => fidoFingerprintsAppDrawer,
Application.slots => otpAppDrawer,
Application.certificates => pivAppDrawer,
Application.management => managementAppDrawer,
Application.home => homeDrawer,
Section.home => homeDrawer,
Section.accounts => oathAppDrawer,
Section.securityKey => u2fAppDrawer,
Section.passkeys => fidoPasskeysAppDrawer,
Section.fingerprints => fidoFingerprintsAppDrawer,
Section.slots => otpAppDrawer,
Section.certificates => pivAppDrawer,
};
}
@ -119,19 +117,18 @@ class NavigationContent extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final supportedApps = ref.watch(supportedAppsProvider);
final supportedSections = ref.watch(supportedSectionsProvider);
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
final availableApps = data != null
? supportedApps
.where(
(app) => app.getAvailability(data) != Availability.unsupported)
final availableSections = data != null
? supportedSections
.where((section) =>
section.getAvailability(data) != Availability.unsupported)
.toList()
: !isAndroid // TODO: Remove check when Home is implemented on Android
? [Application.home]
: <Application>[];
availableApps.remove(Application.management);
final currentApp = ref.watch(currentAppProvider);
? [Section.home]
: <Section>[];
final currentSection = ref.watch(currentSectionProvider);
return Padding(
padding: const EdgeInsets.all(8.0),
@ -147,21 +144,21 @@ class NavigationContent extends ConsumerWidget {
child: Column(
children: [
// Normal YubiKey Applications
...availableApps.map((app) => NavigationItem(
...availableSections.map((app) => NavigationItem(
key: app._key,
title: app.getDisplayName(l10n),
leading:
Icon(app._icon, fill: app == currentApp ? 1.0 : 0.0),
leading: Icon(app._icon,
fill: app == currentSection ? 1.0 : 0.0),
collapsed: !extended,
selected: app == currentApp,
onTap: data == null && currentApp == Application.home ||
selected: app == currentSection,
onTap: data == null && currentSection == Section.home ||
data != null &&
app.getAvailability(data) ==
Availability.enabled
? () {
ref
.read(currentAppProvider.notifier)
.setCurrentApp(app);
.read(currentSectionProvider.notifier)
.setCurrentSection(app);
if (shouldPop) {
Navigator.of(context).pop();
}

View File

@ -48,7 +48,7 @@ extension on Capability {
IconData get _icon => switch (this) {
Capability.oath => Symbols.supervisor_account,
Capability.fido2 => Symbols.passkey,
Capability.piv => Symbols.approval,
Capability.piv => Symbols.badge,
_ => throw UnsupportedError('Icon not defined'),
};
}

View File

@ -106,8 +106,8 @@ class _FidoLockedPage extends ConsumerWidget {
label: Text(l10n.s_setup_fingerprints),
onPressed: () async {
ref
.read(currentAppProvider.notifier)
.setCurrentApp(Application.fingerprints);
.read(currentSectionProvider.notifier)
.setCurrentSection(Section.fingerprints);
},
avatar: const Icon(Symbols.fingerprint),
),
@ -242,8 +242,8 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
label: Text(l10n.s_setup_fingerprints),
onPressed: () async {
ref
.read(currentAppProvider.notifier)
.setCurrentApp(Application.fingerprints);
.read(currentSectionProvider.notifier)
.setCurrentSection(Section.fingerprints);
},
avatar: const Icon(Symbols.fingerprint),
)

View File

@ -27,7 +27,7 @@ class WebAuthnScreen extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return MessagePage(
title: l10n.s_webauthn,
title: l10n.s_security_key,
capabilities: const [Capability.u2f],
header: l10n.l_ready_to_use,
message: l10n.l_register_sk_on_websites,

View File

@ -26,6 +26,7 @@ import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../../app/views/action_list.dart';
import '../../app/views/reset_dialog.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../../management/views/management_screen.dart';
@ -33,11 +34,13 @@ Widget homeBuildActions(
BuildContext context, YubiKeyData? deviceData, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider);
final managementAvailability =
deviceData == null || !hasFeature(features.management)
? Availability.unsupported
: Application.management.getAvailability(deviceData);
final managementAvailability = hasFeature(features.management) &&
switch (deviceData?.info.version) {
Version version => (version.major > 4 || // YK5 and up
(version.major == 4 && version.minor >= 1) || // YK4.1 and up
version.major == 3), // NEO,
null => false,
};
return Column(
children: [
@ -45,7 +48,7 @@ Widget homeBuildActions(
ActionListSection(
l10n.s_device,
children: [
if (managementAvailability == Availability.enabled)
if (managementAvailability)
ActionListItem(
feature: features.management,
icon: const Icon(Symbols.construction),

View File

@ -66,7 +66,7 @@
"s_settings": "Einstellungen",
"l_settings_desc": null,
"s_certificates": null,
"s_webauthn": "WebAuthn",
"s_security_key": null,
"s_slots": null,
"s_help_and_about": "Hilfe und Über",
"l_help_and_about_desc": null,

View File

@ -66,7 +66,7 @@
"s_settings": "Settings",
"l_settings_desc": "Change application preferences",
"s_certificates": "Certificates",
"s_webauthn": "WebAuthn",
"s_security_key": "Security Key",
"s_slots": "Slots",
"s_help_and_about": "Help and about",
"l_help_and_about_desc": "Troubleshoot and support",

View File

@ -66,7 +66,7 @@
"s_settings": "Paramètres",
"l_settings_desc": null,
"s_certificates": "Certificats",
"s_webauthn": "WebAuthn",
"s_security_key": null,
"s_slots": null,
"s_help_and_about": "Aide et à propos",
"l_help_and_about_desc": null,

View File

@ -66,7 +66,7 @@
"s_settings": "設定",
"l_settings_desc": null,
"s_certificates": "証明書",
"s_webauthn": "WebAuthn",
"s_security_key": null,
"s_slots": null,
"s_help_and_about": "ヘルプと概要",
"l_help_and_about_desc": null,

View File

@ -66,7 +66,7 @@
"s_settings": "Ustawienia",
"l_settings_desc": null,
"s_certificates": "Certyfikaty",
"s_webauthn": "WebAuthn",
"s_security_key": null,
"s_slots": "Sloty",
"s_help_and_about": "Pomoc i informacje",
"l_help_and_about_desc": null,

View File

@ -226,7 +226,7 @@ class _CertificateListItem extends ConsumerWidget {
leading: CircleAvatar(
foregroundColor: colorScheme.onSecondary,
backgroundColor: colorScheme.secondary,
child: const Icon(Symbols.approval),
child: const Icon(Symbols.badge),
),
title: slot.getDisplayName(l10n),
subtitle: certInfo != null