From b40f73a3e3cbdc482d188552c87744560955a81e Mon Sep 17 00:00:00 2001 From: shytzedaka Date: Sun, 30 Aug 2026 15:39:43 +0300 Subject: [PATCH] Fluff Linux Update 1.4 --- .PKGINFO | 4 +- CMakeLists.txt | 11 +- README.md | 21 + po/README.md | 5 + po/ar/kcm_fluffupdates.po | 51 ++ po/bg/kcm_fluffupdates.po | 51 ++ po/cs/kcm_fluffupdates.po | 51 ++ po/da/kcm_fluffupdates.po | 51 ++ po/de/kcm_fluffupdates.po | 51 ++ po/el/kcm_fluffupdates.po | 51 ++ po/es/kcm_fluffupdates.po | 51 ++ po/et/kcm_fluffupdates.po | 51 ++ po/fi/kcm_fluffupdates.po | 51 ++ po/fr/kcm_fluffupdates.po | 51 ++ po/ga/kcm_fluffupdates.po | 51 ++ po/he/kcm_fluffupdates.po | 51 ++ po/hr/kcm_fluffupdates.po | 51 ++ po/hu/kcm_fluffupdates.po | 51 ++ po/it/kcm_fluffupdates.po | 51 ++ po/ja/kcm_fluffupdates.po | 51 ++ po/kcm_fluffupdates.pot | 55 ++ po/lt/kcm_fluffupdates.po | 51 ++ po/lv/kcm_fluffupdates.po | 51 ++ po/mt/kcm_fluffupdates.po | 51 ++ po/nl/kcm_fluffupdates.po | 51 ++ po/pl/kcm_fluffupdates.po | 51 ++ po/pt/kcm_fluffupdates.po | 51 ++ po/ro/kcm_fluffupdates.po | 51 ++ po/ru/kcm_fluffupdates.po | 51 ++ po/sk/kcm_fluffupdates.po | 51 ++ po/sl/kcm_fluffupdates.po | 51 ++ po/sv/kcm_fluffupdates.po | 51 ++ src/CMakeLists.txt | 17 +- src/flufflinux-update-package-protection.json | 4 +- src/fluffupdates.cpp | 338 +++++++- src/fluffupdates.h | 26 + src/securitydiagnostics.cpp | 61 ++ src/securitydiagnostics.h | 23 + src/signingkeyrecovery.cpp | 401 ++++++++++ src/signingkeyrecovery.h | 61 ++ src/ui/main.qml | 364 ++++++++- src/updatehelper.cpp | 206 ++++- src/updateworker.cpp | 227 ++++++ tests/CMakeLists.txt | 10 + tests/signingkeyrecoverytest.cpp | 734 ++++++++++++++++++ 45 files changed, 3927 insertions(+), 18 deletions(-) create mode 100644 src/securitydiagnostics.cpp create mode 100644 src/securitydiagnostics.h create mode 100644 src/signingkeyrecovery.cpp create mode 100644 src/signingkeyrecovery.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/signingkeyrecoverytest.cpp diff --git a/.PKGINFO b/.PKGINFO index 75ff1c9..24698ed 100644 --- a/.PKGINFO +++ b/.PKGINFO @@ -1,9 +1,9 @@ pkgname = flufflinux-update pkgbase = flufflinux-update -pkgver = 1.3-1 +pkgver = 1.4-1 pkgdesc = Fluff Linux Update Utility url = https://github.com/FluffNet/flufflinux-update -builddate = 1787644274 +builddate = 1788111852 packager = FluffNet LLC size = 651620 arch = x86_64 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b08375..109816e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.20) -project(flufflinux_update_kcm VERSION 1.3.0 LANGUAGES CXX) +project(flufflinux_update_kcm VERSION 1.4.0 LANGUAGES CXX) + +option(BUILD_TESTING "Build the isolated signing-key tests" OFF) +include(CTest) set(QT_MIN_VERSION "6.9.0") set(KF6_MIN_VERSION "6.0.0") @@ -34,9 +37,15 @@ include(KDECMakeSettings) include(FeatureSummary) find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Gui Network Qml Quick Widgets) +if(BUILD_TESTING) + find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Test) +endif() find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS CoreAddons I18n KCMUtils) add_subdirectory(src) +if(BUILD_TESTING) + add_subdirectory(tests) +endif() # Install gettext catalogs into the fakeroot alongside the KCM. ki18n_install(po) diff --git a/README.md b/README.md index 3bc7472..a070770 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,27 @@ Fluff Linux Update uses files under `/etc/pacman.d/`: - `flufflinux-update-package-protection.json` defines protected, warning, and autoremove classifications. Packages absent from the file use autoremove. +Repository signing-key recovery is fail-closed. If pacman reports a complete +unknown signing-key fingerprint that pacman conclusively attributes to the +`fluffnet` repository, FLU downloads FluffNet's public certificate and expected +primary fingerprint directly from `fluffnet.org`. It parses the certificate in +an isolated disposable GnuPG home, rejects private-key material, verifies the +primary fingerprint, key usability, self-signatures, and any signing-subkey +binding, and only then imports and locally trusts the exact primary key through +the existing privileged helper. It never contacts a third-party keyserver, +changes global GnuPG configuration, or accepts pacman's key prompt. Failures +from other or unidentified repositories do not trigger any FluffNet request or +keyring change. + +The isolated signing-key tests use disposable OpenPGP keys and mocked +fingerprint endpoint, certificate endpoint, and Pacman-key operations: + +```sh +cmake -S . -B build-tests -DBUILD_TESTING=ON +cmake --build build-tests +ctest --test-dir build-tests --output-on-failure +``` + The existing `lastupdate` hook provides this field: ```json diff --git a/po/README.md b/po/README.md index a4ae63b..c2f7c3e 100644 --- a/po/README.md +++ b/po/README.md @@ -97,6 +97,11 @@ means. The English `msgid` must never be changed. Only edit `msgstr`. | To allow system updates to continue, %1 was automatically removed after it was deemed safe to remove. | Ten-second notice for one safe automatic removal. Preserve `%1`. | | To allow system updates to continue, the following packages were automatically removed after they were deemed safe to remove: %1 | Ten-second notice for multiple safe automatic removals. Preserve `%1`. | | A file conflict was detected and resolved. %1 was renamed to %2. The update process has restarted. | Ten-second file conflict recovery notice. Preserve `%1` and `%2`. | +| Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. | Red status shown when verified FluffNet key recovery fails. | +| Repository: %1 | Repository field in signing-key technical details. Preserve `%1`; the inserted repository name remains left-to-right. | +| FLU version: %1 | Installed FLU version in signing-key technical details. Preserve `%1`. | +| Fluff Linux version: %1 | Operating-system version in signing-key technical details. Preserve `%1`. | +| The official FluffNet repository signing key was verified and added to Pacman. | Temporary notice after the verified certificate is imported successfully. | The panel name and description shown in the System Settings search results also have translations in `src/kcm_fluffupdates.json`. The application-menu entry is diff --git a/po/ar/kcm_fluffupdates.po b/po/ar/kcm_fluffupdates.po index bbb3f5e..ecc1ee7 100644 --- a/po/ar/kcm_fluffupdates.po +++ b/po/ar/kcm_fluffupdates.po @@ -231,3 +231,54 @@ msgstr "اكتُشف تعارض في الملفات وتم حله. أُعيدت msgid "The conflicting package could not be removed. Please seek support." msgstr "تعذرت إزالة الحزمة المتعارضة. يُرجى طلب الدعم." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "تعذر على Fluff Linux Update التحقق من مفتاح توقيع مستودع FluffNet. تم إيقاف التحديث لحماية نظامك." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "تعذر على Fluff Linux Update التحقق من مفتاح توقيع مستودع FluffNet. تم إيقاف التحديث لحماية نظامك. قد يشير ذلك إلى مشكلة في الشبكة، أو عدم توفر خادم FluffNet، أو مفتاح توقيع منتهي الصلاحية أو تم تدويره، أو مشكلة في إعداد المستودع، أو مفتاح غير متوقع، أو احتمال اختراق المستودع. لا تقبل مفتاحًا غير متوقع يدويًا إلا إذا نشرت Fluff Linux تعليمات موثقة." + +msgid "Repository: %1" +msgstr "المستودع: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "تم التحقق من مفتاح التوقيع الرسمي لمستودع FluffNet وإضافته إلى Pacman." + +msgid "Technical details" +msgstr "التفاصيل الفنية" + +msgid "Repository signing-key verification technical details" +msgstr "التفاصيل الفنية للتحقق من مفتاح توقيع المستودع" + +msgid "Failure category: %1" +msgstr "فئة الفشل: %1" + +msgid "Expected fingerprint: %1" +msgstr "البصمة المتوقعة: %1" + +msgid "Received fingerprint: %1" +msgstr "البصمة المستلمة: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "بصمة مفتاح التوقيع المطلوبة: %1" + +msgid "Pacman exit status: %1" +msgstr "حالة خروج بكمن: %1" + +msgid "FLU version: %1" +msgstr "إصدار FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "إصدار Fluff Linux: %1" + +msgid "Try Again" +msgstr "حاول ثانية" + +msgid "Copy Technical Details" +msgstr "نسخ التفاصيل الفنية" + +msgid "Open an Issue on GitHub" +msgstr "افتح مشكلة على جيثب" diff --git a/po/bg/kcm_fluffupdates.po b/po/bg/kcm_fluffupdates.po index d000a8f..48ac15d 100644 --- a/po/bg/kcm_fluffupdates.po +++ b/po/bg/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "Файлов конфликт беше открит и разрешен. #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Конфликтният пакет не можа да бъде премахнат. Моля, потърсете поддръжка." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update не можа да провери ключа за подписване на хранилището FluffNet. Обновяването беше спряно, за да се защити системата ви." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update не можа да провери ключа за подписване на хранилището FluffNet. Обновяването беше спряно, за да се защити системата ви. Това може да означава проблем с мрежата, недостъпен сървър на FluffNet, изтекъл или сменен ключ за подписване, проблем с настройката на хранилището, неочакван ключ или възможно компрометиране на хранилището. Не приемайте ръчно неочакван ключ, освен ако Fluff Linux не публикува проверени инструкции." + +msgid "Repository: %1" +msgstr "Хранилище: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Официалният ключ за подписване на хранилището FluffNet беше проверен и добавен към Pacman." + +msgid "Technical details" +msgstr "Технически детайли" + +msgid "Repository signing-key verification technical details" +msgstr "Технически подробности за проверка на ключ за подписване на хранилище" + +msgid "Failure category: %1" +msgstr "Категория на повреда: %1" + +msgid "Expected fingerprint: %1" +msgstr "Очакван пръстов отпечатък: %1" + +msgid "Received fingerprint: %1" +msgstr "Получен пръстов отпечатък: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Поискан пръстов отпечатък на ключ за подписване: %1" + +msgid "Pacman exit status: %1" +msgstr "Статус на излизане от Pacman: %1" + +msgid "FLU version: %1" +msgstr "Версия на FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Версия на Fluff Linux: %1" + +msgid "Try Again" +msgstr "Опитайте отново" + +msgid "Copy Technical Details" +msgstr "Копиране на техническите подробности" + +msgid "Open an Issue on GitHub" +msgstr "Отворете проблем в GitHub" diff --git a/po/cs/kcm_fluffupdates.po b/po/cs/kcm_fluffupdates.po index 01e13a8..f82b494 100644 --- a/po/cs/kcm_fluffupdates.po +++ b/po/cs/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "Byl zjištěn a vyřešen konflikt souborů. %1 byl přejmenován na %2. #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Konfliktní balíček nelze odebrat. Vyhledejte podporu." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nemohl ověřit podpisový klíč repozitáře FluffNet. Aktualizace byla zastavena, aby byl váš systém chráněn." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nemohl ověřit podpisový klíč repozitáře FluffNet. Aktualizace byla zastavena, aby byl váš systém chráněn. Příčinou může být problém se sítí, nedostupný server FluffNet, prošlý nebo vyměněný podpisový klíč, problém s nastavením repozitáře, neočekávaný klíč nebo možné napadení repozitáře. Neočekávaný klíč ručně nepřijímejte, pokud Fluff Linux nezveřejní ověřené pokyny." + +msgid "Repository: %1" +msgstr "Repozitář: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Oficiální podpisový klíč repozitáře FluffNet byl ověřen a přidán do Pacmanu." + +msgid "Technical details" +msgstr "Technické detaily" + +msgid "Repository signing-key verification technical details" +msgstr "Technické podrobnosti ověření podpisového klíče úložiště" + +msgid "Failure category: %1" +msgstr "Kategorie poruch: %1" + +msgid "Expected fingerprint: %1" +msgstr "Očekávaný otisk: %1" + +msgid "Received fingerprint: %1" +msgstr "Přijatý otisk prstu: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Požadovaný otisk podpisového klíče: %1" + +msgid "Pacman exit status: %1" +msgstr "Stav ukončení Pacman: %1" + +msgid "FLU version: %1" +msgstr "Verze FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Verze Fluff Linux: %1" + +msgid "Try Again" +msgstr "Zkuste to znovu" + +msgid "Copy Technical Details" +msgstr "Zkopírujte technické podrobnosti" + +msgid "Open an Issue on GitHub" +msgstr "Otevřete problém na GitHubu" diff --git a/po/da/kcm_fluffupdates.po b/po/da/kcm_fluffupdates.po index 2b53ed8..03c222d 100644 --- a/po/da/kcm_fluffupdates.po +++ b/po/da/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "En filkonflikt blev fundet og løst. %1 blev omdøbt til %2. Opdaterings #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Den modstridende pakke kunne ikke fjernes. Søg venligst support." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update kunne ikke bekræfte signeringsnøglen til FluffNet-arkivet. Opdateringen blev stoppet for at beskytte dit system." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update kunne ikke bekræfte signeringsnøglen til FluffNet-arkivet. Opdateringen blev stoppet for at beskytte dit system. Det kan skyldes et netværksproblem, en utilgængelig FluffNet-server, en udløbet eller udskiftet signeringsnøgle, et konfigurationsproblem med arkivet, en uventet nøgle eller et muligt kompromitteret arkiv. Acceptér ikke en uventet nøgle manuelt, medmindre Fluff Linux offentliggør bekræftede instruktioner." + +msgid "Repository: %1" +msgstr "Arkiv: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Den officielle signeringsnøgle til FluffNet-arkivet blev bekræftet og føjet til Pacman." + +msgid "Technical details" +msgstr "Tekniske detaljer" + +msgid "Repository signing-key verification technical details" +msgstr "Tekniske detaljer for verifikation af lagersigneringsnøgle" + +msgid "Failure category: %1" +msgstr "Fejlkategori: %1" + +msgid "Expected fingerprint: %1" +msgstr "Forventet fingeraftryk: %1" + +msgid "Received fingerprint: %1" +msgstr "Modtaget fingeraftryk: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Anmodet signaturnøgle-fingeraftryk: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman exit status: %1" + +msgid "FLU version: %1" +msgstr "FLU-version: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux-version: %1" + +msgid "Try Again" +msgstr "Prøv igen" + +msgid "Copy Technical Details" +msgstr "Kopiér tekniske detaljer" + +msgid "Open an Issue on GitHub" +msgstr "Åbn et problem på GitHub" diff --git a/po/de/kcm_fluffupdates.po b/po/de/kcm_fluffupdates.po index 369c9b2..459ee70 100644 --- a/po/de/kcm_fluffupdates.po +++ b/po/de/kcm_fluffupdates.po @@ -215,3 +215,54 @@ msgstr "Ein Dateikonflikt wurde erkannt und behoben. %1 wurde in %2 umbenannt. D msgid "The conflicting package could not be removed. Please seek support." msgstr "Das in Konflikt stehende Paket konnte nicht entfernt werden. Bitte wenden Sie sich an den Support." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update konnte den Signaturschlüssel des FluffNet-Repositorys nicht überprüfen. Das Update wurde zum Schutz Ihres Systems gestoppt." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update konnte den Signaturschlüssel des FluffNet-Repositorys nicht überprüfen. Das Update wurde zum Schutz Ihres Systems gestoppt. Dies kann auf ein Netzwerkproblem, einen nicht verfügbaren FluffNet-Server, einen abgelaufenen oder ausgetauschten Signaturschlüssel, ein Repository-Konfigurationsproblem, einen unerwarteten Schlüssel oder eine mögliche Kompromittierung des Repositorys hinweisen. Akzeptieren Sie einen unerwarteten Schlüssel nicht manuell, es sei denn, Fluff Linux veröffentlicht verifizierte Anweisungen." + +msgid "Repository: %1" +msgstr "Repository: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Der offizielle Signaturschlüssel des FluffNet-Repositorys wurde überprüft und zu Pacman hinzugefügt." + +msgid "Technical details" +msgstr "Technische Details" + +msgid "Repository signing-key verification technical details" +msgstr "Technische Details zur Repository-Signaturschlüsselüberprüfung" + +msgid "Failure category: %1" +msgstr "Fehlerkategorie: %1" + +msgid "Expected fingerprint: %1" +msgstr "Erwarteter Fingerabdruck: %1" + +msgid "Received fingerprint: %1" +msgstr "Empfangener Fingerabdruck: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Angeforderter Fingerabdruck des Signaturschlüssels: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman-Exit-Status: %1" + +msgid "FLU version: %1" +msgstr "FLU-Version: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff-Linux-Version: %1" + +msgid "Try Again" +msgstr "Versuchen Sie es erneut" + +msgid "Copy Technical Details" +msgstr "Technische Details kopieren" + +msgid "Open an Issue on GitHub" +msgstr "Öffnen Sie ein Problem auf GitHub" diff --git a/po/el/kcm_fluffupdates.po b/po/el/kcm_fluffupdates.po index 0faace8..bffd23d 100644 --- a/po/el/kcm_fluffupdates.po +++ b/po/el/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "Εντοπίστηκε διένεξη αρχείου και επιλύθ #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Δεν ήταν δυνατή η κατάργηση του πακέτου σε διένεξη. Ζητήστε υποστήριξη." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Το Fluff Linux Update δεν μπόρεσε να επαληθεύσει το κλειδί υπογραφής του αποθετηρίου FluffNet. Η ενημέρωση διακόπηκε για την προστασία του συστήματός σας." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Το Fluff Linux Update δεν μπόρεσε να επαληθεύσει το κλειδί υπογραφής του αποθετηρίου FluffNet. Η ενημέρωση διακόπηκε για την προστασία του συστήματός σας. Αυτό μπορεί να υποδεικνύει πρόβλημα δικτύου, μη διαθέσιμο διακομιστή FluffNet, ληγμένο ή αλλαγμένο κλειδί υπογραφής, πρόβλημα ρύθμισης του αποθετηρίου, απροσδόκητο κλειδί ή πιθανή παραβίαση του αποθετηρίου. Μην αποδέχεστε χειροκίνητα ένα απροσδόκητο κλειδί, εκτός αν το Fluff Linux δημοσιεύσει επαληθευμένες οδηγίες." + +msgid "Repository: %1" +msgstr "Αποθετήριο: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Το επίσημο κλειδί υπογραφής του αποθετηρίου FluffNet επαληθεύτηκε και προστέθηκε στο Pacman." + +msgid "Technical details" +msgstr "Τεχνικές λεπτομέρειες" + +msgid "Repository signing-key verification technical details" +msgstr "Τεχνικές λεπτομέρειες υπογραφής-κλειδιού επαλήθευσης αποθετηρίου" + +msgid "Failure category: %1" +msgstr "Κατηγορία αποτυχίας: %1" + +msgid "Expected fingerprint: %1" +msgstr "Αναμενόμενο δακτυλικό αποτύπωμα: %1" + +msgid "Received fingerprint: %1" +msgstr "Λήψη δακτυλικού αποτυπώματος: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Ζητούμενο δακτυλικό αποτύπωμα κλειδιού υπογραφής: %1" + +msgid "Pacman exit status: %1" +msgstr "Κατάσταση εξόδου Pacman: %1" + +msgid "FLU version: %1" +msgstr "Έκδοση FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Έκδοση Fluff Linux: %1" + +msgid "Try Again" +msgstr "Δοκιμάστε ξανά" + +msgid "Copy Technical Details" +msgstr "Αντιγραφή Τεχνικών Στοιχείων" + +msgid "Open an Issue on GitHub" +msgstr "Ανοίξτε ένα ζήτημα στο GitHub" diff --git a/po/es/kcm_fluffupdates.po b/po/es/kcm_fluffupdates.po index bcdc4f5..cf4d31e 100644 --- a/po/es/kcm_fluffupdates.po +++ b/po/es/kcm_fluffupdates.po @@ -215,3 +215,54 @@ msgstr "Se detectó y resolvió un conflicto de archivos. Se cambió el nombre d msgid "The conflicting package could not be removed. Please seek support." msgstr "No se pudo eliminar el paquete en conflicto. Solicite asistencia." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update no pudo verificar la clave de firma del repositorio FluffNet. La actualización se detuvo para proteger el sistema." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update no pudo verificar la clave de firma del repositorio FluffNet. La actualización se detuvo para proteger el sistema. Esto puede indicar un problema de red, un servidor de FluffNet no disponible, una clave de firma caducada o rotada, un problema de configuración del repositorio, una clave inesperada o una posible vulneración del repositorio. No acepte manualmente una clave inesperada salvo que Fluff Linux publique instrucciones verificadas." + +msgid "Repository: %1" +msgstr "Repositorio: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "La clave de firma oficial del repositorio FluffNet se verificó y se añadió a Pacman." + +msgid "Technical details" +msgstr "Detalles técnicos" + +msgid "Repository signing-key verification technical details" +msgstr "Detalles técnicos de verificación de clave de firma del repositorio" + +msgid "Failure category: %1" +msgstr "Categoría de falla: %1" + +msgid "Expected fingerprint: %1" +msgstr "Huella digital esperada: %1" + +msgid "Received fingerprint: %1" +msgstr "Huella digital recibida: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Huella digital de clave de firma solicitada: %1" + +msgid "Pacman exit status: %1" +msgstr "Estado de salida de Pacman: %1" + +msgid "FLU version: %1" +msgstr "Versión de FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Versión de Fluff Linux: %1" + +msgid "Try Again" +msgstr "Intentar otra vez" + +msgid "Copy Technical Details" +msgstr "Copiar detalles técnicos" + +msgid "Open an Issue on GitHub" +msgstr "Abrir una incidencia en GitHub" diff --git a/po/et/kcm_fluffupdates.po b/po/et/kcm_fluffupdates.po index 0a1ff3a..83b12ac 100644 --- a/po/et/kcm_fluffupdates.po +++ b/po/et/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "Failikonflikt tuvastati ja lahendati. %1 nimetati ümber nimeks %2. Vär #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Vastuolulist paketti ei saanud eemaldada. Palun otsige tuge." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update ei suutnud FluffNeti hoidla allkirjavõtit kontrollida. Uuendamine peatati süsteemi kaitsmiseks." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update ei suutnud FluffNeti hoidla allkirjavõtit kontrollida. Uuendamine peatati süsteemi kaitsmiseks. Põhjuseks võib olla võrguprobleem, kättesaamatu FluffNeti server, aegunud või vahetatud allkirjavõti, hoidla seadistusprobleem, ootamatu võti või hoidla võimalik kompromiteerimine. Ärge võtke ootamatut võtit käsitsi vastu, kui Fluff Linux pole avaldanud kontrollitud juhiseid." + +msgid "Repository: %1" +msgstr "Hoidla: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "FluffNeti hoidla ametlik allkirjavõti kontrolliti ja lisati Pacmani." + +msgid "Technical details" +msgstr "Tehnilised üksikasjad" + +msgid "Repository signing-key verification technical details" +msgstr "Hoidla allkirjastamisvõtme kinnitamise tehnilised üksikasjad" + +msgid "Failure category: %1" +msgstr "Rikkekategooria: %1" + +msgid "Expected fingerprint: %1" +msgstr "Eeldatav sõrmejälg: %1" + +msgid "Received fingerprint: %1" +msgstr "Vastu võetud sõrmejälg: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Nõutud allkirjastamisvõtme sõrmejälg: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacmani väljumise olek: %1" + +msgid "FLU version: %1" +msgstr "FLU versioon: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linuxi versioon: %1" + +msgid "Try Again" +msgstr "Proovige uuesti" + +msgid "Copy Technical Details" +msgstr "Kopeerige tehnilised üksikasjad" + +msgid "Open an Issue on GitHub" +msgstr "Avage GitHubis probleem" diff --git a/po/fi/kcm_fluffupdates.po b/po/fi/kcm_fluffupdates.po index 9df0539..c0458d0 100644 --- a/po/fi/kcm_fluffupdates.po +++ b/po/fi/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "Tiedostoristiriita havaittiin ja korjattiin. %1 nimettiin uudelleen %2:k #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Ristiriitaista pakettia ei voitu poistaa. Pyydä tukea." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update ei voinut vahvistaa FluffNet-tietovaraston allekirjoitusavainta. Päivitys keskeytettiin järjestelmäsi suojaamiseksi." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update ei voinut vahvistaa FluffNet-tietovaraston allekirjoitusavainta. Päivitys keskeytettiin järjestelmäsi suojaamiseksi. Syynä voi olla verkko-ongelma, FluffNet-palvelimen tavoittamattomuus, vanhentunut tai vaihdettu allekirjoitusavain, tietovaraston määritysongelma, odottamaton avain tai tietovaraston mahdollinen vaarantuminen. Älä hyväksy odottamatonta avainta käsin, ellei Fluff Linux julkaise vahvistettuja ohjeita." + +msgid "Repository: %1" +msgstr "Tietovarasto: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "FluffNet-tietovaraston virallinen allekirjoitusavain vahvistettiin ja lisättiin Pacmaniin." + +msgid "Technical details" +msgstr "Tekniset tiedot" + +msgid "Repository signing-key verification technical details" +msgstr "Tietovaraston allekirjoitusavaimen vahvistuksen tekniset tiedot" + +msgid "Failure category: %1" +msgstr "Vikaluokka: %1" + +msgid "Expected fingerprint: %1" +msgstr "Odotettu sormenjälki: %1" + +msgid "Received fingerprint: %1" +msgstr "Vastaanotettu sormenjälki: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Pyydetty allekirjoitusavaimen sormenjälki: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacmanin poistumistila: %1" + +msgid "FLU version: %1" +msgstr "FLU-versio: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux -versio: %1" + +msgid "Try Again" +msgstr "Yritä uudelleen" + +msgid "Copy Technical Details" +msgstr "Kopioi tekniset tiedot" + +msgid "Open an Issue on GitHub" +msgstr "Avaa ongelma GitHubissa" diff --git a/po/fr/kcm_fluffupdates.po b/po/fr/kcm_fluffupdates.po index 1de9c40..d2166ff 100644 --- a/po/fr/kcm_fluffupdates.po +++ b/po/fr/kcm_fluffupdates.po @@ -215,3 +215,54 @@ msgstr "Un conflit de fichiers a été détecté et résolu. %1 a été renommé msgid "The conflicting package could not be removed. Please seek support." msgstr "Le paquet en conflit n’a pas pu être supprimé. Veuillez contacter l’assistance." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update n’a pas pu vérifier la clé de signature du dépôt FluffNet. La mise à jour a été arrêtée afin de protéger votre système." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update n’a pas pu vérifier la clé de signature du dépôt FluffNet. La mise à jour a été arrêtée afin de protéger votre système. Cela peut indiquer un problème réseau, un serveur FluffNet indisponible, une clé de signature expirée ou remplacée, un problème de configuration du dépôt, une clé inattendue ou une possible compromission du dépôt. N’acceptez pas manuellement une clé inattendue, sauf si Fluff Linux publie des instructions vérifiées." + +msgid "Repository: %1" +msgstr "Dépôt : %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "La clé de signature officielle du dépôt FluffNet a été vérifiée et ajoutée à Pacman." + +msgid "Technical details" +msgstr "Détails techniques" + +msgid "Repository signing-key verification technical details" +msgstr "Détails techniques de la vérification de la clé de signature du référentiel" + +msgid "Failure category: %1" +msgstr "Catégorie de défaillance : %1" + +msgid "Expected fingerprint: %1" +msgstr "Empreinte digitale attendue : %1" + +msgid "Received fingerprint: %1" +msgstr "Empreinte digitale reçue : %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Empreinte digitale de clé de signature demandée : %1" + +msgid "Pacman exit status: %1" +msgstr "Statut de sortie de Pacman : %1" + +msgid "FLU version: %1" +msgstr "Version de FLU : %1" + +msgid "Fluff Linux version: %1" +msgstr "Version de Fluff Linux : %1" + +msgid "Try Again" +msgstr "Essayer à nouveau" + +msgid "Copy Technical Details" +msgstr "Copier les détails techniques" + +msgid "Open an Issue on GitHub" +msgstr "Ouvrir un problème sur GitHub" diff --git a/po/ga/kcm_fluffupdates.po b/po/ga/kcm_fluffupdates.po index a37c530..4a97f46 100644 --- a/po/ga/kcm_fluffupdates.po +++ b/po/ga/kcm_fluffupdates.po @@ -293,3 +293,54 @@ msgstr "Braitheadh ​​agus réitíodh coinbhleacht comhaid. Athainmníodh %1 #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Níorbh fhéidir an pacáiste contrártha a bhaint. Iarr tacaíocht le do thoil." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Níorbh fhéidir le Fluff Linux Update eochair shínithe stór FluffNet a fhíorú. Stopadh an nuashonrú chun do chóras a chosaint." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Níorbh fhéidir le Fluff Linux Update eochair shínithe stór FluffNet a fhíorú. Stopadh an nuashonrú chun do chóras a chosaint. D’fhéadfadh fadhb líonra, freastalaí FluffNet nach bhfuil ar fáil, eochair shínithe atá imithe in éag nó athraithe, fadhb cumraíochta stór, eochair gan choinne, nó sárú féideartha ar an stór a bheith i gceist. Ná glac le heochair gan choinne de láimh mura bhfoilsíonn Fluff Linux treoracha fíoraithe." + +msgid "Repository: %1" +msgstr "Stór: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Fíoraíodh eochair shínithe oifigiúil stór FluffNet agus cuireadh le Pacman í." + +msgid "Technical details" +msgstr "Sonraí teicniúla" + +msgid "Repository signing-key verification technical details" +msgstr "Sonraí teicniúla fíoraithe eochairshínithe stórais" + +msgid "Failure category: %1" +msgstr "Catagóir teip: %1" + +msgid "Expected fingerprint: %1" +msgstr "Méarloirg ionchais: %1" + +msgid "Received fingerprint: %1" +msgstr "Méarloirg faighte: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Méarloirg eochair sínithe iarrtha: %1" + +msgid "Pacman exit status: %1" +msgstr "Stádas scoir PacmanName: %1" + +msgid "FLU version: %1" +msgstr "Leagan FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Leagan Fluff Linux: %1" + +msgid "Try Again" +msgstr "Bain triail eile as" + +msgid "Copy Technical Details" +msgstr "Cóipeáil Sonraí Teicniúla" + +msgid "Open an Issue on GitHub" +msgstr "Oscail Saincheist ar GitHub" diff --git a/po/he/kcm_fluffupdates.po b/po/he/kcm_fluffupdates.po index 4248e92..4462b15 100644 --- a/po/he/kcm_fluffupdates.po +++ b/po/he/kcm_fluffupdates.po @@ -223,3 +223,54 @@ msgstr "זוהתה התנגשות קבצים והיא נפתרה. השם של %1 msgid "The conflicting package could not be removed. Please seek support." msgstr "לא ניתן היה להסיר את הרכיב המתנגש. נא לפנות לתמיכה." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "המערכת לא הצליחה לאמת את מפתח החתימה של מאגר FluffNet. העדכון הופסק כדי להגן על המערכת שלך." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "המערכת לא הצליחה לאמת את מפתח החתימה של מאגר FluffNet. העדכון הופסק כדי להגן על המערכת שלך. הדבר עשוי להצביע על בעיה ברשת, שרת FluffNet שאינו זמין, מפתח חתימה שפג תוקפו או הוחלף, בעיה בתצורת המאגר, מפתח בלתי צפוי או אפשרות שמאגר התוכנה נפרץ. אין לאשר ידנית מפתח בלתי צפוי. יש לפתוח דיווח בשביל תמיכה ופרטים נוספים." + +msgid "Repository: %1" +msgstr "מאגר: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "מפתח החתימה הרשמי של מאגר FluffNet אומת ונוסף ל־Pacman." + +msgid "Technical details" +msgstr "פרטים טכניים" + +msgid "Repository signing-key verification technical details" +msgstr "פרטים טכניים על אימות מפתח החתימה של המאגר" + +msgid "Failure category: %1" +msgstr "קטגוריית הכשל: %1" + +msgid "Expected fingerprint: %1" +msgstr "מספר האישור הצפוי: %1" + +msgid "Received fingerprint: %1" +msgstr "מספר האישור שהתקבל: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "מספר האישור של מפתח החתימה המבוקש: %1" + +msgid "Pacman exit status: %1" +msgstr "מצב היציאה של Pacman: %1" + +msgid "FLU version: %1" +msgstr "גרסת FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "גרסת Fluff Linux: %1" + +msgid "Try Again" +msgstr "ניסיון חוזר" + +msgid "Copy Technical Details" +msgstr "העתקת הפרטים הטכניים" + +msgid "Open an Issue on GitHub" +msgstr "פתיחת דיווח ב Github" diff --git a/po/hr/kcm_fluffupdates.po b/po/hr/kcm_fluffupdates.po index 0b38d0e..078c4f0 100644 --- a/po/hr/kcm_fluffupdates.po +++ b/po/hr/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "Sukob datoteka je otkriven i riješen. %1 je preimenovan u %2. Proces a #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Paket u sukobu nije se mogao ukloniti. Molimo tražite podršku." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nije mogao potvrditi ključ za potpisivanje repozitorija FluffNet. Ažuriranje je zaustavljeno radi zaštite sustava." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nije mogao potvrditi ključ za potpisivanje repozitorija FluffNet. Ažuriranje je zaustavljeno radi zaštite sustava. Uzrok može biti mrežni problem, nedostupan poslužitelj FluffNet, istekao ili promijenjen ključ za potpisivanje, problem s postavkama repozitorija, neočekivan ključ ili moguća kompromitacija repozitorija. Nemojte ručno prihvatiti neočekivan ključ osim ako Fluff Linux objavi potvrđene upute." + +msgid "Repository: %1" +msgstr "Repozitorij: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Službeni ključ za potpisivanje repozitorija FluffNet potvrđen je i dodan u Pacman." + +msgid "Technical details" +msgstr "Tehnički detalji" + +msgid "Repository signing-key verification technical details" +msgstr "Tehnički detalji provjere ključa potpisivanja repozitorija" + +msgid "Failure category: %1" +msgstr "Kategorija kvara: %1" + +msgid "Expected fingerprint: %1" +msgstr "Očekivani otisak prsta: %1" + +msgid "Received fingerprint: %1" +msgstr "Primljeni otisak prsta: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Traženi otisak ključa za potpisivanje: %1" + +msgid "Pacman exit status: %1" +msgstr "Status izlaza Pacmana: %1" + +msgid "FLU version: %1" +msgstr "Inačica FLU-a: %1" + +msgid "Fluff Linux version: %1" +msgstr "Inačica Fluff Linuxa: %1" + +msgid "Try Again" +msgstr "Pokušajte ponovno" + +msgid "Copy Technical Details" +msgstr "Kopiraj tehničke detalje" + +msgid "Open an Issue on GitHub" +msgstr "Otvorite problem na GitHubu" diff --git a/po/hu/kcm_fluffupdates.po b/po/hu/kcm_fluffupdates.po index a8f3dc8..0750e7c 100644 --- a/po/hu/kcm_fluffupdates.po +++ b/po/hu/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "A rendszer fájlütközést észlelt és megoldott. Az %1 átnevezték % #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Az ütköző csomagot nem sikerült eltávolítani. Kérjük, kérjen támogatást." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "A Fluff Linux Update nem tudta ellenőrizni a FluffNet tároló aláírókulcsát. A frissítés a rendszer védelme érdekében leállt." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "A Fluff Linux Update nem tudta ellenőrizni a FluffNet tároló aláírókulcsát. A frissítés a rendszer védelme érdekében leállt. Ennek oka hálózati probléma, nem elérhető FluffNet-kiszolgáló, lejárt vagy lecserélt aláírókulcs, tárolóbeállítási probléma, váratlan kulcs vagy a tároló esetleges kompromittálódása lehet. Ne fogadjon el kézzel váratlan kulcsot, hacsak a Fluff Linux nem tesz közzé ellenőrzött utasításokat." + +msgid "Repository: %1" +msgstr "Tároló: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "A FluffNet tároló hivatalos aláírókulcsának ellenőrzése sikerült, és a kulcs bekerült a Pacmanba." + +msgid "Technical details" +msgstr "Műszaki részletek" + +msgid "Repository signing-key verification technical details" +msgstr "Az adattár aláírási kulcs ellenőrzésének technikai részletei" + +msgid "Failure category: %1" +msgstr "Hibakategória: %1" + +msgid "Expected fingerprint: %1" +msgstr "Várható ujjlenyomat: %1" + +msgid "Received fingerprint: %1" +msgstr "Fogadott ujjlenyomat: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Az aláírási kulcs kért ujjlenyomata: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman kilépési állapota: %1" + +msgid "FLU version: %1" +msgstr "FLU-verzió: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux-verzió: %1" + +msgid "Try Again" +msgstr "Próbáld újra" + +msgid "Copy Technical Details" +msgstr "Másolja a műszaki adatokat" + +msgid "Open an Issue on GitHub" +msgstr "Nyisson meg egy problémát a GitHubon" diff --git a/po/it/kcm_fluffupdates.po b/po/it/kcm_fluffupdates.po index 8ddf7bf..a155a09 100644 --- a/po/it/kcm_fluffupdates.po +++ b/po/it/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "È stato rilevato e risolto un conflitto di file. %1 è stato rinominato #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Impossibile rimuovere il pacchetto in conflitto. Si prega di cercare supporto." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update non ha potuto verificare la chiave di firma del repository FluffNet. L’aggiornamento è stato interrotto per proteggere il sistema." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update non ha potuto verificare la chiave di firma del repository FluffNet. L’aggiornamento è stato interrotto per proteggere il sistema. La causa potrebbe essere un problema di rete, un server FluffNet non disponibile, una chiave di firma scaduta o sostituita, un problema di configurazione del repository, una chiave inattesa o una possibile compromissione del repository. Non accettare manualmente una chiave inattesa, a meno che Fluff Linux non pubblichi istruzioni verificate." + +msgid "Repository: %1" +msgstr "Repository: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "La chiave di firma ufficiale del repository FluffNet è stata verificata e aggiunta a Pacman." + +msgid "Technical details" +msgstr "Dettagli tecnici" + +msgid "Repository signing-key verification technical details" +msgstr "Dettagli tecnici sulla verifica della chiave di firma del repository" + +msgid "Failure category: %1" +msgstr "Categoria di guasto: %1" + +msgid "Expected fingerprint: %1" +msgstr "Impronta digitale prevista: %1" + +msgid "Received fingerprint: %1" +msgstr "Impronta ricevuta: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Impronta digitale della chiave di firma richiesta: %1" + +msgid "Pacman exit status: %1" +msgstr "Stato di uscita di Pacman: %1" + +msgid "FLU version: %1" +msgstr "Versione di FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Versione di Fluff Linux: %1" + +msgid "Try Again" +msgstr "Riprova" + +msgid "Copy Technical Details" +msgstr "Copia i dettagli tecnici" + +msgid "Open an Issue on GitHub" +msgstr "Apri un problema su GitHub" diff --git a/po/ja/kcm_fluffupdates.po b/po/ja/kcm_fluffupdates.po index d9dc6b8..da17c8a 100644 --- a/po/ja/kcm_fluffupdates.po +++ b/po/ja/kcm_fluffupdates.po @@ -211,3 +211,54 @@ msgstr "ファイルの競合が検出され、解決されました。%1 は %2 msgid "The conflicting package could not be removed. Please seek support." msgstr "競合するパッケージを削除できませんでした。サポートにお問い合わせください。" + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update は FluffNet リポジトリの署名鍵を検証できませんでした。システムを保護するため、更新を停止しました。" + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update は FluffNet リポジトリの署名鍵を検証できませんでした。システムを保護するため、更新を停止しました。ネットワークの問題、FluffNet サーバーの停止、期限切れまたは更新された署名鍵、リポジトリ設定の問題、予期しない鍵、またはリポジトリの侵害の可能性があります。Fluff Linux が検証済みの手順を公開していない限り、予期しない鍵を手動で承認しないでください。" + +msgid "Repository: %1" +msgstr "リポジトリ: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "FluffNet リポジトリの公式署名鍵を検証し、Pacman に追加しました。" + +msgid "Technical details" +msgstr "技術的な詳細" + +msgid "Repository signing-key verification technical details" +msgstr "リポジトリ署名キー検証の技術的詳細" + +msgid "Failure category: %1" +msgstr "障害カテゴリ: %1" + +msgid "Expected fingerprint: %1" +msgstr "予想されるフィンガープリント: %1" + +msgid "Received fingerprint: %1" +msgstr "受信した指紋: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "要求された署名キーのフィンガープリント: %1" + +msgid "Pacman exit status: %1" +msgstr "パックマンの終了ステータス: %1" + +msgid "FLU version: %1" +msgstr "FLU バージョン: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux バージョン: %1" + +msgid "Try Again" +msgstr "もう一度やり直してください" + +msgid "Copy Technical Details" +msgstr "技術的な詳細をコピーする" + +msgid "Open an Issue on GitHub" +msgstr "GitHub で問題を開く" diff --git a/po/kcm_fluffupdates.pot b/po/kcm_fluffupdates.pot index fd6d998..642cd78 100644 --- a/po/kcm_fluffupdates.pot +++ b/po/kcm_fluffupdates.pot @@ -275,3 +275,58 @@ msgstr "" #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "" + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "" + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "" + +#. Repository associated with the signing-key failure. Keep %1 unchanged. +msgid "Repository: %1" +msgstr "" + +#. Temporary success notice after verified FluffNet key import. +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "" + +msgid "Technical details" +msgstr "" + +msgid "Repository signing-key verification technical details" +msgstr "" + +msgid "Failure category: %1" +msgstr "" + +msgid "Expected fingerprint: %1" +msgstr "" + +msgid "Received fingerprint: %1" +msgstr "" + +msgid "Requested signing-key fingerprint: %1" +msgstr "" + +msgid "Pacman exit status: %1" +msgstr "" + +#. FLU application version in signing-key technical details. +msgid "FLU version: %1" +msgstr "" + +#. Fluff Linux operating-system version in signing-key technical details. +msgid "Fluff Linux version: %1" +msgstr "" + +msgid "Try Again" +msgstr "" + +msgid "Copy Technical Details" +msgstr "" + +msgid "Open an Issue on GitHub" +msgstr "" diff --git a/po/lt/kcm_fluffupdates.po b/po/lt/kcm_fluffupdates.po index 01ce76f..f9d2077 100644 --- a/po/lt/kcm_fluffupdates.po +++ b/po/lt/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "Failų konfliktas buvo aptiktas ir išspręstas. %1 buvo pervadintas į #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Nepavyko pašalinti nesuderinamo paketo. Prašome kreiptis pagalbos." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nepavyko patikrinti FluffNet saugyklos pasirašymo rakto. Siekiant apsaugoti sistemą, atnaujinimas sustabdytas." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nepavyko patikrinti FluffNet saugyklos pasirašymo rakto. Siekiant apsaugoti sistemą, atnaujinimas sustabdytas. Tai gali reikšti tinklo problemą, nepasiekiamą FluffNet serverį, pasibaigusį ar pakeistą pasirašymo raktą, saugyklos konfigūracijos problemą, netikėtą raktą arba galimą saugyklos pažeidimą. Nepriimkite netikėto rakto rankiniu būdu, nebent Fluff Linux paskelbtų patikrintas instrukcijas." + +msgid "Repository: %1" +msgstr "Saugykla: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Oficialus FluffNet saugyklos pasirašymo raktas patikrintas ir pridėtas prie Pacman." + +msgid "Technical details" +msgstr "Techninės detalės" + +msgid "Repository signing-key verification technical details" +msgstr "Saugyklos pasirašymo rakto patvirtinimo techninė informacija" + +msgid "Failure category: %1" +msgstr "Gedimo kategorija: %1" + +msgid "Expected fingerprint: %1" +msgstr "Numatytas pirštų atspaudas: %1" + +msgid "Received fingerprint: %1" +msgstr "Gautas piršto atspaudas: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Reikalingas pasirašymo rakto kontrolinis kodas: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman išėjimo būsena: %1" + +msgid "FLU version: %1" +msgstr "FLU versija: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux versija: %1" + +msgid "Try Again" +msgstr "Bandyk dar kartą" + +msgid "Copy Technical Details" +msgstr "Nukopijuokite techninę informaciją" + +msgid "Open an Issue on GitHub" +msgstr "Atidarykite problemą „GitHub“." diff --git a/po/lv/kcm_fluffupdates.po b/po/lv/kcm_fluffupdates.po index fdedb7e..a8b963d 100644 --- a/po/lv/kcm_fluffupdates.po +++ b/po/lv/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "Failu konflikts tika atklāts un atrisināts. %1 tika pārdēvēts par % #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Nevarēja noņemt konfliktējošo pakotni. Lūdzu, meklējiet atbalstu." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nevarēja pārbaudīt FluffNet repozitorija parakstīšanas atslēgu. Atjaunināšana tika apturēta, lai aizsargātu sistēmu." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nevarēja pārbaudīt FluffNet repozitorija parakstīšanas atslēgu. Atjaunināšana tika apturēta, lai aizsargātu sistēmu. Iemesls var būt tīkla problēma, nepieejams FluffNet serveris, parakstīšanas atslēga ar beigušos termiņu vai nomainīta atslēga, repozitorija konfigurācijas problēma, negaidīta atslēga vai iespējams repozitorija apdraudējums. Nepieņemiet negaidītu atslēgu manuāli, ja vien Fluff Linux nav publicējis pārbaudītas instrukcijas." + +msgid "Repository: %1" +msgstr "Repozitorijs: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "FluffNet repozitorija oficiālā parakstīšanas atslēga tika pārbaudīta un pievienota Pacman." + +msgid "Technical details" +msgstr "Tehniskās detaļas" + +msgid "Repository signing-key verification technical details" +msgstr "Repozitorija parakstīšanas atslēgas verifikācijas tehniskā informācija" + +msgid "Failure category: %1" +msgstr "Kļūmes kategorija: %1" + +msgid "Expected fingerprint: %1" +msgstr "Paredzamais pirkstu nospiedums: %1" + +msgid "Received fingerprint: %1" +msgstr "Saņemtais pirkstu nospiedums: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Pieprasītais parakstīšanas atslēgas pirksta nospiedums: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman izejas statuss: %1" + +msgid "FLU version: %1" +msgstr "FLU versija: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux versija: %1" + +msgid "Try Again" +msgstr "Mēģiniet vēlreiz" + +msgid "Copy Technical Details" +msgstr "Kopējiet tehniskos datus" + +msgid "Open an Issue on GitHub" +msgstr "Atveriet problēmu vietnē GitHub" diff --git a/po/mt/kcm_fluffupdates.po b/po/mt/kcm_fluffupdates.po index 0c84400..395072b 100644 --- a/po/mt/kcm_fluffupdates.po +++ b/po/mt/kcm_fluffupdates.po @@ -289,3 +289,54 @@ msgstr "Ġie skopert u solvut konflitt ta' fajls. %1 ingħata isem ġdid għal % #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Il-pakkett konfliġġenti ma setax jitneħħa. Jekk jogħġbok fittex appoġġ." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update ma setax jivverifika ċ-ċavetta tal-firma tar-repożitorju FluffNet. L-aġġornament twaqqaf biex jipproteġi s-sistema tiegħek." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update ma setax jivverifika ċ-ċavetta tal-firma tar-repożitorju FluffNet. L-aġġornament twaqqaf biex jipproteġi s-sistema tiegħek. Dan jista’ jindika problema tan-netwerk, server FluffNet mhux disponibbli, ċavetta tal-firma skaduta jew mibdula, problema fil-konfigurazzjoni tar-repożitorju, ċavetta mhux mistennija, jew kompromess possibbli tar-repożitorju. Taċċettax ċavetta mhux mistennija manwalment sakemm Fluff Linux ma jippubblikax struzzjonijiet ivverifikati." + +msgid "Repository: %1" +msgstr "Repożitorju: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Iċ-ċavetta uffiċjali tal-firma tar-repożitorju FluffNet ġiet ivverifikata u miżjuda ma’ Pacman." + +msgid "Technical details" +msgstr "Dettalji tekniċi" + +msgid "Repository signing-key verification technical details" +msgstr "Dettalji tekniċi tal-verifika tal-iffirmar tar-repożitorju taċ-ċavetta" + +msgid "Failure category: %1" +msgstr "Kategorija tal-falliment: %1" + +msgid "Expected fingerprint: %1" +msgstr "Marka tas-swaba' mistennija: %1" + +msgid "Received fingerprint: %1" +msgstr "Marki tas-swaba' riċevuti: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Marka tas-swaba' taċ-ċavetta tal-iffirmar mitluba: %1" + +msgid "Pacman exit status: %1" +msgstr "Status ta' ħruġ ta' Pacman: %1" + +msgid "FLU version: %1" +msgstr "Verżjoni ta’ FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Verżjoni ta’ Fluff Linux: %1" + +msgid "Try Again" +msgstr "Erġa' Ipprova" + +msgid "Copy Technical Details" +msgstr "Ikkopja Dettalji Tekniċi" + +msgid "Open an Issue on GitHub" +msgstr "Iftaħ Kwistjoni fuq GitHub" diff --git a/po/nl/kcm_fluffupdates.po b/po/nl/kcm_fluffupdates.po index 9a5aed6..c686d7f 100644 --- a/po/nl/kcm_fluffupdates.po +++ b/po/nl/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "Er is een bestandsconflict gedetecteerd en opgelost. %1 is hernoemd naar #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Het conflicterende pakket kan niet worden verwijderd. Zoek alstublieft steun." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update kon de ondertekeningssleutel van de FluffNet-opslagruimte niet verifiëren. De update is gestopt om uw systeem te beschermen." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update kon de ondertekeningssleutel van de FluffNet-opslagruimte niet verifiëren. De update is gestopt om uw systeem te beschermen. Dit kan wijzen op een netwerkprobleem, een niet-beschikbare FluffNet-server, een verlopen of vervangen ondertekeningssleutel, een configuratieprobleem met de opslagruimte, een onverwachte sleutel of een mogelijk gecompromitteerde opslagruimte. Accepteer een onverwachte sleutel niet handmatig, tenzij Fluff Linux geverifieerde instructies publiceert." + +msgid "Repository: %1" +msgstr "Opslagruimte: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "De officiële ondertekeningssleutel van de FluffNet-opslagruimte is geverifieerd en aan Pacman toegevoegd." + +msgid "Technical details" +msgstr "Technische details" + +msgid "Repository signing-key verification technical details" +msgstr "Technische details voor verificatie van de ondertekeningssleutel van de opslagplaats" + +msgid "Failure category: %1" +msgstr "Storingscategorie: %1" + +msgid "Expected fingerprint: %1" +msgstr "Verwachte vingerafdruk: %1" + +msgid "Received fingerprint: %1" +msgstr "Ontvangen vingerafdruk: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Gevraagde handtekeningsleutelvingerafdruk: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman-afsluitstatus: %1" + +msgid "FLU version: %1" +msgstr "FLU-versie: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux-versie: %1" + +msgid "Try Again" +msgstr "Probeer het opnieuw" + +msgid "Copy Technical Details" +msgstr "Kopieer technische details" + +msgid "Open an Issue on GitHub" +msgstr "Open een probleem op GitHub" diff --git a/po/pl/kcm_fluffupdates.po b/po/pl/kcm_fluffupdates.po index 59692b8..e7fddd4 100644 --- a/po/pl/kcm_fluffupdates.po +++ b/po/pl/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "Wykryto i rozwiązano konflikt plików. Nazwa %1 została zmieniona na % #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Nie można usunąć pakietu powodującego konflikt. Proszę o wsparcie." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nie mógł zweryfikować klucza podpisującego repozytorium FluffNet. Aktualizacja została zatrzymana, aby chronić system." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nie mógł zweryfikować klucza podpisującego repozytorium FluffNet. Aktualizacja została zatrzymana, aby chronić system. Przyczyną może być problem z siecią, niedostępny serwer FluffNet, wygasły lub zmieniony klucz podpisujący, problem z konfiguracją repozytorium, nieoczekiwany klucz albo możliwe naruszenie repozytorium. Nie akceptuj ręcznie nieoczekiwanego klucza, chyba że Fluff Linux opublikuje zweryfikowane instrukcje." + +msgid "Repository: %1" +msgstr "Repozytorium: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Oficjalny klucz podpisujący repozytorium FluffNet został zweryfikowany i dodany do Pacmana." + +msgid "Technical details" +msgstr "Szczegóły techniczne" + +msgid "Repository signing-key verification technical details" +msgstr "Szczegóły techniczne weryfikacji klucza podpisywania repozytorium" + +msgid "Failure category: %1" +msgstr "Kategoria awarii: %1" + +msgid "Expected fingerprint: %1" +msgstr "Oczekiwany odcisk palca: %1" + +msgid "Received fingerprint: %1" +msgstr "Otrzymano odcisk palca: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Żądany odcisk palca klucza podpisującego: %1" + +msgid "Pacman exit status: %1" +msgstr "Status wyjścia Pacmana: %1" + +msgid "FLU version: %1" +msgstr "Wersja FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Wersja Fluff Linux: %1" + +msgid "Try Again" +msgstr "Spróbuj ponownie" + +msgid "Copy Technical Details" +msgstr "Skopiuj szczegóły techniczne" + +msgid "Open an Issue on GitHub" +msgstr "Otwórz problem w GitHub" diff --git a/po/pt/kcm_fluffupdates.po b/po/pt/kcm_fluffupdates.po index bcd730d..787387a 100644 --- a/po/pt/kcm_fluffupdates.po +++ b/po/pt/kcm_fluffupdates.po @@ -281,3 +281,54 @@ msgstr "Um conflito de arquivo foi detectado e resolvido. %1 foi renomeado para #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "O pacote conflitante não pôde ser removido. Por favor, procure apoio." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "O Fluff Linux Update não conseguiu verificar a chave de assinatura do repositório FluffNet. A atualização foi interrompida para proteger o sistema." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "O Fluff Linux Update não conseguiu verificar a chave de assinatura do repositório FluffNet. A atualização foi interrompida para proteger o sistema. Isto pode indicar um problema de rede, um servidor FluffNet indisponível, uma chave de assinatura expirada ou substituída, um problema de configuração do repositório, uma chave inesperada ou uma possível violação do repositório. Não aceite manualmente uma chave inesperada, salvo se o Fluff Linux publicar instruções verificadas." + +msgid "Repository: %1" +msgstr "Repositório: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "A chave de assinatura oficial do repositório FluffNet foi verificada e adicionada ao Pacman." + +msgid "Technical details" +msgstr "Detalhes técnicos" + +msgid "Repository signing-key verification technical details" +msgstr "Detalhes técnicos de verificação da chave de assinatura do repositório" + +msgid "Failure category: %1" +msgstr "Categoria de falha: %1" + +msgid "Expected fingerprint: %1" +msgstr "Impressão digital esperada: %1" + +msgid "Received fingerprint: %1" +msgstr "Impressão digital recebida: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Impressão digital da chave de assinatura solicitada: %1" + +msgid "Pacman exit status: %1" +msgstr "Status de saída do Pacman: %1" + +msgid "FLU version: %1" +msgstr "Versão do FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Versão do Fluff Linux: %1" + +msgid "Try Again" +msgstr "Tente novamente" + +msgid "Copy Technical Details" +msgstr "Copiar detalhes técnicos" + +msgid "Open an Issue on GitHub" +msgstr "Abra um problema no GitHub" diff --git a/po/ro/kcm_fluffupdates.po b/po/ro/kcm_fluffupdates.po index 709621d..42d77e3 100644 --- a/po/ro/kcm_fluffupdates.po +++ b/po/ro/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "A fost detectat și rezolvat un conflict de fișier. %1 a fost redenumit #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Pachetul aflat în conflict nu a putut fi eliminat. Vă rugăm să căutați sprijin." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nu a putut verifica cheia de semnare a depozitului FluffNet. Actualizarea a fost oprită pentru a vă proteja sistemul." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nu a putut verifica cheia de semnare a depozitului FluffNet. Actualizarea a fost oprită pentru a vă proteja sistemul. Cauza poate fi o problemă de rețea, un server FluffNet indisponibil, o cheie de semnare expirată sau schimbată, o problemă de configurare a depozitului, o cheie neașteptată sau o posibilă compromitere a depozitului. Nu acceptați manual o cheie neașteptată decât dacă Fluff Linux publică instrucțiuni verificate." + +msgid "Repository: %1" +msgstr "Depozit: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Cheia oficială de semnare a depozitului FluffNet a fost verificată și adăugată în Pacman." + +msgid "Technical details" +msgstr "Detalii tehnice" + +msgid "Repository signing-key verification technical details" +msgstr "Detalii tehnice de verificare a cheii de semnare a depozitului" + +msgid "Failure category: %1" +msgstr "Categoria de eșec: %1" + +msgid "Expected fingerprint: %1" +msgstr "Amprenta digitală așteptată: %1" + +msgid "Received fingerprint: %1" +msgstr "Amprenta primită: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Amprenta digitală a cheii de semnare solicitată: %1" + +msgid "Pacman exit status: %1" +msgstr "Stare de ieșire Pacman: %1" + +msgid "FLU version: %1" +msgstr "Versiune FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Versiune Fluff Linux: %1" + +msgid "Try Again" +msgstr "Încearcă din nou" + +msgid "Copy Technical Details" +msgstr "Copiați detaliile tehnice" + +msgid "Open an Issue on GitHub" +msgstr "Deschideți o problemă pe GitHub" diff --git a/po/ru/kcm_fluffupdates.po b/po/ru/kcm_fluffupdates.po index 4e0297e..2a795d6 100644 --- a/po/ru/kcm_fluffupdates.po +++ b/po/ru/kcm_fluffupdates.po @@ -219,3 +219,54 @@ msgstr "Конфликт файлов был обнаружен и устран msgid "The conflicting package could not be removed. Please seek support." msgstr "Не удалось удалить конфликтующий пакет. Обратитесь в службу поддержки." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update не удалось проверить ключ подписи репозитория FluffNet. Обновление остановлено для защиты системы." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update не удалось проверить ключ подписи репозитория FluffNet. Обновление остановлено для защиты системы. Причиной может быть проблема с сетью, недоступный сервер FluffNet, истёкший или заменённый ключ подписи, ошибка настройки репозитория, неожиданный ключ или возможная компрометация репозитория. Не принимайте неожиданный ключ вручную, если Fluff Linux не опубликовал проверенные инструкции." + +msgid "Repository: %1" +msgstr "Репозиторий: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Официальный ключ подписи репозитория FluffNet проверен и добавлен в Pacman." + +msgid "Technical details" +msgstr "Технические детали" + +msgid "Repository signing-key verification technical details" +msgstr "Технические подробности проверки ключа подписи репозитория" + +msgid "Failure category: %1" +msgstr "Категория отказа: %1" + +msgid "Expected fingerprint: %1" +msgstr "Ожидаемый отпечаток: %1." + +msgid "Received fingerprint: %1" +msgstr "Получен отпечаток пальца: %1." + +msgid "Requested signing-key fingerprint: %1" +msgstr "Запрошенный отпечаток ключа подписи: %1" + +msgid "Pacman exit status: %1" +msgstr "Статус выхода Pacman: %1" + +msgid "FLU version: %1" +msgstr "Версия FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Версия Fluff Linux: %1" + +msgid "Try Again" +msgstr "Попробуйте еще раз" + +msgid "Copy Technical Details" +msgstr "Копировать технические детали" + +msgid "Open an Issue on GitHub" +msgstr "Откройте проблему на GitHub" diff --git a/po/sk/kcm_fluffupdates.po b/po/sk/kcm_fluffupdates.po index faccf8d..cdc831b 100644 --- a/po/sk/kcm_fluffupdates.po +++ b/po/sk/kcm_fluffupdates.po @@ -285,3 +285,54 @@ msgstr "Bol zistený a vyriešený konflikt súborov. %1 bol premenovaný na %2. #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Konfliktný balík sa nepodarilo odstrániť. Vyhľadajte podporu." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update nemohol overiť podpisový kľúč repozitára FluffNet. Aktualizácia bola zastavená, aby bol váš systém chránený." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update nemohol overiť podpisový kľúč repozitára FluffNet. Aktualizácia bola zastavená, aby bol váš systém chránený. Príčinou môže byť problém so sieťou, nedostupný server FluffNet, vypršaný alebo vymenený podpisový kľúč, problém s nastavením repozitára, neočakávaný kľúč alebo možné napadnutie repozitára. Neočakávaný kľúč ručne neprijímajte, pokiaľ Fluff Linux nezverejní overené pokyny." + +msgid "Repository: %1" +msgstr "Repozitár: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Oficiálny podpisový kľúč repozitára FluffNet bol overený a pridaný do Pacmanu." + +msgid "Technical details" +msgstr "Technické detaily" + +msgid "Repository signing-key verification technical details" +msgstr "Technické podrobnosti overenia podpisového kľúča úložiska" + +msgid "Failure category: %1" +msgstr "Kategória poruchy: %1" + +msgid "Expected fingerprint: %1" +msgstr "Očakávaný odtlačok prsta: %1" + +msgid "Received fingerprint: %1" +msgstr "Prijatý odtlačok prsta: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Požadovaný odtlačok podpisového kľúča: %1" + +msgid "Pacman exit status: %1" +msgstr "Stav ukončenia Pacmana: %1" + +msgid "FLU version: %1" +msgstr "Verzia FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Verzia Fluff Linux: %1" + +msgid "Try Again" +msgstr "Skúste znova" + +msgid "Copy Technical Details" +msgstr "Kopírovať technické podrobnosti" + +msgid "Open an Issue on GitHub" +msgstr "Otvorte problém na GitHub" diff --git a/po/sl/kcm_fluffupdates.po b/po/sl/kcm_fluffupdates.po index 1713ea2..c1a52cd 100644 --- a/po/sl/kcm_fluffupdates.po +++ b/po/sl/kcm_fluffupdates.po @@ -289,3 +289,54 @@ msgstr "Zaznan in rešen je bil konflikt datoteke. %1 je bil preimenovan v %2. P #. Error shown when an approved blocking package could not be removed. msgid "The conflicting package could not be removed. Please seek support." msgstr "Paketa v sporu ni bilo mogoče odstraniti. Prosimo, poiščite podporo." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update ni mogel preveriti podpisnega ključa skladišča FluffNet. Posodobitev je bila ustavljena zaradi zaščite sistema." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update ni mogel preveriti podpisnega ključa skladišča FluffNet. Posodobitev je bila ustavljena zaradi zaščite sistema. Vzrok je lahko težava z omrežjem, nedosegljiv strežnik FluffNet, potekel ali zamenjan podpisni ključ, težava z nastavitvijo skladišča, nepričakovan ključ ali morebitno ogroženo skladišče. Ne sprejmite nepričakovanega ključa ročno, razen če Fluff Linux objavi preverjena navodila." + +msgid "Repository: %1" +msgstr "Skladišče: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Uradni podpisni ključ skladišča FluffNet je bil preverjen in dodan v Pacman." + +msgid "Technical details" +msgstr "Tehnične podrobnosti" + +msgid "Repository signing-key verification technical details" +msgstr "Tehnične podrobnosti preverjanja ključa za podpisovanje repozitorija" + +msgid "Failure category: %1" +msgstr "Kategorija okvare: %1" + +msgid "Expected fingerprint: %1" +msgstr "Pričakovani prstni odtis: %1" + +msgid "Received fingerprint: %1" +msgstr "Prejeti prstni odtis: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Zahtevani prstni odtis podpisnega ključa: %1" + +msgid "Pacman exit status: %1" +msgstr "Stanje izhoda Pacman: %1" + +msgid "FLU version: %1" +msgstr "Različica FLU: %1" + +msgid "Fluff Linux version: %1" +msgstr "Različica Fluff Linux: %1" + +msgid "Try Again" +msgstr "Poskusite znova" + +msgid "Copy Technical Details" +msgstr "Kopiraj tehnične podrobnosti" + +msgid "Open an Issue on GitHub" +msgstr "Odprite izdajo na GitHubu" diff --git a/po/sv/kcm_fluffupdates.po b/po/sv/kcm_fluffupdates.po index fc43714..b5ce1a3 100644 --- a/po/sv/kcm_fluffupdates.po +++ b/po/sv/kcm_fluffupdates.po @@ -215,3 +215,54 @@ msgstr "En filkonflikt upptäcktes och löstes. %1 döptes om till %2. Uppdateri msgid "The conflicting package could not be removed. Please seek support." msgstr "Det motstridiga paketet kunde inte tas bort. Kontakta supporten." + + +#. TRANSLATORS: Security failure shown when FLU refuses an unverified repository key. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system." +msgstr "Fluff Linux Update kunde inte verifiera signeringsnyckeln för FluffNet-arkivet. Uppdateringen stoppades för att skydda systemet." + +#. TRANSLATORS: Full security guidance; keep the warning against manual acceptance. +msgid "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions." +msgstr "Fluff Linux Update kunde inte verifiera signeringsnyckeln för FluffNet-arkivet. Uppdateringen stoppades för att skydda systemet. Det kan bero på ett nätverksproblem, en otillgänglig FluffNet-server, en utgången eller utbytt signeringsnyckel, ett konfigurationsproblem med arkivet, en oväntad nyckel eller att arkivet kan ha komprometterats. Godkänn inte en oväntad nyckel manuellt om inte Fluff Linux publicerar verifierade instruktioner." + +msgid "Repository: %1" +msgstr "Arkiv: %1" + +msgid "The official FluffNet repository signing key was verified and added to Pacman." +msgstr "Den officiella signeringsnyckeln för FluffNet-arkivet verifierades och lades till i Pacman." + +msgid "Technical details" +msgstr "Tekniska detaljer" + +msgid "Repository signing-key verification technical details" +msgstr "Tekniska detaljer för verifiering av signeringsnyckel för förvar" + +msgid "Failure category: %1" +msgstr "Felkategori: %1" + +msgid "Expected fingerprint: %1" +msgstr "Förväntat fingeravtryck: %1" + +msgid "Received fingerprint: %1" +msgstr "Mottaget fingeravtryck: %1" + +msgid "Requested signing-key fingerprint: %1" +msgstr "Begärt fingeravtryck för signeringsnyckel: %1" + +msgid "Pacman exit status: %1" +msgstr "Pacman utgångsstatus: %1" + +msgid "FLU version: %1" +msgstr "FLU-version: %1" + +msgid "Fluff Linux version: %1" +msgstr "Fluff Linux-version: %1" + +msgid "Try Again" +msgstr "Försök igen" + +msgid "Copy Technical Details" +msgstr "Kopiera tekniska detaljer" + +msgid "Open an Issue on GitHub" +msgstr "Öppna ett problem på GitHub" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 599f1ca..40806f0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,10 @@ kcmutils_add_qml_kcm(kcm_fluffupdates) target_sources(kcm_fluffupdates PRIVATE fluffupdates.cpp fluffupdates.h + securitydiagnostics.cpp + securitydiagnostics.h + signingkeyrecovery.cpp + signingkeyrecovery.h kcm_fluffupdates.json ) @@ -20,16 +24,25 @@ target_link_libraries(kcm_fluffupdates PRIVATE target_compile_definitions(kcm_fluffupdates PRIVATE TRANSLATION_DOMAIN=\"kcm_fluffupdates\" FLUFFLINUX_LOCALE_DIR=\"${KDE_INSTALL_FULL_LOCALEDIR}\" + PROJECT_VERSION=\"${PROJECT_VERSION}\" ) add_executable(flufflinux-update-helper updatehelper.cpp) -target_link_libraries(flufflinux-update-helper PRIVATE Qt6::Core) +target_sources(flufflinux-update-helper PRIVATE + signingkeyrecovery.cpp + signingkeyrecovery.h +) +target_link_libraries(flufflinux-update-helper PRIVATE Qt6::Core Qt6::Network) install(TARGETS flufflinux-update-helper RUNTIME DESTINATION lib/flufflinux-update ) add_executable(flufflinux-update-worker updateworker.cpp) -target_link_libraries(flufflinux-update-worker PRIVATE Qt6::Core) +target_sources(flufflinux-update-worker PRIVATE + signingkeyrecovery.cpp + signingkeyrecovery.h +) +target_link_libraries(flufflinux-update-worker PRIVATE Qt6::Core Qt6::Network) install(TARGETS flufflinux-update-worker RUNTIME DESTINATION lib/flufflinux-update ) diff --git a/src/flufflinux-update-package-protection.json b/src/flufflinux-update-package-protection.json index 0a916fc..3977f6f 100644 --- a/src/flufflinux-update-package-protection.json +++ b/src/flufflinux-update-package-protection.json @@ -143,7 +143,6 @@ "system-config-printer", "cups", "firefox", - "gnome-disk-utility", "flufflinux-vlc", "ffmpegthumbs", "kdegraphics-thumbnailers", @@ -205,7 +204,6 @@ "spectacle", "rust", "swtpm", - "mission-center-flufflinux", "bind", "wget", "cronie", @@ -234,7 +232,9 @@ "mtools", "partclone", "qemu-guest-agent", + "gnome-disk-utility", "gnome-calculator", + "mission-center-flufflinux", "hunspell-en_us", "qt5-imageformats", "speech-dispatcher", diff --git a/src/fluffupdates.cpp b/src/fluffupdates.cpp index a50f628..ef1f550 100644 --- a/src/fluffupdates.cpp +++ b/src/fluffupdates.cpp @@ -1,6 +1,9 @@ #include "fluffupdates.h" +#include "securitydiagnostics.h" #include + +#include "signingkeyrecovery.h" #include #include @@ -10,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -20,10 +24,12 @@ #include #include #include +#include #include #include #include #include +#include #include @@ -436,6 +442,81 @@ QString FluffUpdates::totalDownloadSize() const } QString FluffUpdates::downloadSpeed() const { return m_downloadSpeed; } QString FluffUpdates::installError() const { return m_installError; } +bool FluffUpdates::signingKeySecurityError() const +{ + return m_signingKeySecurityError; +} + +QString FluffUpdates::signingKeyTechnicalDetails() const +{ + const auto isolate = [](const auto &value) { + return QString(QChar(0x2066)) + QVariant::fromValue(value).toString() + + QChar(0x2069); + }; + QStringList lines; + if (!m_signingKeyRepository.isEmpty()) { + lines << i18nd("kcm_fluffupdates", "Repository: %1", + isolate(m_signingKeyRepository)); + } + lines << i18nd("kcm_fluffupdates", "Failure category: %1", + isolate(m_signingKeyFailureCategory)); + if (!m_signingKeyExpectedFingerprint.isEmpty()) { + lines << i18nd("kcm_fluffupdates", "Expected fingerprint: %1", + isolate(m_signingKeyExpectedFingerprint)); + } + if (!m_signingKeyReceivedFingerprint.isEmpty()) { + lines << i18nd("kcm_fluffupdates", "Received fingerprint: %1", + isolate(m_signingKeyReceivedFingerprint)); + } + if (!m_signingKeyRequestedFingerprint.isEmpty()) { + lines << i18nd("kcm_fluffupdates", + "Requested signing-key fingerprint: %1", + isolate(m_signingKeyRequestedFingerprint)); + } + lines << i18nd("kcm_fluffupdates", "Pacman exit status: %1", + isolate(m_signingKeyPacmanExitStatus)); + lines << i18nd("kcm_fluffupdates", "FLU version: %1", + isolate(QStringLiteral(PROJECT_VERSION))); + QString fluffLinuxVersion = QStringLiteral("unknown"); + QFile osRelease(QStringLiteral("/etc/os-release")); + if (osRelease.open(QIODevice::ReadOnly | QIODevice::Text)) { + const QRegularExpression version( + QStringLiteral("(?:^|\\n)PRETTY_NAME=(?:\"([^\"]*)\"|([^\\n]*))")); + const auto match = version.match(QString::fromUtf8(osRelease.readAll())); + if (match.hasMatch()) { + fluffLinuxVersion = !match.captured(1).isEmpty() + ? match.captured(1) : match.captured(2).trimmed(); + } + } + lines << i18nd("kcm_fluffupdates", "Fluff Linux version: %1", + isolate(fluffLinuxVersion)); + return lines.join(QLatin1Char('\n')); +} + +QString FluffUpdates::signingKeyIssueUrl() const +{ + QString osVersion = QStringLiteral("unknown"); + QFile osRelease(QStringLiteral("/etc/os-release")); + if (osRelease.open(QIODevice::ReadOnly | QIODevice::Text)) { + const QRegularExpression version( + QStringLiteral("(?:^|\\n)PRETTY_NAME=(?:\"([^\"]*)\"|([^\\n]*))")); + const auto match = version.match(QString::fromUtf8(osRelease.readAll())); + if (match.hasMatch()) { + osVersion = !match.captured(1).isEmpty() + ? match.captured(1) : match.captured(2).trimmed(); + } + } + SigningKeyIssueDetails details; + details.repository = m_signingKeyRepository; + details.fluVersion = QStringLiteral(PROJECT_VERSION); + details.osVersion = osVersion; + details.expectedFingerprint = m_signingKeyExpectedFingerprint; + details.receivedFingerprint = m_signingKeyReceivedFingerprint; + details.requestedFingerprint = m_signingKeyRequestedFingerprint; + details.failureCategory = m_signingKeyFailureCategory; + details.pacmanExitStatus = m_signingKeyPacmanExitStatus; + return ::signingKeyIssueUrl(details).toString(QUrl::FullyEncoded); +} bool FluffUpdates::cancellationNotice() const { return m_cancellationNotice; } bool FluffUpdates::installationSuccessNotice() const { @@ -528,6 +609,7 @@ void FluffUpdates::checkForUpdates() const bool recoveryRestart = m_recoveryRestartPending; m_recoveryRestartPending = false; if (!recoveryRestart) { + m_checkSigningKeyRecoveryAttempted = false; m_approvedRemovals.clear(); m_pendingAutoremovedPackages.clear(); if (!m_recoveryActionState.isEmpty()) { @@ -545,6 +627,13 @@ void FluffUpdates::checkForUpdates() m_ignoreInactiveInstallState = true; m_installPhase = QStringLiteral("idle"); m_installError.clear(); + m_signingKeySecurityError = false; + m_signingKeyRepository.clear(); + m_signingKeyExpectedFingerprint.clear(); + m_signingKeyReceivedFingerprint.clear(); + m_signingKeyRequestedFingerprint.clear(); + m_signingKeyFailureCategory.clear(); + m_signingKeyPacmanExitStatus = -1; if (pacmanRunning()) { m_checkComplete = true; m_updatesAvailable = false; @@ -570,9 +659,21 @@ void FluffUpdates::checkForUpdates() Q_EMIT installStateChanged(); m_checkDatabasePath = QStringLiteral("/tmp/flufflinux-checkupdates-%1").arg(geteuid()); + QDir().mkpath(m_checkDatabasePath); + const QString localDatabase = + QDir(m_checkDatabasePath).filePath(QStringLiteral("local")); + if (!QFileInfo::exists(localDatabase)) { + QFile::link(QStringLiteral("/var/lib/pacman/local"), localDatabase); + } m_checkProcess = new QProcess(this); - m_checkProcess->setProgram(QStringLiteral("checkupdates")); - m_checkProcess->setArguments({QStringLiteral("--nocolor")}); + m_checkProcess->setProgram(QStringLiteral("fakeroot")); + m_checkProcess->setArguments({ + QStringLiteral("--"), QStringLiteral("pacman"), QStringLiteral("-Sy"), + QStringLiteral("--noconfirm"), + QStringLiteral("--disable-sandbox-filesystem"), + QStringLiteral("--dbpath"), m_checkDatabasePath, + QStringLiteral("--logfile"), QStringLiteral("/dev/null"), + }); m_checkProcess->setProcessChannelMode(QProcess::MergedChannels); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); @@ -607,7 +708,66 @@ void FluffUpdates::checkForUpdates() "The update check stopped unexpectedly."); Q_EMIT checkStateChanged(); }); - } else if (exitCode == 2 && !output.contains(QStringLiteral("error:"), Qt::CaseInsensitive)) { + } else if (exitCode != 0) { + if (SigningKeyRecovery::containsUnknownKeyReport(output)) { + recoverCheckSigningKey(output, exitCode); + return; + } + afterMinimumCheckDuration([this, output] { + m_checking = false; + m_checkComplete = true; + m_checkError = friendlyCheckError(output); + Q_EMIT checkStateChanged(); + }); + } else { + startUpdateQuery(); + } + }); + + m_checkProcess->start(); +} + +void FluffUpdates::startUpdateQuery() +{ + m_checkProcess = new QProcess(this); + m_checkProcess->setProgram(QStringLiteral("checkupdates")); + m_checkProcess->setArguments( + {QStringLiteral("--nosync"), QStringLiteral("--nocolor")}); + m_checkProcess->setProcessChannelMode(QProcess::MergedChannels); + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + environment.insert(QStringLiteral("CHECKUPDATES_DB"), m_checkDatabasePath); + environment.insert(QStringLiteral("LC_ALL"), QStringLiteral("C")); + m_checkProcess->setProcessEnvironment(environment); + connect(m_checkProcess, &QProcess::errorOccurred, this, + [this](QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) { + afterMinimumCheckDuration([this] { + m_checking = false; + m_checkComplete = true; + m_checkError = i18nd( + "kcm_fluffupdates", + "The update checker could not be started. Make sure pacman-contrib is installed."); + Q_EMIT checkStateChanged(); + }); + } + }); + connect(m_checkProcess, + qOverload(&QProcess::finished), this, + [this](int exitCode, QProcess::ExitStatus exitStatus) { + const QString output = QString::fromLocal8Bit( + m_checkProcess->readAll()).trimmed(); + m_checkProcess->deleteLater(); + m_checkProcess = nullptr; + if (exitStatus != QProcess::NormalExit) { + afterMinimumCheckDuration([this] { + m_checking = false; + m_checkComplete = true; + m_checkError = i18nd( + "kcm_fluffupdates", + "The update check stopped unexpectedly."); + Q_EMIT checkStateChanged(); + }); + } else if (exitCode == 2) { afterMinimumCheckDuration([this] { m_updatesAvailable = false; if (!hasLastUpdate()) { @@ -632,10 +792,85 @@ void FluffUpdates::checkForUpdates() readTransactionSummary(); } }); - m_checkProcess->start(); } +void FluffUpdates::showCheckSigningKeyFailure( + const QString &category, const QString &repository, + const QString &expected, const QString &received, const QString &requested, + int pacmanExitStatus) +{ + m_signingKeyRepository = repository.left(64); + m_signingKeyFailureCategory = category.left(128); + m_signingKeyExpectedFingerprint = expected.left(40); + m_signingKeyReceivedFingerprint = received.left(40); + m_signingKeyRequestedFingerprint = requested.left(40); + m_signingKeyPacmanExitStatus = pacmanExitStatus; + m_signingKeySecurityError = true; + m_checking = false; + m_checkComplete = true; + m_updatesAvailable = false; + m_checkError = i18nd( + "kcm_fluffupdates", + "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system."); + Q_EMIT checkStateChanged(); + Q_EMIT installStateChanged(); +} + +void FluffUpdates::recoverCheckSigningKey(const QString &output, + int pacmanExitStatus) +{ + const QString requested = + SigningKeyRecovery::requestedFingerprint(output); + const QString repository = SigningKeyRecovery::repositoryName(output); + if (m_checkSigningKeyRecoveryAttempted) { + showCheckSigningKeyFailure(QStringLiteral("recovery-retry-failed"), + repository, {}, {}, requested, + pacmanExitStatus); + return; + } + m_checkSigningKeyRecoveryAttempted = true; + m_summaryProcess = new QProcess(this); + m_summaryProcess->setProgram(QStringLiteral("pkexec")); + m_summaryProcess->setArguments({ + QStringLiteral("/usr/lib/flufflinux-update/flufflinux-update-helper"), + QStringLiteral("--recover-signing-key"), + QString::fromLatin1(output.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)), + }); + m_summaryProcess->setProcessChannelMode(QProcess::MergedChannels); + connect(m_summaryProcess, + qOverload(&QProcess::finished), this, + [this, repository, requested, pacmanExitStatus]( + int exitCode, QProcess::ExitStatus status) { + const QString result = m_summaryProcess + ? QString::fromLocal8Bit(m_summaryProcess->readAll()) : QString(); + if (m_summaryProcess) { + m_summaryProcess->deleteLater(); + m_summaryProcess = nullptr; + } + if (status == QProcess::NormalExit && exitCode == 0) { + showSigningKeyVerifiedNotice(); + restartUpdateCheck(); + return; + } + const QRegularExpression token( + QStringLiteral("FLU_SIGNING_KEY_FAILURE:([^|\\n]*)\\|([^|\\n]*)\\|([^|\\n]*)\\|([^|\\n]*)\\|([^|\\n]*)\\|(-?\\d+)")); + const auto match = token.match(result); + if (match.hasMatch()) { + showCheckSigningKeyFailure( + match.captured(2), match.captured(1), match.captured(3), + match.captured(4), match.captured(5), + match.captured(6).toInt()); + } else { + showCheckSigningKeyFailure( + QStringLiteral("recovery-helper-failed"), repository, {}, {}, + requested, pacmanExitStatus); + } + }); + m_summaryProcess->start(); +} + void FluffUpdates::afterMinimumCheckDuration( std::function completion) { @@ -745,6 +980,7 @@ void FluffUpdates::startInstallation() // Ignore that stale complete/failed/cancelled state until then. m_ignoreInactiveInstallState = true; m_installError.clear(); + m_signingKeySecurityError = false; m_installProgress = 0; Q_EMIT installStateChanged(); @@ -807,6 +1043,36 @@ void FluffUpdates::startInstallation() m_installControlProcess->start(); } +void FluffUpdates::retrySigningKeyUpdate() +{ + if (m_signingKeySecurityError) { + if (m_installPhase == QStringLiteral("idle")) { + m_signingKeySecurityError = false; + Q_EMIT installStateChanged(); + checkForUpdates(); + } else { + startInstallation(); + } + } +} + +void FluffUpdates::copySigningKeyTechnicalDetails() +{ + if (m_signingKeySecurityError) { + QApplication::clipboard()->setText(signingKeyTechnicalDetails()); + } +} + +void FluffUpdates::openSigningKeyIssue() +{ + if (!m_signingKeySecurityError) { + return; + } + openSigningKeyIssueUrl( + QUrl(signingKeyIssueUrl()), + [](const QUrl &url) { return QDesktopServices::openUrl(url); }); +} + void FluffUpdates::cancelInstallation() { if (m_installPhase != QStringLiteral("downloading") @@ -909,6 +1175,21 @@ void FluffUpdates::restartUpdateCheck() QTimer::singleShot(100, this, [this] { checkForUpdates(); }); } +void FluffUpdates::showSigningKeyVerifiedNotice() +{ + m_recoveryNotice = i18nd( + "kcm_fluffupdates", + "The official FluffNet repository signing key was verified and added to Pacman."); + const quint64 generation = ++m_recoveryNoticeGeneration; + Q_EMIT recoveryNoticeChanged(); + QTimer::singleShot(10000, this, [this, generation] { + if (generation == m_recoveryNoticeGeneration) { + m_recoveryNotice.clear(); + Q_EMIT recoveryNoticeChanged(); + } + }); +} + void FluffUpdates::showPendingAutoremoveNotice() { if (m_pendingAutoremovedPackages.isEmpty()) { @@ -1073,6 +1354,9 @@ void FluffUpdates::readTransactionSummary() const auto warningMatch = warningToken.match(output); const auto dependencyWarningMatch = dependencyWarningToken.match(output); const auto recheckMatch = recheckToken.match(output); + const QRegularExpression signingKeyFailureToken( + QStringLiteral("FLU_SIGNING_KEY_FAILURE:([^|\\n]*)\\|([^|\\n]*)\\|([^|\\n]*)\\|([^|\\n]*)\\|([^|\\n]*)\\|(-?\\d+)")); + const auto signingKeyFailureMatch = signingKeyFailureToken.match(output); const QRegularExpression replacementToken( QStringLiteral("FLU_REPLACEMENT:([A-Za-z0-9@._+:-]+)\\|([^|\\s]+)\\|([A-Za-z0-9@._+:-]+)\\|([^|\\s]+)")); @@ -1174,6 +1458,24 @@ void FluffUpdates::readTransactionSummary() restartUpdateCheck(); return; } + if (signingKeyFailureMatch.hasMatch()) { + m_signingKeyRepository = signingKeyFailureMatch.captured(1); + m_signingKeyFailureCategory = signingKeyFailureMatch.captured(2); + m_signingKeyExpectedFingerprint = signingKeyFailureMatch.captured(3); + m_signingKeyReceivedFingerprint = signingKeyFailureMatch.captured(4); + m_signingKeyRequestedFingerprint = signingKeyFailureMatch.captured(5); + m_signingKeyPacmanExitStatus = signingKeyFailureMatch.captured(6).toInt(); + m_signingKeySecurityError = true; + m_checking = false; + m_checkComplete = true; + m_updatesAvailable = false; + m_checkError = i18nd( + "kcm_fluffupdates", + "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system."); + Q_EMIT checkStateChanged(); + Q_EMIT installStateChanged(); + return; + } const QString lowerOutput = output.toLower(); if (exitStatus != QProcess::NormalExit) { @@ -1283,11 +1585,17 @@ void FluffUpdates::readInstallState() state.value(QStringLiteral("recovery_notice_id")).toInteger(); if (recoveryNoticeId > 0 && recoveryNoticeId != m_lastRecoveryNoticeId) { m_lastRecoveryNoticeId = recoveryNoticeId; + const QString noticeType = + state.value(QStringLiteral("recovery_notice_type")).toString(); const QString original = state.value(QStringLiteral("recovery_original_file")).toString(); const QString preserved = state.value(QStringLiteral("recovery_preserved_file")).toString(); - if (!original.isEmpty() && !preserved.isEmpty()) { + if (noticeType == QStringLiteral("signing-key-verified")) { + m_recoveryNotice = i18nd( + "kcm_fluffupdates", + "The official FluffNet repository signing key was verified and added to Pacman."); + } else if (!original.isEmpty() && !preserved.isEmpty()) { m_recoveryNotice = i18nd( "kcm_fluffupdates", "A file conflict was detected and resolved. %1 was renamed to %2. The update process has restarted.", @@ -1328,9 +1636,27 @@ void FluffUpdates::readInstallState() } m_installError.clear(); + m_signingKeySecurityError = false; if (phase == QStringLiteral("failed")) { const QString error = state.value(QStringLiteral("error")).toString(); - if (error == QStringLiteral("DOWNLOAD_FAILED") + if (error == QStringLiteral("SIGNING_KEY_VERIFICATION_FAILED")) { + m_signingKeySecurityError = true; + m_signingKeyRepository = state.value( + QStringLiteral("security_repository")).toString().left(64); + m_signingKeyExpectedFingerprint = state.value( + QStringLiteral("security_expected_fingerprint")).toString(); + m_signingKeyReceivedFingerprint = state.value( + QStringLiteral("security_received_fingerprint")).toString(); + m_signingKeyRequestedFingerprint = state.value( + QStringLiteral("security_requested_fingerprint")).toString(); + m_signingKeyFailureCategory = state.value( + QStringLiteral("security_failure_category")).toString().left(128); + m_signingKeyPacmanExitStatus = state.value( + QStringLiteral("security_pacman_exit_status")).toInt(-1); + m_installError = i18nd( + "kcm_fluffupdates", + "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system."); + } else if (error == QStringLiteral("DOWNLOAD_FAILED") || error == QStringLiteral("DOWNLOAD_CONNECTION_FAILED") || error == QStringLiteral("TRANSACTION_PREPARE_FAILED")) { m_installError = i18nd( diff --git a/src/fluffupdates.h b/src/fluffupdates.h index 33a4676..db4fb30 100644 --- a/src/fluffupdates.h +++ b/src/fluffupdates.h @@ -40,6 +40,9 @@ class FluffUpdates final : public KQuickConfigModule Q_PROPERTY(QString totalDownloadSize READ totalDownloadSize NOTIFY installStateChanged) Q_PROPERTY(QString downloadSpeed READ downloadSpeed NOTIFY installStateChanged) Q_PROPERTY(QString installError READ installError NOTIFY installStateChanged) + Q_PROPERTY(bool signingKeySecurityError READ signingKeySecurityError NOTIFY installStateChanged) + Q_PROPERTY(QString signingKeyTechnicalDetails READ signingKeyTechnicalDetails NOTIFY installStateChanged) + Q_PROPERTY(QString signingKeyIssueUrl READ signingKeyIssueUrl NOTIFY installStateChanged) Q_PROPERTY(bool cancellationNotice READ cancellationNotice NOTIFY installStateChanged) Q_PROPERTY(bool installationSuccessNotice READ installationSuccessNotice NOTIFY installStateChanged) Q_PROPERTY(bool pacmanView READ pacmanView WRITE setPacmanView NOTIFY pacmanViewChanged) @@ -80,6 +83,9 @@ class FluffUpdates final : public KQuickConfigModule QString totalDownloadSize() const; QString downloadSpeed() const; QString installError() const; + bool signingKeySecurityError() const; + QString signingKeyTechnicalDetails() const; + QString signingKeyIssueUrl() const; bool cancellationNotice() const; bool installationSuccessNotice() const; bool pacmanView() const; @@ -98,6 +104,9 @@ class FluffUpdates final : public KQuickConfigModule Q_INVOKABLE void clearCheckResult(); Q_INVOKABLE void startInstallation(); Q_INVOKABLE void cancelInstallation(); + Q_INVOKABLE void retrySigningKeyUpdate(); + Q_INVOKABLE void copySigningKeyTechnicalDetails(); + Q_INVOKABLE void openSigningKeyIssue(); Q_INVOKABLE void setPacmanView(bool enabled); Q_INVOKABLE void saveUpdateWindowState(int width, int height, bool maximized); @@ -117,6 +126,7 @@ class FluffUpdates final : public KQuickConfigModule private: void readStateFile(); + void startUpdateQuery(); void readTransactionSummary(); void recordInitialUpdate(); void afterMinimumCheckDuration(std::function completion); @@ -124,7 +134,15 @@ class FluffUpdates final : public KQuickConfigModule void updateNetworkState(); void updateBatteryState(); void removeBlockingPackage(const QString &package); + void recoverCheckSigningKey(const QString &output, int pacmanExitStatus); + void showCheckSigningKeyFailure(const QString &category, + const QString &repository, + const QString &expected, + const QString &received, + const QString &requested, + int pacmanExitStatus); void restartUpdateCheck(); + void showSigningKeyVerifiedNotice(); void showPendingAutoremoveNotice(); QString m_lastUpdate; @@ -138,6 +156,7 @@ class FluffUpdates final : public KQuickConfigModule QProcess *m_installControlProcess = nullptr; QString m_checkDatabasePath; bool m_checking = false; + bool m_checkSigningKeyRecoveryAttempted = false; qint64 m_checkStartedAtMs = 0; bool m_checkComplete = false; bool m_updatesAvailable = false; @@ -154,6 +173,13 @@ class FluffUpdates final : public KQuickConfigModule qint64 m_totalDownloadBytes = 0; QString m_downloadSpeed; QString m_installError; + bool m_signingKeySecurityError = false; + QString m_signingKeyRepository; + QString m_signingKeyExpectedFingerprint; + QString m_signingKeyReceivedFingerprint; + QString m_signingKeyRequestedFingerprint; + QString m_signingKeyFailureCategory; + int m_signingKeyPacmanExitStatus = -1; bool m_cancellationNotice = false; bool m_installationSuccessNotice = false; quint64 m_installationSuccessNoticeGeneration = 0; diff --git a/src/securitydiagnostics.cpp b/src/securitydiagnostics.cpp new file mode 100644 index 0000000..0b14562 --- /dev/null +++ b/src/securitydiagnostics.cpp @@ -0,0 +1,61 @@ +#include "securitydiagnostics.h" + +#include +#include + +QString sanitizedDiagnosticValue(const QString &value, int maximumLength) +{ + QString sanitized = value; + sanitized.replace( + QRegularExpression(QStringLiteral("[\\r\\n\\x00-\\x1f]")), + QStringLiteral(" ")); + sanitized.replace( + QRegularExpression( + QStringLiteral("(?:/home/|/Users/)[^\\s]+"), + QRegularExpression::CaseInsensitiveOption), + QStringLiteral("[redacted-path]")); + sanitized.replace( + QRegularExpression( + QStringLiteral("(?:password|passwd|token|secret|authorization)" + "\\s*[:=]\\s*[^\\s]+"), + QRegularExpression::CaseInsensitiveOption), + QStringLiteral("[redacted-secret]")); + return sanitized.simplified().left(maximumLength); +} + +QUrl signingKeyIssueUrl(const SigningKeyIssueDetails &details) +{ + const QString body = QStringLiteral( + "Repository: %1\nFLU version: %2\nOS version: %3\nExpected fingerprint: %4\n" + "Received fingerprint: %5\nRequested signing-subkey fingerprint: %6\n" + "Failure category: %7\nPacman exit status: %8") + .arg(sanitizedDiagnosticValue(details.repository, 64), + sanitizedDiagnosticValue(details.fluVersion, 32), + sanitizedDiagnosticValue(details.osVersion, 160), + sanitizedDiagnosticValue(details.expectedFingerprint, 40), + sanitizedDiagnosticValue(details.receivedFingerprint, 40), + sanitizedDiagnosticValue(details.requestedFingerprint, 40), + sanitizedDiagnosticValue(details.failureCategory, 128), + QString::number(details.pacmanExitStatus)); + QUrl url(QStringLiteral( + "https://github.com/FluffNet/flufflinux-update/issues/new")); + QUrlQuery query; + query.addQueryItem( + QStringLiteral("title"), + QStringLiteral("Repository signing-key verification failed")); + query.addQueryItem(QStringLiteral("body"), body); + url.setQuery(query); + return url; +} + +bool openSigningKeyIssueUrl( + const QUrl &url, const std::function &opener) +{ + if (!opener || url.scheme() != QStringLiteral("https") + || url.host() != QStringLiteral("github.com") + || url.path() != QStringLiteral( + "/FluffNet/flufflinux-update/issues/new")) { + return false; + } + return opener(url); +} diff --git a/src/securitydiagnostics.h b/src/securitydiagnostics.h new file mode 100644 index 0000000..b0f9ce5 --- /dev/null +++ b/src/securitydiagnostics.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include + +struct SigningKeyIssueDetails +{ + QString repository; + QString fluVersion; + QString osVersion; + QString expectedFingerprint; + QString receivedFingerprint; + QString requestedFingerprint; + QString failureCategory; + int pacmanExitStatus = -1; +}; + +QString sanitizedDiagnosticValue(const QString &value, int maximumLength); +QUrl signingKeyIssueUrl(const SigningKeyIssueDetails &details); +bool openSigningKeyIssueUrl( + const QUrl &url, const std::function &opener); diff --git a/src/signingkeyrecovery.cpp b/src/signingkeyrecovery.cpp new file mode 100644 index 0000000..caad331 --- /dev/null +++ b/src/signingkeyrecovery.cpp @@ -0,0 +1,401 @@ +#include "signingkeyrecovery.h" + +#include +#include +#include +#include +#include + +#include + +namespace +{ +bool commandSucceeded(const SigningKeyCommandResult &result) +{ + return result.started && result.finished && result.exitCode == 0; +} + +QString normalizedRecordFingerprint(const QByteArray &value) +{ + const QString candidate = QString::fromLatin1(value).trimmed().toUpper(); + static const QRegularExpression fingerprint(QStringLiteral("^[0-9A-F]{40}$")); + return fingerprint.match(candidate).hasMatch() ? candidate : QString{}; +} + +struct CertificateDescription +{ + QString primary; + bool primaryUsable = false; + QStringList allSubkeys; + QStringList usableSigningSubkeys; +}; + +CertificateDescription describeCertificate(const QByteArray &listing) +{ + CertificateDescription description; + QByteArray pendingRecord; + bool pendingUsable = false; + bool pendingSigningCapable = false; + const QList lines = listing.split('\n'); + for (const QByteArray &line : lines) { + const QList fields = line.split(':'); + if (fields.isEmpty()) { + continue; + } + if (fields[0] == "pub" || fields[0] == "sub") { + pendingRecord = fields[0]; + const QByteArray validity = fields.size() > 1 ? fields[1] : QByteArray{}; + pendingUsable = validity != "r" && validity != "e" + && validity != "d" && validity != "i"; + const QByteArray capabilities = + fields.size() > 11 ? fields[11].toLower() : QByteArray{}; + pendingSigningCapable = capabilities.contains('s'); + continue; + } + if (fields[0] != "fpr" || fields.size() <= 9 || pendingRecord.isEmpty()) { + continue; + } + const QString fingerprint = normalizedRecordFingerprint(fields[9]); + if (fingerprint.isEmpty()) { + pendingRecord.clear(); + continue; + } + if (pendingRecord == "pub" && description.primary.isEmpty()) { + description.primary = fingerprint; + description.primaryUsable = pendingUsable; + } else if (pendingRecord == "sub") { + description.allSubkeys.append(fingerprint); + if (pendingUsable && pendingSigningCapable) { + description.usableSigningSubkeys.append(fingerprint); + } + } + pendingRecord.clear(); + } + return description; +} + +bool hasValidSubkeyBinding(const QByteArray &checkedListing, + const QString &primaryFingerprint, + const QString &requestedSubkeyFingerprint) +{ + const QByteArray primaryKeyId = primaryFingerprint.right(16).toLatin1(); + QString currentSubkey; + bool awaitingSubkeyFingerprint = false; + const QList lines = checkedListing.split('\n'); + for (const QByteArray &line : lines) { + const QList fields = line.split(':'); + if (fields.isEmpty()) { + continue; + } + if (fields[0] == "sub") { + awaitingSubkeyFingerprint = true; + currentSubkey.clear(); + continue; + } + if (awaitingSubkeyFingerprint && fields[0] == "fpr" + && fields.size() > 9) { + currentSubkey = normalizedRecordFingerprint(fields[9]); + awaitingSubkeyFingerprint = false; + continue; + } + if (fields.size() <= 10 || fields[0] != "sig" || fields[1] != "!" + || currentSubkey != requestedSubkeyFingerprint) { + continue; + } + const QByteArray signer = fields[4].toUpper(); + const QByteArray signatureClass = fields[10].toLower(); + if (signer.endsWith(primaryKeyId) + && signatureClass.startsWith("18")) { + return true; + } + } + return false; +} + +bool hasValidPrimarySelfSignature(const QByteArray &checkedListing, + const QString &primaryFingerprint) +{ + const QByteArray primaryKeyId = primaryFingerprint.right(16).toLatin1(); + for (const QByteArray &line : checkedListing.split('\n')) { + const QList fields = line.split(':'); + if (fields.size() <= 10 || fields[0] != "sig" || fields[1] != "!") { + continue; + } + const QByteArray signer = fields[4].toUpper(); + const QByteArray signatureClass = fields[10].toLower(); + if (signer.endsWith(primaryKeyId) + && signatureClass.startsWith("13")) { + return true; + } + } + return false; +} +} + +SigningKeyRecovery::SigningKeyRecovery(SigningKeyRecoveryConfig config, + Fetcher fingerprintFetcher, + Fetcher certificateFetcher, + Runner runner) + : m_config(std::move(config)) + , m_fingerprintFetcher(std::move(fingerprintFetcher)) + , m_certificateFetcher(std::move(certificateFetcher)) + , m_runner(std::move(runner)) +{ +} + +QString SigningKeyRecovery::normalizeFingerprint(const QByteArray &value) +{ + QByteArray candidate = value; + if (candidate.endsWith('\n')) { + candidate.chop(1); + } + if (candidate.size() != 40) { + return {}; + } + static const QRegularExpression fingerprint( + QStringLiteral("^[0-9A-F]{40}$")); + const QString text = QString::fromLatin1(candidate); + return fingerprint.match(text).hasMatch() ? text : QString{}; +} + +QString SigningKeyRecovery::requestedFingerprint(const QString &pacmanOutput) +{ + QString plain = pacmanOutput; + plain.remove(QRegularExpression(QStringLiteral("\\x1b\\[[0-9;]*[A-Za-z]"))); + static const QRegularExpression request( + QStringLiteral("(?:unknown[ -]key|signing[ -]key|key)[^\\n\\r]*?" + "((?:[0-9A-Fa-f][[:space:]:-]*){40})" + "(?![0-9A-Fa-f])"), + QRegularExpression::CaseInsensitiveOption); + const auto match = request.match(plain); + if (!match.hasMatch()) { + return {}; + } + QString candidate = match.captured(1); + candidate.remove(QRegularExpression(QStringLiteral("[:\\-\\s]"))); + return normalizedRecordFingerprint(candidate.toLatin1()); +} + +QString SigningKeyRecovery::repositoryName(const QString &pacmanOutput) +{ + QString plain = pacmanOutput; + plain.remove(QRegularExpression(QStringLiteral("\\x1b\\[[0-?]*[ -/]*[@-~]"))); + static const QRegularExpression repository( + QStringLiteral("(?:^|[\\r\\n])\\s*error:\\s*([A-Za-z0-9@._+:-]+):" + "\\s*key\\s*[\\\"']?[0-9A-Fa-f]{40}[\\\"']?" + "\\s+is unknown(?:\\s|$)"), + QRegularExpression::CaseInsensitiveOption); + const auto match = repository.match(plain); + return match.hasMatch() ? match.captured(1).toLower() : QString{}; +} + +bool SigningKeyRecovery::containsUnknownKeyReport(const QString &pacmanOutput) +{ + QString plain = pacmanOutput; + plain.remove(QRegularExpression(QStringLiteral("\\x1b\\[[0-9;]*[A-Za-z]"))); + const QString lower = plain.toLower(); + return lower.contains(QStringLiteral("unknown signing key")) + || lower.contains(QStringLiteral("unknown key")) + || (lower.contains(QStringLiteral("is unknown")) + && !requestedFingerprint(plain).isEmpty()) + || lower.contains(QStringLiteral("required key missing from keyring")) + || lower.contains(QStringLiteral("key could not be looked up remotely")); +} + +SigningKeyCommandResult SigningKeyRecovery::run( + const QString &program, const QStringList &arguments) const +{ + return m_runner ? m_runner(program, arguments) : SigningKeyCommandResult{}; +} + +SigningKeyRecoveryResult SigningKeyRecovery::recover( + const QString &pacmanOutput) const +{ + SigningKeyRecoveryResult result; + result.repository = repositoryName(pacmanOutput); + if (result.repository.isEmpty()) { + result.category = QStringLiteral("repository-unidentified"); + return result; + } + if (result.repository != QStringLiteral("fluffnet")) { + result.category = QStringLiteral("repository-not-fluffnet"); + return result; + } + result.requestedFingerprint = requestedFingerprint(pacmanOutput); + if (result.requestedFingerprint.isEmpty()) { + result.category = QStringLiteral("requested-fingerprint-invalid"); + return result; + } + + QString fetchFailure; + const QByteArray response = m_fingerprintFetcher + ? m_fingerprintFetcher(&fetchFailure) : QByteArray{}; + if (!fetchFailure.isEmpty()) { + result.category = fetchFailure; + return result; + } + if (response.size() > m_config.maximumFingerprintResponseBytes) { + result.category = QStringLiteral("fingerprint-response-oversized"); + return result; + } + result.expectedFingerprint = normalizeFingerprint(response); + if (result.expectedFingerprint.isEmpty()) { + result.category = QStringLiteral("fingerprint-response-malformed"); + return result; + } + + QString certificateFetchFailure; + const QByteArray certificateResponse = m_certificateFetcher + ? m_certificateFetcher(&certificateFetchFailure) : QByteArray{}; + if (!certificateFetchFailure.isEmpty()) { + result.category = certificateFetchFailure; + return result; + } + if (certificateResponse.isEmpty()) { + result.category = QStringLiteral("certificate-response-empty"); + return result; + } + if (certificateResponse.size() > m_config.maximumCertificateResponseBytes) { + result.category = QStringLiteral("certificate-response-oversized"); + return result; + } + + QTemporaryDir temporaryDirectory( + QDir::tempPath() + QStringLiteral("/flufflinux-update-key-XXXXXX")); + temporaryDirectory.setAutoRemove(true); + if (!temporaryDirectory.isValid() + || !QFile::setPermissions(temporaryDirectory.path(), + QFileDevice::ReadOwner | QFileDevice::WriteOwner + | QFileDevice::ExeOwner)) { + result.category = QStringLiteral("temporary-keyring-failed"); + return result; + } + + const QStringList common{ + QStringLiteral("--batch"), QStringLiteral("--no-tty"), + QStringLiteral("--homedir"), temporaryDirectory.path(), + }; + const QString downloadedCertificatePath = + temporaryDirectory.path() + QStringLiteral("/downloaded-certificate.asc"); + QFile downloadedCertificate(downloadedCertificatePath); + if (!downloadedCertificate.open(QIODevice::WriteOnly) + || downloadedCertificate.write(certificateResponse) + != certificateResponse.size() + || !downloadedCertificate.flush()) { + result.category = QStringLiteral("certificate-write-failed"); + return result; + } + downloadedCertificate.close(); + + QStringList packets = common; + packets << QStringLiteral("--list-packets") << downloadedCertificatePath; + const SigningKeyCommandResult packetListing = run(m_config.gpgPath, packets); + const QByteArray lowerPacketListing = packetListing.output.toLower(); + if (!commandSucceeded(packetListing)) { + result.category = QStringLiteral("certificate-parse-failed"); + return result; + } + if (lowerPacketListing.contains("secret key packet") + || lowerPacketListing.contains("secret sub key packet")) { + result.category = QStringLiteral("certificate-contains-private-key"); + return result; + } + + QStringList import = common; + import << QStringLiteral("--import") << downloadedCertificatePath; + if (!commandSucceeded(run(m_config.gpgPath, import))) { + result.category = QStringLiteral("certificate-import-failed"); + return result; + } + + QStringList list = common; + list << QStringLiteral("--with-colons") << QStringLiteral("--fixed-list-mode") + << QStringLiteral("--fingerprint") << QStringLiteral("--fingerprint") + << QStringLiteral("--list-keys") << result.requestedFingerprint; + const SigningKeyCommandResult listing = run(m_config.gpgPath, list); + if (!commandSucceeded(listing)) { + result.category = QStringLiteral("certificate-inspection-failed"); + return result; + } + const CertificateDescription certificate = describeCertificate(listing.output); + result.receivedFingerprint = certificate.primary; + if (certificate.primary.isEmpty()) { + result.category = QStringLiteral("certificate-fingerprint-invalid"); + return result; + } + if (certificate.primary != result.expectedFingerprint) { + result.category = QStringLiteral("primary-fingerprint-mismatch"); + return result; + } + if (!certificate.primaryUsable) { + result.category = QStringLiteral("primary-key-unusable"); + return result; + } + + const bool requestedPrimary = + result.requestedFingerprint == certificate.primary; + const bool requestedSubkey = + certificate.allSubkeys.contains(result.requestedFingerprint); + if (!requestedPrimary && !requestedSubkey) { + result.category = QStringLiteral("requested-key-not-in-certificate"); + return result; + } + if (requestedSubkey + && !certificate.usableSigningSubkeys.contains( + result.requestedFingerprint)) { + result.category = QStringLiteral("signing-key-unusable"); + return result; + } + QStringList check = common; + check << QStringLiteral("--with-colons") + << QStringLiteral("--fingerprint") + << QStringLiteral("--fingerprint") + << QStringLiteral("--check-sigs") << certificate.primary; + const SigningKeyCommandResult checked = run(m_config.gpgPath, check); + if (!commandSucceeded(checked) + || !hasValidPrimarySelfSignature(checked.output, + certificate.primary)) { + result.category = QStringLiteral("certificate-signature-invalid"); + return result; + } + if (requestedSubkey + && !hasValidSubkeyBinding(checked.output, certificate.primary, + result.requestedFingerprint)) { + result.category = QStringLiteral("subkey-binding-invalid"); + return result; + } + + const QString certificatePath = + temporaryDirectory.path() + QStringLiteral("/certificate.gpg"); + QStringList exportArguments = common; + exportArguments << QStringLiteral("--output") << certificatePath + << QStringLiteral("--export") << certificate.primary; + if (!commandSucceeded(run(m_config.gpgPath, exportArguments)) + || !QFileInfo(certificatePath).isFile()) { + result.category = QStringLiteral("certificate-export-failed"); + return result; + } + + const SigningKeyCommandResult existing = run( + m_config.pacmanKeyPath, + {QStringLiteral("--finger"), certificate.primary}); + const bool wasAlreadyPresent = commandSucceeded(existing); + if (!wasAlreadyPresent + && !commandSucceeded(run(m_config.pacmanKeyPath, + {QStringLiteral("--add"), certificatePath}))) { + result.category = QStringLiteral("key-import-failed"); + return result; + } + if (!commandSucceeded(run(m_config.pacmanKeyPath, + {QStringLiteral("--lsign-key"), certificate.primary}))) { + if (!wasAlreadyPresent) { + run(m_config.pacmanKeyPath, + {QStringLiteral("--delete"), certificate.primary}); + } + result.category = QStringLiteral("key-trust-failed"); + return result; + } + + result.recovered = true; + return result; +} diff --git a/src/signingkeyrecovery.h b/src/signingkeyrecovery.h new file mode 100644 index 0000000..4a96b75 --- /dev/null +++ b/src/signingkeyrecovery.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include + +struct SigningKeyCommandResult +{ + bool started = false; + bool finished = false; + int exitCode = -1; + QByteArray output; +}; + +struct SigningKeyRecoveryResult +{ + bool recovered = false; + QString repository; + QString category; + QString expectedFingerprint; + QString receivedFingerprint; + QString requestedFingerprint; +}; + +struct SigningKeyRecoveryConfig +{ + QString gpgPath = QStringLiteral("/usr/bin/gpg"); + QString pacmanKeyPath = QStringLiteral("/usr/bin/pacman-key"); + int maximumFingerprintResponseBytes = 4096; + int maximumCertificateResponseBytes = 256 * 1024; +}; + +class SigningKeyRecovery +{ +public: + using Fetcher = std::function; + using Runner = std::function; + + SigningKeyRecovery(SigningKeyRecoveryConfig config, + Fetcher fingerprintFetcher, + Fetcher certificateFetcher, Runner runner); + + SigningKeyRecoveryResult recover(const QString &pacmanOutput) const; + + static QString normalizeFingerprint(const QByteArray &value); + static QString requestedFingerprint(const QString &pacmanOutput); + static QString repositoryName(const QString &pacmanOutput); + static bool containsUnknownKeyReport(const QString &pacmanOutput); + +private: + SigningKeyCommandResult run(const QString &program, + const QStringList &arguments) const; + + SigningKeyRecoveryConfig m_config; + Fetcher m_fingerprintFetcher; + Fetcher m_certificateFetcher; + Runner m_runner; +}; diff --git a/src/ui/main.qml b/src/ui/main.qml index b9a6dd8..b0a6327 100644 --- a/src/ui/main.qml +++ b/src/ui/main.qml @@ -15,7 +15,19 @@ KCMUtils.SimpleKCM { && !kcm.updatesAvailable && kcm.checkError.length === 0 && kcm.recoveryActionState.length === 0 readonly property bool recoveryActionRequired: - kcm.recoveryActionState.length > 0 + kcm.recoveryActionState.length > 0 || kcm.signingKeySecurityError + readonly property bool criticalActionRequired: + kcm.recoveryActionState === "protected" || kcm.signingKeySecurityError + property bool signingKeyDialogDismissed: false + + Connections { + target: kcm + function onInstallStateChanged() { + if (kcm.signingKeySecurityError) { + root.signingKeyDialogDismissed = false + } + } + } // Qt selects RightToLeft for Arabic and Hebrew. Explicit mirroring makes // every nested row follow that direction as well. @@ -77,7 +89,7 @@ KCMUtils.SimpleKCM { ? "#d71920" : kcm.freshnessColor Accessible.name: root.recoveryActionRequired - ? (kcm.recoveryActionState === "protected" + ? (root.criticalActionRequired ? i18nd("kcm_fluffupdates", "Action is required!") : i18nd("kcm_fluffupdates", "Action is required")) : root.systemUpToDate @@ -97,7 +109,7 @@ KCMUtils.SimpleKCM { Text { anchors.centerIn: parent - visible: kcm.recoveryActionState === "protected" + visible: root.criticalActionRequired text: "\u00d7" color: "white" font.pixelSize: parent.width * 0.72 @@ -117,7 +129,7 @@ KCMUtils.SimpleKCM { Layout.fillWidth: true wrapMode: Text.WordWrap text: root.recoveryActionRequired - ? (kcm.recoveryActionState === "protected" + ? (root.criticalActionRequired ? i18nd("kcm_fluffupdates", "Action is required!") : i18nd("kcm_fluffupdates", "Action is required")) : root.systemUpToDate @@ -358,6 +370,7 @@ KCMUtils.SimpleKCM { Layout.fillWidth: true visible: kcm.installError.length > 0 type: Kirigami.MessageType.Error + icon.name: "dialog-error" text: kcm.installError } @@ -533,6 +546,168 @@ KCMUtils.SimpleKCM { return widest } + function beginWheelGesture(handler) { + handler.touchpadGesture = false + handler.velocityX = 0 + handler.velocityY = 0 + handler.lastEventTime = Date.now() + } + + function blendWheelVelocity(currentVelocity, + instantaneousVelocity) { + if (currentVelocity === 0 + || currentVelocity * instantaneousVelocity < 0) + return instantaneousVelocity + + return currentVelocity * 0.65 + + instantaneousVelocity * 0.35 + } + + function scrollFromWheel(flickable, wheel, handler, + momentum, horizontal) { + const pixelX = wheel.pixelDelta.x + const pixelY = wheel.pixelDelta.y + const preciseGesture = pixelX !== 0 || pixelY !== 0 + + if (preciseGesture) { + const now = Date.now() + const elapsed = Math.max(8, + Math.min(50, now - handler.lastEventTime)) + handler.lastEventTime = now + handler.touchpadGesture = true + + if (horizontal && pixelX !== 0) { + if (momentum.velocityX * pixelX < 0) + momentum.velocityX *= 0.7 + const maximumX = Math.max(flickable.originX, + flickable.originX + flickable.contentWidth + - flickable.width) + flickable.contentX = Math.max(flickable.originX, + Math.min(maximumX, + flickable.contentX - pixelX)) + const instantaneousX = pixelX * 1000 / elapsed + handler.velocityX = blendWheelVelocity( + handler.velocityX, instantaneousX) + } + + if (pixelY !== 0) { + if (momentum.velocityY * pixelY < 0) + momentum.velocityY *= 0.7 + const maximumY = Math.max(flickable.originY, + flickable.originY + flickable.contentHeight + - flickable.height) + flickable.contentY = Math.max(flickable.originY, + Math.min(maximumY, + flickable.contentY - pixelY)) + const instantaneousY = pixelY * 1000 / elapsed + handler.velocityY = blendWheelVelocity( + handler.velocityY, instantaneousY) + } + } else { + const wheelStep = Kirigami.Units.gridUnit * 5 + const deltaX = wheel.angleDelta.x / 120 * wheelStep + const deltaY = wheel.angleDelta.y / 120 * wheelStep + + if (horizontal && deltaX !== 0) { + if (momentum.velocityX * deltaX < 0) + momentum.velocityX *= 0.5 + const maximumX = Math.max(flickable.originX, + flickable.originX + flickable.contentWidth + - flickable.width) + flickable.contentX = Math.max(flickable.originX, + Math.min(maximumX, + flickable.contentX - deltaX)) + } + + if (deltaY !== 0) { + if (momentum.velocityY * deltaY < 0) + momentum.velocityY *= 0.5 + const maximumY = Math.max(flickable.originY, + flickable.originY + flickable.contentHeight + - flickable.height) + flickable.contentY = Math.max(flickable.originY, + Math.min(maximumY, + flickable.contentY - deltaY)) + } + } + + wheel.accepted = true + } + + function finishWheelGesture(flickable, handler, + momentum, horizontal) { + if (!handler.touchpadGesture) + return + + const accumulatedX = momentum.velocityX + * handler.velocityX > 0 + ? handler.velocityX + momentum.velocityX * 0.75 + : handler.velocityX + const accumulatedY = momentum.velocityY + * handler.velocityY > 0 + ? handler.velocityY + momentum.velocityY * 0.75 + : handler.velocityY + const velocityX = horizontal + ? Math.max(-flickable.maximumFlickVelocity, + Math.min(flickable.maximumFlickVelocity, + accumulatedX)) : 0 + const velocityY = Math.max(-flickable.maximumFlickVelocity, + Math.min(flickable.maximumFlickVelocity, + accumulatedY)) + + momentum.velocityX = Math.abs(velocityX) >= 80 + ? velocityX : 0 + momentum.velocityY = Math.abs(velocityY) >= 80 + ? velocityY : 0 + momentum.lastFrameTime = Date.now() + if (momentum.velocityX !== 0 || momentum.velocityY !== 0) + momentum.start() + } + + function advanceWheelMomentum(flickable, momentum, + horizontal, gestureActive) { + const now = Date.now() + const elapsed = Math.max(1, + Math.min(32, now - momentum.lastFrameTime)) + momentum.lastFrameTime = now + + if (horizontal && momentum.velocityX !== 0) { + const minimumX = flickable.originX + const maximumX = Math.max(minimumX, + minimumX + flickable.contentWidth - flickable.width) + const nextX = flickable.contentX + - momentum.velocityX * elapsed / 1000 + flickable.contentX = Math.max(minimumX, + Math.min(maximumX, nextX)) + if (nextX <= minimumX || nextX >= maximumX) + momentum.velocityX = 0 + } + + if (momentum.velocityY !== 0) { + const minimumY = flickable.originY + const maximumY = Math.max(minimumY, + minimumY + flickable.contentHeight - flickable.height) + const nextY = flickable.contentY + - momentum.velocityY * elapsed / 1000 + flickable.contentY = Math.max(minimumY, + Math.min(maximumY, nextY)) + if (nextY <= minimumY || nextY >= maximumY) + momentum.velocityY = 0 + } + + const friction = gestureActive ? 0.006 : 0.002 + const decay = Math.pow(1 - friction, elapsed) + momentum.velocityX *= decay + momentum.velocityY *= decay + + if (Math.abs(momentum.velocityX) < 20) + momentum.velocityX = 0 + if (Math.abs(momentum.velocityY) < 20) + momentum.velocityY = 0 + if (momentum.velocityX === 0 && momentum.velocityY === 0) + momentum.stop() + } + readonly property real preferredPackageWidth: Math.max( Kirigami.Units.gridUnit * 8, widestField("name") + widestField("currentVersion") @@ -724,8 +899,54 @@ KCMUtils.SimpleKCM { model: kcm.updatePackages spacing: Kirigami.Units.smallSpacing boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: 6000 pixelAligned: false + WheelHandler { + id: comparisonWheelHandler + + property bool touchpadGesture: false + property real velocityX: 0 + property real velocityY: 0 + property real lastEventTime: 0 + + target: null + acceptedDevices: PointerDevice.Mouse + | PointerDevice.TouchPad + activeTimeout: 0.05 + blocking: true + onActiveChanged: { + if (active) { + updatesWindow.beginWheelGesture( + comparisonWheelHandler) + } else { + updatesWindow.finishWheelGesture( + updateList, comparisonWheelHandler, + comparisonMomentum, true) + } + } + onWheel: function(event) { + updatesWindow.scrollFromWheel( + updateList, event, + comparisonWheelHandler, + comparisonMomentum, true) + } + } + + Timer { + id: comparisonMomentum + + property real velocityX: 0 + property real velocityY: 0 + property real lastFrameTime: 0 + + interval: 16 + repeat: true + onTriggered: updatesWindow.advanceWheelMomentum( + updateList, comparisonMomentum, true, + comparisonWheelHandler.active) + } + Controls.ScrollBar.vertical: Controls.ScrollBar { id: comparisonVerticalScrollBar @@ -889,8 +1110,55 @@ KCMUtils.SimpleKCM { contentWidth: width contentHeight: pacmanPackageFlow.implicitHeight boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: 6000 pixelAligned: false + WheelHandler { + id: pacmanWheelHandler + + property bool touchpadGesture: false + property real velocityX: 0 + property real velocityY: 0 + property real lastEventTime: 0 + + target: null + acceptedDevices: PointerDevice.Mouse + | PointerDevice.TouchPad + activeTimeout: 0.05 + blocking: true + onActiveChanged: { + if (active) { + updatesWindow.beginWheelGesture( + pacmanWheelHandler) + } else { + updatesWindow.finishWheelGesture( + pacmanViewFlickable, + pacmanWheelHandler, + pacmanMomentum, false) + } + } + onWheel: function(event) { + updatesWindow.scrollFromWheel( + pacmanViewFlickable, event, + pacmanWheelHandler, + pacmanMomentum, false) + } + } + + Timer { + id: pacmanMomentum + + property real velocityX: 0 + property real velocityY: 0 + property real lastFrameTime: 0 + + interval: 16 + repeat: true + onTriggered: updatesWindow.advanceWheelMomentum( + pacmanViewFlickable, pacmanMomentum, false, + pacmanWheelHandler.active) + } + Controls.ScrollBar.vertical: Controls.ScrollBar { id: pacmanVerticalScrollBar @@ -1089,4 +1357,92 @@ KCMUtils.SimpleKCM { } } } + + Controls.Dialog { + id: signingKeySecurityDialog + + anchors.centerIn: parent + modal: true + title: i18nd("kcm_fluffupdates", "Action required") + visible: kcm.signingKeySecurityError + && !root.signingKeyDialogDismissed + onRejected: root.signingKeyDialogDismissed = true + + contentItem: ColumnLayout { + width: Math.min(Kirigami.Units.gridUnit * 34, + root.width - Kirigami.Units.largeSpacing * 4) + spacing: Kirigami.Units.largeSpacing + + RowLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: Kirigami.Units.iconSizes.huge + Layout.preferredHeight: width + source: "dialog-error" + } + + Controls.Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: i18nd("kcm_fluffupdates", + "Fluff Linux Update could not verify the FluffNet repository signing key. The update was stopped to protect your system. This may indicate a network problem, an unavailable FluffNet server, an expired or rotated signing key, a repository configuration problem, an unexpected key, or a possible repository compromise. Do not manually accept an unexpected key unless Fluff Linux publishes verified instructions.") + } + } + + Controls.Label { + Layout.fillWidth: true + text: i18nd("kcm_fluffupdates", "Technical details") + font.bold: true + } + + Controls.TextArea { + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 7 + LayoutMirroring.enabled: false + readOnly: true + selectByMouse: true + wrapMode: TextEdit.WrapAnywhere + horizontalAlignment: Text.AlignLeft + text: kcm.signingKeyTechnicalDetails + Accessible.name: i18nd("kcm_fluffupdates", + "Repository signing-key verification technical details") + } + } + + footer: Controls.DialogButtonBox { + Controls.Button { + text: i18nd("kcm_fluffupdates", "Try Again") + icon.name: "view-refresh" + Controls.DialogButtonBox.buttonRole: + Controls.DialogButtonBox.ActionRole + onClicked: kcm.retrySigningKeyUpdate() + } + + Controls.Button { + text: i18nd("kcm_fluffupdates", "Copy Technical Details") + icon.name: "edit-copy" + Controls.DialogButtonBox.buttonRole: + Controls.DialogButtonBox.ActionRole + onClicked: kcm.copySigningKeyTechnicalDetails() + } + + Controls.Button { + text: i18nd("kcm_fluffupdates", "Open an Issue on GitHub") + icon.name: "internet-services" + Controls.DialogButtonBox.buttonRole: + Controls.DialogButtonBox.ActionRole + onClicked: kcm.openSigningKeyIssue() + } + + Controls.Button { + text: i18nd("kcm_fluffupdates", "Close") + icon.name: "dialog-close" + Controls.DialogButtonBox.buttonRole: + Controls.DialogButtonBox.RejectRole + } + } + } } diff --git a/src/updatehelper.cpp b/src/updatehelper.cpp index dc24509..1a8faea 100644 --- a/src/updatehelper.cpp +++ b/src/updatehelper.cpp @@ -1,15 +1,25 @@ #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include #include #include #include #include #include #include +#include +#include + +#include "signingkeyrecovery.h" #include #include @@ -27,11 +37,154 @@ constexpr auto LastUpdateKey = "last_successful_system_update"; constexpr auto LockPath = "/var/lib/pacman/db.lck"; constexpr auto ProtectionPath = "/etc/pacman.d/flufflinux-update-package-protection.json"; +constexpr auto KeyRecoveryLockPath = "/run/flufflinux-update-key-recovery.lock"; +constexpr auto FingerprintUrl = + "https://fluffnet.org/flufflinux-fnrepo/packages/" + "flufflinux-signing-key.fingerprint"; +constexpr auto CertificateUrl = + "https://fluffnet.org/flufflinux-fnrepo/packages/" + "flufflinux-signing-key.asc"; enum class RemovalClass { Protected, Warning, Autoremove }; bool pacmanRunning(); +QByteArray fetchHttpsArtifact(const char *url, qsizetype maximumBytes, + const QString &oversizedCategory, + const QString &unavailableCategory, + QString *failureCategory) +{ + QNetworkAccessManager manager; + const QUrl expectedUrl(QString::fromLatin1(url)); + QNetworkRequest request(expectedUrl); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::SameOriginRedirectPolicy); + request.setTransferTimeout(15000); + QNetworkReply *reply = manager.get(request); + QByteArray response; + bool oversized = false; + QEventLoop eventLoop; + QTimer timeout; + timeout.setSingleShot(true); + QObject::connect(reply, &QNetworkReply::readyRead, &eventLoop, [&] { + response += reply->readAll(); + if (response.size() > maximumBytes) { + oversized = true; + reply->abort(); + } + }); + QObject::connect(reply, &QNetworkReply::finished, &eventLoop, + &QEventLoop::quit); + QObject::connect(&timeout, &QTimer::timeout, reply, &QNetworkReply::abort); + timeout.start(15000); + eventLoop.exec(); + response += reply->readAll(); + if (oversized) { + *failureCategory = oversizedCategory; + reply->deleteLater(); + return {}; + } + const bool secureResponse = reply->url().scheme() == QStringLiteral("https") + && reply->url().host() == expectedUrl.host(); + const bool succeeded = reply->error() == QNetworkReply::NoError; + reply->deleteLater(); + if (!secureResponse || !succeeded) { + *failureCategory = unavailableCategory; + return {}; + } + return response; +} + +QByteArray fetchOfficialFingerprint(QString *failureCategory) +{ + return fetchHttpsArtifact( + FingerprintUrl, 4096, + QStringLiteral("fingerprint-response-oversized"), + QStringLiteral("fingerprint-endpoint-unavailable"), failureCategory); +} + +QByteArray fetchOfficialCertificate(QString *failureCategory) +{ + return fetchHttpsArtifact( + CertificateUrl, 256 * 1024, + QStringLiteral("certificate-response-oversized"), + QStringLiteral("certificate-endpoint-unavailable"), failureCategory); +} + +SigningKeyCommandResult runSigningKeyCommand(const QString &program, + const QStringList &arguments) +{ + QProcess process; + process.setProgram(program); + process.setArguments(arguments); + process.setProcessChannelMode(QProcess::MergedChannels); + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + environment.insert(QStringLiteral("LC_ALL"), QStringLiteral("C")); + process.setProcessEnvironment(environment); + process.start(); + SigningKeyCommandResult result; + result.started = process.waitForStarted(5000); + if (!result.started) { + return result; + } + result.finished = process.waitForFinished(30000); + if (!result.finished) { + process.kill(); + process.waitForFinished(5000); + } + result.output = process.readAll(); + result.exitCode = process.exitStatus() == QProcess::NormalExit + ? process.exitCode() : -1; + return result; +} + +int recoverSigningKeyForCheck(const QString &encodedOutput) +{ + const QByteArray decoded = QByteArray::fromBase64( + encodedOutput.toLatin1(), QByteArray::Base64UrlEncoding); + if (decoded.isEmpty() || decoded.size() > 1024 * 1024) { + std::cout << "FLU_SIGNING_KEY_FAILURE:|check-output-invalid||||1\n"; + return 24; + } + const QString output = QString::fromUtf8(decoded); + const QString repository = SigningKeyRecovery::repositoryName(output); + const QString requested = SigningKeyRecovery::requestedFingerprint(output); + if (repository != QStringLiteral("fluffnet")) { + const QString category = repository.isEmpty() + ? QStringLiteral("repository-unidentified") + : QStringLiteral("repository-not-fluffnet"); + std::cout << "FLU_SIGNING_KEY_FAILURE:" + << repository.toStdString() << '|' + << category.toStdString() << "|||" + << requested.toStdString() << "|1\n"; + return 24; + } + QLockFile recoveryLock(QString::fromLatin1(KeyRecoveryLockPath)); + recoveryLock.setStaleLockTime(0); + if (!recoveryLock.tryLock(100)) { + std::cout << "FLU_SIGNING_KEY_FAILURE:" + << repository.toStdString() + << "|recovery-already-running|||" + << requested.toStdString() << "|1\n"; + return 24; + } + SigningKeyRecovery recovery(SigningKeyRecoveryConfig{}, + fetchOfficialFingerprint, + fetchOfficialCertificate, + runSigningKeyCommand); + const SigningKeyRecoveryResult result = recovery.recover(output); + if (result.recovered) { + return 0; + } + std::cout << "FLU_SIGNING_KEY_FAILURE:" + << result.repository.toStdString() << '|' + << result.category.toStdString() << '|' + << result.expectedFingerprint.toStdString() << '|' + << result.receivedFingerprint.toStdString() << '|' + << result.requestedFingerprint.toStdString() << "|1\n"; + return 24; +} + struct ProtectionPolicy { QSet protectedPackages; QSet warningPackages; @@ -243,7 +396,8 @@ bool writeState(const QJsonObject &state) } int transactionSummary(const QString &requestedDatabase, - const QSet &approvedRemovals) + const QSet &approvedRemovals, + bool signingKeyRetry = false) { Q_UNUSED(approvedRemovals); const QByteArray invokingUid = qgetenv("PKEXEC_UID"); @@ -363,6 +517,50 @@ int transactionSummary(const QString &requestedDatabase, } output += pacman.readAll(); + if (pacman.exitCode() != 0 + && SigningKeyRecovery::containsUnknownKeyReport( + QString::fromLocal8Bit(output))) { + SigningKeyRecoveryResult recoveryResult; + recoveryResult.repository = SigningKeyRecovery::repositoryName( + QString::fromLocal8Bit(output)); + recoveryResult.requestedFingerprint = + SigningKeyRecovery::requestedFingerprint( + QString::fromLocal8Bit(output)); + if (recoveryResult.repository != QStringLiteral("fluffnet")) { + recoveryResult.category = recoveryResult.repository.isEmpty() + ? QStringLiteral("repository-unidentified") + : QStringLiteral("repository-not-fluffnet"); + } else if (signingKeyRetry) { + recoveryResult.category = QStringLiteral("recovery-retry-failed"); + } else { + QLockFile recoveryLock(QString::fromLatin1(KeyRecoveryLockPath)); + recoveryLock.setStaleLockTime(0); + if (!recoveryLock.tryLock(100)) { + recoveryResult.category = + QStringLiteral("recovery-already-running"); + } else { + SigningKeyRecovery recovery( + SigningKeyRecoveryConfig{}, fetchOfficialFingerprint, + fetchOfficialCertificate, + runSigningKeyCommand); + recoveryResult = recovery.recover( + QString::fromLocal8Bit(output)); + } + } + if (recoveryResult.recovered) { + QFile::remove(temporaryLock); + return transactionSummary(requestedDatabase, approvedRemovals, true); + } + std::cout << "FLU_SIGNING_KEY_FAILURE:" + << recoveryResult.repository.toStdString() << '|' + << recoveryResult.category.toStdString() << '|' + << recoveryResult.expectedFingerprint.toStdString() << '|' + << recoveryResult.receivedFingerprint.toStdString() << '|' + << recoveryResult.requestedFingerprint.toStdString() << '|' + << pacman.exitCode() << "\n"; + return 24; + } + for (const auto &[oldPackage, newPackage] : replacements) { const QString oldVersion = installedPackageVersion(oldPackage); const QString newVersion = syncPackageVersion(requestedDatabase, @@ -564,6 +762,12 @@ int recordCurrentUpdate() int main(int argc, char **argv) { + QCoreApplication application(argc, argv); + if (argc == 3 + && QString::fromLocal8Bit(argv[1]) + == QStringLiteral("--recover-signing-key")) { + return recoverSigningKeyForCheck(QString::fromLocal8Bit(argv[2])); + } if ((argc == 2 || argc == 3) && QString::fromLocal8Bit(argv[1]).startsWith( QString::fromLatin1(DatabasePrefix))) { QSet approved; diff --git a/src/updateworker.cpp b/src/updateworker.cpp index ec8089a..d958a4a 100644 --- a/src/updateworker.cpp +++ b/src/updateworker.cpp @@ -1,12 +1,17 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -16,6 +21,8 @@ #include #include +#include "signingkeyrecovery.h" + #include namespace @@ -26,6 +33,13 @@ constexpr auto LogPath = "/etc/pacman.d/flufflinux-update.log"; constexpr auto PacmanLogPath = "/var/log/pacman.log"; constexpr auto CachePath = "/var/cache/pacman/pkg"; constexpr auto LockPath = "/var/lib/pacman/db.lck"; +constexpr auto KeyRecoveryLockPath = "/run/flufflinux-update-key-recovery.lock"; +constexpr auto FingerprintUrl = + "https://fluffnet.org/flufflinux-fnrepo/packages/" + "flufflinux-signing-key.fingerprint"; +constexpr auto CertificateUrl = + "https://fluffnet.org/flufflinux-fnrepo/packages/" + "flufflinux-signing-key.asc"; QJsonObject readState() { @@ -89,6 +103,198 @@ class UpdateWorker final : public QObject } private: + QByteArray fetchHttpsArtifact(const char *url, qsizetype maximumBytes, + const QString &oversizedCategory, + const QString &unavailableCategory, + QString *failureCategory) + { + QNetworkAccessManager manager; + const QUrl expectedUrl(QString::fromLatin1(url)); + QNetworkRequest request(expectedUrl); + request.setAttribute( + QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::SameOriginRedirectPolicy); + request.setTransferTimeout(15000); + QNetworkReply *reply = manager.get(request); + QByteArray response; + bool oversized = false; + QEventLoop eventLoop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::readyRead, &eventLoop, [&] { + response += reply->readAll(); + if (response.size() > maximumBytes) { + oversized = true; + reply->abort(); + } + }); + connect(reply, &QNetworkReply::finished, &eventLoop, + &QEventLoop::quit); + connect(&timeout, &QTimer::timeout, reply, &QNetworkReply::abort); + timeout.start(15000); + eventLoop.exec(); + response += reply->readAll(); + if (oversized) { + *failureCategory = oversizedCategory; + reply->deleteLater(); + return {}; + } + const bool secureResponse = reply->url().scheme() == QStringLiteral("https") + && reply->url().host() == expectedUrl.host(); + const bool succeeded = reply->error() == QNetworkReply::NoError; + reply->deleteLater(); + if (!secureResponse || !succeeded) { + *failureCategory = unavailableCategory; + return {}; + } + return response; + } + + QByteArray fetchOfficialFingerprint(QString *failureCategory) + { + return fetchHttpsArtifact( + FingerprintUrl, 4096, + QStringLiteral("fingerprint-response-oversized"), + QStringLiteral("fingerprint-endpoint-unavailable"), failureCategory); + } + + QByteArray fetchOfficialCertificate(QString *failureCategory) + { + return fetchHttpsArtifact( + CertificateUrl, 256 * 1024, + QStringLiteral("certificate-response-oversized"), + QStringLiteral("certificate-endpoint-unavailable"), failureCategory); + } + + SigningKeyCommandResult runSigningKeyCommand( + const QString &program, const QStringList &arguments) + { + QProcess process; + process.setProgram(program); + process.setArguments(arguments); + process.setProcessChannelMode(QProcess::MergedChannels); + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + environment.insert(QStringLiteral("LC_ALL"), QStringLiteral("C")); + process.setProcessEnvironment(environment); + process.start(); + SigningKeyCommandResult result; + result.started = process.waitForStarted(5000); + if (!result.started) { + return result; + } + result.finished = process.waitForFinished(30000); + if (!result.finished) { + process.kill(); + process.waitForFinished(5000); + } + result.output = process.readAll(); + result.exitCode = process.exitStatus() == QProcess::NormalExit + ? process.exitCode() : -1; + return result; + } + + bool recoverSigningKey(const QString &output, int pacmanExitStatus, + const std::function &retry) + { + const QString repository = + SigningKeyRecovery::repositoryName(output); + if (SigningKeyRecovery::containsUnknownKeyReport(output) + && repository != QStringLiteral("fluffnet")) { + failSigningKey( + repository, + repository.isEmpty() + ? QStringLiteral("repository-unidentified") + : QStringLiteral("repository-not-fluffnet"), + {}, {}, SigningKeyRecovery::requestedFingerprint(output), + pacmanExitStatus); + return true; + } + const QString requested = + SigningKeyRecovery::requestedFingerprint(output); + if (requested.isEmpty()) { + if (SigningKeyRecovery::containsUnknownKeyReport(output)) { + failSigningKey( + repository, + QStringLiteral("requested-fingerprint-invalid"), {}, {}, + {}, pacmanExitStatus); + return true; + } + return false; + } + if (m_attemptedSigningKeys.contains(requested)) { + failSigningKey(repository, + QStringLiteral("recovery-retry-failed"), {}, {}, + requested, pacmanExitStatus); + return true; + } + m_attemptedSigningKeys.insert(requested); + + QLockFile recoveryLock(QString::fromLatin1(KeyRecoveryLockPath)); + recoveryLock.setStaleLockTime(0); + if (!recoveryLock.tryLock(100)) { + failSigningKey(repository, + QStringLiteral("recovery-already-running"), {}, {}, + requested, pacmanExitStatus); + return true; + } + + SigningKeyRecovery recovery( + SigningKeyRecoveryConfig{}, + [this](QString *category) { + return fetchOfficialFingerprint(category); + }, + [this](QString *category) { + return fetchOfficialCertificate(category); + }, + [this](const QString &program, const QStringList &arguments) { + return runSigningKeyCommand(program, arguments); + }); + const SigningKeyRecoveryResult result = recovery.recover(output); + if (!result.recovered) { + failSigningKey(result.repository, result.category, + result.expectedFingerprint, + result.receivedFingerprint, + result.requestedFingerprint, pacmanExitStatus); + return true; + } + + appendLog(QStringLiteral("\n[verified repository signing key imported] " + "%1\n") + .arg(result.receivedFingerprint)); + m_state[QStringLiteral("security_repository")] = result.repository; + m_state.remove(QStringLiteral("security_failure_category")); + m_state.remove(QStringLiteral("security_expected_fingerprint")); + m_state.remove(QStringLiteral("security_received_fingerprint")); + m_state.remove(QStringLiteral("security_requested_fingerprint")); + m_state.remove(QStringLiteral("security_pacman_exit_status")); + m_state[QStringLiteral("recovery_notice_type")] = + QStringLiteral("signing-key-verified"); + m_state[QStringLiteral("recovery_notice_id")] = + QDateTime::currentMSecsSinceEpoch(); + writeState(m_state); + QTimer::singleShot(300, this, retry); + return true; + } + + void failSigningKey(const QString &repository, const QString &category, + const QString &expected, + const QString &received, const QString &requested, + int pacmanExitStatus) + { + m_state[QStringLiteral("phase")] = QStringLiteral("failed"); + m_state[QStringLiteral("error")] = + QStringLiteral("SIGNING_KEY_VERIFICATION_FAILED"); + m_state[QStringLiteral("speed")] = QString(); + m_state[QStringLiteral("security_repository")] = repository.left(64); + m_state[QStringLiteral("security_failure_category")] = category.left(128); + m_state[QStringLiteral("security_expected_fingerprint")] = expected.left(40); + m_state[QStringLiteral("security_received_fingerprint")] = received.left(40); + m_state[QStringLiteral("security_requested_fingerprint")] = requested.left(40); + m_state[QStringLiteral("security_pacman_exit_status")] = pacmanExitStatus; + writeState(m_state); + QCoreApplication::quit(); + } + void determinePackages() { m_state[QStringLiteral("phase")] = QStringLiteral("starting"); @@ -116,6 +322,10 @@ class UpdateWorker final : public QObject + QStringLiteral("\n")); process->deleteLater(); if (status != QProcess::NormalExit || exitCode != 0) { + if (recoverSigningKey(output, exitCode, + [this] { determinePackages(); })) { + return; + } fail(QStringLiteral("TRANSACTION_PREPARE_FAILED")); return; } @@ -189,6 +399,7 @@ class UpdateWorker final : public QObject void startDownload() { appendLog(QStringLiteral("\n[download]\n")); + m_downloadOutput.clear(); const qint64 initialBytes = cachedBytes(); m_state[QStringLiteral("phase")] = QStringLiteral("downloading"); m_state[QStringLiteral("completed_packages")] = completedDownloads(); @@ -246,10 +457,18 @@ class UpdateWorker final : public QObject connect(m_process, qOverload(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus status) { m_speedTimer->stop(); + m_speedTimer->deleteLater(); + m_speedTimer = nullptr; const QString trailing = QString::fromLocal8Bit(m_process->readAll()); m_downloadOutput += trailing; appendLog(trailing); if (status != QProcess::NormalExit || exitCode != 0) { + m_process->deleteLater(); + m_process = nullptr; + if (recoverSigningKey(m_downloadOutput, exitCode, + [this] { startDownload(); })) { + return; + } const QString lower = m_downloadOutput.toLower(); const bool connectionFailure = lower.contains(QStringLiteral("failed retrieving file")) @@ -277,6 +496,7 @@ class UpdateWorker final : public QObject m_state[QStringLiteral("progress")] = 0; m_state[QStringLiteral("speed")] = QString(); m_fullInstallOutput.clear(); + m_installOutput.clear(); writeState(m_state); m_pacmanLogOffset = QFileInfo(QString::fromLatin1(PacmanLogPath)).size(); @@ -309,9 +529,15 @@ class UpdateWorker final : public QObject QString::fromLocal8Bit(m_process->readAll()) + QLatin1Char('\n')); readPacmanInstallLog(); m_installProgressTimer->stop(); + m_installProgressTimer->deleteLater(); + m_installProgressTimer = nullptr; if (status != QProcess::NormalExit || exitCode != 0) { m_process->deleteLater(); m_process = nullptr; + if (recoverSigningKey(m_fullInstallOutput, exitCode, + [this] { startInstall(); })) { + return; + } if (recoverFileConflict(m_fullInstallOutput)) { return; } @@ -504,6 +730,7 @@ class UpdateWorker final : public QObject int m_logCompleted = 0; int m_outputCompleted = 0; int m_fileRecoveryCount = 0; + QSet m_attemptedSigningKeys; }; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..a3a8ecf --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(signingkeyrecoverytest + signingkeyrecoverytest.cpp + ../src/signingkeyrecovery.cpp + ../src/signingkeyrecovery.h + ../src/securitydiagnostics.cpp + ../src/securitydiagnostics.h +) +target_include_directories(signingkeyrecoverytest PRIVATE ../src) +target_link_libraries(signingkeyrecoverytest PRIVATE Qt6::Core Qt6::Test) +add_test(NAME signingkeyrecoverytest COMMAND signingkeyrecoverytest) diff --git a/tests/signingkeyrecoverytest.cpp b/tests/signingkeyrecoverytest.cpp new file mode 100644 index 0000000..bbf921a --- /dev/null +++ b/tests/signingkeyrecoverytest.cpp @@ -0,0 +1,734 @@ +#include "signingkeyrecovery.h" +#include "securitydiagnostics.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +SigningKeyCommandResult execute(const QString &program, + const QStringList &arguments) +{ + QProcess process; + process.setProgram(program); + process.setArguments(arguments); + process.setProcessChannelMode(QProcess::MergedChannels); + process.start(); + SigningKeyCommandResult result; + result.started = process.waitForStarted(5000); + result.finished = result.started && process.waitForFinished(30000); + result.output = process.readAll(); + result.exitCode = result.finished ? process.exitCode() : -1; + return result; +} + +QString firstFingerprint(const QString &home) +{ + const auto result = execute( + QStringLiteral("/usr/bin/gpg"), + {QStringLiteral("--batch"), QStringLiteral("--homedir"), home, + QStringLiteral("--with-colons"), QStringLiteral("--fingerprint"), + QStringLiteral("--list-keys")}); + for (const QByteArray &line : result.output.split('\n')) { + const QList fields = line.split(':'); + if (fields.size() > 9 && fields[0] == "fpr") { + return QString::fromLatin1(fields[9]); + } + } + return {}; +} + +QStringList primaryFingerprints(const QString &home) +{ + const auto result = execute( + QStringLiteral("/usr/bin/gpg"), + {QStringLiteral("--batch"), QStringLiteral("--homedir"), home, + QStringLiteral("--with-colons"), QStringLiteral("--fingerprint"), + QStringLiteral("--list-keys")}); + QStringList fingerprints; + bool awaitingPrimary = false; + for (const QByteArray &line : result.output.split('\n')) { + const QList fields = line.split(':'); + if (!fields.isEmpty() && fields[0] == "pub") { + awaitingPrimary = true; + } else if (awaitingPrimary && fields.size() > 9 + && fields[0] == "fpr") { + fingerprints << QString::fromLatin1(fields[9]); + awaitingPrimary = false; + } + } + return fingerprints; +} + +QStringList subkeyFingerprints(const QString &home, const QString &primary) +{ + const auto result = execute( + QStringLiteral("/usr/bin/gpg"), + {QStringLiteral("--batch"), QStringLiteral("--homedir"), home, + QStringLiteral("--with-colons"), QStringLiteral("--fingerprint"), + QStringLiteral("--fingerprint"), QStringLiteral("--list-keys"), + primary}); + QStringList fingerprints; + bool awaitingSubkey = false; + for (const QByteArray &line : result.output.split('\n')) { + const QList fields = line.split(':'); + if (!fields.isEmpty() && fields[0] == "sub") { + awaitingSubkey = true; + } else if (awaitingSubkey && fields.size() > 9 + && fields[0] == "fpr") { + fingerprints << QString::fromLatin1(fields[9]); + awaitingSubkey = false; + } + } + return fingerprints; +} + +QByteArray exportedCertificate(const QString &home, const QString &primary, + bool secret = false) +{ + return execute( + QStringLiteral("/usr/bin/gpg"), + {QStringLiteral("--batch"), QStringLiteral("--homedir"), home, + QStringLiteral("--armor"), + secret ? QStringLiteral("--export-secret-keys") + : QStringLiteral("--export"), + primary}).output; +} + +QString unknownKey(const QString &repository, const QString &fingerprint) +{ + return QStringLiteral("error: %1: key \"%2\" is unknown\n" + "error: keyring is not writable") + .arg(repository, fingerprint); +} + +SigningKeyCommandResult completed(int exitCode = 0) +{ + SigningKeyCommandResult result; + result.started = true; + result.finished = true; + result.exitCode = exitCode; + return result; +} + +QByteArray mutateFirstSubkey(const QByteArray &listing, + const QByteArray &validity, + bool removeSigningCapability) +{ + QList lines = listing.split('\n'); + for (QByteArray &line : lines) { + QList fields = line.split(':'); + if (fields.isEmpty() || fields[0] != "sub") { + continue; + } + if (fields.size() > 1 && !validity.isEmpty()) { + fields[1] = validity; + } + if (fields.size() > 11 && removeSigningCapability) { + fields[11].replace("s", ""); + fields[11].replace("S", ""); + } + line = fields.join(':'); + break; + } + return lines.join('\n'); +} + +QByteArray removeSignatureClass(const QByteArray &listing, + const QByteArray &signatureClassPrefix) +{ + QList retained; + for (const QByteArray &line : listing.split('\n')) { + const QList fields = line.split(':'); + if (fields.size() > 10 && fields[0] == "sig" + && fields[10].toLower().startsWith(signatureClassPrefix)) { + continue; + } + retained.append(line); + } + return retained.join('\n'); +} +} + +class SigningKeyRecoveryTest final : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + if (!QFileInfo::exists(QStringLiteral("/usr/bin/gpg"))) { + QSKIP("GnuPG is required for the disposable certificate tests"); + } + QVERIFY(m_keys.isValid()); + QVERIFY(QFile::setPermissions( + m_keys.path(), QFileDevice::ReadOwner | QFileDevice::WriteOwner + | QFileDevice::ExeOwner)); + + const QStringList common{ + QStringLiteral("--batch"), QStringLiteral("--homedir"), + m_keys.path(), QStringLiteral("--pinentry-mode"), + QStringLiteral("loopback"), QStringLiteral("--passphrase"), + QString(), + }; + QStringList generate = common; + generate << QStringLiteral("--quick-generate-key") + << QStringLiteral("Expected Test Key ") + << QStringLiteral("ed25519") << QStringLiteral("cert") + << QStringLiteral("1d"); + QCOMPARE(execute(QStringLiteral("/usr/bin/gpg"), generate).exitCode, 0); + m_expectedPrimary = firstFingerprint(m_keys.path()); + QCOMPARE(m_expectedPrimary.size(), 40); + + for (int index = 0; index < 2; ++index) { + QStringList add = common; + add << QStringLiteral("--quick-add-key") << m_expectedPrimary + << QStringLiteral("ed25519") << QStringLiteral("sign") + << QStringLiteral("1d"); + QCOMPARE(execute(QStringLiteral("/usr/bin/gpg"), add).exitCode, 0); + } + m_expectedSubkeys = subkeyFingerprints(m_keys.path(), m_expectedPrimary); + QCOMPARE(m_expectedSubkeys.size(), 2); + + QStringList unrelated = common; + unrelated << QStringLiteral("--quick-generate-key") + << QStringLiteral("Unrelated Test Key ") + << QStringLiteral("ed25519") << QStringLiteral("cert") + << QStringLiteral("1d"); + QCOMPARE(execute(QStringLiteral("/usr/bin/gpg"), unrelated).exitCode, 0); + const QStringList primaries = primaryFingerprints(m_keys.path()); + QCOMPARE(primaries.size(), 2); + m_unrelatedPrimary = primaries[1]; + + QStringList unrelatedSigning = common; + unrelatedSigning << QStringLiteral("--quick-add-key") + << m_unrelatedPrimary << QStringLiteral("ed25519") + << QStringLiteral("sign") << QStringLiteral("1d"); + QCOMPARE(execute(QStringLiteral("/usr/bin/gpg"), unrelatedSigning).exitCode, + 0); + m_unrelatedSubkey = + subkeyFingerprints(m_keys.path(), m_unrelatedPrimary).constFirst(); + } + + void normalizeFingerprint_data() + { + QTest::addColumn("response"); + QTest::addColumn("valid"); + const QByteArray fingerprint = m_expectedPrimary.toLatin1(); + QTest::newRow("exact") << fingerprint << true; + QTest::newRow("one-trailing-newline") << fingerprint + '\n' << true; + QTest::newRow("lowercase") << fingerprint.toLower() << false; + QTest::newRow("leading-space") << ' ' + fingerprint << false; + QTest::newRow("embedded-whitespace") + << fingerprint.left(20) + " \t" + fingerprint.mid(20) << false; + QTest::newRow("crlf") << fingerprint + "\r\n" << false; + QTest::newRow("empty") << QByteArray{} << false; + QTest::newRow("malformed") << QByteArray("not-a-fingerprint") << false; + QTest::newRow("multiple") << fingerprint + '\n' + fingerprint << false; + QTest::newRow("oversized") << QByteArray(5000, 'A') << false; + } + + void normalizeFingerprint() + { + QFETCH(QByteArray, response); + QFETCH(bool, valid); + QCOMPARE(!SigningKeyRecovery::normalizeFingerprint(response).isEmpty(), + valid); + } + + void acceptsPrimaryAndCertifiedRotatedSubkeys_data() + { + QTest::addColumn("requested"); + QTest::newRow("primary") << m_expectedPrimary; + QTest::newRow("current-subkey") << m_expectedSubkeys[0]; + QTest::newRow("rotated-subkey") << m_expectedSubkeys[1]; + } + + void acceptsPrimaryAndCertifiedRotatedSubkeys() + { + QFETCH(QString, requested); + QStringList operations; + SigningKeyRecovery recovery = makeRecovery( + m_expectedPrimary, m_expectedPrimary, &operations); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), requested)); + QVERIFY2(result.recovered, qPrintable(result.category)); + QCOMPARE(result.repository, QStringLiteral("fluffnet")); + QCOMPARE(result.receivedFingerprint, m_expectedPrimary); + QVERIFY(operations.contains(QStringLiteral("--add"))); + QVERIFY(operations.contains(QStringLiteral("--lsign-key"))); + } + + void rejectsUnrelatedPrimaryAndSubkey() + { + QStringList operations; + SigningKeyRecovery recovery = makeRecovery( + m_expectedPrimary, m_unrelatedPrimary, &operations); + auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_unrelatedPrimary)); + QCOMPARE(result.category, QStringLiteral("primary-fingerprint-mismatch")); + QVERIFY(operations.isEmpty()); + + operations.clear(); + recovery = makeRecovery(m_expectedPrimary, m_unrelatedPrimary, + &operations); + result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_unrelatedSubkey)); + QCOMPARE(result.category, QStringLiteral("primary-fingerprint-mismatch")); + QVERIFY(operations.isEmpty()); + } + + void repositoryIsolation() + { + const QStringList repositories{ + QStringLiteral("core"), QStringLiteral("extra"), + QStringLiteral("thirdparty"), QStringLiteral("local"), + QStringLiteral("closed-network"), + }; + for (const QString &repository : repositories) { + int fingerprintFetches = 0; + int certificateFetches = 0; + int pacmanOperations = 0; + SigningKeyRecovery recovery( + testConfig(), + [this, &fingerprintFetches](QString *) { + ++fingerprintFetches; + return m_expectedPrimary.toLatin1(); + }, + [this, &certificateFetches](QString *) { + ++certificateFetches; + return exportedCertificate(m_keys.path(), m_expectedPrimary); + }, + [&pacmanOperations](const QString &program, + const QStringList &arguments) { + if (program.endsWith(QStringLiteral("pacman-key"))) { + ++pacmanOperations; + } + return execute(program, arguments); + }); + const auto result = recovery.recover( + unknownKey(repository, m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("repository-not-fluffnet")); + QCOMPARE(result.repository, repository); + QCOMPARE(fingerprintFetches, 0); + QCOMPARE(certificateFetches, 0); + QCOMPARE(pacmanOperations, 0); + } + + int fetches = 0; + SigningKeyRecovery unidentified( + testConfig(), + [&fetches](QString *) { ++fetches; return QByteArray{}; }, + [&fetches](QString *) { ++fetches; return QByteArray{}; }, + [](const QString &, const QStringList &) { return completed(); }); + const auto result = unidentified.recover( + QStringLiteral("unknown signing key %1").arg(m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("repository-unidentified")); + QCOMPARE(fetches, 0); + } + + void endpointAndCertificateFailuresAreSafe() + { + SigningKeyRecovery fingerprintFailure( + testConfig(), + [](QString *category) { + *category = QStringLiteral("fingerprint-endpoint-unavailable"); + return QByteArray{}; + }, + [](QString *) { return QByteArray{}; }, + [](const QString &, const QStringList &) { return completed(); }); + auto result = fingerprintFailure.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, + QStringLiteral("fingerprint-endpoint-unavailable")); + + SigningKeyRecovery certificateFailure( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [](QString *category) { + *category = QStringLiteral("certificate-endpoint-unavailable"); + return QByteArray{}; + }, + [](const QString &, const QStringList &) { return completed(); }); + result = certificateFailure.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, + QStringLiteral("certificate-endpoint-unavailable")); + + SigningKeyRecovery emptyCertificate( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [](QString *) { return QByteArray{}; }, + [](const QString &, const QStringList &) { return completed(); }); + result = emptyCertificate.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("certificate-response-empty")); + + SigningKeyRecovery oversizedCertificate( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [](QString *) { return QByteArray(256 * 1024 + 1, 'A'); }, + [](const QString &, const QStringList &) { return completed(); }); + result = oversizedCertificate.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, + QStringLiteral("certificate-response-oversized")); + + SigningKeyRecovery malformedCertificate( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [](QString *) { return QByteArray("not an OpenPGP certificate"); }, + [](const QString &program, const QStringList &arguments) { + return execute(program, arguments); + }); + result = malformedCertificate.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("certificate-parse-failed")); + } + + void rejectsPrivateKeyPackets() + { + int pacmanOperations = 0; + SigningKeyRecovery recovery( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [this](QString *) { + return exportedCertificate(m_keys.path(), m_expectedPrimary, + true); + }, + [&pacmanOperations](const QString &program, + const QStringList &arguments) { + if (program.endsWith(QStringLiteral("pacman-key"))) { + ++pacmanOperations; + } + return execute(program, arguments); + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, + QStringLiteral("certificate-contains-private-key")); + QCOMPARE(pacmanOperations, 0); + } + + void fingerprintResponsesFailClosed_data() + { + QTest::addColumn("response"); + QTest::addColumn("category"); + const QByteArray fingerprint = m_expectedPrimary.toLatin1(); + QTest::newRow("empty") + << QByteArray{} << QStringLiteral("fingerprint-response-malformed"); + QTest::newRow("lowercase") + << fingerprint.toLower() + << QStringLiteral("fingerprint-response-malformed"); + QTest::newRow("multiple") + << fingerprint + '\n' + fingerprint + << QStringLiteral("fingerprint-response-malformed"); + QTest::newRow("whitespace") + << fingerprint.left(20) + ' ' + fingerprint.mid(20) + << QStringLiteral("fingerprint-response-malformed"); + QTest::newRow("oversized") + << QByteArray(4097, 'A') + << QStringLiteral("fingerprint-response-oversized"); + } + + void fingerprintResponsesFailClosed() + { + QFETCH(QByteArray, response); + QFETCH(QString, category); + int certificateFetches = 0; + int commands = 0; + SigningKeyRecovery recovery( + testConfig(), + [response](QString *) { return response; }, + [&certificateFetches](QString *) { + ++certificateFetches; + return QByteArray{}; + }, + [&commands](const QString &, const QStringList &) { + ++commands; + return completed(); + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, category); + QCOMPARE(certificateFetches, 0); + QCOMPARE(commands, 0); + } + + void certificateImportFailureDoesNotReachPacman() + { + int pacmanOperations = 0; + SigningKeyRecovery recovery( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [this](QString *) { + return exportedCertificate(m_keys.path(), m_expectedPrimary); + }, + [&pacmanOperations](const QString &program, + const QStringList &arguments) { + if (program.endsWith(QStringLiteral("pacman-key"))) { + ++pacmanOperations; + } + if (program.endsWith(QStringLiteral("gpg")) + && arguments.contains(QStringLiteral("--import"))) { + return completed(1); + } + return execute(program, arguments); + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("certificate-import-failed")); + QCOMPARE(pacmanOperations, 0); + } + + void rejectsUnusableSigningSubkey_data() + { + QTest::addColumn("validity"); + QTest::addColumn("removeSigningCapability"); + QTest::newRow("expired") << QByteArray("e") << false; + QTest::newRow("revoked") << QByteArray("r") << false; + QTest::newRow("disabled") << QByteArray("d") << false; + QTest::newRow("invalid") << QByteArray("i") << false; + QTest::newRow("non-signing") << QByteArray{} << true; + } + + void rejectsUnusableSigningSubkey() + { + QFETCH(QByteArray, validity); + QFETCH(bool, removeSigningCapability); + SigningKeyRecovery recovery( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [this](QString *) { + return exportedCertificate(m_keys.path(), m_expectedPrimary); + }, + [validity, removeSigningCapability]( + const QString &program, const QStringList &arguments) { + SigningKeyCommandResult result = execute(program, arguments); + if (arguments.contains(QStringLiteral("--list-keys"))) { + result.output = mutateFirstSubkey( + result.output, validity, removeSigningCapability); + } + return result; + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("signing-key-unusable")); + } + + void rejectsMissingBindingAndSelfSignatures_data() + { + QTest::addColumn("signatureClass"); + QTest::addColumn("category"); + QTest::newRow("missing-binding") + << QByteArray("18") << QStringLiteral("subkey-binding-invalid"); + QTest::newRow("missing-self-signature") + << QByteArray("13") + << QStringLiteral("certificate-signature-invalid"); + } + + void rejectsMissingBindingAndSelfSignatures() + { + QFETCH(QByteArray, signatureClass); + QFETCH(QString, category); + SigningKeyRecovery recovery( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [this](QString *) { + return exportedCertificate(m_keys.path(), m_expectedPrimary); + }, + [signatureClass](const QString &program, + const QStringList &arguments) { + SigningKeyCommandResult result = execute(program, arguments); + if (arguments.contains(QStringLiteral("--check-sigs"))) { + result.output = + removeSignatureClass(result.output, signatureClass); + } + return result; + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, category); + } + + void rejectsShortKeyIdsAndLeavesNormalFailuresAlone() + { + QCOMPARE(SigningKeyRecovery::requestedFingerprint( + QStringLiteral("unknown key DEADBEEF12345678")), + QString{}); + QVERIFY(SigningKeyRecovery::containsUnknownKeyReport( + QStringLiteral("unknown key DEADBEEF12345678"))); + const QString pacmanOutput = unknownKey( + QStringLiteral("fluffnet"), QStringLiteral( + "D22EA9EEF63938678070E27DBD8F8D65E5D7DA6F")); + QCOMPARE(SigningKeyRecovery::repositoryName(pacmanOutput), + QStringLiteral("fluffnet")); + QCOMPARE(SigningKeyRecovery::requestedFingerprint(pacmanOutput), + QStringLiteral( + "D22EA9EEF63938678070E27DBD8F8D65E5D7DA6F")); + QVERIFY(!SigningKeyRecovery::containsUnknownKeyReport( + QStringLiteral("error: failed retrieving file"))); + } + + void failedTrustRollsBackNewImport() + { + QStringList operations; + const QString sourceHome = m_keys.path(); + SigningKeyRecovery recovery( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [sourceHome, this](QString *) { + return exportedCertificate(sourceHome, m_expectedPrimary); + }, + [&operations](const QString &program, + const QStringList &arguments) { + if (program.endsWith(QStringLiteral("pacman-key"))) { + const QString operation = arguments.value(0); + operations << operation; + return completed(operation == QStringLiteral("--lsign-key") + || operation == QStringLiteral("--finger") + ? 1 + : 0); + } + return execute(program, arguments); + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QCOMPARE(result.category, QStringLiteral("key-trust-failed")); + QVERIFY(operations.contains(QStringLiteral("--add"))); + QVERIFY(operations.contains(QStringLiteral("--delete"))); + } + + void temporaryKeyringIsRemoved() + { + QString temporaryHome; + QStringList operations; + const QString sourceHome = m_keys.path(); + SigningKeyRecovery recovery( + testConfig(), + [this](QString *) { return m_expectedPrimary.toLatin1(); }, + [sourceHome, this](QString *) { + return exportedCertificate(sourceHome, m_expectedPrimary); + }, + [&temporaryHome, &operations](const QString &program, + const QStringList &arguments) { + const int homeIndex = arguments.indexOf(QStringLiteral("--homedir")); + if (homeIndex >= 0) { + temporaryHome = arguments.value(homeIndex + 1); + } + if (program.endsWith(QStringLiteral("pacman-key"))) { + operations << arguments.value(0); + return completed(arguments.value(0) + == QStringLiteral("--finger") + ? 1 + : 0); + } + return execute(program, arguments); + }); + const auto result = recovery.recover( + unknownKey(QStringLiteral("fluffnet"), m_expectedSubkeys[0])); + QVERIFY2(result.recovered, qPrintable(result.category)); + QVERIFY(!temporaryHome.isEmpty()); + QVERIFY(!QDir(temporaryHome).exists()); + } + + void concurrentLockIsExclusive() + { + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QString path = directory.path() + QStringLiteral("/recovery.lock"); + QLockFile first(path); + QLockFile second(path); + first.setStaleLockTime(0); + second.setStaleLockTime(0); + QVERIFY(first.tryLock(0)); + QVERIFY(!second.tryLock(0)); + } + + void issueDiagnosticsAreWhitelistedAndSanitized() + { + SigningKeyIssueDetails details; + details.repository = QStringLiteral("fluffnet"); + details.fluVersion = QStringLiteral("1.4.0"); + details.osVersion = QStringLiteral( + "Fluff Linux /home/alice/private token=supersecret"); + details.expectedFingerprint = m_expectedPrimary; + details.receivedFingerprint = m_unrelatedPrimary; + details.requestedFingerprint = m_expectedSubkeys[0]; + details.failureCategory = QStringLiteral("primary-fingerprint-mismatch"); + details.pacmanExitStatus = 1; + const QUrl url = signingKeyIssueUrl(details); + QCOMPARE(url.host(), QStringLiteral("github.com")); + QCOMPARE(url.path(), + QStringLiteral("/FluffNet/flufflinux-update/issues/new")); + const QString body = + QUrlQuery(url).queryItemValue(QStringLiteral("body")); + QVERIFY(body.contains(QStringLiteral("Repository: fluffnet"))); + QVERIFY(!body.contains(QStringLiteral("alice"))); + QVERIFY(!body.contains(QStringLiteral("supersecret"))); + QVERIFY(!body.contains(QStringLiteral("/home/"))); + QVERIFY(body.contains(m_expectedPrimary)); + QVERIFY(body.contains(m_expectedSubkeys[0])); + + int opened = 0; + QVERIFY(openSigningKeyIssueUrl(url, [&opened](const QUrl &) { + ++opened; + return true; + })); + QCOMPARE(opened, 1); + QVERIFY(!openSigningKeyIssueUrl( + QUrl(QStringLiteral("https://example.invalid/issues/new")), + [&opened](const QUrl &) { + ++opened; + return true; + })); + QCOMPARE(opened, 1); + } + +private: + SigningKeyRecoveryConfig testConfig() const + { + return {}; + } + + SigningKeyRecovery makeRecovery(const QString &expectedPrimary, + const QString &certificatePrimary, + QStringList *pacmanOperations) + { + const QString sourceHome = m_keys.path(); + return SigningKeyRecovery( + testConfig(), + [expectedPrimary](QString *) { + return expectedPrimary.toLatin1(); + }, + [sourceHome, certificatePrimary](QString *) { + return exportedCertificate(sourceHome, certificatePrimary); + }, + [pacmanOperations](const QString &program, + const QStringList &arguments) { + if (program.endsWith(QStringLiteral("pacman-key"))) { + pacmanOperations->append(arguments.value(0)); + return completed(arguments.value(0) + == QStringLiteral("--finger") + ? 1 + : 0); + } + return execute(program, arguments); + }); + } + + QTemporaryDir m_keys; + QString m_expectedPrimary; + QStringList m_expectedSubkeys; + QString m_unrelatedPrimary; + QString m_unrelatedSubkey; +}; + +QTEST_GUILESS_MAIN(SigningKeyRecoveryTest) + +#include "signingkeyrecoverytest.moc"