fingerprintd/interfaces/Fingerprintd-Store.cppm
Jorijn van der Graaf f2a696bbbe Drop the gid 60 compatibility path
gid 60 was never a decision -- it was the ENROLL token's timeout field being
read as a gid, then made self-consistent. Keeping it readable alongside gid =
uid would be a second code path, a second group to reason about, and a
migration to eventually retire, all to avoid re-enrolling one finger on one dev
phone. Re-enrol it instead.
2026-09-02 17:28:47 +02:00

174 lines
7.2 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// lint-disable-file fixed-width-types
/*
Fingerprintd:Store — the name map between two vocabularies.
The trustlet stores templates itself, in QTEE-encrypted containers under a
group id, and identifies a finger by an opaque 32-bit id it chose. fprintd
speaks users and finger names ("right-index-finger"). Neither knows about the
other, so something has to hold the correspondence, and it cannot be the
trustlet — it has no field for a name.
What this module is NOT: template storage. No biometric data passes through
here. A template is a ~252 KB container the trustlet encrypts and QTEE
anti-rollback protects; this side holds a small table of {finger name -> the id
the trustlet reported}, which is worth no more than a username.
Encode and decode only — the daemon shell owns the file.
*/
export module Fingerprintd:Store;
import std;
export namespace fingerprintd::store {
// ---- Fingers ----------------------------------------------------------
//
// fprintd's vocabulary, which is libfprint's. "any" is not a finger: it is
// what a client passes to VerifyStart to mean "identify against everything
// enrolled", and it must never be stored as one.
enum class Finger {
LeftThumb, LeftIndex, LeftMiddle, LeftRing, LeftLittle,
RightThumb, RightIndex, RightMiddle, RightRing, RightLittle,
};
inline constexpr std::array<std::pair<Finger, std::string_view>, 10> FingerNames = {{
{ Finger::LeftThumb, "left-thumb" },
{ Finger::LeftIndex, "left-index-finger" },
{ Finger::LeftMiddle, "left-middle-finger" },
{ Finger::LeftRing, "left-ring-finger" },
{ Finger::LeftLittle, "left-little-finger" },
{ Finger::RightThumb, "right-thumb" },
{ Finger::RightIndex, "right-index-finger" },
{ Finger::RightMiddle, "right-middle-finger" },
{ Finger::RightRing, "right-ring-finger" },
{ Finger::RightLittle, "right-little-finger" },
}};
inline constexpr std::string_view AnyFinger = "any";
inline constexpr std::string_view NameOf(Finger f) {
for (auto& [k, v] : FingerNames)
if (k == f) return v;
return {};
}
inline std::optional<Finger> FingerFromName(std::string_view name) {
for (auto& [k, v] : FingerNames)
if (v == name) return k;
return std::nullopt;
}
// ---- Groups -----------------------------------------------------------
//
// The trustlet's group id is Android's per-user id. SET_ACTIVE_GROUP
// writes it to device+0x30 and AUTHENTICATE compares its own argument
// against that field, returning -200 on a mismatch — so the two only have
// to agree with each other and the value itself is ours to choose.
//
// We choose the caller's Linux uid, which makes the mapping total and
// needs no allocation table.
//
// The research harness used gid 60, which was never a decision -- it was
// the ENROLL token's timeout field being read as the gid, then made
// self-consistent. There is no compatibility path for it: the finger it
// enrolled gets re-enrolled under the uid. Nothing is worth carrying a
// second group id for.
using Gid = std::uint32_t;
inline Gid GidForUid(std::uint32_t uid) { return uid; }
// common.max_enrolling_fingers, from the trustlet config we ship.
inline constexpr std::size_t MaxFingersPerGroup = 5;
// ---- The map ----------------------------------------------------------
struct Entry {
Finger finger;
std::uint32_t fid = 0; // what the trustlet reported at enrolment
};
class Map {
public:
// Enrolling the same finger twice replaces the old id rather than
// accumulating: the trustlet allocated a new template and the old id
// is no longer what a match will report.
bool Add(Finger f, std::uint32_t fid) {
if (fid == 0) return false; // 0 is the trustlet's "no match"
for (Entry& e : entries_) {
if (e.finger == f) { e.fid = fid; return true; }
}
if (entries_.size() >= MaxFingersPerGroup) return false;
entries_.push_back({ f, fid });
return true;
}
bool Remove(Finger f) {
auto n = std::erase_if(entries_, [&](const Entry& e) { return e.finger == f; });
return n > 0;
}
void Clear() { entries_.clear(); }
// A match reports an id; this turns it back into a name.
std::optional<Finger> Lookup(std::uint32_t fid) const {
if (fid == 0) return std::nullopt;
for (const Entry& e : entries_)
if (e.fid == fid) return e.finger;
return std::nullopt;
}
bool Has(Finger f) const {
return std::ranges::any_of(entries_, [&](const Entry& e) { return e.finger == f; });
}
std::size_t Size() const { return entries_.size(); }
bool Full() const { return entries_.size() >= MaxFingersPerGroup; }
const std::vector<Entry>& Entries() const { return entries_; }
// ---- Serialisation
//
// One "name fid" per line. Deliberately boring and greppable: this
// file is recoverable by hand if it is ever lost, because losing it
// costs names, not templates.
std::string Encode() const {
std::string out;
for (const Entry& e : entries_)
out += std::format("{} {}\n", NameOf(e.finger), e.fid);
return out;
}
// Unknown finger names and malformed lines are skipped, not fatal: a
// corrupt map must degrade to "fewer names known", never to a daemon
// that will not start and therefore a phone that cannot be unlocked.
static Map Decode(std::string_view text) {
Map m;
std::size_t pos = 0;
while (pos <= text.size()) {
std::size_t nl = text.find('\n', pos);
std::string_view line = text.substr(pos, nl == std::string_view::npos
? std::string_view::npos : nl - pos);
pos = (nl == std::string_view::npos) ? text.size() + 1 : nl + 1;
if (line.empty()) continue;
std::size_t sp = line.find(' ');
if (sp == std::string_view::npos) continue;
auto f = FingerFromName(line.substr(0, sp));
if (!f) continue;
std::uint32_t fid = 0;
auto num = line.substr(sp + 1);
auto [ptr, ec] = std::from_chars(num.data(), num.data() + num.size(), fid);
if (ec != std::errc{} || ptr != num.data() + num.size()) continue;
m.Add(*f, fid);
}
return m;
}
private:
std::vector<Entry> entries_;
};
// Per-user file under the daemon's state directory. Root-only: it is not
// secret, but it decides which name an authentication reports.
inline std::string PathForUid(std::string_view stateDir, std::uint32_t uid) {
return std::format("{}/fingers-{}.map", stateDir, uid);
}
}