fingerprintd/project.cpp
Jorijn van der Graaf 1934822554 An agent, so a finger can mean something in your session
The daemon announces every matched finger on the system bus and stops there,
because root has no session bus, no display and no business starting your
applications. fingerprintd-agent is the other half: it runs as you, subscribes
properly rather than parsing gdbus monitor output, filters by uid because the
signal is visible to every local user, and maps fingers to commands from a file
you own and can edit without restarting anything.

It is a separate binary and a separate subpackage because it is a separate
trust domain. /etc/fingerprintd/actions.conf is a root shell and is guarded
like one; ~/.config/fingerprintd/fingers.conf runs your commands as you, so it
is an ordinary dotfile.

Demonstrated on the phone: one press of the unlock finger both unlocks it and
opens plasma-camera.
2026-09-05 06:21:53 +02:00

161 lines
6.9 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, 9> ifaces = {
"interfaces/Fingerprintd",
"interfaces/Fingerprintd-Sfs",
"interfaces/Fingerprintd-Rpmb",
"interfaces/Fingerprintd-Ta",
"interfaces/Fingerprintd-Engine",
"interfaces/Fingerprintd-Store",
"interfaces/Fingerprintd-Tee",
"interfaces/Fingerprintd-Sensor",
"interfaces/Fingerprintd-Actions",
};
std::array<fs::path, 0> impls = {};
Core->GetInterfacesAndImplementations(ifaces, impls);
}
// fingerprintd-agent — the session half. Built with --product=agent.
// It is a separate binary and not a mode of the daemon because it is a
// different user, a different trust domain and a different lifetime: the
// daemon is root and boot-long, this is you and session-long. It links
// the core only for the finger-name vocabulary, and never touches the TEE.
for (std::string_view a : args) {
if (a != "--product=agent") continue;
Configuration agent;
agent.path = "./";
agent.name = "fingerprintd-agent";
agent.outputName = "fingerprintd-agent";
ApplyStandardArgs(agent, args);
agent.type = ConfigurationType::Executable;
agent.dependencies = { Core.get() };
std::array<fs::path, 0> noIfaces = {};
std::array<fs::path, 1> agentImpls = { "implementations/agent" };
agent.GetInterfacesAndImplementations(noIfaces, agentImpls);
ApplyGioFlags(agent);
ProjectLint::AddProjectLintRules(agent);
return agent;
}
// 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() });
cfg.AddTest("Actions").Dependencies({ Core.get() });
ProjectLint::AddProjectLintRules(cfg);
return cfg;
}