diff --git a/README.md b/README.md index 31dc3b3..a3bdbb6 100644 --- a/README.md +++ b/README.md @@ -33,29 +33,6 @@ fprintd-verify user (wrong finger) verify-no-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 The `focal64` trustlet is proprietary and is not in this repo. It is diff --git a/implementations/main.cpp b/implementations/main.cpp index 7ad1923..693085f 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -68,7 +68,7 @@ namespace { // Bumping this is what publishes a package: the registry answers 409 for a // version it already has, which a build treats as a no-op. -constexpr const char* Version = "0.2.5"; +constexpr const char* Version = "0.2.4"; bool g_verbose = false; // 500 ms was the research harness's pace, chosen so a human could read the @@ -1943,7 +1943,7 @@ private: // jobs; results and progress come back through the GLib main loop. // ============================================================================= struct Job { - enum class Kind { Claim, Enroll, Verify, Remove, Standby } kind; + enum class Kind { Claim, Enroll, Verify, Remove } kind; std::uint32_t uid = 0; std::string finger; std::vector acceptFids; // Verify: which fids count @@ -1955,7 +1955,7 @@ struct Job { // so the D-Bus emission happens on the thread that owns the connection. struct Event { enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus, - RemoveDone, StandbyStatus } kind; + RemoveDone } kind; bool ok = false; bool done = false; std::string status; @@ -1966,14 +1966,6 @@ struct Event { void PostEvent(std::unique_ptr 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 g_standbyWanted{false}; -std::atomic g_standbyRunning{false}; - class Worker { public: void Start() { @@ -2084,42 +2076,6 @@ private: PostEvent(std::move(ev)); 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{ .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: { // fprintd's contract is that a verify runs until the client // stops it. The frame cap bounds one trustlet scan session, @@ -2327,9 +2283,6 @@ Claim g_claim; GDBusMethodInvocation* g_pendingClaim = nullptr; guint g_claimWatch = 0; -void StandbyEvaluate(); -void StandbyCancel(); - void DropClaim(const char* why) { if (g_claimWatch) { g_dbus_connection_signal_unsubscribe(g_conn, g_claimWatch); @@ -2339,7 +2292,6 @@ void DropClaim(const char* why) { if (g_claim.held) std::println("claim by {} for {} dropped: {}", g_claim.sender, g_claim.user, why); g_claim = {}; - StandbyEvaluate(); // the sensor is free again } // A claim is held by a bus connection. If that connection goes away -- the @@ -2366,16 +2318,16 @@ std::string MapPath(std::uint32_t uid) { return fingerprintd::store::PathForUid(g_stateDir, uid); } -fingerprintd::store::Map LoadMap(std::uint32_t uid, bool quiet = false) { +fingerprintd::store::Map LoadMap(std::uint32_t uid) { std::string path = MapPath(uid); std::ifstream f(path); if (!f) { - if (!quiet) std::println("map {}: {}", path, ::strerror(errno)); + std::println("map {}: {}", path, ::strerror(errno)); return {}; } std::string text((std::istreambuf_iterator(f)), std::istreambuf_iterator()); auto m = fingerprintd::store::Map::Decode(text); - if (!quiet) std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size()); + std::println("map {}: {} bytes, {} finger(s)", path, text.size(), m.Size()); return m; } @@ -2438,302 +2390,6 @@ bool Authorised(GDBusMethodInvocation* inv, std::uint32_t 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 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(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 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 ev) { g_idle_add([](gpointer data) -> gboolean { std::unique_ptr ev(static_cast(data)); @@ -2741,10 +2397,6 @@ void PostEvent(std::unique_ptr ev) { case Event::Kind::Ready: g_ready = true; std::println("fingerprintd: ready"); - StandbyEvaluate(); - break; - case Event::Kind::StandbyStatus: - StandbyOnStatus(*ev); break; case Event::Kind::StartFailed: std::println(std::cerr, "fingerprintd: session bring-up failed -- exiting for systemd"); @@ -2761,9 +2413,6 @@ void PostEvent(std::unique_ptr ev) { ReturnError(ev->invocation, "Internal", "could not select the user's group"); } 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; case Event::Kind::RemoveDone: @@ -2893,7 +2542,6 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant* g_claim.fingers = LoadMap(g_claim.uid); g_pendingClaim = inv; 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 }); return; } @@ -2955,13 +2603,6 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant* if (one) m.Remove(*one); else m.Clear(); SaveMap(uid, 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 // purpose: a finger whose template survives a failed removal is a slot // leak, while a name that survives a successful one would keep @@ -3090,7 +2731,6 @@ void OnBusAcquired(GDBusConnection* conn, const gchar*, gpointer) { g_dbus_connection_register_object(conn, DevicePath, node->interfaces[1], &g_vtable, nullptr, nullptr, nullptr); g_dbus_node_info_unref(node); std::println("objects registered at {} and {}", ManagerPath, DevicePath); - StandbyInit(); } gboolean OnTerm(gpointer) { @@ -3312,8 +2952,6 @@ int main(int argc, char** argv) { // The observer thread and the loop would both drain the same fd, so // the diagnostic and the wake are mutually exclusive. 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("--state-dir=")) g_stateDir = a.substr(12); if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9))); @@ -3347,8 +2985,6 @@ int main(int argc, char** argv) { " template, as stock does (default on, 8)\n" " --actions=FILE per-finger actions (default\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", Version); return 1; diff --git a/packaging/APKBUILD b/packaging/APKBUILD index 3c4264a..1320d3c 100644 --- a/packaging/APKBUILD +++ b/packaging/APKBUILD @@ -10,7 +10,7 @@ # Alpine, so an APKBUILD that compiled from source could not be built by # anyone but us either. pkgname=fingerprintd -pkgver=0.2.5 +pkgver=0.2.4 pkgrel=0 pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)" url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd"