This commit is contained in:
Elias Bonnici 2024-03-22 10:18:22 +01:00
commit b04920b018
No known key found for this signature in database
GPG Key ID: 5EAC28EA3F980CCF
15 changed files with 561 additions and 37 deletions

View File

@ -416,6 +416,26 @@ class SlotNode(RpcNode):
self._refresh()
return dict()
@action(condition=lambda self: self.metadata)
def move_key(self, params, event, signal):
destination = params.pop("destination")
overwrite_key = params.pop("overwrite_key")
include_certificate = params.pop("include_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
self._refresh()
return dict()
@action
def import_file(self, params, event, signal):
data = bytes.fromhex(params.pop("data"))

View File

@ -324,6 +324,20 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier {
ref.invalidateSelf();
}
@override
Future<void> moveKey(SlotId source, SlotId destination, bool overwriteKey,
bool includeCertificate) async {
await _session.command('move_key', target: [
'slots',
source.hexId
], params: {
'destination': destination.hexId,
'overwrite_key': overwriteKey,
'include_certificate': includeCertificate
});
ref.invalidateSelf();
}
@override
Future<PivGenerateResult> generate(
SlotId slot,

View File

@ -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",
@ -523,6 +524,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,
@ -565,6 +568,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": {
@ -572,6 +597,8 @@
"slot": {}
}
},
"l_key_moved": null,
"l_key_and_certificate_moved": null,
"p_subject_desc": null,
"l_rfc4514_invalid": null,
"rfc4514_examples": null,
@ -595,6 +622,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,

View File

@ -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",
@ -523,6 +524,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",
@ -542,21 +545,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": {}
@ -565,6 +568,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": {
@ -572,6 +597,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",
@ -595,6 +622,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",

View File

@ -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",
@ -523,6 +524,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",
@ -565,6 +568,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": {
@ -572,6 +597,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",
@ -595,6 +622,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",

View File

@ -28,6 +28,7 @@
"s_cancel": "キャンセル",
"s_close": "閉じる",
"s_delete": "消去",
"s_move": null,
"s_quit": "終了",
"s_status": null,
"s_unlock": "ロック解除",
@ -523,6 +524,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": "指紋",
@ -565,6 +568,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": {
@ -572,6 +597,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",
@ -595,6 +622,12 @@
"hexid": {}
}
},
"s_retired_slot_display_name": null,
"@s_retired_slot_display_name": {
"placeholders": {
"hexid": {}
}
},
"s_slot_9a": "認証",
"s_slot_9c": "デジタル署名",
"s_slot_9d": "鍵の管理",

View File

@ -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",
@ -523,6 +524,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",
@ -565,6 +568,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": {
@ -572,6 +597,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",
@ -595,6 +622,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",

View File

@ -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');

View File

@ -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');

View File

@ -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)
};
}

View File

@ -79,4 +79,6 @@ abstract class PivSlotsNotifier
TouchPolicy touchPolicy = TouchPolicy.dfault,
});
Future<void> delete(SlotId slot, bool deleteCert, bool deleteKey);
Future<void> moveKey(SlotId source, SlotId destination, bool overwriteKey,
bool includeCertificate);
}

View File

@ -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<bool> _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<MoveIntent>(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,8 +313,9 @@ List<ActionItem> 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 [
if (!slot.slot.isRetired) ...[
ActionItem(
key: keys.generateAction,
feature: features.slotsGenerate,
@ -307,6 +333,7 @@ List<ActionItem> buildSlotActions(
subtitle: l10n.l_import_desc,
intent: ImportIntent(slot),
),
],
if (hasCert) ...[
ActionItem(
key: keys.exportAction,
@ -326,18 +353,28 @@ List<ActionItem> buildSlotActions(
intent: ExportIntent(slot),
),
],
if (hasCert || canDeleteKey)
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,
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

View File

@ -0,0 +1,162 @@
/*
* 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<ConsumerStatefulWidget> createState() => _MoveKeyDialogState();
}
class _MoveKeyDialogState extends ConsumerState<MoveKeyDialog> {
SlotId? _destination;
late bool _includeCertificate;
@override
void initState() {
super.initState();
_includeCertificate = widget.pivSlot.certInfo != null;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ResponsiveDialog(
title: Text(l10n.l_move_key),
actions: [
TextButton(
key: keys.deleteButton,
onPressed: _destination != null
? () async {
try {
final pivSlots =
ref.read(pivSlotsProvider(widget.devicePath)).asData;
if (pivSlots != null) {
final destination = pivSlots.value.firstWhere(
(element) => element.slot == _destination);
if (!await confirmOverwrite(context, destination,
writeKey: true, writeCert: _includeCertificate)) {
return;
}
await ref
.read(pivSlotsProvider(widget.devicePath).notifier)
.moveKey(
widget.pivSlot.slot,
destination.slot,
destination.metadata != null,
_includeCertificate);
await ref.read(withContextProvider)(
(context) async {
String message;
if (_includeCertificate) {
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(_destination == null
? l10n.q_move_key_confirm(
widget.pivSlot.slot.getDisplayName(l10n))
: widget.pivSlot.certInfo != null && _includeCertificate
? l10n.q_move_key_and_certificate_to_slot_confirm(
widget.pivSlot.slot.getDisplayName(l10n),
_destination!.getDisplayName(l10n))
: l10n.q_move_key_to_slot_confirm(
widget.pivSlot.slot.getDisplayName(l10n),
_destination!.getDisplayName(l10n))),
Wrap(
spacing: 4.0,
runSpacing: 8.0,
children: [
ChoiceFilterChip<SlotId?>(
menuConstraints: const BoxConstraints(maxHeight: 200),
value: _destination,
items: SlotId.values
.where((element) => element != widget.pivSlot.slot)
.toList(),
labelBuilder: (value) => Text(_destination == null
? l10n.l_select_destination_slot
: _destination!.getDisplayName(l10n)),
itemBuilder: (value) => Text(value!.getDisplayName(l10n)),
onChanged: (value) {
setState(() {
_destination = value;
});
},
),
if (widget.pivSlot.certInfo != null)
FilterChip(
label: Text(l10n.l_include_certificate),
selected: _includeCertificate,
onSelected: (value) {
setState(() {
_includeCertificate = value;
});
})
],
),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: e,
))
.toList(),
),
),
);
}
}

View File

@ -72,6 +72,16 @@ class _PivScreenState extends ConsumerState<PivScreen> {
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,8 +194,7 @@ class _PivScreenState extends ConsumerState<PivScreen> {
},
child: Column(
children: [
if (pivSlots?.hasValue == true)
...pivSlots!.value.map(
...normalSlots.map(
(e) => _CertificateListItem(
pivState,
e,
@ -193,6 +202,14 @@ class _PivScreenState extends ConsumerState<PivScreen> {
selected: e == selected,
),
),
...shownRetiredSlots.map(
(e) => _CertificateListItem(
pivState,
e,
expanded: expanded,
selected: e == selected,
),
)
],
),
);
@ -229,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
@ -258,12 +275,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
};
}

View File

@ -29,6 +29,7 @@ class ChoiceFilterChip<T> 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<T> extends StatefulWidget {
this.selected = false,
this.disableHover,
this.labelBuilder,
this.menuConstraints,
});
@override
@ -63,6 +65,7 @@ class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
Offset.zero & overlay.size,
);
return await showMenu(
constraints: widget.menuConstraints,
context: context,
position: position,
shape: const RoundedRectangleBorder(