This commit is contained in:
Dain Nilsson 2024-08-13 17:37:51 +02:00
commit bb1027f531
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
13 changed files with 250 additions and 47 deletions

View File

@ -30,6 +30,7 @@ import '../../widgets/delayed_visibility.dart';
import '../../widgets/file_drop_target.dart';
import '../message.dart';
import '../shortcuts.dart';
import '../state.dart';
import 'fs_dialog.dart';
import 'keys.dart';
import 'navigation.dart';
@ -764,23 +765,49 @@ class _GestureDetectorAppBar extends StatelessWidget
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class CapabilityBadge extends StatelessWidget {
class CapabilityBadge extends ConsumerWidget {
final Capability capability;
final bool noTooltip;
const CapabilityBadge(this.capability, {super.key});
const CapabilityBadge(this.capability, {super.key, this.noTooltip = false});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final colorScheme = Theme.of(context).colorScheme;
final text = Text(capability.getDisplayName(l10n));
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(capability) ??
(false, false);
final label = fipsCapable
? Row(
children: [
Icon(
Symbols.shield,
color: colorScheme.onSecondaryContainer,
size: 12,
fill: fipsApproved ? 1 : 0,
),
const SizedBox(width: 4),
text,
],
)
: text;
return Badge(
backgroundColor: colorScheme.secondaryContainer,
textColor: colorScheme.onSecondaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 6),
largeSize: MediaQuery.of(context).textScaler.scale(20),
label: Text(
capability.getDisplayName(l10n),
),
label: fipsCapable && !noTooltip
? Tooltip(
message:
fipsApproved ? l10n.l_fips_approved : l10n.l_fips_capable,
child: label,
)
: label,
);
}
}

View File

@ -92,9 +92,14 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
runSpacing: 8,
children: Capability.values
.where((c) => enabledCapabilities & c.value != 0)
.map((c) => CapabilityBadge(c))
.map((c) => CapabilityBadge(c, noTooltip: true))
.toList(),
),
if (widget.deviceData.info.fipsCapable != 0)
Padding(
padding: const EdgeInsets.only(top: 16),
child: _FipsLegend(),
),
if (serial != null) ...[
const SizedBox(height: 32.0),
_DeviceColor(
@ -131,6 +136,62 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
}
}
class _FipsLegend extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Opacity(
opacity: 0.6,
child: Wrap(
runSpacing: 0,
spacing: 16,
children: [
RichText(
text: TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(
Symbols.shield,
size: 12,
fill: 0.0,
),
),
),
TextSpan(
text: l10n.l_fips_capable,
style: Theme.of(context).textTheme.bodySmall),
],
),
),
RichText(
text: TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(
Symbols.shield,
size: 12,
fill: 1.0,
),
),
),
TextSpan(
text: l10n.l_fips_approved,
style: Theme.of(context).textTheme.bodySmall),
],
),
),
],
),
);
}
}
class _DeviceContent extends ConsumerWidget {
final YubiKeyData deviceData;
final KeyCustomization? initialCustomization;
@ -195,6 +256,29 @@ class _DeviceContent extends ConsumerWidget {
.titleSmall
?.apply(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
if (deviceData.info.pinComplexity)
Padding(
padding: const EdgeInsets.only(top: 12),
child: RichText(
text: TextSpan(children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: const EdgeInsets.only(right: 4),
child: Icon(
Symbols.check,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
)),
TextSpan(
text: l10n.l_pin_complexity,
style: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
]),
),
),
],
);
}

View File

@ -34,6 +34,7 @@ Widget homeBuildActions(
BuildContext context, YubiKeyData? deviceData, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider);
final interfacesLocked = deviceData?.info.resetBlocked != 0;
final managementAvailability = hasFeature(features.management) &&
switch (deviceData?.info.version) {
Version version => (version.major > 4 || // YK5 and up
@ -56,16 +57,21 @@ Widget homeBuildActions(
title: deviceData.info.version.major > 4
? l10n.s_toggle_applications
: l10n.s_toggle_interfaces,
subtitle: deviceData.info.version.major > 4
? l10n.l_toggle_applications_desc
: l10n.l_toggle_interfaces_desc,
onTap: (context) {
Navigator.of(context).popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) => ManagementScreen(deviceData),
);
},
subtitle: interfacesLocked
? 'Requires factory reset' // TODO: Replace with l10n
: (deviceData.info.version.major > 4
? l10n.l_toggle_applications_desc
: l10n.l_toggle_interfaces_desc),
onTap: interfacesLocked
? null
: (context) {
Navigator.of(context)
.popUntil((route) => route.isFirst);
showBlurDialog(
context: context,
builder: (context) => ManagementScreen(deviceData),
);
},
),
if (getResetCapabilities(hasFeature).any((c) =>
c.value &

View File

@ -160,6 +160,8 @@
}
},
"l_firmware_version": null,
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "YubiKey anschließen",
@ -345,6 +347,7 @@
"l_warning_default_puk": null,
"l_default_pin_used": null,
"l_default_puk_used": null,
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "Passwort",
@ -354,6 +357,7 @@
"s_show_password": null,
"s_hide_password": null,
"l_optional_password_protection": "Optionaler Passwortschutz",
"l_password_protection": null,
"s_new_password": "Neues Passwort",
"s_current_password": "Aktuelles Passwort",
"s_confirm_password": "Passwort bestätigen",
@ -367,6 +371,7 @@
"l_keystore_unavailable": "Passwortspeicher des Betriebssystems nicht verfügbar",
"l_remember_pw_failed": "Konnte Passwort nicht speichern",
"l_unlock_first": "Zuerst mit Passwort entsperren",
"l_set_password_first": null,
"l_enter_oath_pw": "Das OATH-Passwort für Ihren YubiKey eingeben",
"p_enter_current_password_or_reset": "Geben Sie Ihr aktuelles Passwort ein. Wenn Sie Ihr Passwort nicht wissen, müssen Sie den YubiKey zurücksetzen.",
"p_enter_new_password": "Geben Sie Ihr neues Passwort ein. Ein Passwort kann Buchstaben, Ziffern und spezielle Zeichen enthalten.",

View File

@ -160,6 +160,8 @@
}
},
"l_firmware_version": "Firmware version: {version}",
"l_fips_capable": "FIPS capable",
"l_fips_approved": "FIPS approved",
"@_yubikey_interactions": {},
"l_insert_yk": "Insert your YubiKey",
@ -345,6 +347,7 @@
"l_warning_default_puk": "Warning: Default PUK used",
"l_default_pin_used": "Default PIN used",
"l_default_puk_used": "Default PUK used",
"l_pin_complexity": "PIN complexity enforced",
"@_passwords": {},
"s_password": "Password",
@ -354,6 +357,7 @@
"s_show_password": "Show password",
"s_hide_password": "Hide password",
"l_optional_password_protection": "Optional password protection",
"l_password_protection": "Password protection of accounts",
"s_new_password": "New password",
"s_current_password": "Current password",
"s_confirm_password": "Confirm password",
@ -367,6 +371,7 @@
"l_keystore_unavailable": "OS Keystore unavailable",
"l_remember_pw_failed": "Failed to remember password",
"l_unlock_first": "Unlock with password first",
"l_set_password_first": "Set a password first",
"l_enter_oath_pw": "Enter the OATH password for your YubiKey",
"p_enter_current_password_or_reset": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.",
"p_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.",

View File

@ -160,6 +160,8 @@
}
},
"l_firmware_version": "Version du firmware : {version}",
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "Insérez votre YubiKey",
@ -345,6 +347,7 @@
"l_warning_default_puk": "Attention : PUK par défaut utilisé",
"l_default_pin_used": "Code PIN par défaut utilisé",
"l_default_puk_used": "PUK par défaut utilisé",
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "Mot de passe",
@ -354,6 +357,7 @@
"s_show_password": "Montrer mot de passe",
"s_hide_password": "Masquer mot de passe",
"l_optional_password_protection": "Protection par mot de passe facultative",
"l_password_protection": null,
"s_new_password": "Nouveau mot de passe",
"s_current_password": "Mot de passe actuel",
"s_confirm_password": "Confirmer mot de passe",
@ -367,6 +371,7 @@
"l_keystore_unavailable": "OS Keystore indisponible",
"l_remember_pw_failed": "Mémorisation mot de passe impossible",
"l_unlock_first": "Débloquez d'abord avec mot de passe",
"l_set_password_first": null,
"l_enter_oath_pw": "Saisissez le mot de passe OATH de votre YubiKey",
"p_enter_current_password_or_reset": "Saisissez votre mot de passe actuel. Vous ne connaissez votre mot de passe\u00a0? Réinitialisez la YubiKey.",
"p_enter_new_password": "Saisissez votre nouveau mot de passe. Un mot de passe peut inclure des lettres, chiffres et caractères spéciaux.",

View File

@ -160,6 +160,8 @@
}
},
"l_firmware_version": "ファームウェアバージョン: {version}",
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "YubiKeyを挿入してください",
@ -345,6 +347,7 @@
"l_warning_default_puk": "警告: デフォルトのPUKが使用されています",
"l_default_pin_used": "デフォルトのPINが使用されています",
"l_default_puk_used": "既定のPUKを使用",
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "パスワード",
@ -354,6 +357,7 @@
"s_show_password": "パスワードを表示",
"s_hide_password": "パスワードを非表示",
"l_optional_password_protection": "オプションのパスワード保護",
"l_password_protection": null,
"s_new_password": "新しいパスワード",
"s_current_password": "現在のパスワード",
"s_confirm_password": "パスワードを確認",
@ -367,6 +371,7 @@
"l_keystore_unavailable": "OSのキーストアを利用できません",
"l_remember_pw_failed": "パスワードを記憶できませんでした",
"l_unlock_first": "最初にパスワードでロックを解除",
"l_set_password_first": null,
"l_enter_oath_pw": "YubiKeyのOATHパスワードを入力",
"p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります。",
"p_enter_new_password": "新しいパスワードを入力してください。パスワードには文字、数字、特殊文字を含めることができます。",

View File

@ -160,6 +160,8 @@
}
},
"l_firmware_version": null,
"l_fips_capable": null,
"l_fips_approved": null,
"@_yubikey_interactions": {},
"l_insert_yk": "Podłącz klucz YubiKey",
@ -345,6 +347,7 @@
"l_warning_default_puk": null,
"l_default_pin_used": null,
"l_default_puk_used": null,
"l_pin_complexity": null,
"@_passwords": {},
"s_password": "Hasło",
@ -354,6 +357,7 @@
"s_show_password": "Pokaż hasło",
"s_hide_password": "Ukryj hasło",
"l_optional_password_protection": "Opcjonalna ochrona hasłem",
"l_password_protection": null,
"s_new_password": "Nowe hasło",
"s_current_password": "Aktualne hasło",
"s_confirm_password": "Potwierdź hasło",
@ -367,6 +371,7 @@
"l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny",
"l_remember_pw_failed": "Nie udało się zapamiętać hasła",
"l_unlock_first": "Najpierw odblokuj hasłem",
"l_set_password_first": null,
"l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey",
"p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.",
"p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.",

View File

@ -23,6 +23,7 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/action_list.dart';
import '../../management/models.dart';
import '../features.dart' as features;
import '../icon_provider/icon_pack_dialog.dart';
import '../keys.dart' as keys;
@ -39,6 +40,28 @@ Widget oathBuildActions(
}) {
final l10n = AppLocalizations.of(context)!;
final capacity = oathState.capacity;
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.oath) ??
(false, false);
final String? subtitle;
final bool enabled;
if (used == null) {
subtitle = l10n.l_unlock_first;
enabled = false;
} else if (fipsCapable & !fipsApproved) {
subtitle = l10n.l_set_password_first;
enabled = false;
} else if (capacity != null) {
subtitle = l10n.l_accounts_used(used, capacity);
enabled = capacity > used;
} else {
subtitle = null;
enabled = true;
}
return Column(
children: [
@ -47,14 +70,10 @@ Widget oathBuildActions(
feature: features.actionsAdd,
key: keys.addAccountAction,
title: l10n.s_add_account,
subtitle: used == null
? l10n.l_unlock_first
: (capacity != null
? l10n.l_accounts_used(used, capacity)
: null),
subtitle: subtitle,
actionStyle: ActionStyle.primary,
icon: const Icon(Symbols.person_add_alt),
onTap: used != null && (capacity == null || capacity > used)
onTap: enabled
? (context) async {
Navigator.of(context).popUntil((route) => route.isFirst);
await addOathAccount(context, ref, devicePath, oathState);
@ -82,7 +101,7 @@ Widget oathBuildActions(
feature: features.actionsPassword,
title:
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password,
subtitle: l10n.l_optional_password_protection,
subtitle: l10n.l_password_protection,
icon: const Icon(Symbols.password),
onTap: (context) {
Navigator.of(context).popUntil((route) => route.isFirst);

View File

@ -310,7 +310,16 @@ class PivActions extends ConsumerWidget {
}
List<ActionItem> buildSlotActions(
PivState pivState, PivSlot slot, AppLocalizations l10n) {
PivState pivState, PivSlot slot, bool fipsUnready, AppLocalizations l10n) {
if (fipsUnready) {
// TODO: Decide on final look and move strings to .arb file.
return [
ActionItem(
icon: const Icon(Symbols.add),
title: 'Provision slot',
subtitle: 'Change from default PIN/PUK/Management key first'),
];
}
final hasCert = slot.certInfo != null;
final hasKey = slot.metadata != null;
final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7);

View File

@ -25,6 +25,7 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../core/models.dart';
import '../../management/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_field.dart';
import '../../widgets/app_text_form_field.dart';
@ -176,6 +177,17 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
? currentKeyOrPin.length >= 4
: currentKeyOrPin.length == currentType.keyLength * 2;
final newLenOk = _keyController.text.length == hexLength;
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.piv) ??
(false, false);
final fipsUnready = fipsCapable && !fipsApproved;
final managementKeyTypes = ManagementKeyType.values.toList();
if (fipsCapable) {
managementKeyTypes.remove(ManagementKeyType.tdes);
}
return ResponsiveDialog(
title: Text(l10n.l_change_management_key),
@ -334,7 +346,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
children: [
if (widget.pivState.metadata != null)
ChoiceFilterChip<ManagementKeyType>(
items: ManagementKeyType.values,
items: managementKeyTypes,
value: _keyType,
selected: _keyType != currentType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
@ -344,16 +356,17 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
});
},
),
FilterChip(
key: keys.pinLockManagementKeyChip,
label: Text(l10n.s_protect_key),
selected: _storeKey,
onSelected: (value) {
setState(() {
_storeKey = value;
});
},
),
if (!fipsUnready)
FilterChip(
key: keys.pinLockManagementKeyChip,
label: Text(l10n.s_protect_key),
selected: _storeKey,
onSelected: (value) {
setState(() {
_storeKey = value;
});
},
),
]),
]
.map((e) => Padding(

View File

@ -24,6 +24,7 @@ import 'package:material_symbols_icons/symbols.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/action_list.dart';
import '../../app/views/app_failure_page.dart';
import '../../app/views/app_list_item.dart';
@ -57,6 +58,14 @@ class _PivScreenState extends ConsumerState<PivScreen> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider);
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.piv) ??
(false, false);
final fipsUnready = fipsCapable && !fipsApproved;
return ref.watch(pivStateProvider(widget.devicePath)).when(
loading: () => MessagePage(
title: l10n.s_certificates,
@ -168,8 +177,8 @@ class _PivScreenState extends ConsumerState<PivScreen> {
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
actions:
buildSlotActions(pivState, selected, l10n),
actions: buildSlotActions(
pivState, selected, fipsUnready, l10n),
),
],
)
@ -210,6 +219,7 @@ class _PivScreenState extends ConsumerState<PivScreen> {
e,
expanded: expanded,
selected: e == selected,
fipsUnready: fipsUnready,
),
),
...shownRetiredSlots.map(
@ -218,6 +228,7 @@ class _PivScreenState extends ConsumerState<PivScreen> {
e,
expanded: expanded,
selected: e == selected,
fipsUnready: fipsUnready,
),
)
],
@ -238,9 +249,12 @@ class _CertificateListItem extends ConsumerWidget {
final PivSlot pivSlot;
final bool expanded;
final bool selected;
final bool fipsUnready;
const _CertificateListItem(this.pivState, this.pivSlot,
{required this.expanded, required this.selected});
{required this.expanded,
required this.selected,
required this.fipsUnready});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -275,8 +289,8 @@ class _CertificateListItem extends ConsumerWidget {
),
tapIntent: isDesktop && !expanded ? null : OpenIntent(pivSlot),
doubleTapIntent: isDesktop && !expanded ? OpenIntent(pivSlot) : null,
buildPopupActions: hasFeature(features.slots)
? (context) => buildSlotActions(pivState, pivSlot, l10n)
buildPopupActions: hasFeature(features.slots) && !fipsUnready
? (context) => buildSlotActions(pivState, pivSlot, fipsUnready, l10n)
: null,
);
}

View File

@ -22,6 +22,7 @@ import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/action_list.dart';
import '../../app/views/fs_dialog.dart';
import '../../management/models.dart';
import '../models.dart';
import '../state.dart';
import 'actions.dart';
@ -34,12 +35,13 @@ class SlotDialog extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node;
if (node == null) {
var keyData = ref.watch(currentDeviceDataProvider).valueOrNull;
if (keyData == null) {
// The rest of this method assumes there is a device, and will throw an exception if not.
// This will never be shown, as the dialog will be immediately closed
return const SizedBox();
}
final devicePath = keyData.node.path;
final l10n = AppLocalizations.of(context)!;
final textTheme = Theme.of(context).textTheme;
@ -48,8 +50,11 @@ class SlotDialog extends ConsumerWidget {
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
final (fipsCapable, fipsApproved) =
keyData.info.getFipsStatus(Capability.piv);
final pivState = ref.watch(pivStateProvider(devicePath)).valueOrNull;
final slotData = ref.watch(pivSlotsProvider(devicePath).select((value) =>
value.whenOrNull(
data: (data) =>
data.firstWhere((element) => element.slot == pivSlot))));
@ -61,7 +66,7 @@ class SlotDialog extends ConsumerWidget {
final certInfo = slotData.certInfo;
final metadata = slotData.metadata;
return PivActions(
devicePath: node.path,
devicePath: devicePath,
pivState: pivState,
builder: (context) => ItemShortcuts(
item: slotData,
@ -113,7 +118,8 @@ class SlotDialog extends ConsumerWidget {
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
actions: buildSlotActions(pivState, slotData, l10n),
actions: buildSlotActions(
pivState, slotData, fipsCapable && !fipsApproved, l10n),
),
],
),