From 7739685796b872395718513c4e5fb544153c35a1 Mon Sep 17 00:00:00 2001 From: Ridwan <49554047+Ridzzz0Alam@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:49:38 +0200 Subject: [PATCH] Issue #1396: remove an imported digital ID (#1515) Digital IDs added through Sign with a Digital ID (imported .p12/.pfx files or new self-signed ones) are saved with the app settings, but nothing ever took one off the list, so an old or mistakenly imported certificate stayed there for good (#1396). Sign with a Digital ID now has Remove next to Configure New Digital ID. It acts on the selected file-based ID, asks first ("Remove "" from this list? The file itself is not deleted."), and takes the entry off the saved list. The .p12 file stays on disk. Keychain and Windows store IDs can't be removed here: the OS manages them and they are listed again on every signing. - sign_ui: PdfCraftApp::remove_digital_id refuses OS store IDs and stale indexes, keeps the dialog's selection on the same ID (or the first usable one), and goes to Configure when no IDs are left. - SignDraft::confirm_remove holds the entry awaiting confirmation; clicking another row cancels the question. - i18n: the confirmation is translated in all 21 catalogs; ATTRIBUTION.toml hashes updated. - Tests: a unit test for the list, the selection and persist/restore, and a real-shell test that imports a .p12, cancels, then removes it. Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 42 +++++++-------- crates/ui-egui/src/i18n/ar.tsv | 1 + crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/kk.tsv | 1 + crates/ui-egui/src/i18n/lt.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sl.tsv | 1 + crates/ui-egui/src/i18n/te.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/sign_ui.rs | 83 +++++++++++++++++++++++++++++ crates/ui-egui/tests/signing.rs | 45 ++++++++++++++++ 24 files changed, 170 insertions(+), 21 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 649ecc66..a3da9f86 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "28141072a01326a59e873958c20780688da68bbce6fee6d8a0598b3b2eaf64c8" +sha256 = "a05bbd3b58f64e8c2a06cb7803009c81de049716ef089fdd0abc56c7d0eb394b" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "55741bc14dd319319db883308cee6400a95cc86a11e54ddcf84921b07d7aa5b6" +sha256 = "30f26c55038c23956d79b6b85821890a55c82bc99108a0c982b679fccf918eeb" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "77a0c98f38aceee0a2b63002a0348bc10fb6982ac76fe80d020d030c640c7dfb" +sha256 = "c92d6c1b4f92fe6cc0dda954da593e7c22821f524bff77c2cdf71796ba0ea8e3" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b29359b5f0b2be96cdb9c1600187aa738d789db13d74afda26de8d7efbe71d5f" +sha256 = "2a9b1347fe5eec8cf144c315592adb2999206c91c6be3c96115fa8871d2d18b0" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "69623423479cb67f011a7725d6174385285cc1e6e7f46785d6c2cd1c403f5f71" +sha256 = "b267b363ffd9a3d0b610f14ecf505543d53ce08c0603b78306be9a70a77d15ef" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b5c78c84b6b4d418e77b1b106f88fbbe4dad8e3faf06e5d134f069d4eeaff49c" +sha256 = "6ed7cd84692db0c02b322207206f9f1b79b47825c247e6bc4ea8e8231e3bdda2" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e891fcdfa020a94d129f5f56a27a85841153311afd45c1b066895927e24468e0" +sha256 = "bc7823dca413e745370b5fb1640cfd80c8633dac959e3f9766a4e10271b323b7" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5f8bca9c22a5a7f0015b3ef4bbc173c3040f23789a145e51119e38419a3e476a" +sha256 = "a0d155d28ea5784bcd0bff539e3282e394ae74d08ee9ee2c156c3340009f74a8" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bbc72e480ee0b22da792a95643b851c0752d69543472d79c2edc7978c9afc4fb" +sha256 = "ef3e873eadfaf5367e19834dcc892487b0eae1d089b11c0913835e0e24e9bd83" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ea26a00d59bca609e44eb2d59e493da0aa41f49c3e206f3a9ba9a4d0886d263c" +sha256 = "980062f140bbfe78ea077d1bc6ff66ea856c7e267d444054319d91b9b1fb915b" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8f45e60c676b33c2c8298274c44311900364e5ed96258bf86d4cc371a84d65ad" +sha256 = "1433b66e2b40b590b6be1089d4201d6f74aac8116f67b79caca9048bfec444bf" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "527b6f96e54741387ded3d94484c6f70bd75246a83873beeaad284531cabf710" +sha256 = "fa5ecd1eb447cce1255098829a03af6909cd48fdaf3d4853c3ec322746b6f367" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c4d0cae1401ac1aae2eb50f426c9ba0361aaeef3ea56835eeada9632b0ba4101" +sha256 = "20ef278f871692830b2021996bd7038bf7ed5bfae959d8aaaddbdfb573a33d98" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "64912fc6c3e232d8a90c6b2fe476a5a383178aeba5f1d32cc27602d5e65f141b" +sha256 = "38f753d18d4bac285d8b4eeb4cf5f84e97ae5fabbe7b63502fe46c4fe9cbdbde" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f669ad8797a518ad8e849b2332043367b0e04479d4d8de03f7e565dddb53a123" +sha256 = "aae6a9f14b7b5d4ad702aaf2dc07046216ab1b4366c65215f52ff0cbb682ff04" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e57905062cc7d54185120c66435be590cd778a98b91d158fe8f40acf17636787" +sha256 = "42fe836106db7bce7a8e492b9642569df942a0695b330f8701cb85e0d6ea93ea" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "24b37ec8b7c16eee4347c3b3e45c574d339a097dc24aad2c35524c2dbf589c40" +sha256 = "8c9d7fc2e7c040b40a4efbdad38183cc6e40adc5936f8faa1b23580b9adce704" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7d3f5fbc9016ad86441bd8b6858627f64971facc8c5bbbcb339521007669fdb1" +sha256 = "a18e085d8d9797a50d37bcb06daef2ce9675a2450d4db86c77faeeb6a66fd644" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bcfe8940ac0f1887b4b2692a7796bbe79048a414891c16159f09040948619a21" +sha256 = "448606a33ff4e953579ac38f7d614d2193b06771141b5cba005a35f6926a1d01" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8982df2065edb0aaa98e7e3606521ced403a0cc6e5ff1f70ba0e272dd5dc74e5" +sha256 = "8e2d2bf1ec678cbdaf25a8c7f952b5bfb88d162a75bf8b627a742d823b131df7" [[asset]] path = "assets/icons/accessibility.svg" @@ -3351,4 +3351,4 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "649ead1e9be5f868571144e9964856077aa769e5bdb0b8d3c530d6bce0e29219" +sha256 = "fef63bd8be5ab43be8e48add9719b8418ddeb483f766408b34d280cdc08a3018" diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index d4934b88..08b78f1a 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -855,6 +855,7 @@ , Expires: ، تنتهي: No digital IDs yet. لا توجد معرّفات رقمية بعد. Configure New Digital ID إعداد معرّف رقمي جديد + Remove "{name}" from this list? The file itself is not deleted. إزالة "{name}" من هذه القائمة؟ لن يُحذف الملف نفسه. Continue متابعة Configure a Digital ID for Signing إعداد معرّف رقمي للتوقيع Use a Digital ID from a file استخدام معرّف رقمي من ملف diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e4142dfa..25828d13 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -530,6 +530,7 @@ , Expires: , валиден до: No digital IDs yet. Все още няма цифрови идентификатори. Configure New Digital ID Конфигурирай нов цифров идентификатор + Remove "{name}" from this list? The file itself is not deleted. Да се премахне ли „{name}“ от този списък? Самият файл няма да бъде изтрит. Continue Продължи Configure a Digital ID for Signing Конфигуриране на цифров идентификатор за подписване Use a Digital ID from a file Използвай цифров идентификатор от файл diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 70226cf7..c99248b1 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -929,6 +929,7 @@ key Space Mezerník , Expires: , platnost do: No digital IDs yet. Zatím žádná digitální ID. Configure New Digital ID Nastavit nové digitální ID + Remove "{name}" from this list? The file itself is not deleted. Odebrat „{name}“ z tohoto seznamu? Samotný soubor se nesmaže. Continue Pokračovat Configure a Digital ID for Signing Nastavení digitálního ID pro podepisování Use a Digital ID from a file Použít digitální ID ze souboru diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index f83b7f8f..a49e5f87 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -833,6 +833,7 @@ , Expires: , läuft ab: No digital IDs yet. Noch keine digitalen IDs. Configure New Digital ID Neue digitale ID einrichten + Remove "{name}" from this list? The file itself is not deleted. „{name}“ aus dieser Liste entfernen? Die Datei selbst wird nicht gelöscht. Continue Weiter Configure a Digital ID for Signing Digitale ID zum Signieren einrichten Use a Digital ID from a file Digitale ID aus einer Datei verwenden diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index bb768fef..ea65545a 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -464,6 +464,7 @@ , Expires: , Λήξη: No digital IDs yet. Δεν υπάρχουν ακόμη ψηφιακές ταυτότητες. Configure New Digital ID Ρύθμιση νέας ψηφιακής ταυτότητας + Remove "{name}" from this list? The file itself is not deleted. Αφαίρεση του «{name}» από αυτή τη λίστα; Το ίδιο το αρχείο δεν διαγράφεται. Configure a Digital ID for Signing Ρύθμιση ψηφιακής ταυτότητας για υπογραφή Use a Digital ID from a file Χρήση ψηφιακής ταυτότητας από αρχείο Create a new Digital ID (self-signed, saved to a password-protected file) Δημιουργία νέας ψηφιακής ταυτότητας (αυτοϋπογεγραμμένης, αποθηκευμένης σε αρχείο με κωδικό πρόσβασης) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 77ca546b..ae62fca6 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -849,6 +849,7 @@ , Expires: , caduca: No digital IDs yet. Aún no hay ningún ID digital. Configure New Digital ID Configurar un nuevo ID digital + Remove "{name}" from this list? The file itself is not deleted. ¿Quitar «{name}» de esta lista? El archivo en sí no se elimina. Continue Continuar Configure a Digital ID for Signing Configurar un ID digital para firmar Use a Digital ID from a file Usar un ID digital de un archivo diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a136546f..02492cb4 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -829,6 +829,7 @@ , Expires: , expire : No digital IDs yet. Aucun ID numérique pour l'instant. Configure New Digital ID Configurer un nouvel ID numérique + Remove "{name}" from this list? The file itself is not deleted. Retirer « {name} » de cette liste ? Le fichier lui-même n’est pas supprimé. Continue Continuer Configure a Digital ID for Signing Configurer un ID numérique pour signer Use a Digital ID from a file Utiliser un ID numérique provenant d'un fichier diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 0ea0ed1c..756d144f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -826,6 +826,7 @@ , Expires: , lejár: No digital IDs yet. Még nincs digitális azonosító. Configure New Digital ID Új digitális azonosító beállítása + Remove "{name}" from this list? The file itself is not deleted. Eltávolítja a(z) „{name}” elemet erről a listáról? Maga a fájl nem törlődik. Continue Tovább Configure a Digital ID for Signing Digitális azonosító beállítása aláíráshoz Use a Digital ID from a file Digitális azonosító használata fájlból diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9f822b3b..f853a638 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -852,6 +852,7 @@ , Expires: , Scadenza: No digital IDs yet. Ancora nessun ID digitale. Configure New Digital ID Configura nuovo ID digitale + Remove "{name}" from this list? The file itself is not deleted. Rimuovere "{name}" da questo elenco? Il file non viene eliminato. Continue Continua Configure a Digital ID for Signing Configura un ID digitale per la firma Use a Digital ID from a file Usa un ID digitale da file diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 08b1e4a3..fa3d3e19 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -856,6 +856,7 @@ , Expires: 、有効期限: No digital IDs yet. デジタル ID はまだありません。 Configure New Digital ID 新しいデジタル ID を設定 + Remove "{name}" from this list? The file itself is not deleted. 「{name}」をこの一覧から削除しますか?ファイル自体は削除されません。 Continue 続行 Configure a Digital ID for Signing 署名用のデジタル ID を設定 Use a Digital ID from a file ファイルのデジタル ID を使用 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 2dd9c027..b50c64f0 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -811,6 +811,7 @@ , Expires: , Жарамдылық мерзімі: No digital IDs yet. Әзірге цифрлық ID жоқ. Configure New Digital ID Жаңа цифрлық ID теңшеу + Remove "{name}" from this list? The file itself is not deleted. «{name}» осы тізімнен алып тасталсын ба? Файлдың өзі жойылмайды. Continue Жалғастыру Configure a Digital ID for Signing Қол қоюға арналған цифрлық ID теңшеу Use a Digital ID from a file Файлдағы цифрлық ID-ді пайдалану diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 50d99081..caffba40 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -390,6 +390,7 @@ Compress document structure (object streams) Suglaudinti dokumento struktūrą (objektų srautus) Compression Glaudinimas Configure New Digital ID Konfigūruoti naują skaitmeninį ID + Remove "{name}" from this list? The file itself is not deleted. Pašalinti „{name}“ iš šio sąrašo? Pats failas nebus ištrintas. Confirm Password Patvirtinti slaptažodį Conformance level: Atitikties lygis: Continue Tęsti diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index f3d27bdc..a300c85c 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -847,6 +847,7 @@ , Expires: , Wygasa: No digital IDs yet. Brak identyfikatorów cyfrowych. Configure New Digital ID Skonfiguruj nowy identyfikator cyfrowy + Remove "{name}" from this list? The file itself is not deleted. Usunąć „{name}” z tej listy? Sam plik nie zostanie usunięty. Continue Kontynuuj Configure a Digital ID for Signing Skonfiguruj identyfikator cyfrowy do podpisywania Use a Digital ID from a file Użyj identyfikatora cyfrowego z pliku diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 6d3c4437..f43aa0f9 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -484,6 +484,7 @@ , Expires: , Expira em: No digital IDs yet. Nenhuma ID digital ainda. Configure New Digital ID Configurar nova ID digital + Remove "{name}" from this list? The file itself is not deleted. Remover "{name}" desta lista? O arquivo em si não é excluído. Continue Continuar Configure a Digital ID for Signing Configurar uma ID digital para assinatura Use a Digital ID from a file Usar uma ID digital de um arquivo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 481bde08..5c8cf255 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -849,6 +849,7 @@ , Expires: , Истекает: No digital IDs yet. Нет цифровых идентификаторов. Configure New Digital ID Настроить новый цифровой идентификатор + Remove "{name}" from this list? The file itself is not deleted. Убрать «{name}» из этого списка? Сам файл не удаляется. Continue Продолжить Configure a Digital ID for Signing Настроить цифровой идентификатор для подписи Use a Digital ID from a file Использовать цифровой идентификатор из файла diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index b9fdfabb..a35d51b7 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -425,6 +425,7 @@ Compress document structure (object streams) Stisni strukturo dokumenta (tokovi objektov) Compression Stiskanje Configure New Digital ID Nastavi novo digitalno identiteto + Remove "{name}" from this list? The file itself is not deleted. Odstranim »{name}« s tega seznama? Sama datoteka ne bo izbrisana. Configure a Digital ID for Signing Nastavite digitalno identiteto za podpisovanje Confirm Password Potrdite geslo Conformance level: Raven skladnosti: diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 00372324..1beb1105 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -828,6 +828,7 @@ comment menu Edit సవరించు , Expires: , గడువు: No digital IDs yet. ఇంకా డిజిటల్ ఐడీలు ఏవీ లేవు. Configure New Digital ID కొత్త డిజిటల్ ఐడీని కాన్ఫిగర్ చేయి + Remove "{name}" from this list? The file itself is not deleted. ఈ జాబితా నుండి "{name}"ని తీసివేయాలా? ఫైల్ తొలగించబడదు. Continue కొనసాగించు Configure a Digital ID for Signing సంతకం కోసం డిజిటల్ ఐడీని కాన్ఫిగర్ చేయి Use a Digital ID from a file ఫైల్‌లోని డిజిటల్ ఐడీని ఉపయోగించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f8ce83c0..82ad855f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -810,6 +810,7 @@ , Expires: , Son kullanma tarihi: No digital IDs yet. Henüz dijital kimlik yok. Configure New Digital ID Yeni Dijital Kimlik Yapılandır + Remove "{name}" from this list? The file itself is not deleted. "{name}" bu listeden kaldırılsın mı? Dosyanın kendisi silinmez. Continue Devam Et Configure a Digital ID for Signing İmzalama için Dijital Kimlik Yapılandır Use a Digital ID from a file Dosyadan Dijital Kimlik Kullan diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index dd9e5e61..a178110b 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -853,6 +853,7 @@ , Expires: , Дійсний до: No digital IDs yet. Ще немає цифрових посвідчень. Configure New Digital ID Налаштувати нове цифрове посвідчення + Remove "{name}" from this list? The file itself is not deleted. Прибрати «{name}» з цього списку? Сам файл не видаляється. Continue Продовжити Configure a Digital ID for Signing Налаштувати цифрове посвідчення для підписування Use a Digital ID from a file Використати цифрове посвідчення з файлу diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f1946b5e..805bb1cc 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -537,6 +537,7 @@ , Expires: ,有效期至: No digital IDs yet. 尚无数字 ID。 Configure New Digital ID 配置新数字 ID + Remove "{name}" from this list? The file itself is not deleted. 从此列表中移除“{name}”?文件本身不会被删除。 Continue 继续 Configure a Digital ID for Signing 配置用于签名的数字 ID Use a Digital ID from a file 使用文件中的数字 ID diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d812565f..422f669c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -489,6 +489,7 @@ , Expires: ,有效期至: No digital IDs yet. 尚無數位 ID。 Configure New Digital ID 配置新數位 ID + Remove "{name}" from this list? The file itself is not deleted. 從此清單中移除「{name}」?檔案本身不會被刪除。 Continue 繼續 Configure a Digital ID for Signing 設定簽署用的數位 ID Use a Digital ID from a file 使用檔案中的數位 ID diff --git a/crates/ui-egui/src/sign_ui.rs b/crates/ui-egui/src/sign_ui.rs index b2f0d8cb..14aa4516 100644 --- a/crates/ui-egui/src/sign_ui.rs +++ b/crates/ui-egui/src/sign_ui.rs @@ -111,6 +111,8 @@ pub struct SignDraft { pub image: Option, pub new_id: NewIdDraft, pub error: Option, + /// Sign with a Digital ID ▸ Remove was clicked for this list entry: asking to confirm. + pub confirm_remove: Option, } impl SignDraft { @@ -132,6 +134,7 @@ impl SignDraft { image: None, new_id: NewIdDraft::default(), error: None, + confirm_remove: None, } } } @@ -325,6 +328,28 @@ impl PdfCraftApp { self.digital_ids.len() - 1 } + /// Take the digital ID file at `index` off the list (issue #1396). The file itself stays on + /// disk. Keychain and Windows store IDs belong to the OS and are listed again on every + /// signing, so they can't be removed here. The open dialog's selection follows the list. + pub fn remove_digital_id(&mut self, index: usize) -> bool { + if self.digital_ids.get(index).is_none_or(|e| e.path.starts_with("keychain:") || e.path.starts_with("windows:")) { + return false; + } + self.digital_ids.remove(index); + if let Some(d) = self.sign_draft.as_mut() { + d.confirm_remove = None; + d.selected = match d.selected { + Some(s) if s > index => Some(s - 1), + Some(s) if s == index => self.digital_ids.iter().position(|e| e.unusable.is_none()), + s => s, + }; + if self.digital_ids.is_empty() { + d.step = SignStep::Configure; + } + } + true + } + /// Configure New Digital ID ▸ Create: a self-signed ID saved as a .p12 next to the app data /// (or in `export_dir_override`). Returns its list index. fn create_digital_id(&mut self) -> Result { @@ -620,6 +645,7 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { } if resp.clicked() { d.selected = Some(i); + d.confirm_remove = None; } if resp.double_clicked() { d.step = SignStep::SignAs; @@ -630,11 +656,29 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { } }); ui.add_space(10.0); + // Remove takes a digital ID file off the list after asking (#1396); OS key store IDs stay. + let mut remove = None; + if let Some(e) = d.confirm_remove.and_then(|i| ids.get(i)) { + ui.label(crate::i18n::fmt(tl!("Remove \"{name}\" from this list? The file itself is not deleted."), &[("name", &e.name)])); + ui.horizontal(|ui| { + if widgets::pill_button(ui, tl!("Remove"), true).clicked() { + remove = d.confirm_remove; + } + if widgets::pill_button(ui, tl!("Cancel"), false).clicked() { + d.confirm_remove = None; + } + }); + ui.add_space(10.0); + } + let removable = d.selected.filter(|&i| ids.get(i).is_some_and(|e| !e.path.starts_with("keychain:") && !e.path.starts_with("windows:"))); ui.horizontal(|ui| { if widgets::pill_button(ui, tl!("Configure New Digital ID"), false).clicked() { d.step = SignStep::Configure; d.error = None; } + if d.confirm_remove.is_none() && ui.add_enabled_ui(removable.is_some(), |ui| widgets::pill_button(ui, tl!("Remove"), false)).inner.clicked() { + d.confirm_remove = removable; + } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if ui.add_enabled_ui(d.selected.is_some(), |ui| widgets::pill_button(ui, tl!("Continue"), true)).inner.clicked() { d.step = SignStep::SignAs; @@ -645,6 +689,9 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { } }); }); + if let Some(i) = remove { + app.remove_digital_id(i); + } close } @@ -1373,4 +1420,40 @@ mod tests { assert_eq!(mode & 0o077, 0, "the private key is not readable by others: {mode:o}"); let _ = std::fs::remove_dir_all(&dir); } + + fn listed(path: &str, name: &str) -> DigitalIdEntry { + DigitalIdEntry { path: path.into(), name: name.into(), issuer: String::new(), email: String::new(), expires: String::new(), unusable: None } + } + + /// Issue #1396: an imported ID can be taken off the list (its file stays), the dialog's + /// selection follows, an OS key store ID or a stale index is refused, and the removal is saved. + #[test] + fn a_digital_id_file_is_removed_from_the_list_and_stays_removed() { + let dir = scratch(); + let file = dir.join("old.p12"); + std::fs::write(&file, b"not opened").unwrap(); + let mut app = PdfCraftApp::new(); + app.digital_ids = vec![listed("windows:abc", "Store"), listed(&file.to_string_lossy(), "Old"), listed("/ids/new.p12", "New")]; + let mut draft = SignDraft::new(0, None, None, None, &app.digital_ids); + draft.selected = Some(2); + draft.confirm_remove = Some(1); + app.sign_draft = Some(draft); + + assert!(!app.remove_digital_id(0), "OS key store IDs stay"); + assert!(!app.remove_digital_id(9), "an index past the list is refused"); + assert!(app.remove_digital_id(1)); + assert_eq!(app.digital_ids.iter().map(|e| e.name.as_str()).collect::>(), ["Store", "New"]); + assert!(file.exists(), "the file itself is not deleted"); + let d = app.sign_draft.as_ref().unwrap(); + assert_eq!((d.selected, d.confirm_remove, d.step), (Some(1), None, SignStep::Choose), "still New, no pending question"); + + // Removing the selected ID selects the first usable one; removing the last file ID + // leaves only the store ID, which isn't saved. + assert!(app.remove_digital_id(1)); + assert_eq!(app.sign_draft.as_ref().unwrap().selected, Some(0)); + let mut fresh = PdfCraftApp::new(); + fresh.restore(&app.persist()); + assert!(fresh.digital_ids.is_empty(), "the removal survives a restart: {:?}", fresh.digital_ids); + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/crates/ui-egui/tests/signing.rs b/crates/ui-egui/tests/signing.rs index 9a6558d2..a5b5cf51 100644 --- a/crates/ui-egui/tests/signing.rs +++ b/crates/ui-egui/tests/signing.rs @@ -444,3 +444,48 @@ fn an_unusable_id_is_shown_with_its_reason_and_cannot_be_chosen() { h.run_steps(3); assert_eq!(h.state().sign_draft.as_ref().unwrap().selected, Some(1)); } + +/// Issue #1396: an imported digital ID can be removed from Sign with a Digital ID, after a +/// confirmation, and its file stays where it was. +#[test] +fn an_imported_digital_id_can_be_removed() { + let dir = dir().join("remove"); + std::fs::create_dir_all(&dir).unwrap(); + let p12 = dir.join("signer.p12"); + std::fs::copy(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12"), &p12).unwrap(); + let mut h = harness(dir.clone()); + assert!(h.state_mut().execute("sign.certify_invisible")); + h.run_steps(2); + { + let d = h.state_mut().sign_draft.as_mut().unwrap(); + d.new_id.create = false; + d.new_id.file = p12.to_string_lossy().into_owned(); + d.new_id.file_password = "test".into(); + } + h.run_steps(1); + h.get_by_label("Continue").click(); + h.run_steps(4); + assert_eq!(h.state().digital_ids.len(), 1); + assert_eq!(h.state().sign_draft.as_ref().map(|d| d.step), Some(SignStep::Choose)); + + // Remove asks first, and Cancel keeps the ID. The dialog changes size with the question; + // egui centres it from the previous frame's size, so let it settle before each click. + h.get_by_label("Remove").click(); + h.run_steps(4); + h.get_by_label("Remove \"Test Signer EC\" from this list? The file itself is not deleted."); + // The question's Cancel comes before the dialog's own. + h.get_all_by_label("Cancel").next().unwrap().click(); + h.run_steps(4); + assert!(h.query_by_label_contains("from this list?").is_none()); + assert_eq!(h.state().digital_ids.len(), 1); + + h.get_by_label("Remove").click(); + h.run_steps(4); + h.get_by_label("Remove").click(); + h.run_steps(3); + assert!(h.state().digital_ids.is_empty()); + assert!(p12.exists(), "the file itself is not deleted"); + // With no IDs left the dialog offers to configure one. + assert_eq!(h.state().sign_draft.as_ref().map(|d| d.step), Some(SignStep::Configure)); + h.get_by_label("Configure a Digital ID for Signing"); +}