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.
161 lines
6.9 KiB
C++
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;
|
|
}
|