From aadf81a653b24f7aa29c70815b398d4718327412 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Wed, 20 Mar 2024 14:35:17 +0100 Subject: [PATCH 1/3] Add command to move key --- helper/helper/piv.py | 21 ++++ lib/desktop/piv/state.dart | 14 +++ lib/l10n/app_de.arb | 33 ++++++ lib/l10n/app_en.arb | 41 +++++++- lib/l10n/app_fr.arb | 33 ++++++ lib/l10n/app_ja.arb | 33 ++++++ lib/l10n/app_pl.arb | 33 ++++++ lib/piv/features.dart | 1 + lib/piv/keys.dart | 41 ++++++++ lib/piv/models.dart | 26 ++++- lib/piv/state.dart | 2 + lib/piv/views/actions.dart | 43 +++++++- lib/piv/views/move_key_dialog.dart | 153 ++++++++++++++++++++++++++++ lib/piv/views/piv_screen.dart | 60 +++++++++-- lib/widgets/choice_filter_chip.dart | 3 + 15 files changed, 518 insertions(+), 19 deletions(-) create mode 100644 lib/piv/views/move_key_dialog.dart diff --git a/helper/helper/piv.py b/helper/helper/piv.py index b5f43b0d..1618147a 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -416,6 +416,27 @@ class SlotNode(RpcNode): self._refresh() return dict() + @action(condition=lambda self: self.metadata) + def move_key(self, params, event, signal): + to_slot = params.pop("to_slot", None) + needs_overwrite = params.pop("needs_overwrite", False) + move_cert = params.pop("move_cert", False) + + if not to_slot: + raise ValueError("Missing destination slot") + + to_slot = SLOT(int(to_slot, base=16)) + if needs_overwrite: + self.session.delete_key(to_slot) + self.session.move_key(self.slot, to_slot) + if move_cert: + self.session.put_certificate(to_slot, self.certificate) + self.session.delete_certificate(self.slot) + self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) + self.certificate = None + self._refresh() + return dict() + @action def import_file(self, params, event, signal): data = bytes.fromhex(params.pop("data")) diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index 17b38143..9b5c27d3 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -324,6 +324,20 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { ref.invalidateSelf(); } + @override + Future moveKey(SlotId fromSlot, SlotId toSlot, bool needsOverwrite, + bool moveCert) async { + await _session.command('move_key', target: [ + 'slots', + fromSlot.hexId + ], params: { + 'to_slot': toSlot.hexId, + 'needs_overwrite': needsOverwrite, + 'move_cert': moveCert + }); + ref.invalidateSelf(); + } + @override Future generate( SlotId slot, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 94b43152..00e7d064 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -28,6 +28,7 @@ "s_cancel": "Abbrechen", "s_close": "Schließen", "s_delete": "Löschen", + "s_move": null, "s_quit": "Beenden", "s_status": null, "s_unlock": "Entsperren", @@ -509,6 +510,8 @@ "l_delete_key_desc": null, "l_delete_certificate_or_key": null, "l_delete_certificate_or_key_desc": null, + "l_move_key": null, + "l_move_key_desc": null, "s_issuer": null, "s_serial": null, "s_certificate_fingerprint": null, @@ -551,6 +554,28 @@ "l_certificate_deleted": null, "l_key_deleted": null, "l_certificate_and_key_deleted": null, + "l_include_certificate": null, + "l_select_destination_slot": null, + "q_move_key_confirm": null, + "@q_move_key_confirm": { + "placeholders": { + "from_slot": {} + } + }, + "q_move_key_to_slot_confirm": null, + "@q_move_key_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "q_move_key_and_certificate_to_slot_confirm": null, + "@q_move_key_and_certificate_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, "p_password_protected_file": null, "p_import_items_desc": null, "@p_import_items_desc": { @@ -558,6 +583,8 @@ "slot": {} } }, + "l_key_moved": null, + "l_key_and_certificate_moved": null, "p_subject_desc": null, "l_rfc4514_invalid": null, "rfc4514_examples": null, @@ -581,6 +608,12 @@ "hexid": {} } }, + "s_retired_slot_display_name": null, + "@s_retired_slot_display_name": { + "placeholders": { + "hexid": {} + } + }, "s_slot_9a": null, "s_slot_9c": null, "s_slot_9d": null, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8568c435..8ae53bc3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -28,6 +28,7 @@ "s_cancel": "Cancel", "s_close": "Close", "s_delete": "Delete", + "s_move": "Move", "s_quit": "Quit", "s_status": "Status", "s_unlock": "Unlock", @@ -509,6 +510,8 @@ "l_delete_key_desc": "Remove the key from your YubiKey", "l_delete_certificate_or_key": "Delete certificate/key", "l_delete_certificate_or_key_desc": "Remove the certificate or key from your YubiKey", + "l_move_key": "Move key", + "l_move_key_desc": "Move a key from one PIV slot into another", "s_issuer": "Issuer", "s_serial": "Serial", "s_certificate_fingerprint": "Fingerprint", @@ -528,21 +531,21 @@ "s_private_key_generated": "Private key generated", "p_select_what_to_delete": "Select what to delete from the slot.", "p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.", - "p_warning_delete_key": "Warning! This action will delete the key from your YubiKey.", - "p_warning_delete_certificate_and_key": "Warning! This action will delete the certificate and key from your YubiKey.", + "p_warning_delete_key": "Warning! This action will delete the private key from your YubiKey.", + "p_warning_delete_certificate_and_key": "Warning! This action will delete the certificate and private key from your YubiKey.", "q_delete_certificate_confirm": "Delete the certificate in PIV slot {slot}?", "@q_delete_certificate_confirm": { "placeholders": { "slot": {} } }, - "q_delete_key_confirm": "Delete the key in PIV slot {slot}?", + "q_delete_key_confirm": "Delete the private key in PIV slot {slot}?", "@q_delete_key_confirm": { "placeholders": { "slot": {} } }, - "q_delete_certificate_and_key_confirm": "Delete the certificate and key in PIV slot {slot}?", + "q_delete_certificate_and_key_confirm": "Delete the certificate and private key in PIV slot {slot}?", "@q_delete_certificate_and_key_confirm": { "placeholders": { "slot": {} @@ -551,6 +554,28 @@ "l_certificate_deleted": "Certificate deleted", "l_key_deleted": "Key deleted", "l_certificate_and_key_deleted": "Certificate and key deleted", + "l_include_certificate": "Include certificate", + "l_select_destination_slot": "Select destination slot", + "q_move_key_confirm": "Move the private key in PIV slot {from_slot}?", + "@q_move_key_confirm": { + "placeholders": { + "from_slot": {} + } + }, + "q_move_key_to_slot_confirm": "Move the private key in PIV slot {from_slot} to slot {to_slot}?", + "@q_move_key_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "q_move_key_and_certificate_to_slot_confirm": "Move the private key and certificate in PIV slot {from_slot} to slot {to_slot}?", + "@q_move_key_and_certificate_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, "p_password_protected_file": "The selected file is password protected. Enter the password to proceed.", "p_import_items_desc": "The following item(s) will be imported into PIV slot {slot}.", "@p_import_items_desc": { @@ -558,6 +583,8 @@ "slot": {} } }, + "l_key_moved": "Key moved", + "l_key_and_certificate_moved": "Key and certificate moved", "p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.", "l_rfc4514_invalid": "Invalid RFC 4514 format", "rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", @@ -581,6 +608,12 @@ "hexid": {} } }, + "s_retired_slot_display_name": "Retired Key Management ({hexid})", + "@s_retired_slot_display_name": { + "placeholders": { + "hexid": {} + } + }, "s_slot_9a": "Authentication", "s_slot_9c": "Digital Signature", "s_slot_9d": "Key Management", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8564be9d..8c7f20c4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -28,6 +28,7 @@ "s_cancel": "Annuler", "s_close": "Fermer", "s_delete": "Supprimer", + "s_move": null, "s_quit": "Quitter", "s_status": null, "s_unlock": "Déverrouiller", @@ -509,6 +510,8 @@ "l_delete_key_desc": null, "l_delete_certificate_or_key": null, "l_delete_certificate_or_key_desc": null, + "l_move_key": null, + "l_move_key_desc": null, "s_issuer": "Émetteur", "s_serial": "Série", "s_certificate_fingerprint": "Empreinte digitale", @@ -551,6 +554,28 @@ "l_certificate_deleted": "Certificat supprimé", "l_key_deleted": null, "l_certificate_and_key_deleted": null, + "l_include_certificate": null, + "l_select_destination_slot": null, + "q_move_key_confirm": null, + "@q_move_key_confirm": { + "placeholders": { + "from_slot": {} + } + }, + "q_move_key_to_slot_confirm": null, + "@q_move_key_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "q_move_key_and_certificate_to_slot_confirm": null, + "@q_move_key_and_certificate_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, "p_password_protected_file": "Le fichier sélectionné est protégé par un mot de passe. Enterez le mot de passe pour continuer.", "p_import_items_desc": "Les éléments suivants seront importés dans le slot PIV {slot}.", "@p_import_items_desc": { @@ -558,6 +583,8 @@ "slot": {} } }, + "l_key_moved": null, + "l_key_and_certificate_moved": null, "p_subject_desc": "Un nom distinctif (DN) formaté conformément à la spécification RFC 4514.", "l_rfc4514_invalid": "Format RFC 4514 invalide", "rfc4514_examples": "Exemples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", @@ -581,6 +608,12 @@ "hexid": {} } }, + "s_retired_slot_display_name": null, + "@s_retired_slot_display_name": { + "placeholders": { + "hexid": {} + } + }, "s_slot_9a": "Authentification", "s_slot_9c": "Signature digitale", "s_slot_9d": "Gestion des clés", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index d6d62b5f..d5c8d323 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -28,6 +28,7 @@ "s_cancel": "キャンセル", "s_close": "閉じる", "s_delete": "消去", + "s_move": null, "s_quit": "終了", "s_status": null, "s_unlock": "ロック解除", @@ -509,6 +510,8 @@ "l_delete_key_desc": null, "l_delete_certificate_or_key": null, "l_delete_certificate_or_key_desc": null, + "l_move_key": null, + "l_move_key_desc": null, "s_issuer": "発行者", "s_serial": "シリアル番号", "s_certificate_fingerprint": "指紋", @@ -551,6 +554,28 @@ "l_certificate_deleted": "証明書が削除されました", "l_key_deleted": null, "l_certificate_and_key_deleted": null, + "l_include_certificate": null, + "l_select_destination_slot": null, + "q_move_key_confirm": null, + "@q_move_key_confirm": { + "placeholders": { + "from_slot": {} + } + }, + "q_move_key_to_slot_confirm": null, + "@q_move_key_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "q_move_key_and_certificate_to_slot_confirm": null, + "@q_move_key_and_certificate_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, "p_password_protected_file": "選択したファイルはパスワードで保護されています。パスワードを入力して続行します", "p_import_items_desc": "次のアイテムはPIVスロット{slot}にインポートされます", "@p_import_items_desc": { @@ -558,6 +583,8 @@ "slot": {} } }, + "l_key_moved": null, + "l_key_and_certificate_moved": null, "p_subject_desc": "RFC 4514フォーマットの識別名識別名 (DN)", "l_rfc4514_invalid": "無効な RFC 4514 形式です", "rfc4514_examples": "例:\nCN=Example Name\nCN=jsmith,DC=example,DC=net", @@ -581,6 +608,12 @@ "hexid": {} } }, + "s_retired_slot_display_name": null, + "@s_retired_slot_display_name": { + "placeholders": { + "hexid": {} + } + }, "s_slot_9a": "認証", "s_slot_9c": "デジタル署名", "s_slot_9d": "鍵の管理", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 1511069d..52bea735 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -28,6 +28,7 @@ "s_cancel": "Anuluj", "s_close": "Zamknij", "s_delete": "Usuń", + "s_move": null, "s_quit": "Wyjdź", "s_status": "Status", "s_unlock": "Odblokuj", @@ -509,6 +510,8 @@ "l_delete_key_desc": null, "l_delete_certificate_or_key": null, "l_delete_certificate_or_key_desc": null, + "l_move_key": null, + "l_move_key_desc": null, "s_issuer": "Wydawca", "s_serial": "Nr. seryjny", "s_certificate_fingerprint": "Odcisk palca", @@ -551,6 +554,28 @@ "l_certificate_deleted": "Certyfikat został usunięty", "l_key_deleted": null, "l_certificate_and_key_deleted": null, + "l_include_certificate": null, + "l_select_destination_slot": null, + "q_move_key_confirm": null, + "@q_move_key_confirm": { + "placeholders": { + "from_slot": {} + } + }, + "q_move_key_to_slot_confirm": null, + "@q_move_key_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, + "q_move_key_and_certificate_to_slot_confirm": null, + "@q_move_key_and_certificate_to_slot_confirm": { + "placeholders": { + "from_slot": {}, + "to_slot": {} + } + }, "p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.", "p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.", "@p_import_items_desc": { @@ -558,6 +583,8 @@ "slot": {} } }, + "l_key_moved": null, + "l_key_and_certificate_moved": null, "p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.", "l_rfc4514_invalid": "Nieprawidłowy format RFC 4514", "rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl", @@ -581,6 +608,12 @@ "hexid": {} } }, + "s_retired_slot_display_name": null, + "@s_retired_slot_display_name": { + "placeholders": { + "hexid": {} + } + }, "s_slot_9a": "Uwierzytelnienie", "s_slot_9c": "Cyfrowy podpis", "s_slot_9d": "Menedżer kluczy", diff --git a/lib/piv/features.dart b/lib/piv/features.dart index a96629e6..9d4c4ea9 100644 --- a/lib/piv/features.dart +++ b/lib/piv/features.dart @@ -29,3 +29,4 @@ final slotsGenerate = slots.feature('generate'); final slotsImport = slots.feature('import'); final slotsExport = slots.feature('export'); final slotsDelete = slots.feature('delete'); +final slotsMove = slots.feature('move'); diff --git a/lib/piv/keys.dart b/lib/piv/keys.dart index 23394177..33293c12 100644 --- a/lib/piv/keys.dart +++ b/lib/piv/keys.dart @@ -32,6 +32,7 @@ const generateAction = Key('$_slotAction.generate'); const importAction = Key('$_slotAction.import'); const exportAction = Key('$_slotAction.export'); const deleteAction = Key('$_slotAction.delete'); +const moveAction = Key('$_slotAction.move'); const saveButton = Key('$_prefix.save'); const deleteButton = Key('$_prefix.delete'); @@ -50,11 +51,51 @@ const meatballButton9a = Key('$_prefix.9a.meatball.button'); const meatballButton9c = Key('$_prefix.9c.meatball.button'); const meatballButton9d = Key('$_prefix.9d.meatball.button'); const meatballButton9e = Key('$_prefix.9e.meatball.button'); +const meatballButton82 = Key('$_prefix.82.meatball.button'); +const meatballButton83 = Key('$_prefix.83.meatball.button'); +const meatballButton84 = Key('$_prefix.84.meatball.button'); +const meatballButton85 = Key('$_prefix.85.meatball.button'); +const meatballButton86 = Key('$_prefix.86.meatball.button'); +const meatballButton87 = Key('$_prefix.87.meatball.button'); +const meatballButton88 = Key('$_prefix.88.meatball.button'); +const meatballButton89 = Key('$_prefix.89.meatball.button'); +const meatballButton8a = Key('$_prefix.8a.meatball.button'); +const meatballButton8b = Key('$_prefix.8b.meatball.button'); +const meatballButton8c = Key('$_prefix.8c.meatball.button'); +const meatballButton8d = Key('$_prefix.8d.meatball.button'); +const meatballButton8e = Key('$_prefix.8e.meatball.button'); +const meatballButton8f = Key('$_prefix.8f.meatball.button'); +const meatballButton90 = Key('$_prefix.90.meatball.button'); +const meatballButton91 = Key('$_prefix.91.meatball.button'); +const meatballButton92 = Key('$_prefix.92.meatball.button'); +const meatballButton93 = Key('$_prefix.93.meatball.button'); +const meatballButton94 = Key('$_prefix.94.meatball.button'); +const meatballButton95 = Key('$_prefix.95.meatball.button'); const appListItem9a = Key('$_prefix.9a.applistitem'); const appListItem9c = Key('$_prefix.9c.applistitem'); const appListItem9d = Key('$_prefix.9d.applistitem'); const appListItem9e = Key('$_prefix.9e.applistitem'); +const appListItem82 = Key('$_prefix.82.applistitem'); +const appListItem83 = Key('$_prefix.83.applistitem'); +const appListItem84 = Key('$_prefix.84.applistitem'); +const appListItem85 = Key('$_prefix.85.applistitem'); +const appListItem86 = Key('$_prefix.86.applistitem'); +const appListItem87 = Key('$_prefix.87.applistitem'); +const appListItem88 = Key('$_prefix.88.applistitem'); +const appListItem89 = Key('$_prefix.89.applistitem'); +const appListItem8a = Key('$_prefix.8a.applistitem'); +const appListItem8b = Key('$_prefix.8b.applistitem'); +const appListItem8c = Key('$_prefix.8c.applistitem'); +const appListItem8d = Key('$_prefix.8d.applistitem'); +const appListItem8e = Key('$_prefix.8e.applistitem'); +const appListItem8f = Key('$_prefix.8f.applistitem'); +const appListItem90 = Key('$_prefix.90.applistitem'); +const appListItem91 = Key('$_prefix.91.applistitem'); +const appListItem92 = Key('$_prefix.92.applistitem'); +const appListItem93 = Key('$_prefix.93.applistitem'); +const appListItem94 = Key('$_prefix.94.applistitem'); +const appListItem95 = Key('$_prefix.95.applistitem'); // SlotMetadata body keys const slotMetadataKeyType = Key('$_prefix.slotMetadata.keyType'); diff --git a/lib/piv/models.dart b/lib/piv/models.dart index 3d790024..bd28e24d 100644 --- a/lib/piv/models.dart +++ b/lib/piv/models.dart @@ -47,10 +47,31 @@ enum SlotId { authentication(0x9a), signature(0x9c), keyManagement(0x9d), - cardAuth(0x9e); + cardAuth(0x9e), + retired1(0x82, true), + retired2(0x83, true), + retired3(0x84, true), + retired4(0x85, true), + retired5(0x86, true), + retired6(0x87, true), + retired7(0x88, true), + retired8(0x89, true), + retired9(0x8a, true), + retired10(0x8b, true), + retired11(0x8c, true), + retired12(0x8d, true), + retired13(0x8e, true), + retired14(0x8f, true), + retired15(0x90, true), + retired16(0x91, true), + retired17(0x92, true), + retired18(0x93, true), + retired19(0x94, true), + retired20(0x95, true); final int id; - const SlotId(this.id); + final bool isRetired; + const SlotId(this.id, [this.isRetired = false]); String get hexId => id.toRadixString(16).padLeft(2, '0'); @@ -61,6 +82,7 @@ enum SlotId { SlotId.signature => nameFor(l10n.s_slot_9c), SlotId.keyManagement => nameFor(l10n.s_slot_9d), SlotId.cardAuth => nameFor(l10n.s_slot_9e), + _ => l10n.s_retired_slot_display_name(hexId) }; } diff --git a/lib/piv/state.dart b/lib/piv/state.dart index 7e99f7a0..55368b19 100644 --- a/lib/piv/state.dart +++ b/lib/piv/state.dart @@ -67,4 +67,6 @@ abstract class PivSlotsNotifier TouchPolicy touchPolicy = TouchPolicy.dfault, }); Future delete(SlotId slot, bool deleteCert, bool deleteKey); + Future moveKey( + SlotId fromSlot, SlotId toSlot, bool needsOverwrite, bool moveCert); } diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index 2e202266..117ac680 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -35,6 +35,7 @@ import 'authentication_dialog.dart'; import 'delete_certificate_dialog.dart'; import 'generate_key_dialog.dart'; import 'import_file_dialog.dart'; +import 'move_key_dialog.dart'; import 'pin_dialog.dart'; class GenerateIntent extends Intent { @@ -52,6 +53,11 @@ class ExportIntent extends Intent { const ExportIntent(this.slot); } +class MoveIntent extends Intent { + final PivSlot slot; + const MoveIntent(this.slot); +} + Future _authIfNeeded(BuildContext context, WidgetRef ref, DevicePath devicePath, PivState pivState) async { if (pivState.needsAuth) { @@ -270,6 +276,25 @@ class PivActions extends ConsumerWidget { false); return deleted; }), + if (hasFeature(features.slotsMove)) + MoveIntent: CallbackAction(onInvoke: (intent) async { + if (!await withContext((context) => + _authIfNeeded(context, ref, devicePath, pivState))) { + return false; + } + + final bool? moved = await withContext((context) async => + await showBlurDialog( + context: context, + builder: (context) => MoveKeyDialog( + devicePath, + pivState, + intent.slot, + ), + ) ?? + false); + return moved; + }), }, child: Builder( // Builder to ensure new scope for actions, they can invoke parent actions @@ -288,7 +313,7 @@ List buildSlotActions( PivState pivState, PivSlot slot, AppLocalizations l10n) { final hasCert = slot.certInfo != null; final hasKey = slot.metadata != null; - final canDeleteKey = hasKey && pivState.version.isAtLeast(5, 7); + final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7); return [ ActionItem( key: keys.generateAction, @@ -326,23 +351,33 @@ List buildSlotActions( intent: ExportIntent(slot), ), ], - if (hasCert || canDeleteKey) + if (hasCert || canDeleteOrMoveKey) ActionItem( key: keys.deleteAction, feature: features.slotsDelete, actionStyle: ActionStyle.error, icon: const Icon(Symbols.delete), - title: hasCert && canDeleteKey + title: hasCert && canDeleteOrMoveKey ? l10n.l_delete_certificate_or_key : hasCert ? l10n.l_delete_certificate : l10n.l_delete_key, - subtitle: hasCert && canDeleteKey + subtitle: hasCert && canDeleteOrMoveKey ? l10n.l_delete_certificate_or_key_desc : hasCert ? l10n.l_delete_certificate_desc : l10n.l_delete_key_desc, intent: DeleteIntent(slot), ), + if (canDeleteOrMoveKey) + ActionItem( + key: keys.moveAction, + feature: features.slotsMove, + actionStyle: ActionStyle.error, + icon: const Icon(Symbols.move_item), + title: l10n.l_move_key, + subtitle: l10n.l_move_key_desc, + intent: MoveIntent(slot), + ), ]; } diff --git a/lib/piv/views/move_key_dialog.dart b/lib/piv/views/move_key_dialog.dart new file mode 100644 index 00000000..ab63ee9c --- /dev/null +++ b/lib/piv/views/move_key_dialog.dart @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +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/choice_filter_chip.dart'; +import '../../widgets/responsive_dialog.dart'; +import '../keys.dart' as keys; +import '../models.dart'; +import '../state.dart'; +import 'overwrite_confirm_dialog.dart'; + +class MoveKeyDialog extends ConsumerStatefulWidget { + final DevicePath devicePath; + final PivState pivState; + final PivSlot pivSlot; + const MoveKeyDialog(this.devicePath, this.pivState, this.pivSlot, + {super.key}); + + @override + ConsumerState createState() => _MoveKeyDialogState(); +} + +class _MoveKeyDialogState extends ConsumerState { + SlotId? _toSlot; + bool _moveCert = true; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return ResponsiveDialog( + title: Text(l10n.l_move_key), + actions: [ + TextButton( + key: keys.deleteButton, + onPressed: _toSlot != null + ? () async { + try { + final pivSlots = + ref.read(pivSlotsProvider(widget.devicePath)).asData; + if (pivSlots != null) { + final toSlot = pivSlots.value + .firstWhere((element) => element.slot == _toSlot); + + if (!await confirmOverwrite(context, toSlot, + writeKey: true, writeCert: _moveCert)) { + return; + } + + await ref + .read(pivSlotsProvider(widget.devicePath).notifier) + .moveKey(widget.pivSlot.slot, toSlot.slot, + toSlot.metadata != null, _moveCert); + + await ref.read(withContextProvider)( + (context) async { + String message; + if (_moveCert) { + message = l10n.l_key_and_certificate_moved; + } else { + message = l10n.l_key_moved; + } + + Navigator.of(context).pop(true); + showMessage(context, message); + }, + ); + } + } on CancellationException catch (_) { + // ignored + } + } + : null, + child: Text(l10n.s_move), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_toSlot == null + ? l10n.q_move_key_confirm( + widget.pivSlot.slot.getDisplayName(l10n)) + : widget.pivSlot.certInfo != null && _moveCert + ? l10n.q_move_key_and_certificate_to_slot_confirm( + widget.pivSlot.slot.getDisplayName(l10n), + _toSlot!.getDisplayName(l10n)) + : l10n.q_move_key_to_slot_confirm( + widget.pivSlot.slot.getDisplayName(l10n), + _toSlot!.getDisplayName(l10n))), + Wrap( + spacing: 4.0, + runSpacing: 8.0, + children: [ + ChoiceFilterChip( + menuConstraints: const BoxConstraints(maxHeight: 200), + value: _toSlot, + items: SlotId.values + .where((element) => element != widget.pivSlot.slot) + .toList(), + labelBuilder: (value) => Text(_toSlot == null + ? l10n.l_select_destination_slot + : _toSlot!.getDisplayName(l10n)), + itemBuilder: (value) => Text(value!.getDisplayName(l10n)), + onChanged: (value) { + setState(() { + _toSlot = value; + }); + }, + ), + if (widget.pivSlot.certInfo != null) + FilterChip( + label: Text(l10n.l_include_certificate), + selected: _moveCert, + onSelected: (value) { + setState(() { + _moveCert = value; + }); + }) + ], + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } +} diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index 6a8c0f62..a80a2ec4 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -185,14 +185,16 @@ class _PivScreenState extends ConsumerState { child: Column( children: [ if (pivSlots?.hasValue == true) - ...pivSlots!.value.map( - (e) => _CertificateListItem( - pivState, - e, - expanded: expanded, - selected: e == selected, - ), - ), + ...pivSlots!.value + .where((element) => !element.slot.isRetired) + .map( + (e) => _CertificateListItem( + pivState, + e, + expanded: expanded, + selected: e == selected, + ), + ), ], ), ); @@ -258,12 +260,52 @@ class _CertificateListItem extends ConsumerWidget { SlotId.signature => meatballButton9c, SlotId.keyManagement => meatballButton9d, SlotId.cardAuth => meatballButton9e, + SlotId.retired1 => meatballButton82, + SlotId.retired2 => meatballButton83, + SlotId.retired3 => meatballButton84, + SlotId.retired4 => meatballButton85, + SlotId.retired5 => meatballButton86, + SlotId.retired6 => meatballButton87, + SlotId.retired7 => meatballButton88, + SlotId.retired8 => meatballButton89, + SlotId.retired9 => meatballButton8a, + SlotId.retired10 => meatballButton8b, + SlotId.retired11 => meatballButton8c, + SlotId.retired12 => meatballButton8d, + SlotId.retired13 => meatballButton8e, + SlotId.retired14 => meatballButton8f, + SlotId.retired15 => meatballButton90, + SlotId.retired16 => meatballButton91, + SlotId.retired17 => meatballButton92, + SlotId.retired18 => meatballButton93, + SlotId.retired19 => meatballButton94, + SlotId.retired20 => meatballButton95 }; Key _getAppListItemKey(SlotId slotId) => switch (slotId) { SlotId.authentication => appListItem9a, SlotId.signature => appListItem9c, SlotId.keyManagement => appListItem9d, - SlotId.cardAuth => appListItem9e + SlotId.cardAuth => appListItem9e, + SlotId.retired1 => appListItem82, + SlotId.retired2 => appListItem83, + SlotId.retired3 => appListItem84, + SlotId.retired4 => appListItem85, + SlotId.retired5 => appListItem86, + SlotId.retired6 => appListItem87, + SlotId.retired7 => appListItem88, + SlotId.retired8 => appListItem89, + SlotId.retired9 => appListItem8a, + SlotId.retired10 => appListItem8b, + SlotId.retired11 => appListItem8c, + SlotId.retired12 => appListItem8d, + SlotId.retired13 => appListItem8e, + SlotId.retired14 => appListItem8f, + SlotId.retired15 => appListItem90, + SlotId.retired16 => appListItem91, + SlotId.retired17 => appListItem92, + SlotId.retired18 => appListItem93, + SlotId.retired19 => appListItem94, + SlotId.retired20 => appListItem95 }; } diff --git a/lib/widgets/choice_filter_chip.dart b/lib/widgets/choice_filter_chip.dart index 93ef92d6..b66c82b3 100755 --- a/lib/widgets/choice_filter_chip.dart +++ b/lib/widgets/choice_filter_chip.dart @@ -29,6 +29,7 @@ class ChoiceFilterChip extends StatefulWidget { final Widget? avatar; final bool selected; final bool? disableHover; + final BoxConstraints? menuConstraints; const ChoiceFilterChip({ super.key, required this.value, @@ -40,6 +41,7 @@ class ChoiceFilterChip extends StatefulWidget { this.selected = false, this.disableHover, this.labelBuilder, + this.menuConstraints, }); @override @@ -63,6 +65,7 @@ class _ChoiceFilterChipState extends State> { Offset.zero & overlay.size, ); return await showMenu( + constraints: widget.menuConstraints, context: context, position: position, shape: const RoundedRectangleBorder( From c626f799792c8765091df1c39576a9f88841c332 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Wed, 20 Mar 2024 15:42:17 +0100 Subject: [PATCH 2/3] Show retired slots --- lib/piv/views/actions.dart | 56 ++++++++++++++++++----------------- lib/piv/views/piv_screen.dart | 39 ++++++++++++++++-------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/lib/piv/views/actions.dart b/lib/piv/views/actions.dart index 117ac680..d70bb3ac 100644 --- a/lib/piv/views/actions.dart +++ b/lib/piv/views/actions.dart @@ -315,23 +315,25 @@ List buildSlotActions( final hasKey = slot.metadata != null; final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7); return [ - ActionItem( - key: keys.generateAction, - feature: features.slotsGenerate, - icon: const Icon(Symbols.add), - actionStyle: ActionStyle.primary, - title: l10n.s_generate_key, - subtitle: l10n.l_generate_desc, - intent: GenerateIntent(slot), - ), - ActionItem( - key: keys.importAction, - feature: features.slotsImport, - icon: const Icon(Symbols.file_download), - title: l10n.l_import_file, - subtitle: l10n.l_import_desc, - intent: ImportIntent(slot), - ), + if (!slot.slot.isRetired) ...[ + ActionItem( + key: keys.generateAction, + feature: features.slotsGenerate, + icon: const Icon(Symbols.add), + actionStyle: ActionStyle.primary, + title: l10n.s_generate_key, + subtitle: l10n.l_generate_desc, + intent: GenerateIntent(slot), + ), + ActionItem( + key: keys.importAction, + feature: features.slotsImport, + icon: const Icon(Symbols.file_download), + title: l10n.l_import_file, + subtitle: l10n.l_import_desc, + intent: ImportIntent(slot), + ), + ], if (hasCert) ...[ ActionItem( key: keys.exportAction, @@ -351,6 +353,16 @@ List buildSlotActions( intent: ExportIntent(slot), ), ], + if (canDeleteOrMoveKey) + ActionItem( + key: keys.moveAction, + feature: features.slotsMove, + actionStyle: ActionStyle.error, + icon: const Icon(Symbols.move_item), + title: l10n.l_move_key, + subtitle: l10n.l_move_key_desc, + intent: MoveIntent(slot), + ), if (hasCert || canDeleteOrMoveKey) ActionItem( key: keys.deleteAction, @@ -369,15 +381,5 @@ List buildSlotActions( : l10n.l_delete_key_desc, intent: DeleteIntent(slot), ), - if (canDeleteOrMoveKey) - ActionItem( - key: keys.moveAction, - feature: features.slotsMove, - actionStyle: ActionStyle.error, - icon: const Icon(Symbols.move_item), - title: l10n.l_move_key, - subtitle: l10n.l_move_key_desc, - intent: MoveIntent(slot), - ), ]; } diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index a80a2ec4..b52f2400 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -72,6 +72,16 @@ class _PivScreenState extends ConsumerState { final selected = _selected != null ? pivSlots?.value.firstWhere((e) => e.slot == _selected) : null; + final normalSlots = pivSlots?.value + .where((element) => !element.slot.isRetired) + .toList() ?? + []; + final shownRetiredSlots = pivSlots?.value + .where((element) => + element.slot.isRetired && + (element.certInfo != null && element.metadata != null)) + .toList() ?? + []; final theme = Theme.of(context); final textTheme = theme.textTheme; // This is what ListTile uses for subtitle @@ -184,17 +194,22 @@ class _PivScreenState extends ConsumerState { }, child: Column( children: [ - if (pivSlots?.hasValue == true) - ...pivSlots!.value - .where((element) => !element.slot.isRetired) - .map( - (e) => _CertificateListItem( - pivState, - e, - expanded: expanded, - selected: e == selected, - ), - ), + ...normalSlots.map( + (e) => _CertificateListItem( + pivState, + e, + expanded: expanded, + selected: e == selected, + ), + ), + ...shownRetiredSlots.map( + (e) => _CertificateListItem( + pivState, + e, + expanded: expanded, + selected: e == selected, + ), + ) ], ), ); @@ -231,7 +246,7 @@ class _CertificateListItem extends ConsumerWidget { leading: CircleAvatar( foregroundColor: colorScheme.onSecondary, backgroundColor: colorScheme.secondary, - child: const Icon(Symbols.badge), + child: Icon(slot.isRetired ? Symbols.manage_history : Symbols.badge), ), title: slot.getDisplayName(l10n), subtitle: certInfo != null From 0fa89e27c187966d38e93a336ae1549768f5c502 Mon Sep 17 00:00:00 2001 From: Elias Bonnici Date: Thu, 21 Mar 2024 14:23:29 +0100 Subject: [PATCH 3/3] Skip parsing of cert to avoid compression failures --- helper/helper/piv.py | 23 +++++++------- lib/desktop/piv/state.dart | 12 ++++---- lib/piv/state.dart | 4 +-- lib/piv/views/move_key_dialog.dart | 49 ++++++++++++++++++------------ 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/helper/helper/piv.py b/helper/helper/piv.py index 1618147a..42787226 100644 --- a/helper/helper/piv.py +++ b/helper/helper/piv.py @@ -418,19 +418,18 @@ class SlotNode(RpcNode): @action(condition=lambda self: self.metadata) def move_key(self, params, event, signal): - to_slot = params.pop("to_slot", None) - needs_overwrite = params.pop("needs_overwrite", False) - move_cert = params.pop("move_cert", False) + destination = params.pop("destination") + overwrite_key = params.pop("overwrite_key") + include_certificate = params.pop("include_certificate") - if not to_slot: - raise ValueError("Missing destination slot") - - to_slot = SLOT(int(to_slot, base=16)) - if needs_overwrite: - self.session.delete_key(to_slot) - self.session.move_key(self.slot, to_slot) - if move_cert: - self.session.put_certificate(to_slot, self.certificate) + if include_certificate: + source_object = self.session.get_object(OBJECT_ID.from_slot(self.slot)) + destination = SLOT(int(destination, base=16)) + if overwrite_key: + self.session.delete_key(destination) + self.session.move_key(self.slot, destination) + if include_certificate: + self.session.put_object(OBJECT_ID.from_slot(destination), source_object) self.session.delete_certificate(self.slot) self.session.put_object(OBJECT_ID.CHUID, generate_chuid()) self.certificate = None diff --git a/lib/desktop/piv/state.dart b/lib/desktop/piv/state.dart index 9b5c27d3..6ba6638d 100644 --- a/lib/desktop/piv/state.dart +++ b/lib/desktop/piv/state.dart @@ -325,15 +325,15 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier { } @override - Future moveKey(SlotId fromSlot, SlotId toSlot, bool needsOverwrite, - bool moveCert) async { + Future moveKey(SlotId source, SlotId destination, bool overwriteKey, + bool includeCertificate) async { await _session.command('move_key', target: [ 'slots', - fromSlot.hexId + source.hexId ], params: { - 'to_slot': toSlot.hexId, - 'needs_overwrite': needsOverwrite, - 'move_cert': moveCert + 'destination': destination.hexId, + 'overwrite_key': overwriteKey, + 'include_certificate': includeCertificate }); ref.invalidateSelf(); } diff --git a/lib/piv/state.dart b/lib/piv/state.dart index 55368b19..1ba4ca54 100644 --- a/lib/piv/state.dart +++ b/lib/piv/state.dart @@ -67,6 +67,6 @@ abstract class PivSlotsNotifier TouchPolicy touchPolicy = TouchPolicy.dfault, }); Future delete(SlotId slot, bool deleteCert, bool deleteKey); - Future moveKey( - SlotId fromSlot, SlotId toSlot, bool needsOverwrite, bool moveCert); + Future moveKey(SlotId source, SlotId destination, bool overwriteKey, + bool includeCertificate); } diff --git a/lib/piv/views/move_key_dialog.dart b/lib/piv/views/move_key_dialog.dart index ab63ee9c..e8a1b2a6 100644 --- a/lib/piv/views/move_key_dialog.dart +++ b/lib/piv/views/move_key_dialog.dart @@ -41,8 +41,14 @@ class MoveKeyDialog extends ConsumerStatefulWidget { } class _MoveKeyDialogState extends ConsumerState { - SlotId? _toSlot; - bool _moveCert = true; + SlotId? _destination; + late bool _includeCertificate; + + @override + void initState() { + super.initState(); + _includeCertificate = widget.pivSlot.certInfo != null; + } @override Widget build(BuildContext context) { @@ -53,29 +59,32 @@ class _MoveKeyDialogState extends ConsumerState { actions: [ TextButton( key: keys.deleteButton, - onPressed: _toSlot != null + onPressed: _destination != null ? () async { try { final pivSlots = ref.read(pivSlotsProvider(widget.devicePath)).asData; if (pivSlots != null) { - final toSlot = pivSlots.value - .firstWhere((element) => element.slot == _toSlot); + final destination = pivSlots.value.firstWhere( + (element) => element.slot == _destination); - if (!await confirmOverwrite(context, toSlot, - writeKey: true, writeCert: _moveCert)) { + if (!await confirmOverwrite(context, destination, + writeKey: true, writeCert: _includeCertificate)) { return; } await ref .read(pivSlotsProvider(widget.devicePath).notifier) - .moveKey(widget.pivSlot.slot, toSlot.slot, - toSlot.metadata != null, _moveCert); + .moveKey( + widget.pivSlot.slot, + destination.slot, + destination.metadata != null, + _includeCertificate); await ref.read(withContextProvider)( (context) async { String message; - if (_moveCert) { + if (_includeCertificate) { message = l10n.l_key_and_certificate_moved; } else { message = l10n.l_key_moved; @@ -99,43 +108,43 @@ class _MoveKeyDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_toSlot == null + Text(_destination == null ? l10n.q_move_key_confirm( widget.pivSlot.slot.getDisplayName(l10n)) - : widget.pivSlot.certInfo != null && _moveCert + : widget.pivSlot.certInfo != null && _includeCertificate ? l10n.q_move_key_and_certificate_to_slot_confirm( widget.pivSlot.slot.getDisplayName(l10n), - _toSlot!.getDisplayName(l10n)) + _destination!.getDisplayName(l10n)) : l10n.q_move_key_to_slot_confirm( widget.pivSlot.slot.getDisplayName(l10n), - _toSlot!.getDisplayName(l10n))), + _destination!.getDisplayName(l10n))), Wrap( spacing: 4.0, runSpacing: 8.0, children: [ ChoiceFilterChip( menuConstraints: const BoxConstraints(maxHeight: 200), - value: _toSlot, + value: _destination, items: SlotId.values .where((element) => element != widget.pivSlot.slot) .toList(), - labelBuilder: (value) => Text(_toSlot == null + labelBuilder: (value) => Text(_destination == null ? l10n.l_select_destination_slot - : _toSlot!.getDisplayName(l10n)), + : _destination!.getDisplayName(l10n)), itemBuilder: (value) => Text(value!.getDisplayName(l10n)), onChanged: (value) { setState(() { - _toSlot = value; + _destination = value; }); }, ), if (widget.pivSlot.certInfo != null) FilterChip( label: Text(l10n.l_include_certificate), - selected: _moveCert, + selected: _includeCertificate, onSelected: (value) { setState(() { - _moveCert = value; + _includeCertificate = value; }); }) ],