diff --git a/doc/plasma-face-unlock.1.scd b/doc/plasma-face-unlock.1.scd index 65332b1..de8e312 100644 --- a/doc/plasma-face-unlock.1.scd +++ b/doc/plasma-face-unlock.1.scd @@ -181,6 +181,11 @@ After a scan that did not get anybody in, the next one waits for the person to be still for two seconds and then touch something again, so typing the password does not start a scan with every key. +Unlocked through logind, the lock screen cuts off its own password prompt and +counts that as a wrong password. After a face unlock the daemon resets the +failed logins of *pam_faillock*(8), as a correct password does, so face unlocks +never lock the account. + The bubble is a layer-shell surface that KWin keeps above the lock screen (kde_lockscreen_overlay_v1). KWin only allows that for a program whose desktop file asks for it, which is diff --git a/res/systemd/plasma-face-unlockd.service b/res/systemd/plasma-face-unlockd.service index 430c785..516f5a3 100644 --- a/res/systemd/plasma-face-unlockd.service +++ b/res/systemd/plasma-face-unlockd.service @@ -19,6 +19,8 @@ CapabilityBoundingSet=CAP_DAC_READ_SEARCH # runtime directory, to tell the bubble about a sudo scan. NoNewPrivileges=yes ProtectSystem=strict +# To take back the failed login the lock screen counts for a face unlock. +ReadWritePaths=-/run/faillock ProtectHome=read-only PrivateTmp=yes PrivateNetwork=yes diff --git a/src/daemon/server.cpp b/src/daemon/server.cpp index 902d379..ca17b53 100644 --- a/src/daemon/server.cpp +++ b/src/daemon/server.cpp @@ -69,6 +69,10 @@ QJsonObject result(bool ok, const QString &reason = {}) return o; } +// A face unlock and the lock screen going away this soon after belong +// together. +constexpr qint64 UnlockPairSeconds = 30; + bool isFailureThatCounts(const QString &reason) { // A face that did not match, a fake, or a match that never showed a sign @@ -403,6 +407,15 @@ void Server::handleWatch(Client *client) void Server::handleUnlocked(Client *client) { UserState state = UserState::load(m_options.stateDir, client->uid()); + + // Our unlock goes through logind. The lock screen's password prompt is + // cut off mid-question by it and counts that as a wrong password, so a + // few face unlocks would lock the account (pam_faillock). Taken back here. + const qint64 now = QDateTime::currentSecsSinceEpoch(); + if (geteuid() == 0 && state.lastPurpose == u"unlock" && now - state.lastUnlock <= UnlockPairSeconds) { + System::resetFailedLogins(client->uid()); + } + if (state.failures || state.lockedUntil) { state.failures = 0; state.lockedUntil = 0; diff --git a/src/daemon/system.cpp b/src/daemon/system.cpp index 3d1e1a0..34a8d13 100644 --- a/src/daemon/system.cpp +++ b/src/daemon/system.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include @@ -79,4 +81,17 @@ quint64 processStartTime(pid_t pid) const QList fields = line.mid(close + 2).split(' '); return fields.size() > 19 ? fields.at(19).toULongLong() : 0; } + +bool resetFailedLogins(uid_t uid) +{ + // Not from PATH: this runs as root. + const QString faillock = QStandardPaths::findExecutable(QStringLiteral("faillock"), + {QStringLiteral("/usr/sbin"), QStringLiteral("/usr/bin"), QStringLiteral("/sbin")}); + if (faillock.isEmpty()) { + return false; + } + QProcess p; + p.start(faillock, {QStringLiteral("--user"), nameOf(uid), QStringLiteral("--reset")}); + return p.waitForFinished(3000) && p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0; +} } // namespace System diff --git a/src/daemon/system.h b/src/daemon/system.h index 1e9b3ad..6e3c69c 100644 --- a/src/daemon/system.h +++ b/src/daemon/system.h @@ -20,4 +20,8 @@ bool lidClosed(); // When a process started, in clock ticks since boot, as polkit wants it to // tell a process from a later one that got the same pid. quint64 processStartTime(pid_t pid); + +// Forget the failed logins pam_faillock counted for somebody, as a correct +// password does. False without faillock. +bool resetFailedLogins(uid_t uid); } // namespace System