From 19df3c3a75d33f6a45554efdeffdda635d66e92e Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Fri, 4 Sep 2026 23:16:15 +0000 Subject: [PATCH] Add opt-in Plasma fingerprint workaround --- README.md | 18 ++ plasma-fingerprint-workaround/README.md | 60 ++++ .../kscreenlocker/kscreenlocker.spec | 108 +++++++ ...ont-cancel-authentication-on-suspend.patch | 68 ++++ ...trying-noninteractive-authentication.patch | 30 ++ ...-after-transient-auth-unavailability.patch | 28 ++ ...-unavailability-for-fingerprint-only.patch | 68 ++++ .../restart-fprintd-after-resume | 6 + plasma-fingerprint-workaround/payload.conf | 2 + .../plasma-fingerprint-workaround | 306 ++++++++++++++++++ .../plasma-fingerprint-workaround.spec | 54 ++++ .../tests/test-controller | 23 ++ .../tests/test-patched-kscreenlocker-rpm | 16 + scripts/build-patched-kscreenlocker-rpm | 60 ++++ .../build-plasma-fingerprint-workaround-rpm | 20 ++ .../publish-fingerprint-workaround-payload | 53 +++ 16 files changed, 920 insertions(+) create mode 100644 plasma-fingerprint-workaround/README.md create mode 100644 plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec create mode 100644 plasma-fingerprint-workaround/kscreenlocker/patches/0001-dont-cancel-authentication-on-suspend.patch create mode 100644 plasma-fingerprint-workaround/kscreenlocker/patches/0002-allow-retrying-noninteractive-authentication.patch create mode 100644 plasma-fingerprint-workaround/kscreenlocker/patches/0003-allow-retry-after-transient-auth-unavailability.patch create mode 100644 plasma-fingerprint-workaround/kscreenlocker/patches/0004-retry-transient-unavailability-for-fingerprint-only.patch create mode 100755 plasma-fingerprint-workaround/kscreenlocker/restart-fprintd-after-resume create mode 100644 plasma-fingerprint-workaround/payload.conf create mode 100755 plasma-fingerprint-workaround/plasma-fingerprint-workaround create mode 100644 plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec create mode 100755 plasma-fingerprint-workaround/tests/test-controller create mode 100755 plasma-fingerprint-workaround/tests/test-patched-kscreenlocker-rpm create mode 100755 scripts/build-patched-kscreenlocker-rpm create mode 100755 scripts/build-plasma-fingerprint-workaround-rpm create mode 100755 scripts/publish-fingerprint-workaround-payload diff --git a/README.md b/README.md index 6e2f30e..5de9492 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ sudo dnf config-manager addrepo \ sudo dnf install touchpad-hold-tap sudo dnf install plasma-always-show-unlock sudo dnf install plasma-task-group-shortcuts +sudo dnf install plasma-fingerprint-workaround ``` ## Compatibility @@ -82,3 +83,20 @@ removing it, restore them with: ```bash plasma-task-group-shortcuts --restore-shortcuts ``` + +### Plasma fingerprint workaround + +Experimental and unaudited. The `plasma-fingerprint-workaround` package is an +inert controller for installing a patched Fedora KScreenLocker build that +recovers fingerprint authentication after suspend. It makes no lock-screen +changes when installed and must be enabled explicitly: + +```bash +sudo plasma-fingerprint-workaround enable +``` + +The patched build is not present in the DNF repository and therefore cannot be +selected by a normal system update. The controller can also install a locally +built RPM and restore Fedora's original package. See the tool's +[`README`](plasma-fingerprint-workaround/README.md) for the security warning, +build instructions, version checks, and force-downgrade option. diff --git a/plasma-fingerprint-workaround/README.md b/plasma-fingerprint-workaround/README.md new file mode 100644 index 0000000..90bcc90 --- /dev/null +++ b/plasma-fingerprint-workaround/README.md @@ -0,0 +1,60 @@ +# Plasma fingerprint workaround + +This is an experimental and unaudited replacement for Fedora's KScreenLocker +package. It changes security-sensitive session-locking and authentication code. +Review the patches and build the package yourself if you do not trust the +provided binary. + +Installing `plasma-fingerprint-workaround` does not enable the workaround or +modify KScreenLocker. The controller only acts when explicitly asked: + +```bash +sudo plasma-fingerprint-workaround enable +plasma-fingerprint-workaround status +sudo plasma-fingerprint-workaround disable +``` + +The default payload is kept outside the DNF repository. The controller checks +its pinned SHA-256 digest, RPM integrity, package name, architecture, and +workaround metadata before asking DNF to install it. Disabling the workaround +restores the current KScreenLocker package from Fedora's `fedora` or `updates` +repository. + +The provided build currently supports Fedora 44 on x86_64 and is based on +`kscreenlocker-6.7.4-1.fc44`. A different base version is rejected by default. +To intentionally install the older provided build after a KScreenLocker update: + +```bash +sudo plasma-fingerprint-workaround enable --force +``` + +## Locally built package + +Build dependencies can be installed and the package built with: + +```bash +sudo dnf5 builddep plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec +./scripts/build-patched-kscreenlocker-rpm +``` + +Install the resulting package directly through the controller: + +```bash +sudo plasma-fingerprint-workaround enable \ + --rpm rpmbuild/RPMS/x86_64/kscreenlocker-6.7.4-1.fc44.ajp5.x86_64.rpm +``` + +For another upstream version, supply its version, Fedora base release, and +source archive checksum. The patches must still apply and compile cleanly. + +```bash +KSCREENLOCKER_VERSION=6.7.5 \ +KSCREENLOCKER_BASE_RELEASE=1 \ +KSCREENLOCKER_SOURCE_SHA256=SHA256 \ + ./scripts/build-patched-kscreenlocker-rpm +``` + +A locally built package based on a different installed version requires +`--force`. This flag permits the version mismatch and allows DNF to downgrade; +it does not bypass the package identity, architecture, integrity, or workaround +metadata checks. diff --git a/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec b/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec new file mode 100644 index 0000000..915c12f --- /dev/null +++ b/plasma-fingerprint-workaround/kscreenlocker/kscreenlocker.spec @@ -0,0 +1,108 @@ +%{!?upstream_version:%global upstream_version 6.7.4} +%{!?base_release:%global base_release 1} +%{!?workaround_release:%global workaround_release 5} + +Name: kscreenlocker +Version: %{upstream_version} +Release: %{base_release}%{?dist}.ajp%{workaround_release} +Summary: Library and components for secure lock screen architecture + +License: BSD-3-Clause AND CC0-1.0 AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-only AND LGPL-2.1-only AND LGPL-3.0-only AND (GPL-2.0-only OR GPL-3.0-only) AND (LGPL-2.1-only OR LGPL-3.0-only) +URL: https://invent.kde.org/plasma/%{name} +Source0: https://download.kde.org/stable/plasma/%{version}/%{name}-%{version}.tar.xz +Source1: restart-fprintd-after-resume + +Patch0: 0001-dont-cancel-authentication-on-suspend.patch +Patch1: 0002-allow-retrying-noninteractive-authentication.patch +Patch2: 0003-allow-retry-after-transient-auth-unavailability.patch +Patch3: 0004-retry-transient-unavailability-for-fingerprint-only.patch + +# Marks packages that the controller may install and records the Fedora build +# on which the patches were based. +Provides: plasma-fingerprint-workaround-payload = 1 +Provides: plasma-fingerprint-workaround-base = %{version}-%{base_release}%{?dist} +Provides: kscreenlocker = %{version}-%{base_release}%{?dist} + +# Help upgrades; kscreenlocker was split from plasma-workspace in Plasma 5.5. +Conflicts: plasma-workspace < 5.5 + +BuildRequires: cmake(LayerShellQt) +BuildRequires: perl-generators +BuildRequires: qt6-qtbase-devel +BuildRequires: qt6-qtbase-private-devel +BuildRequires: cmake(Qt6Quick) +BuildRequires: kf6-rpm-macros +BuildRequires: extra-cmake-modules +BuildRequires: cmake(KF6Crash) +BuildRequires: cmake(KF6GlobalAccel) +BuildRequires: cmake(KF6I18n) +BuildRequires: cmake(KF6IdleTime) +BuildRequires: cmake(KF6KCMUtils) +BuildRequires: cmake(KF6KIO) +BuildRequires: cmake(KF6Notifications) +BuildRequires: cmake(KF6Screen) +BuildRequires: cmake(KF6Solid) +BuildRequires: cmake(KF6Svg) +BuildRequires: cmake(KF6XmlGui) +BuildRequires: cmake(KF6GuiAddons) +BuildRequires: cmake(KF6KirigamiPlatform) +BuildRequires: libX11-devel +BuildRequires: xcb-util-keysyms-devel +BuildRequires: wayland-devel +BuildRequires: pkgconfig(xi) +BuildRequires: pkgconfig(xcb-event) +BuildRequires: cmake(PlasmaQuick) +BuildRequires: libXcursor-devel +BuildRequires: pam-devel + +%description +%{summary}. + +This experimental Fedora 44 build contains local fingerprint authentication +workarounds. It has not received the security review of Fedora's package. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +The %{name}-devel package contains libraries and header files for developing +applications that use %{name}. + +%prep +%autosetup -p1 + +%build +%cmake_kf6 +%cmake_build + +%install +%cmake_install +install -D -m 0755 %{SOURCE1} \ + %{buildroot}%{_prefix}/lib/systemd/system-sleep/restart-fprintd-after-resume + +%find_lang %{name} --with-qt --all-name + +%files -f %{name}.lang +%license COPYING +%{_kf6_libdir}/libKScreenLocker.so.* +%{_kf6_datadir}/knotifications6/*.notifyrc +%{_libexecdir}/kscreenlocker_greet +%dir %{_kf6_datadir}/ksmserver/ +%{_kf6_datadir}/ksmserver/screenlocker/ +%{_kf6_datadir}/applications/kcm_screenlocker.desktop +%{_kf6_qtplugindir}/plasma/kcms/systemsettings/kcm_screenlocker.so +%{_kf6_datadir}/qlogging-categories6/kscreenlocker.categories +%{_prefix}/lib/systemd/system-sleep/restart-fprintd-after-resume + +%files devel +%{_kf6_libdir}/libKScreenLocker.so +%{_kf6_libdir}/cmake/ScreenSaverDBusInterface/ +%{_kf6_libdir}/cmake/KScreenLocker/ +%{_includedir}/KScreenLocker/ +%{_datadir}/dbus-1/interfaces/*.xml + +%changelog +* Fri Sep 04 2026 Anton - 6.7.4-1.fc44.ajp5 +- Package the fprintd resume hook and compatibility metadata +- Apply the tested fingerprint retry patch set diff --git a/plasma-fingerprint-workaround/kscreenlocker/patches/0001-dont-cancel-authentication-on-suspend.patch b/plasma-fingerprint-workaround/kscreenlocker/patches/0001-dont-cancel-authentication-on-suspend.patch new file mode 100644 index 0000000..c520009 --- /dev/null +++ b/plasma-fingerprint-workaround/kscreenlocker/patches/0001-dont-cancel-authentication-on-suspend.patch @@ -0,0 +1,68 @@ +From: Alex Sanchez-Stern +Subject: [PATCH] Don't cancel in-progress authentication on suspend + +Aborting an in-progress PAM conversation when the system suspends makes +pam_authenticate() report a failure. The greeter then displays a failed +login on resume, and authentication backends such as fingerprint can be +left unusable for the remainder of that lock-screen session. + +Leave the conversation parked across suspend and resume instead. This is +the change proposed upstream in KScreenLocker merge request 340, adapted +to the Plasma 6.7.4 source shipped by Fedora 44. +--- + greeter/greeterapp.cpp | 6 ------ + greeter/greeterapp.h | 2 -- + 2 files changed, 8 deletions(-) + +diff --git a/greeter/greeterapp.cpp b/greeter/greeterapp.cpp +index b1de459..08d8134 100644 +--- a/greeter/greeterapp.cpp ++++ b/greeter/greeterapp.cpp +@@ -10,8 +10,6 @@ SPDX-License-Identifier: GPL-2.0-or-later + #include "shell_integration.h" + #include "wallpaper_integration.h" + +-#include "../logind.h" +- + #include + #include + #include +@@ -133,7 +131,6 @@ UnlockApp::UnlockApp(int &argc, char **argv) + , m_graceTime(0) + , m_noLock(false) + , m_shellIntegration(new ShellIntegration(this)) +- , m_logindIntegration(new LogindIntegration(this)) + { + auto interactive = std::make_unique(QStringLiteral(KSCREENLOCKER_PAM_SERVICE), KUser().loginName()); + std::vector> noninteractive; +@@ -142,9 +139,6 @@ UnlockApp::UnlockApp(int &argc, char **argv) + noninteractive.push_back( + std::make_unique(QStringLiteral(KSCREENLOCKER_PAM_SMARTCARD_SERVICE), KUser().loginName(), PamAuthenticator::Smartcard)); + m_authenticators = new PamAuthenticators(std::move(interactive), std::move(noninteractive), this); +- connect(m_logindIntegration, &LogindIntegration::prepareForSleep, m_authenticators, [this] { +- m_authenticators->cancel(); +- }); + initialize(); + + if (KWindowSystem::isPlatformX11()) { +diff --git a/greeter/greeterapp.h b/greeter/greeterapp.h +index 2362dd2..ff355de 100644 +--- a/greeter/greeterapp.h ++++ b/greeter/greeterapp.h +@@ -25,7 +25,6 @@ class QuickViewSharedEngine; + } + + class Authenticator; +-class LogindIntegration; + + struct org_kde_ksld; + +@@ -94,6 +93,5 @@ private: + + KPackage::Package m_wallpaperPackage; + ShellIntegration *m_shellIntegration; +- LogindIntegration *m_logindIntegration; + }; + } // namespace +-- +2.51.0 diff --git a/plasma-fingerprint-workaround/kscreenlocker/patches/0002-allow-retrying-noninteractive-authentication.patch b/plasma-fingerprint-workaround/kscreenlocker/patches/0002-allow-retrying-noninteractive-authentication.patch new file mode 100644 index 0000000..9e8d3b9 --- /dev/null +++ b/plasma-fingerprint-workaround/kscreenlocker/patches/0002-allow-retrying-noninteractive-authentication.patch @@ -0,0 +1,30 @@ +From: Anton Partanen +Subject: [PATCH] Allow retrying non-interactive authentication + +An active password conversation should not prevent a completed fingerprint +conversation from being started again. PamAuthenticator::tryUnlock() already +ignores authenticators that are still running, so let subsequent start +requests reach each authenticator. + +This is a minimal backport of the state-guard removal in upstream commit +a5ed9ca0. It allows a fingerprint attempt interrupted by suspend to restart +without cancelling the password conversation. +--- + greeter/pamauthenticators.cpp | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/greeter/pamauthenticators.cpp b/greeter/pamauthenticators.cpp +index 7c53b73..f60516d 100644 +--- a/greeter/pamauthenticators.cpp ++++ b/greeter/pamauthenticators.cpp +@@ -141,7 +141,7 @@ PamAuthenticators::AuthenticatorsState PamAuthenticators::state() const + + void PamAuthenticators::startAuthenticating() + { +- if (d->state == AuthenticatorsState::Authenticating || d->graceLocked) { ++ if (d->graceLocked) { + return; + } + +-- +2.51.0 diff --git a/plasma-fingerprint-workaround/kscreenlocker/patches/0003-allow-retry-after-transient-auth-unavailability.patch b/plasma-fingerprint-workaround/kscreenlocker/patches/0003-allow-retry-after-transient-auth-unavailability.patch new file mode 100644 index 0000000..f22fdf3 --- /dev/null +++ b/plasma-fingerprint-workaround/kscreenlocker/patches/0003-allow-retry-after-transient-auth-unavailability.patch @@ -0,0 +1,28 @@ +From: Anton Partanen +Subject: [PATCH] Allow retry after transient authentication unavailability + +PAM_AUTHINFO_UNAVAIL is not necessarily permanent. In particular, +pam_fprintd returns it when an active verification is interrupted by system +suspend. Treat that result as a failed attempt so a later start request can +retry it. Continue to cache PAM_MODULE_UNKNOWN as permanent unavailability. +--- + greeter/pamauthenticator.cpp | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/greeter/pamauthenticator.cpp b/greeter/pamauthenticator.cpp +index e2011fd..bb2521f 100644 +--- a/greeter/pamauthenticator.cpp ++++ b/greeter/pamauthenticator.cpp +@@ -176,7 +176,9 @@ void PamWorker::authenticate() + pam_setcred(m_handle, PAM_REFRESH_CRED); + /* ignore errors on refresh credentials. If this did not work we use the old ones. */ + Q_EMIT succeeded(); +- } else if (rc == PAM_AUTHINFO_UNAVAIL || rc == PAM_MODULE_UNKNOWN) { ++ } else if (rc == PAM_AUTHINFO_UNAVAIL) { ++ Q_EMIT failed(); ++ } else if (rc == PAM_MODULE_UNKNOWN) { + m_unavailable = true; + Q_EMIT unavailabilityChanged(m_unavailable); + } else { +-- +2.51.0 diff --git a/plasma-fingerprint-workaround/kscreenlocker/patches/0004-retry-transient-unavailability-for-fingerprint-only.patch b/plasma-fingerprint-workaround/kscreenlocker/patches/0004-retry-transient-unavailability-for-fingerprint-only.patch new file mode 100644 index 0000000..73d5c6c --- /dev/null +++ b/plasma-fingerprint-workaround/kscreenlocker/patches/0004-retry-transient-unavailability-for-fingerprint-only.patch @@ -0,0 +1,68 @@ +From: Anton Partanen +Subject: [PATCH] Retry transient unavailability for fingerprint only + +Other non-interactive authenticators, notably smartcards, also use +PAM_AUTHINFO_UNAVAIL to report that no credential is present. Restrict the +retry behavior to fingerprint authenticators so absent smartcards remain +unavailable instead of repeatedly flashing in the UI. +--- + greeter/pamauthenticator.cpp | 12 +++++++----- + 1 file changed, 7 insertions(+), 5 deletions(-) + +diff --git a/greeter/pamauthenticator.cpp b/greeter/pamauthenticator.cpp +index bb2521f..e27e61c 100644 +--- a/greeter/pamauthenticator.cpp ++++ b/greeter/pamauthenticator.cpp +@@ -27,7 +27,7 @@ class PamWorker : public QObject + { + Q_OBJECT + public: +- PamWorker(); ++ explicit PamWorker(bool retryOnAuthInfoUnavailable); + ~PamWorker() override; + Q_DISABLE_COPY_MOVE(PamWorker) + void start(const QString &service, const QString &user); +@@ -52,6 +52,7 @@ private: + bool m_unavailable = false; + bool m_inAuthenticate = false; + std::chrono::steady_clock::time_point m_nextAttemptAllowedTime; ++ const bool m_retryOnAuthInfoUnavailable; + int m_result = -1; + QString m_service; + }; +@@ -163,10 +164,11 @@ int PamWorker::converse(int n, const struct pam_message **msg, struct pam_respons + return PAM_SUCCESS; + } + +-PamWorker::PamWorker() ++PamWorker::PamWorker(bool retryOnAuthInfoUnavailable) + : QObject(nullptr) + , m_conv({&PamWorker::converse, this}) + , m_nextAttemptAllowedTime(std::chrono::steady_clock::now()) ++ , m_retryOnAuthInfoUnavailable(retryOnAuthInfoUnavailable) + { + } + +@@ -176,9 +178,9 @@ void PamWorker::authenticate() + pam_setcred(m_handle, PAM_REFRESH_CRED); + /* ignore errors on refresh credentials. If this did not work we use the old ones. */ + Q_EMIT succeeded(); +- } else if (rc == PAM_AUTHINFO_UNAVAIL) { ++ } else if (rc == PAM_AUTHINFO_UNAVAIL && m_retryOnAuthInfoUnavailable) { + Q_EMIT failed(); +- } else if (rc == PAM_MODULE_UNKNOWN) { ++ } else if (rc == PAM_AUTHINFO_UNAVAIL || rc == PAM_MODULE_UNKNOWN) { + m_unavailable = true; + Q_EMIT unavailabilityChanged(m_unavailable); + } else { +@@ -268,7 +271,7 @@ PamAuthenticator::PamAuthenticator(const QString &service, const QString &user, N + }) + , m_service(service) + , m_authenticatorType(types) +- , d(new PamWorker) ++ , d(new PamWorker(types.testFlag(NoninteractiveAuthenticatorType::Fingerprint))) + { + d->moveToThread(&m_thread); + +-- +2.51.0 diff --git a/plasma-fingerprint-workaround/kscreenlocker/restart-fprintd-after-resume b/plasma-fingerprint-workaround/kscreenlocker/restart-fprintd-after-resume new file mode 100755 index 0000000..aa2685a --- /dev/null +++ b/plasma-fingerprint-workaround/kscreenlocker/restart-fprintd-after-resume @@ -0,0 +1,6 @@ +#!/bin/sh + +[ "$1" = post ] || exit 0 +systemctl --quiet is-active fprintd.service || exit 0 + +systemctl --no-block restart fprintd.service diff --git a/plasma-fingerprint-workaround/payload.conf b/plasma-fingerprint-workaround/payload.conf new file mode 100644 index 0000000..cdd18ae --- /dev/null +++ b/plasma-fingerprint-workaround/payload.conf @@ -0,0 +1,2 @@ +payload_url=https://git.ajpanton.se/api/packages/ajp_anton/generic/plasma-fingerprint-workaround/6.7.4-1.fc44.ajp5/kscreenlocker-6.7.4-1.fc44.ajp5.x86_64.rpm +payload_sha256=d64515f3c7cb9c44b1c8fc194e90503be5944b1d24094aab2f68d989b3eca381 diff --git a/plasma-fingerprint-workaround/plasma-fingerprint-workaround b/plasma-fingerprint-workaround/plasma-fingerprint-workaround new file mode 100755 index 0000000..962db13 --- /dev/null +++ b/plasma-fingerprint-workaround/plasma-fingerprint-workaround @@ -0,0 +1,306 @@ +#!/usr/bin/bash + +set -euo pipefail + +PATH=/usr/sbin:/usr/bin +readonly data_dir=/usr/share/plasma-fingerprint-workaround +readonly payload_config="$data_dir/payload.conf" +readonly program=${0##*/} +temporary_dir= + +usage() { + cat <&2 + exit 1 +} + +require_root() { + (( EUID == 0 )) || die "run this command as root" +} + +require_fedora() { + # shellcheck source=/dev/null + source /etc/os-release + [[ ${ID:-} == fedora ]] || die "this workaround only supports Fedora Linux" +} + +cleanup() { + [[ -n $temporary_dir ]] || return + rm -f -- "$temporary_dir/kscreenlocker.rpm" + rmdir -- "$temporary_dir" +} + +package_value() { + local mode=$1 + local format=$2 + local package=$3 + + rpm "$mode" --qf "$format" -- "$package" +} + +package_base() { + local mode=$1 + local package=$2 + local provides + + provides=$(rpm "$mode" --provides -- "$package") + sed -n 's/^plasma-fingerprint-workaround-base = //p' <<<"$provides" +} + +is_workaround_package() { + local mode=$1 + local package=$2 + local provides + + provides=$(rpm "$mode" --provides -- "$package") + grep -qx 'plasma-fingerprint-workaround-payload = 1' <<<"$provides" +} + +is_legacy_workaround() { + [[ $(installed_value '%{RELEASE}') =~ \.ajp[[:digit:]]+$ ]] +} + +is_any_workaround() { + is_workaround_package -q kscreenlocker || is_legacy_workaround +} + +installed_value() { + package_value -q "$1" kscreenlocker +} + +installed_base() { + local base + + base=$(package_base -q kscreenlocker) + if [[ -n $base ]]; then + printf '%s\n' "$base" + elif is_legacy_workaround; then + printf '%s-%s\n' \ + "$(installed_value '%{VERSION}')" \ + "$(installed_value '%{RELEASE}' | sed 's/\.ajp[[:digit:]]*$//')" + else + installed_value '%{EVR}' + fi +} + +show_status() { + if ! rpm -q kscreenlocker >/dev/null 2>&1; then + printf 'KScreenLocker is not installed.\n' + return 1 + fi + + if is_workaround_package -q kscreenlocker; then + printf 'Fingerprint workaround: enabled\n' + printf 'Installed package: kscreenlocker-%s.%s\n' \ + "$(installed_value '%{EVR}')" "$(installed_value '%{ARCH}')" + printf 'Based on Fedora package: kscreenlocker-%s\n' "$(installed_base)" + elif is_legacy_workaround; then + printf 'Fingerprint workaround: enabled (legacy unmanaged build)\n' + printf 'Installed package: kscreenlocker-%s.%s\n' \ + "$(installed_value '%{EVR}')" "$(installed_value '%{ARCH}')" + printf 'Based on Fedora package: kscreenlocker-%s\n' "$(installed_base)" + else + printf 'Fingerprint workaround: disabled\n' + printf 'Installed package: kscreenlocker-%s.%s\n' \ + "$(installed_value '%{EVR}')" "$(installed_value '%{ARCH}')" + fi +} + +confirm() { + local answer + + if (( assume_yes )); then + return + fi + + printf 'Continue? [y/N] ' + read -r answer || exit 0 + [[ $answer == y || $answer == Y ]] || exit 0 +} + +load_payload_config() { + [[ -r $payload_config ]] || die "payload configuration is missing" + + unset payload_url payload_sha256 + # This root-owned file is installed by the controller RPM. + # shellcheck source=/dev/null + source "$payload_config" + + [[ ${payload_url:-} == https://* ]] || die "invalid payload URL" + [[ ${payload_sha256:-} =~ ^[[:xdigit:]]{64}$ ]] || + die "invalid payload checksum" +} + +validate_candidate() { + local candidate=$1 + local name arch candidate_evr candidate_base current_base current_evr + + rpm -K --nosignature "$candidate" >/dev/null || die "the RPM is corrupt" + + name=$(package_value -qp '%{NAME}' "$candidate") + [[ $name == kscreenlocker ]] || + die "expected a kscreenlocker RPM, got $name" + + is_workaround_package -qp "$candidate" || + die "the RPM was not built from this workaround's KScreenLocker spec" + + arch=$(package_value -qp '%{ARCH}' "$candidate") + [[ $arch == "$(installed_value '%{ARCH}')" ]] || + die "RPM architecture $arch does not match installed architecture $(installed_value '%{ARCH}')" + + candidate_base=$(package_base -qp "$candidate") + [[ -n $candidate_base ]] || die "the RPM has no base-version marker" + + candidate_evr=$(package_value -qp '%{EVR}' "$candidate") + current_base=$(installed_base) + current_evr=$(installed_value '%{EVR}') + + printf 'Installed: kscreenlocker-%s\n' "$current_evr" + printf 'Candidate: kscreenlocker-%s (based on %s)\n' \ + "$candidate_evr" "$candidate_base" + + if [[ $candidate_base != "$current_base" ]]; then + if (( ! force )); then + die "candidate base $candidate_base does not match installed base $current_base; use --force to allow the version change" + fi + printf 'Warning: forcing a KScreenLocker base-version change.\n' >&2 + fi +} + +enable_workaround() { + local rpm_path='' candidate + local -a dnf_args + + force=0 + assume_yes=0 + while (( $# )); do + case $1 in + --rpm) + (( $# >= 2 )) || die "--rpm requires a path" + rpm_path=$2 + shift 2 + ;; + --force) + force=1 + shift + ;; + --yes) + assume_yes=1 + shift + ;; + *) + die "unknown enable option: $1" + ;; + esac + done + + require_root + require_fedora + rpm -q kscreenlocker >/dev/null 2>&1 || die "KScreenLocker is not installed" + + umask 077 + temporary_dir=$(mktemp -d /var/tmp/plasma-fingerprint-workaround.XXXXXX) + trap cleanup EXIT + candidate="$temporary_dir/kscreenlocker.rpm" + + if [[ -n $rpm_path ]]; then + [[ -f $rpm_path ]] || die "RPM not found: $rpm_path" + install -m 0600 -- "$rpm_path" "$candidate" + else + load_payload_config + printf 'Downloading the tested KScreenLocker build...\n' + curl --fail --location --show-error --silent \ + --proto '=https' --proto-redir '=https' \ + --output "$candidate" "$payload_url" + printf '%s %s\n' "$payload_sha256" "$candidate" | + sha256sum --check --status - || die "downloaded RPM checksum does not match" + fi + + validate_candidate "$candidate" + printf '\nThis replaces Fedora\x27s security-sensitive KScreenLocker package.\n' + confirm + + dnf_args=(install --assumeyes) + (( force )) && dnf_args+=(--allow-downgrade) + dnf5 "${dnf_args[@]}" "$candidate" + + is_workaround_package -q kscreenlocker || + die "DNF completed without enabling the workaround" + printf 'Fingerprint workaround enabled. Reboot before testing it.\n' +} + +disable_workaround() { + assume_yes=0 + while (( $# )); do + case $1 in + --yes) + assume_yes=1 + shift + ;; + *) + die "unknown disable option: $1" + ;; + esac + done + + require_root + require_fedora + if ! is_any_workaround; then + printf 'Fingerprint workaround is already disabled.\n' + return + fi + + printf 'This will replace the workaround with Fedora\x27s current KScreenLocker package.\n' + confirm + + dnf5 \ + --repo=fedora,updates \ + --refresh \ + distro-sync \ + --assumeyes \ + kscreenlocker + + if is_any_workaround; then + die "no suitable Fedora KScreenLocker package was installed" + fi + printf 'Fingerprint workaround disabled. Reboot before testing the lock screen.\n' +} + +command=${1:-} +[[ -n $command ]] || { + usage + exit 2 +} +shift + +case $command in + status) + (( $# == 0 )) || die "status takes no options" + show_status + ;; + enable) + enable_workaround "$@" + ;; + disable) + disable_workaround "$@" + ;; + -h|--help|help) + usage + ;; + *) + usage >&2 + exit 2 + ;; +esac diff --git a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec new file mode 100644 index 0000000..b60abc9 --- /dev/null +++ b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec @@ -0,0 +1,54 @@ +Name: plasma-fingerprint-workaround +Version: 0.1.0 +Release: 1%{?dist} +Summary: Opt-in installer for a patched Fedora KScreenLocker + +License: MIT +URL: https://git.ajpanton.se/ajp_anton/fedora-tools +Source0: plasma-fingerprint-workaround +Source1: payload.conf +Source2: LICENSE +Source3: README.md +Source4: test-controller + +BuildArch: noarch +BuildRequires: bash +BuildRequires: grep +Requires: bash +Requires: coreutils +Requires: curl +Requires: dnf5 +Requires: grep +Requires: rpm +Requires: sed + +%description +Provides an explicit, reversible installer for an experimental KScreenLocker +fingerprint workaround. Installing this package alone makes no system changes. + +%prep + +%build + +%check +bash %{SOURCE4} %{SOURCE0} %{SOURCE1} + +%install +install -D -m 0755 %{SOURCE0} \ + %{buildroot}%{_bindir}/plasma-fingerprint-workaround +install -D -m 0644 %{SOURCE1} \ + %{buildroot}%{_datadir}/plasma-fingerprint-workaround/payload.conf +install -D -m 0644 %{SOURCE2} \ + %{buildroot}%{_licensedir}/%{name}/LICENSE +install -D -m 0644 %{SOURCE3} \ + %{buildroot}%{_docdir}/%{name}/README.md + +%files +%license %{_licensedir}/%{name}/LICENSE +%doc %{_docdir}/%{name}/README.md +%{_bindir}/plasma-fingerprint-workaround +%{_datadir}/plasma-fingerprint-workaround/payload.conf + +%changelog +* Fri Sep 04 2026 Anton - 0.1.0-1 +- Initial package diff --git a/plasma-fingerprint-workaround/tests/test-controller b/plasma-fingerprint-workaround/tests/test-controller new file mode 100755 index 0000000..225e472 --- /dev/null +++ b/plasma-fingerprint-workaround/tests/test-controller @@ -0,0 +1,23 @@ +#!/usr/bin/bash + +set -euo pipefail + +controller=$1 +payload_config=$2 + +bash -n "$controller" +"$controller" --help | grep -q '^ .* enable \[--rpm PATH\] \[--force\] \[--yes\]$' + +if "$controller" unknown-command >/dev/null 2>&1; then + printf 'Unknown command unexpectedly succeeded.\n' >&2 + exit 1 +fi + +payload_url= +payload_sha256= +# shellcheck source=/dev/null +source "$payload_config" +[[ $payload_url == https://*/*.rpm ]] +[[ $payload_sha256 =~ ^[[:xdigit:]]{64}$ ]] + +printf 'Controller tests passed.\n' diff --git a/plasma-fingerprint-workaround/tests/test-patched-kscreenlocker-rpm b/plasma-fingerprint-workaround/tests/test-patched-kscreenlocker-rpm new file mode 100755 index 0000000..8d03ebf --- /dev/null +++ b/plasma-fingerprint-workaround/tests/test-patched-kscreenlocker-rpm @@ -0,0 +1,16 @@ +#!/usr/bin/bash + +set -euo pipefail + +rpm_path=$1 +expected_base=$2 +provides=$(rpm -qp --provides "$rpm_path") +files=$(rpm -qpl "$rpm_path") + +rpm -K --nosignature "$rpm_path" >/dev/null +[[ $(rpm -qp --qf '%{NAME}' "$rpm_path") == kscreenlocker ]] +grep -qx 'plasma-fingerprint-workaround-payload = 1' <<<"$provides" +grep -qx "plasma-fingerprint-workaround-base = $expected_base" <<<"$provides" +grep -qx '/usr/lib/systemd/system-sleep/restart-fprintd-after-resume' <<<"$files" + +printf 'Patched KScreenLocker package tests passed.\n' diff --git a/scripts/build-patched-kscreenlocker-rpm b/scripts/build-patched-kscreenlocker-rpm new file mode 100755 index 0000000..bde5f67 --- /dev/null +++ b/scripts/build-patched-kscreenlocker-rpm @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +source_dir="$repo_root/plasma-fingerprint-workaround/kscreenlocker" +topdir="$repo_root/rpmbuild" +version=${KSCREENLOCKER_VERSION:-6.7.4} +base_release=${KSCREENLOCKER_BASE_RELEASE:-1} +workaround_release=${KSCREENLOCKER_WORKAROUND_RELEASE:-5} +archive="$topdir/SOURCES/kscreenlocker-$version.tar.xz" +archive_url="https://download.kde.org/stable/plasma/$version/kscreenlocker-$version.tar.xz" + +if [[ $version == 6.7.4 ]]; then + default_archive_sha256=f9cd525d501f08931f2c0b4d08f5384588967bc89fac62e39ed4ae2da2925a39 +else + default_archive_sha256= +fi +archive_sha256=${KSCREENLOCKER_SOURCE_SHA256:-$default_archive_sha256} +if [[ ! $archive_sha256 =~ ^[[:xdigit:]]{64}$ ]]; then + printf 'Error: set KSCREENLOCKER_SOURCE_SHA256 for KScreenLocker %s.\n' \ + "$version" >&2 + exit 2 +fi + +mkdir -p "$topdir"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + +if [[ ! -f $archive ]]; then + temporary_archive="$archive.download" + trap 'rm -f -- "$temporary_archive"' EXIT + curl --fail --location --show-error \ + --proto '=https' --proto-redir '=https' \ + --output "$temporary_archive" "$archive_url" + mv -- "$temporary_archive" "$archive" + trap - EXIT +fi + +printf '%s %s\n' "$archive_sha256" "$archive" | + sha256sum --check --status - || { + printf 'Error: KScreenLocker source archive checksum does not match.\n' >&2 + exit 1 + } + +for patch in "$source_dir"/patches/*.patch; do + install -m 0644 "$patch" "$topdir/SOURCES/${patch##*/}" +done +install -m 0755 \ + "$source_dir/restart-fprintd-after-resume" \ + "$topdir/SOURCES/restart-fprintd-after-resume" + +rpmbuild \ + --define "_topdir $topdir" \ + --define "upstream_version $version" \ + --define "base_release $base_release" \ + --define "workaround_release $workaround_release" \ + -ba "$source_dir/kscreenlocker.spec" + +rpm_path="$topdir/RPMS/$(rpm --eval '%{_arch}')/kscreenlocker-$version-$base_release$(rpm --eval '%{?dist}').ajp$workaround_release.$(rpm --eval '%{_arch}').rpm" +bash "$repo_root/plasma-fingerprint-workaround/tests/test-patched-kscreenlocker-rpm" \ + "$rpm_path" "$version-$base_release$(rpm --eval '%{?dist}')" diff --git a/scripts/build-plasma-fingerprint-workaround-rpm b/scripts/build-plasma-fingerprint-workaround-rpm new file mode 100755 index 0000000..152d365 --- /dev/null +++ b/scripts/build-plasma-fingerprint-workaround-rpm @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +tool_dir="$repo_root/plasma-fingerprint-workaround" +topdir="$repo_root/rpmbuild" + +mkdir -p "$topdir"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} +install -m 0755 \ + "$tool_dir/plasma-fingerprint-workaround" \ + "$topdir/SOURCES/plasma-fingerprint-workaround" +install -m 0644 "$tool_dir/payload.conf" "$topdir/SOURCES/payload.conf" +install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE" +install -m 0644 "$tool_dir/README.md" "$topdir/SOURCES/README.md" +install -m 0755 "$tool_dir/tests/test-controller" "$topdir/SOURCES/test-controller" + +rpmbuild \ + --define "_topdir $topdir" \ + -bb "$tool_dir/plasma-fingerprint-workaround.spec" diff --git a/scripts/publish-fingerprint-workaround-payload b/scripts/publish-fingerprint-workaround-payload new file mode 100755 index 0000000..e3e9ef4 --- /dev/null +++ b/scripts/publish-fingerprint-workaround-payload @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + printf 'Usage: GITEA_TOKEN=... %s PATH_TO_RPM\n' "$0" >&2 +} + +if (( $# != 1 )); then + usage + exit 2 +fi + +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +payload_config="$repo_root/plasma-fingerprint-workaround/payload.conf" +rpm_path=$1 + +if [[ ! -f $rpm_path || $rpm_path != *.rpm ]]; then + printf 'Error: %s is not an RPM file.\n' "$rpm_path" >&2 + exit 2 +fi + +if [[ -z ${GITEA_TOKEN:-} ]]; then + printf 'Error: set GITEA_TOKEN to a Gitea access token with package write permission.\n' >&2 + exit 2 +fi + +payload_url= +payload_sha256= +# shellcheck source=/dev/null +source "$payload_config" + +actual_sha256=$(sha256sum "$rpm_path") +actual_sha256=${actual_sha256%% *} +if [[ $actual_sha256 != "$payload_sha256" ]]; then + printf 'Error: RPM checksum does not match payload.conf.\n' >&2 + exit 1 +fi + +if [[ ${rpm_path##*/} != "${payload_url##*/}" ]]; then + printf 'Error: RPM filename does not match payload.conf.\n' >&2 + exit 1 +fi + +curl \ + --fail-with-body \ + --show-error \ + --silent \ + --user "ajp_anton:${GITEA_TOKEN}" \ + --upload-file "$rpm_path" \ + "$payload_url" + +printf 'Published %s as a generic Gitea package.\n' "$rpm_path"