fingerprintd/project.cpp
Jorijn van der Graaf e0bc02332f Become a daemon: a held session, a worker, and net.reactivated.Fprint
The probe becomes the thing the plan was for. Three threads: the supplicant
services QTEE's callbacks; the worker owns the sensor rail, the QTEE session
and the trustlet and is the only thread that ever invokes it, so every
enrolment and authentication is serialised by construction; the main thread
runs the GLib loop and speaks fprintd's own D-Bus interface, never touching
the trustlet directly.

Session is the bring-up from a cold /dev/tee0 to a calibrated sensor, plus the
enrol and verify loops as methods that take a cancel flag and progress
callbacks. Worker is a job queue on a pthread with an 8 MiB stack -- musl's
default is 128 KiB and the session keeps request buffers on the stack. Results
come back through g_idle_add so signals are emitted on the thread that owns
the connection.

net.reactivated.Fprint is implemented rather than wrapped: Manager with
GetDevices/GetDefaultDevice, Device with Claim/Release, EnrollStart/Stop,
VerifyStart/Stop, ListEnrolledFingers and the three Delete variants, the three
signals, and the five properties. Owning fprintd's name is what lets
pam_fprintd, the Plasma KCM and fprintd-enroll work unmodified.

Two honest limits. Authorisation is the conservative rule -- you may act on
your own prints, root on anyone's -- because polkit is not in this milestone.
And DeleteEnrolledFingers removes the finger's NAME only: FF_CMD_TA_REMOVE
exists but its payload is not reverse-engineered, and guessing at a command
that writes to the store is exactly how an index got invalidated earlier
today. A deleted finger loses its name and stops being offered; its template
still occupies a slot in the group. Logged as such.

The finger-name map is written per user under the state directory, tmp-file
and rename. An enrolment records the fid the trustlet reported in the touch
event's response; if none was reported the finger cannot be named yet, and
the daemon says so rather than inventing one.

Verified on the phone as a systemd unit: owns the bus name, init chain
complete, floor calibrated, ready.
2026-09-02 22:10:51 +02:00

137 lines
5.8 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// lint-disable-file no-char-pointer
import std;
import Crafter.Build;
#include "lint-rules.h"
namespace fs = std::filesystem;
using namespace Crafter;
// libqcomtee — Qualcomm's BSD-3 userspace client for QTEE, built by
// packaging/make-libqcomtee.sh into a per-target cache dir. It is not vendored
// here: it is upstream code we pin, and the script builds it WITHOUT QCBOR
// (see the script for why that dependency is avoidable).
static void ApplyQcomteeFlags(Configuration& cfg) {
fs::path base = fs::path(std::getenv("HOME") ? std::getenv("HOME") : ".")
/ ".cache" / "fingerprintd";
// make-libqcomtee.sh names its output dir after --target, and plain
// "libqcomtee" when built for the host. cfg.target is always populated
// (it defaults to the host triple), so try the target-specific dir first
// and fall back to the host one.
std::error_code ec;
fs::path root = base / ("libqcomtee-" + cfg.target);
if (!fs::exists(root / "libqcomtee.a", ec))
root = base / "libqcomtee";
if (!fs::exists(root / "libqcomtee.a", ec)) {
std::println(std::cerr,
"libqcomtee not built for '{}'. Run:\n"
" packaging/make-libqcomtee.sh{}{}",
cfg.target.empty() ? std::string("native") : cfg.target,
cfg.target.empty() ? std::string() : " --target=" + cfg.target,
cfg.sysroot.empty() ? std::string() : " --sysroot=" + cfg.sysroot);
}
cfg.compileFlags.push_back("-I" + (root / "include").string());
cfg.linkFlags.push_back((root / "libqcomtee.a").string());
}
// pkg-config wrapper (std-only: no popen in import std). Returns the flags
// split on whitespace, or empty if pkg-config is unavailable.
static std::vector<std::string> PkgConfig(std::string_view args) {
fs::path tmp = fs::temp_directory_path() /
std::format("fingerprintd-pkgconfig-{}.txt", std::hash<std::string_view>{}(args));
std::string cmd = std::format("pkg-config {} > {} 2>/dev/null", args, tmp.string());
std::vector<std::string> flags;
if (std::system(cmd.c_str()) == 0) {
std::ifstream f(tmp);
std::string flag;
while (f >> flag)
flags.push_back(flag);
}
std::error_code ec;
fs::remove(tmp, ec);
return flags;
}
// GDBus (gio-2.0) for net.reactivated.Fprint. Same choice imsd made, for the
// same reason: the platform stack is GLib, so a GMainLoop is wanted regardless.
static void ApplyGioFlags(Configuration& cfg) {
if (!cfg.sysroot.empty()) {
// Cross build: the host's pkg-config would answer for the wrong
// architecture. glib's include layout is stable; `-I=` resolves
// inside the sysroot.
cfg.compileFlags.push_back("-I=/usr/include/glib-2.0");
cfg.compileFlags.push_back("-I=/usr/lib/glib-2.0/include");
for (const char* l : { "-lgio-2.0", "-lgobject-2.0", "-lglib-2.0" })
cfg.linkFlags.push_back(l);
} else {
for (std::string& f : PkgConfig("--cflags gio-2.0"))
if (f.starts_with("-I") || f.starts_with("-D")) cfg.compileFlags.push_back(std::move(f));
std::vector<std::string> libs;
for (std::string& f : PkgConfig("--libs gio-2.0"))
if (f.starts_with("-l") || f.starts_with("-L")) libs.push_back(std::move(f));
if (libs.empty()) libs = { "-lgio-2.0", "-lgobject-2.0", "-lglib-2.0" };
for (std::string& f : libs)
cfg.linkFlags.push_back(std::move(f));
}
}
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
// fingerprintd-core — the wire formats and state machines as a static
// library of pure C++ modules. Deliberately free of GLib, libqcomtee and
// every system header: the byte-level decisions in here were the whole
// cost of this project, so they are pinned by tests that run on a dev box
// with no phone, no TEE and no sensor.
static auto Core = std::make_unique<Configuration>();
Core->path = "./";
Core->name = "fingerprintd-core";
Core->outputName = "fingerprintd-core";
ApplyStandardArgs(*Core, args);
Core->type = ConfigurationType::LibraryStatic;
{
std::array<fs::path, 8> ifaces = {
"interfaces/Fingerprintd",
"interfaces/Fingerprintd-Sfs",
"interfaces/Fingerprintd-Rpmb",
"interfaces/Fingerprintd-Ta",
"interfaces/Fingerprintd-Engine",
"interfaces/Fingerprintd-Store",
"interfaces/Fingerprintd-Tee",
"interfaces/Fingerprintd-Sensor",
};
std::array<fs::path, 0> impls = {};
Core->GetInterfacesAndImplementations(ifaces, impls);
}
// fingerprintd — the daemon: sensor rail, QTEE session, the gpfile and
// RPMB listeners, and the bus surface, wrapped around the core.
Configuration cfg;
cfg.path = "./";
cfg.name = "fingerprintd";
cfg.outputName = "fingerprintd";
ApplyStandardArgs(cfg, args);
cfg.type = ConfigurationType::Executable;
cfg.dependencies = { Core.get() };
{
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "implementations/main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
}
ApplyQcomteeFlags(cfg);
ApplyGioFlags(cfg);
cfg.linkFlags.push_back("-lpthread"); // the supplicant and worker threads
cfg.AddTest("Sfs").Dependencies({ Core.get() });
cfg.AddTest("Rpmb").Dependencies({ Core.get() });
cfg.AddTest("Ta").Dependencies({ Core.get() });
cfg.AddTest("Engine").Dependencies({ Core.get() });
cfg.AddTest("Store").Dependencies({ Core.get() });
cfg.AddTest("Tee").Dependencies({ Core.get() });
cfg.AddTest("Sensor").Dependencies({ Core.get() });
ProjectLint::AddProjectLintRules(cfg);
return cfg;
}