standby unlock: a locked session unlocks by finger without PAM
Phosh's lock screen cannot take a fingerprint: its PAM conversation answers every prompt with the password. So the daemon follows the seat session that is locked and in front (logind LockedHint + Active), runs a verify of its own while the display is on (backlight bl_power), and answers a match with logind's Session.Unlock. Only fingers named in that session owner's map unlock. The map is re-read at every arm and verdict, and a delete cancels a running standby verify, so a finger deleted while locked stops counting at once. A verdict from an earlier lock or another session is dropped. no-unlock rules and FingerMatched apply as for a client match. A client Claim cancels standby, and its Release, or a claim that fails while pending, re-arms it. --standby-unlock=off disables it. Tested on the dev phone before the review fixes (map re-read, front-session tracking, cancel ordering, pending-claim re-arm): under Phosh, six unlocks by two fingers and an unnamed template refused; under Plasma, kscreenlocker's pam_fprintd unlocks while standby yields and re-arms. With the fixes: suites 8/8, and a daemon restart under a lock screen picks the locked session up. Not exercised: standby's own Session.Unlock on kscreenlocker, user switching.
This commit is contained in:
parent
823c710b15
commit
ac0a147e95
2 changed files with 392 additions and 5 deletions
23
README.md
23
README.md
|
|
@ -33,6 +33,29 @@ fprintd-verify user (wrong finger) verify-no-match, on the first press
|
||||||
fprintd-verify user (enrolled finger) verify-match, on the first press
|
fprintd-verify user (enrolled finger) verify-match, on the first press
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Lock-screen unlock
|
||||||
|
|
||||||
|
Two routes, both on by default:
|
||||||
|
|
||||||
|
- **PAM.** The daemon provides `fprintd`, so a lock screen that runs
|
||||||
|
`pam_fprintd` beside its password prompt unlocks by finger the usual way.
|
||||||
|
Plasma's does (`kde-fingerprint`; the `fingerprint-auth` and `postlogin`
|
||||||
|
services it needs are shipped here). Phosh's cannot: its PAM conversation
|
||||||
|
answers every prompt with the password, so a fingerprint module in its stack
|
||||||
|
would only stall the password behind the fingerprint timeout.
|
||||||
|
- **Standby.** While the session in front on its seat is locked and the
|
||||||
|
display is on, the daemon runs a verify of its own and answers a match with
|
||||||
|
logind's `Session.Unlock`, which Phosh honours. No verify runs while the
|
||||||
|
display is off (an open verify polls the sensor at ~5 Hz; the display state
|
||||||
|
comes from the backlight's `bl_power` or the panel's DRM `dpms`, and a
|
||||||
|
device with neither is treated as always on). Any fprintd client takes
|
||||||
|
precedence: a `Claim` cancels the standby verify, a `Release` re-arms it.
|
||||||
|
Only fingers named in that session owner's map unlock, and the map is
|
||||||
|
re-read at every arm, so a finger deleted while locked stops counting at
|
||||||
|
once. Per-finger `no-unlock` rules apply here too, and every match is
|
||||||
|
announced as `FingerMatched` for the session agent. `--standby-unlock=off`
|
||||||
|
disables it.
|
||||||
|
|
||||||
## Runtime dependencies
|
## Runtime dependencies
|
||||||
|
|
||||||
The `focal64` trustlet is proprietary and is not in this repo. It is
|
The `focal64` trustlet is proprietary and is not in this repo. It is
|
||||||
|
|
|
||||||
|
|
@ -1943,7 +1943,7 @@ private:
|
||||||
// jobs; results and progress come back through the GLib main loop.
|
// jobs; results and progress come back through the GLib main loop.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
struct Job {
|
struct Job {
|
||||||
enum class Kind { Claim, Enroll, Verify, Remove } kind;
|
enum class Kind { Claim, Enroll, Verify, Remove, Standby } kind;
|
||||||
std::uint32_t uid = 0;
|
std::uint32_t uid = 0;
|
||||||
std::string finger;
|
std::string finger;
|
||||||
std::vector<std::uint32_t> acceptFids; // Verify: which fids count
|
std::vector<std::uint32_t> acceptFids; // Verify: which fids count
|
||||||
|
|
@ -1955,7 +1955,7 @@ struct Job {
|
||||||
// so the D-Bus emission happens on the thread that owns the connection.
|
// so the D-Bus emission happens on the thread that owns the connection.
|
||||||
struct Event {
|
struct Event {
|
||||||
enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus,
|
enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus,
|
||||||
RemoveDone } kind;
|
RemoveDone, StandbyStatus } kind;
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
bool done = false;
|
bool done = false;
|
||||||
std::string status;
|
std::string status;
|
||||||
|
|
@ -1966,6 +1966,14 @@ struct Event {
|
||||||
|
|
||||||
void PostEvent(std::unique_ptr<Event> ev); // defined with the D-Bus code
|
void PostEvent(std::unique_ptr<Event> ev); // defined with the D-Bus code
|
||||||
|
|
||||||
|
// Standby unlock (see the block before PostEvent): "wanted" is the main
|
||||||
|
// thread's intent, read by the worker when a standby job starts and between
|
||||||
|
// its scan windows; "running" is the worker's word that a standby verify is
|
||||||
|
// the op in progress, so a cancel from the main thread can be sure whose op
|
||||||
|
// it is stopping.
|
||||||
|
std::atomic<bool> g_standbyWanted{false};
|
||||||
|
std::atomic<bool> g_standbyRunning{false};
|
||||||
|
|
||||||
class Worker {
|
class Worker {
|
||||||
public:
|
public:
|
||||||
void Start() {
|
void Start() {
|
||||||
|
|
@ -2076,6 +2084,42 @@ private:
|
||||||
PostEvent(std::move(ev));
|
PostEvent(std::move(ev));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Job::Kind::Standby: {
|
||||||
|
// The daemon's own verify, no client behind it. Same scan as
|
||||||
|
// a client's, but it ends the moment the main thread stops
|
||||||
|
// wanting it (display off, session unlocked, a client
|
||||||
|
// claiming the device), and its verdict goes to the standby
|
||||||
|
// code rather than onto the bus.
|
||||||
|
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::StandbyStatus });
|
||||||
|
// `running` is published BEFORE `wanted` is read; StandbyCancel
|
||||||
|
// does the reverse. Whichever side goes second sees the other,
|
||||||
|
// so a cancel cannot fall between this check and the verify
|
||||||
|
// (cancel_ was reset at dequeue, so a CancelOp from here on holds).
|
||||||
|
g_standbyRunning.store(true);
|
||||||
|
if (!g_standbyWanted.load()) {
|
||||||
|
g_standbyRunning.store(false);
|
||||||
|
ev->status = ""; ev->done = false; // cancelled while queued
|
||||||
|
PostEvent(std::move(ev));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
||||||
|
while (!o.decided && !o.cancelled && o.presses == 0 && g_standbyWanted.load())
|
||||||
|
o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600, j.acceptFids);
|
||||||
|
g_standbyRunning.store(false);
|
||||||
|
std::println("standby verify: {} over {} press(es), {} frame(s)",
|
||||||
|
o.cancelled ? "cancelled" : !o.decided ? "undecided"
|
||||||
|
: o.matched ? "MATCH" : "NO MATCH", o.presses, o.frames);
|
||||||
|
ev->done = true;
|
||||||
|
ev->fid = o.fid;
|
||||||
|
if (o.cancelled || (!o.decided && o.presses == 0)) { ev->status = ""; ev->done = false; }
|
||||||
|
else if (!o.decided) ev->status = "verify-retry-scan";
|
||||||
|
else if (o.matched) ev->status = "verify-match";
|
||||||
|
else ev->status = "verify-no-match";
|
||||||
|
PostEvent(std::move(ev));
|
||||||
|
if (o.matched && g_learn) session_.HarvestTemplate(g_learnMaxFrames);
|
||||||
|
session_.FlushTemplate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Job::Kind::Verify: {
|
case Job::Kind::Verify: {
|
||||||
// fprintd's contract is that a verify runs until the client
|
// fprintd's contract is that a verify runs until the client
|
||||||
// stops it. The frame cap bounds one trustlet scan session,
|
// stops it. The frame cap bounds one trustlet scan session,
|
||||||
|
|
@ -2283,6 +2327,9 @@ Claim g_claim;
|
||||||
GDBusMethodInvocation* g_pendingClaim = nullptr;
|
GDBusMethodInvocation* g_pendingClaim = nullptr;
|
||||||
guint g_claimWatch = 0;
|
guint g_claimWatch = 0;
|
||||||
|
|
||||||
|
void StandbyEvaluate();
|
||||||
|
void StandbyCancel();
|
||||||
|
|
||||||
void DropClaim(const char* why) {
|
void DropClaim(const char* why) {
|
||||||
if (g_claimWatch) {
|
if (g_claimWatch) {
|
||||||
g_dbus_connection_signal_unsubscribe(g_conn, g_claimWatch);
|
g_dbus_connection_signal_unsubscribe(g_conn, g_claimWatch);
|
||||||
|
|
@ -2292,6 +2339,7 @@ void DropClaim(const char* why) {
|
||||||
if (g_claim.held)
|
if (g_claim.held)
|
||||||
std::println("claim by {} for {} dropped: {}", g_claim.sender, g_claim.user, why);
|
std::println("claim by {} for {} dropped: {}", g_claim.sender, g_claim.user, why);
|
||||||
g_claim = {};
|
g_claim = {};
|
||||||
|
StandbyEvaluate(); // the sensor is free again
|
||||||
}
|
}
|
||||||
|
|
||||||
// A claim is held by a bus connection. If that connection goes away -- the
|
// A claim is held by a bus connection. If that connection goes away -- the
|
||||||
|
|
@ -2318,16 +2366,16 @@ std::string MapPath(std::uint32_t uid) {
|
||||||
return fingerprintd::store::PathForUid(g_stateDir, uid);
|
return fingerprintd::store::PathForUid(g_stateDir, uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprintd::store::Map LoadMap(std::uint32_t uid) {
|
fingerprintd::store::Map LoadMap(std::uint32_t uid, bool quiet = false) {
|
||||||
std::string path = MapPath(uid);
|
std::string path = MapPath(uid);
|
||||||
std::ifstream f(path);
|
std::ifstream f(path);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
std::println("map {}: {}", path, ::strerror(errno));
|
if (!quiet) std::println("map {}: {}", path, ::strerror(errno));
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::string text((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
std::string text((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
auto m = fingerprintd::store::Map::Decode(text);
|
auto m = fingerprintd::store::Map::Decode(text);
|
||||||
std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size());
|
if (!quiet) std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size());
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2390,6 +2438,302 @@ bool Authorised(GDBusMethodInvocation* inv, std::uint32_t targetUid) {
|
||||||
return caller && (*caller == 0 || *caller == targetUid);
|
return caller && (*caller == 0 || *caller == targetUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Standby unlock: a verify the daemon arms ITSELF while a seat session is
|
||||||
|
// locked and its display is on, answered with logind's Session.Unlock.
|
||||||
|
//
|
||||||
|
// The PAM route needs a lock screen that runs pam_fprintd beside its password
|
||||||
|
// prompt. Plasma's does; Phosh's cannot (its PAM conversation answers every
|
||||||
|
// prompt with the password, so a fingerprint module would only stall the
|
||||||
|
// password behind its timeout). Phosh honours logind's Unlock, and root may
|
||||||
|
// call it, so this needs no polkit and no desktop cooperation. It yields to
|
||||||
|
// any fprintd client at once: a Claim cancels the standby verify and the
|
||||||
|
// claim's release re-arms it.
|
||||||
|
//
|
||||||
|
// It follows ONE session: the one that is locked AND in front on its seat,
|
||||||
|
// i.e. the lock screen actually on the display. Whose finger counts is
|
||||||
|
// decided by that session's owner and a map re-read at every arm and verdict,
|
||||||
|
// so a finger deleted while locked stops counting at once.
|
||||||
|
//
|
||||||
|
// Cost: an open verify polls the trustlet at ~5 Hz, so it only runs while the
|
||||||
|
// display is on (sysfs bl_power, the DRM connector's dpms as a fallback; a
|
||||||
|
// device with neither is treated as always on). While a session is locked the
|
||||||
|
// display state itself is read at 2 Hz. --standby-unlock=off turns it all off.
|
||||||
|
bool g_standbyEnabled = true;
|
||||||
|
struct Standby {
|
||||||
|
std::string sessionPath, sessionId;
|
||||||
|
std::uint32_t uid = 0;
|
||||||
|
bool locked = false;
|
||||||
|
bool screenOn = false;
|
||||||
|
bool armed = false; // a standby job is queued or running
|
||||||
|
unsigned lockGen = 0; // bumped whenever a session starts being followed
|
||||||
|
unsigned armedGen = 0; // lockGen the outstanding job was armed under
|
||||||
|
fingerprintd::store::Map fingers;
|
||||||
|
guint pollSource = 0;
|
||||||
|
guint rearmSource = 0;
|
||||||
|
};
|
||||||
|
Standby g_standby;
|
||||||
|
std::string g_blPowerPath, g_dpmsPath;
|
||||||
|
|
||||||
|
void FindDisplayNodes() {
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto& d : std::filesystem::directory_iterator("/sys/class/backlight", ec)) {
|
||||||
|
auto p = d.path() / "bl_power";
|
||||||
|
if (std::filesystem::exists(p, ec)) { g_blPowerPath = p.string(); break; }
|
||||||
|
}
|
||||||
|
for (const auto& d : std::filesystem::directory_iterator("/sys/class/drm", ec)) {
|
||||||
|
std::string n = d.path().filename().string();
|
||||||
|
if (n.find("-DSI-") == std::string::npos && n.find("-eDP-") == std::string::npos) continue;
|
||||||
|
auto p = d.path() / "dpms";
|
||||||
|
if (std::filesystem::exists(p, ec)) { g_dpmsPath = p.string(); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadTrimmed(const std::string& path, std::string& out) {
|
||||||
|
std::ifstream f(path);
|
||||||
|
if (!f) return false;
|
||||||
|
std::getline(f, out);
|
||||||
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ')) out.pop_back();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bl_power: 0 = FB_BLANK_UNBLANK, anything else is some flavour of off.
|
||||||
|
bool ScreenOn() {
|
||||||
|
std::string v;
|
||||||
|
if (!g_blPowerPath.empty() && ReadTrimmed(g_blPowerPath, v)) return v == "0";
|
||||||
|
if (!g_dpmsPath.empty() && ReadTrimmed(g_dpmsPath, v)) return v == "On";
|
||||||
|
return true; // nothing to read: better armed than deaf
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandbyCancel() {
|
||||||
|
g_standbyWanted.store(false);
|
||||||
|
if (g_standbyRunning.load() && g_worker) g_worker->CancelOp();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandbyEvaluate() {
|
||||||
|
bool want = g_ready && g_standbyEnabled && g_worker && g_standby.locked && g_standby.screenOn
|
||||||
|
&& !g_claim.held && !g_pendingClaim;
|
||||||
|
if (want && !g_standby.armed) {
|
||||||
|
g_standby.fingers = LoadMap(g_standby.uid, /*quiet=*/true);
|
||||||
|
if (g_standby.fingers.Size() == 0) return;
|
||||||
|
std::vector<std::uint32_t> fids;
|
||||||
|
for (const auto& e : g_standby.fingers.Entries()) fids.push_back(e.fid);
|
||||||
|
g_standby.armed = true;
|
||||||
|
g_standby.armedGen = g_standby.lockGen;
|
||||||
|
g_standbyWanted.store(true);
|
||||||
|
std::println("standby: listening for a finger (session {}, uid {}, {} finger(s))",
|
||||||
|
g_standby.sessionId, g_standby.uid, fids.size());
|
||||||
|
g_worker->Post(Job{ .kind = Job::Kind::Standby, .uid = g_standby.uid,
|
||||||
|
.acceptFids = std::move(fids) });
|
||||||
|
} else if (!want && g_standby.armed) {
|
||||||
|
StandbyCancel(); // `armed` clears when the job reports back
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandbyScheduleEvaluate(unsigned ms) {
|
||||||
|
if (g_standby.rearmSource) g_source_remove(g_standby.rearmSource);
|
||||||
|
g_standby.rearmSource = g_timeout_add(ms, [](gpointer) -> gboolean {
|
||||||
|
g_standby.rearmSource = 0;
|
||||||
|
StandbyEvaluate();
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean StandbyPoll(gpointer) {
|
||||||
|
bool on = ScreenOn();
|
||||||
|
if (on != g_standby.screenOn) {
|
||||||
|
g_standby.screenOn = on;
|
||||||
|
std::println("standby: display {}", on ? "on" : "off");
|
||||||
|
StandbyEvaluate();
|
||||||
|
}
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandbyOnStatus(const Event& ev) {
|
||||||
|
namespace ac = fingerprintd::actions;
|
||||||
|
namespace store = fingerprintd::store;
|
||||||
|
g_standby.armed = false;
|
||||||
|
if (ev.status.empty()) { // cancelled, or skipped while queued
|
||||||
|
StandbyEvaluate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!g_standby.locked) return;
|
||||||
|
if (g_standby.armedGen != g_standby.lockGen) { // armed for a lock we have since left
|
||||||
|
StandbyEvaluate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.status == "verify-match" && ev.fid != 0) {
|
||||||
|
g_standby.fingers = LoadMap(g_standby.uid, /*quiet=*/true);
|
||||||
|
auto matched = g_standby.fingers.Lookup(ev.fid);
|
||||||
|
const ac::Rule* rule = matched ? ac::Find(g_actions, *matched) : nullptr;
|
||||||
|
if (!matched) {
|
||||||
|
std::println("standby: matched fid {} that no finger name maps to; not unlocking", ev.fid);
|
||||||
|
} else if (rule && !rule->unlocks) {
|
||||||
|
std::println("standby: {} is configured no-unlock; not unlocking", store::NameOf(*matched));
|
||||||
|
} else {
|
||||||
|
std::println("standby: {} matched -- unlocking session {}",
|
||||||
|
store::NameOf(*matched), g_standby.sessionId);
|
||||||
|
g_dbus_connection_call(
|
||||||
|
g_conn, "org.freedesktop.login1", g_standby.sessionPath.c_str(),
|
||||||
|
"org.freedesktop.login1.Session", "Unlock", nullptr, nullptr,
|
||||||
|
G_DBUS_CALL_FLAGS_NONE, 5000, nullptr,
|
||||||
|
[](GObject* src, GAsyncResult* res, gpointer) {
|
||||||
|
GError* err = nullptr;
|
||||||
|
GVariant* r = g_dbus_connection_call_finish(G_DBUS_CONNECTION(src), res, &err);
|
||||||
|
if (!r) {
|
||||||
|
std::println(std::cerr, "standby: Unlock failed: {}", err ? err->message : "?");
|
||||||
|
if (err) g_error_free(err);
|
||||||
|
} else {
|
||||||
|
g_variant_unref(r);
|
||||||
|
}
|
||||||
|
}, nullptr);
|
||||||
|
}
|
||||||
|
// The same announcement a client-driven match makes (see VerifyStatus).
|
||||||
|
if (matched && g_conn)
|
||||||
|
g_dbus_connection_emit_signal(
|
||||||
|
g_conn, nullptr, DevicePath, ActionIface, "FingerMatched",
|
||||||
|
g_variant_new("(su)", std::string(store::NameOf(*matched)).c_str(),
|
||||||
|
static_cast<guint32>(g_standby.uid)),
|
||||||
|
nullptr);
|
||||||
|
if (rule && !rule->command.empty()) {
|
||||||
|
std::println("action: {} -> running {}", store::NameOf(*matched), rule->command);
|
||||||
|
RunSystemAction(rule->command, std::string(store::NameOf(*matched)));
|
||||||
|
}
|
||||||
|
// LockedHint normally flips first and disarms us; if the unlock did
|
||||||
|
// not take, this listens again.
|
||||||
|
StandbyScheduleEvaluate(1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::println("standby: {} -- listening again", ev.status);
|
||||||
|
StandbyScheduleEvaluate(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SessionInfo { std::string id, seat; std::uint32_t uid = 0; bool locked = false, active = false; };
|
||||||
|
|
||||||
|
std::optional<SessionInfo> QuerySession(const std::string& path) {
|
||||||
|
GError* err = nullptr;
|
||||||
|
GVariant* r = g_dbus_connection_call_sync(
|
||||||
|
g_conn, "org.freedesktop.login1", path.c_str(), "org.freedesktop.DBus.Properties", "GetAll",
|
||||||
|
g_variant_new("(s)", "org.freedesktop.login1.Session"), G_VARIANT_TYPE("(a{sv})"),
|
||||||
|
G_DBUS_CALL_FLAGS_NONE, 3000, nullptr, &err);
|
||||||
|
if (!r) {
|
||||||
|
std::println(std::cerr, "standby: cannot read session {}: {}", path, err ? err->message : "?");
|
||||||
|
if (err) g_error_free(err);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
SessionInfo si;
|
||||||
|
GVariant* dict = g_variant_get_child_value(r, 0);
|
||||||
|
const gchar* id = nullptr;
|
||||||
|
if (g_variant_lookup(dict, "Id", "&s", &id) && id) si.id = id;
|
||||||
|
guint32 uid = 0; const gchar* upath = nullptr;
|
||||||
|
if (g_variant_lookup(dict, "User", "(u&o)", &uid, &upath)) si.uid = uid;
|
||||||
|
const gchar* seat = nullptr; const gchar* spath = nullptr;
|
||||||
|
if (g_variant_lookup(dict, "Seat", "(&s&o)", &seat, &spath) && seat) si.seat = seat;
|
||||||
|
gboolean locked = FALSE, active = FALSE;
|
||||||
|
if (g_variant_lookup(dict, "LockedHint", "b", &locked)) si.locked = locked;
|
||||||
|
if (g_variant_lookup(dict, "Active", "b", &active)) si.active = active;
|
||||||
|
g_variant_unref(dict);
|
||||||
|
g_variant_unref(r);
|
||||||
|
return si;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-reads logind and follows the seat session that is locked AND active, or
|
||||||
|
// none. Called on every LockedHint/Active change and session removal, so a
|
||||||
|
// lock, an unlock and a user switch all land here.
|
||||||
|
void StandbyRescan() {
|
||||||
|
GError* err = nullptr;
|
||||||
|
GVariant* r = g_dbus_connection_call_sync(
|
||||||
|
g_conn, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager",
|
||||||
|
"ListSessions", nullptr, G_VARIANT_TYPE("(a(susso))"), G_DBUS_CALL_FLAGS_NONE, 3000, nullptr, &err);
|
||||||
|
if (!r) {
|
||||||
|
std::println(std::cerr, "standby: ListSessions failed: {}", err ? err->message : "?");
|
||||||
|
if (err) g_error_free(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string frontPath;
|
||||||
|
SessionInfo front;
|
||||||
|
GVariant* arr = g_variant_get_child_value(r, 0);
|
||||||
|
GVariantIter it;
|
||||||
|
g_variant_iter_init(&it, arr);
|
||||||
|
const gchar *id, *user, *seat, *path; guint32 uid;
|
||||||
|
while (g_variant_iter_next(&it, "(&su&s&s&o)", &id, &uid, &user, &seat, &path)) {
|
||||||
|
if (!seat || !*seat) continue; // not a seat session: nothing to unlock on
|
||||||
|
auto si = QuerySession(path);
|
||||||
|
if (si && si->locked && si->active && !si->seat.empty()) { frontPath = path; front = *si; break; }
|
||||||
|
}
|
||||||
|
g_variant_unref(arr);
|
||||||
|
g_variant_unref(r);
|
||||||
|
|
||||||
|
if (g_standby.locked && g_standby.sessionPath == frontPath) return; // unchanged
|
||||||
|
if (g_standby.locked) {
|
||||||
|
std::println("standby: session {} unlocked or no longer in front", g_standby.sessionId);
|
||||||
|
g_standby.locked = false;
|
||||||
|
if (g_standby.pollSource) { g_source_remove(g_standby.pollSource); g_standby.pollSource = 0; }
|
||||||
|
if (g_standby.armed) StandbyCancel(); // armed for the session just left
|
||||||
|
}
|
||||||
|
if (!frontPath.empty()) {
|
||||||
|
g_standby.sessionPath = frontPath;
|
||||||
|
g_standby.sessionId = front.id;
|
||||||
|
g_standby.uid = front.uid;
|
||||||
|
g_standby.lockGen++;
|
||||||
|
g_standby.fingers = LoadMap(front.uid);
|
||||||
|
g_standby.locked = true;
|
||||||
|
g_standby.screenOn = ScreenOn();
|
||||||
|
if (!g_standby.pollSource) g_standby.pollSource = g_timeout_add(500, StandbyPoll, nullptr);
|
||||||
|
std::println("standby: session {} (uid {}) locked; display {}; {} finger(s) enrolled",
|
||||||
|
front.id, front.uid, g_standby.screenOn ? "on" : "off", g_standby.fingers.Size());
|
||||||
|
}
|
||||||
|
StandbyEvaluate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PropertyTouched(GVariant* changed, GVariant* invalidated, std::string_view key) {
|
||||||
|
if (GVariant* v = g_variant_lookup_value(changed, std::string(key).c_str(), nullptr)) {
|
||||||
|
g_variant_unref(v);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
GVariantIter it;
|
||||||
|
const gchar* name = nullptr;
|
||||||
|
g_variant_iter_init(&it, invalidated);
|
||||||
|
while (g_variant_iter_next(&it, "&s", &name))
|
||||||
|
if (name && key == name) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnLogindProperties(GDBusConnection*, const gchar*, const gchar* path, const gchar*, const gchar*,
|
||||||
|
GVariant* params, gpointer) {
|
||||||
|
if (!path || std::string_view(path).find("/org/freedesktop/login1/session/") != 0) return;
|
||||||
|
const gchar* iface = nullptr;
|
||||||
|
GVariant* changed = nullptr;
|
||||||
|
GVariant* invalidated = nullptr;
|
||||||
|
g_variant_get(params, "(&s@a{sv}@as)", &iface, &changed, &invalidated);
|
||||||
|
if (iface && std::string_view(iface) == "org.freedesktop.login1.Session"
|
||||||
|
&& (PropertyTouched(changed, invalidated, "LockedHint")
|
||||||
|
|| PropertyTouched(changed, invalidated, "Active")))
|
||||||
|
StandbyRescan();
|
||||||
|
g_variant_unref(changed);
|
||||||
|
g_variant_unref(invalidated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnSessionRemoved(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar*,
|
||||||
|
GVariant*, gpointer) {
|
||||||
|
StandbyRescan();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StandbyInit() {
|
||||||
|
if (!g_standbyEnabled) { std::println("standby unlock: off"); return; }
|
||||||
|
FindDisplayNodes();
|
||||||
|
std::println("standby unlock: on (display state from {})",
|
||||||
|
!g_blPowerPath.empty() ? g_blPowerPath : !g_dpmsPath.empty() ? g_dpmsPath : "nothing -- assumed on");
|
||||||
|
g_dbus_connection_signal_subscribe(g_conn, "org.freedesktop.login1", "org.freedesktop.DBus.Properties",
|
||||||
|
"PropertiesChanged", nullptr, nullptr, G_DBUS_SIGNAL_FLAGS_NONE,
|
||||||
|
OnLogindProperties, nullptr, nullptr);
|
||||||
|
g_dbus_connection_signal_subscribe(g_conn, "org.freedesktop.login1", "org.freedesktop.login1.Manager",
|
||||||
|
"SessionRemoved", "/org/freedesktop/login1", nullptr,
|
||||||
|
G_DBUS_SIGNAL_FLAGS_NONE, OnSessionRemoved, nullptr, nullptr);
|
||||||
|
StandbyRescan(); // a session locked before we started (a daemon restart under a lock screen)
|
||||||
|
}
|
||||||
|
|
||||||
void PostEvent(std::unique_ptr<Event> ev) {
|
void PostEvent(std::unique_ptr<Event> ev) {
|
||||||
g_idle_add([](gpointer data) -> gboolean {
|
g_idle_add([](gpointer data) -> gboolean {
|
||||||
std::unique_ptr<Event> ev(static_cast<Event*>(data));
|
std::unique_ptr<Event> ev(static_cast<Event*>(data));
|
||||||
|
|
@ -2397,6 +2741,10 @@ void PostEvent(std::unique_ptr<Event> ev) {
|
||||||
case Event::Kind::Ready:
|
case Event::Kind::Ready:
|
||||||
g_ready = true;
|
g_ready = true;
|
||||||
std::println("fingerprintd: ready");
|
std::println("fingerprintd: ready");
|
||||||
|
StandbyEvaluate();
|
||||||
|
break;
|
||||||
|
case Event::Kind::StandbyStatus:
|
||||||
|
StandbyOnStatus(*ev);
|
||||||
break;
|
break;
|
||||||
case Event::Kind::StartFailed:
|
case Event::Kind::StartFailed:
|
||||||
std::println(std::cerr, "fingerprintd: session bring-up failed -- exiting for systemd");
|
std::println(std::cerr, "fingerprintd: session bring-up failed -- exiting for systemd");
|
||||||
|
|
@ -2413,6 +2761,9 @@ void PostEvent(std::unique_ptr<Event> ev) {
|
||||||
ReturnError(ev->invocation, "Internal", "could not select the user's group");
|
ReturnError(ev->invocation, "Internal", "could not select the user's group");
|
||||||
}
|
}
|
||||||
g_pendingClaim = nullptr;
|
g_pendingClaim = nullptr;
|
||||||
|
// A claim dropped while it was pending (the claimant left, or
|
||||||
|
// the group failed) evaluated standby too early to arm it.
|
||||||
|
StandbyEvaluate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Event::Kind::RemoveDone:
|
case Event::Kind::RemoveDone:
|
||||||
|
|
@ -2542,6 +2893,7 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
||||||
g_claim.fingers = LoadMap(g_claim.uid);
|
g_claim.fingers = LoadMap(g_claim.uid);
|
||||||
g_pendingClaim = inv;
|
g_pendingClaim = inv;
|
||||||
WatchClaimant(g_claim.sender);
|
WatchClaimant(g_claim.sender);
|
||||||
|
StandbyCancel(); // the client's op comes first; standby returns with Release
|
||||||
g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid, .invocation = inv });
|
g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid, .invocation = inv });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2603,6 +2955,13 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
||||||
if (one) m.Remove(*one); else m.Clear();
|
if (one) m.Remove(*one); else m.Clear();
|
||||||
SaveMap(uid, m);
|
SaveMap(uid, m);
|
||||||
if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m;
|
if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m;
|
||||||
|
// A standby verify for this user holds the fids it was armed with and
|
||||||
|
// keeps the worker busy; stop it so the removal below runs, and so it
|
||||||
|
// re-arms from the new map. The verdict check re-reads the map anyway.
|
||||||
|
if (g_standby.locked && g_standby.uid == uid) {
|
||||||
|
g_standby.fingers = m;
|
||||||
|
StandbyCancel();
|
||||||
|
}
|
||||||
// The name is dropped first and the template second, in that order on
|
// The name is dropped first and the template second, in that order on
|
||||||
// purpose: a finger whose template survives a failed removal is a slot
|
// purpose: a finger whose template survives a failed removal is a slot
|
||||||
// leak, while a name that survives a successful one would keep
|
// leak, while a name that survives a successful one would keep
|
||||||
|
|
@ -2731,6 +3090,7 @@ void OnBusAcquired(GDBusConnection* conn, const gchar*, gpointer) {
|
||||||
g_dbus_connection_register_object(conn, DevicePath, node->interfaces[1], &g_vtable, nullptr, nullptr, nullptr);
|
g_dbus_connection_register_object(conn, DevicePath, node->interfaces[1], &g_vtable, nullptr, nullptr, nullptr);
|
||||||
g_dbus_node_info_unref(node);
|
g_dbus_node_info_unref(node);
|
||||||
std::println("objects registered at {} and {}", ManagerPath, DevicePath);
|
std::println("objects registered at {} and {}", ManagerPath, DevicePath);
|
||||||
|
StandbyInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean OnTerm(gpointer) {
|
gboolean OnTerm(gpointer) {
|
||||||
|
|
@ -2952,6 +3312,8 @@ int main(int argc, char** argv) {
|
||||||
// The observer thread and the loop would both drain the same fd, so
|
// The observer thread and the loop would both drain the same fd, so
|
||||||
// the diagnostic and the wake are mutually exclusive.
|
// the diagnostic and the wake are mutually exclusive.
|
||||||
if (a == "--edge-wake") g_edgeWake = true;
|
if (a == "--edge-wake") g_edgeWake = true;
|
||||||
|
if (a == "--standby-unlock=off") g_standbyEnabled = false;
|
||||||
|
if (a == "--standby-unlock=on") g_standbyEnabled = true;
|
||||||
if (a.starts_with("--log-dir=")) { g_logDir = a.substr(10); g_logDirExplicit = true; }
|
if (a.starts_with("--log-dir=")) { g_logDir = a.substr(10); g_logDirExplicit = true; }
|
||||||
if (a.starts_with("--state-dir=")) g_stateDir = a.substr(12);
|
if (a.starts_with("--state-dir=")) g_stateDir = a.substr(12);
|
||||||
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
|
if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));
|
||||||
|
|
@ -2985,6 +3347,8 @@ int main(int argc, char** argv) {
|
||||||
" template, as stock does (default on, 8)\n"
|
" template, as stock does (default on, 8)\n"
|
||||||
" --actions=FILE per-finger actions (default\n"
|
" --actions=FILE per-finger actions (default\n"
|
||||||
" /etc/fingerprintd/actions.conf, absent = off)\n"
|
" /etc/fingerprintd/actions.conf, absent = off)\n"
|
||||||
|
" --standby-unlock=on|off unlock the locked session in front by finger,\n"
|
||||||
|
" via logind (default on)\n"
|
||||||
" --sfs-root=DIR --sfs-writable --rpmb-write storage policy",
|
" --sfs-root=DIR --sfs-writable --rpmb-write storage policy",
|
||||||
Version);
|
Version);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue