[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81805":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":13,"stars7d":15,"stars30d":16,"stars90d":14,"forks30d":14,"starsTrendScore":15,"compositeScore":17,"rankGlobal":9,"rankLanguage":9,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":9,"pushedAt":9,"updatedAt":23,"readmeContent":24,"aiSummary":25,"trendingCount":14,"starSnapshotCount":14,"syncStatus":12,"lastSyncTime":26,"discoverSource":27},81805,"linux-fingerprint-r503","matpb\u002Flinux-fingerprint-r503","matpb","Linux desktop fingerprint login using a Grow R503 sensor + Arduino + a Rust fprintd-replacement daemon",null,"Rust",43,2,1,0,3,7,45.63,"MIT License",false,"main",true,[],"2026-06-12 04:01:35","# linux-fingerprint-r503 — fingerprint login for Linux using a Grow R503 + Arduino\n\n[![CI](https:\u002F\u002Fgithub.com\u002Fmatpb\u002Flinux-fingerprint-r503\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fmatpb\u002Flinux-fingerprint-r503\u002Factions\u002Fworkflows\u002Fci.yml)\n[![r503d](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fr503d-1.1.0-success)](pcside\u002Fdaemon\u002FCargo.toml)\n[![firmware](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Ffirmware-fw%201.1-success)](firmware\u002Fr503fp\u002Fr503fp.ino)\n[![Rust 2024](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FRust-2024_edition-CE412B?logo=rust&logoColor=white)](pcside\u002Fdaemon\u002FCargo.toml)\n![Platform: Linux](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fplatform-Linux-1793D1?logo=linux&logoColor=white)\n[![License: MIT](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-MIT-blue.svg)](LICENSE)\n\nA from-parts USB fingerprint reader for Linux desktops. Total parts cost\nunder $15. Drop-in replacement for upstream `fprintd` — PAM, KDE Settings,\nGNOME Settings, `fprintd-verify`, `sudo` with finger, screen-unlock with\nfinger all work.\n\n**As of `fw=1.0` \u002F `r503d 1.0.0`** the Arduino↔host wire is authenticated:\nevery command and response carries a SipHash-2-4 MAC keyed to a\nTOFU-paired secret in EEPROM. Replay and hot-swap attacks against the USB\nserial link are blocked. See [`SPEC.md` §13](SPEC.md) for the full design,\nincluding what the threat model *doesn't* cover.\n\n![R503 sensor mounted in a hand-cut wooden enclosure, blue ring glowing](docs\u002Fimages\u002Fhero.jpg)\n\n*wish I had a 3d printer…*\n\n```\n   ┌──────────┐   UART    ┌─────────────┐   USB-CDC   ┌──────────────────┐\n   │  Grow    │  57600 8N1│  Arduino    │  \u002Fdev\u002Fr503  │  r503d daemon    │\n   │  R503    │◀─────────▶│  (firmware) │◀──────────▶│  net.reactivated │\n   │  sensor  │  3.3V TTL │             │  framed,    │  .Fprint on D-Bus│\n   └──────────┘           └─────────────┘  MAC'd      └──────────────────┘\n                                                              │\n                                                              ▼\n                                                       PAM, KDE, GNOME,\n                                                       fprintd-verify, …\n```\n\n## Why\n\nHardware USB fingerprint readers for Linux are scarce, expensive, and the\nones that exist (Validity, Synaptics, etc.) are reverse-engineered through\nunstable libfprint drivers that break with vendor firmware updates. The\nGrow R503's protocol is **public**, the Arduino side is your own code,\nand the libfprint compatibility layer is just D-Bus.\n\nYou also end up with a fingerprint reader you can read the source of, top\nto bottom.\n\n## Bill of materials\n\n| Part | Notes | Approx cost |\n|------|-------|------|\n| Grow R503 capacitive fingerprint sensor | The round one with the RGB ring | ~$10 |\n| Arduino Uno R3 \u002F Nano \u002F Mega \u002F any ATmega328 board | Anything that runs SoftwareSerial | $5–$25 |\n| 4–6 jumper wires | Dupont \u002F breadboard | trivial |\n\nThat's it. **No level shifter, no voltage divider** — see [`SPEC.md` §3.1](SPEC.md)\nfor why (the R503's RX line is 5V-tolerant in practice; the datasheet lies).\n\n## Wiring\n\n```\nR503             Arduino (Uno R3 \u002F Nano \u002F etc.)\n----             ------------------------------\nRed (VCC)        3V3\nWhite (3.3VT)    3V3                  (touch-IC supply; shares rail with red)\nBlack (GND)      GND\nYellow (TXD)     D2  ── SoftwareSerial RX\nBrown (RXD)      D3  ── SoftwareSerial TX   (direct — no divider!)\nBlue (WAKEUP)    D4                          (optional; not used by firmware yet)\n```\n\nIf your R503 ships with the JST-SH connector, snip a 6-pin JST-SH-to-Dupont\npigtail to break the wires out. Brown is sometimes green depending on the\nseller — verify against the wire that goes into the RXD pin of the JST\nheader, not the colour.\n\n## Prerequisites\n\nTested on Fedora 44 KDE; should work on any systemd-based distro with\n`fprintd`, `pam_fprintd`, and a recent Rust toolchain.\n\n**System packages:**\n\n| Distro | Build | Runtime |\n|---|---|---|\n| Fedora \u002F RHEL | `rust cargo arduino-cli tpm2-tss-devel` | `fprintd pam fprintd-pam tpm2-tss` |\n| Debian \u002F Ubuntu | `rustc cargo arduino-cli libtss2-dev` | `fprintd libpam-fprintd libtss2-esys-3.0.2-0` |\n\nThe `tss-esapi` packages are only needed if you plan to use `--pair --seal-tpm`\n(SPEC §13.12). The daemon builds and runs without a TPM otherwise — `tss-esapi`\nis a hard build dep but a soft runtime dep (the code path is only entered when\n`\u002Fvar\u002Flib\u002Fr503d\u002Fkey.tpm` exists).\n\nRust 1.95+, `arduino-cli` on your `$PATH`.\n\n**Do you have a TPM2?**\n\n```bash\nls \u002Fdev\u002Ftpmrm0 && tpm2_pcrread sha256:7 | head -3\n```\n\nIf both succeed, your host can use the sealed-key path. If `\u002Fdev\u002Ftpmrm0` is\nmissing (older hardware, TPM disabled in BIOS, or a VM without a virtual TPM),\nstick with the default plaintext-key flow.\n\n## Build & install\n\n### 1. Flash the firmware\n\nOpen `firmware\u002Fr503fp\u002Fr503fp.ino` in the Arduino IDE and upload. Or with\n`arduino-cli`:\n\n```bash\n# Uno R3:\narduino-cli compile --fqbn arduino:avr:uno firmware\u002Fr503fp\u002F\narduino-cli upload  --fqbn arduino:avr:uno --port \u002Fdev\u002FttyACM0 firmware\u002Fr503fp\u002F\n\n# Nano (modern Optiboot, including most Elegoo \u002F WAVGAT clones):\narduino-cli compile --fqbn arduino:avr:nano:cpu=atmega328 firmware\u002Fr503fp\u002F\narduino-cli upload  --fqbn arduino:avr:nano:cpu=atmega328 --port \u002Fdev\u002FttyUSB0 firmware\u002Fr503fp\u002F\n\n# Nano with legacy 57600-baud bootloader (older clones):\n#   replace `cpu=atmega328` with `cpu=atmega328old`\n```\n\nThe firmware uses `Adafruit_Fingerprint`. The IDE will offer to install it\non first compile.\n\nIf `arduino-cli upload` fails with `not in sync: resp=0x7e`, your bootloader\nis the other variant — swap `atmega328` ↔ `atmega328old` and retry. Both\nwork; the difference is just bootloader baud rate.\n\n### 2. Build the daemon\n\nRequires Rust 1.95+.\n\n```bash\ncd pcside\u002Fdaemon\ncargo build --release\n```\n\n### 3. Install\n\n```bash\nsudo bash pcside\u002Fdaemon\u002Fdist\u002Finstall.sh\n```\n\nThat script:\n\n- installs `target\u002Frelease\u002Fr503d` to `\u002Fusr\u002Flocal\u002Fbin\u002Fr503d`\n- creates `\u002Fvar\u002Flib\u002Fr503d\u002F` (mode 0700 root:root) for the key, state, and\n  user-slot registry\n- writes the udev rule that exposes the Arduino as `\u002Fdev\u002Fr503` and locks the\n  device node to `root:root 0600` (only the daemon, running as root, needs it;\n  this closes the default `0660 root:dialout` path so no other local user can\n  open the port — security audit 2026-05-28 \u002F H1). **Consequence:** after\n  install, any manual `arduino-cli`\u002Fserial-monitor command against `\u002Fdev\u002Fr503`\n  needs `sudo`.\n- installs the systemd unit (`\u002Fetc\u002Fsystemd\u002Fsystem\u002Fr503d.service`)\n- overrides the D-Bus autolaunch entry for `net.reactivated.Fprint`\n- installs the polkit action\n  (`\u002Fusr\u002Fshare\u002Fpolkit-1\u002Factions\u002Fnet.reactivated.fprint.device.r503d.policy`)\n  used by the caller-identity gate\n- installs the restrictive system bus policy\n  (`\u002Fetc\u002Fdbus-1\u002Fsystem.d\u002Fnet.reactivated.Fprint.conf`) — only `root` and\n  `wheel` members can talk to the daemon; everyone else hits\n  `AccessDenied` at the broker, before the daemon sees the call\n- stops and masks upstream `fprintd.service`\n- starts `r503d.service`\n\nIt's idempotent — re-run it after every `cargo build --release` to\nredeploy the new binary.\n\n### 4. Pair the Nano with the daemon\n\nA freshly-flashed Nano is unpaired — the daemon would talk to it but the\nfirmware would reject every framed command. **Pick one of the two flows\nbelow**; both end with a paired Nano and a working daemon. The TPM-sealed\nflow is recommended if your host has a TPM2 (see Prerequisites for the\nquick check).\n\nThe opt-in file (`\u002Fetc\u002Fr503d\u002Fallow-pair`) used in both flows exists to\ndefeat an attacker racing to your desk with their own Nano — pairing\nwithout root is impossible. `r503d --pair` deletes the marker **before**\nsending the key to the Nano: if the host crashes between Nano-side commit\nand host-side persistence, the gate is already closed, so the next pair\nattempt requires admin to `touch` the marker again. A pre-send bail\n(no marker, or \"already paired\") leaves the marker intact for retry.\n\n#### 4a. Plaintext-key pairing (default)\n\nUse this if you don't have a TPM2 device, or if you don't need offline-disk\nattack resistance.\n\n```bash\nsudo systemctl stop r503d\nsudo mkdir -p \u002Fetc\u002Fr503d\nsudo touch \u002Fetc\u002Fr503d\u002Fallow-pair          # opt-in (see SPEC §13.5)\nsudo r503d --pair                         # 128-bit key → \u002Fvar\u002Flib\u002Fr503d\u002Fkey\nsudo systemctl start r503d\n```\n\n```bash\nsudo r503d --status\n# port:             \u002Fdev\u002Fr503\n# firmware:         fw=1.1 fmt=2\n# firmware paired:  true\n# firmware counter: 42\n# host key.tpm:     (absent)\n# host key:         \u002Fvar\u002Flib\u002Fr503d\u002Fkey\n# host key.bak:     \u002Fvar\u002Flib\u002Fr503d\u002Fkey.bak\n# tpm device:       (absent)\n# allow-pair:       (absent)\n```\n\n#### 4b. TPM-sealed pairing (recommended on TPM2 hosts, SPEC §13.12)\n\nSame flow, plus `--seal-tpm`. The generated key is sealed to **PCR7**\n(Secure Boot policy + keys) and written to `\u002Fvar\u002Flib\u002Fr503d\u002Fkey.tpm`\ninstead of the plaintext `key` file. Offline-disk attackers (`dd` of an\nunmounted partition, SSD swap into a hostile host) get ciphertext only.\n\n```bash\nsudo systemctl stop r503d\nsudo mkdir -p \u002Fetc\u002Fr503d\nsudo touch \u002Fetc\u002Fr503d\u002Fallow-pair\nsudo r503d --pair --seal-tpm              # seals new key to current PCR7\nsudo systemctl start r503d\n```\n\n```bash\nsudo r503d --status\n# port:             \u002Fdev\u002Fr503\n# firmware:         fw=1.1 fmt=2\n# firmware paired:  true\n# firmware counter: 12\n# host key.tpm:     \u002Fvar\u002Flib\u002Fr503d\u002Fkey.tpm\n# host key:         (missing)\n# host key.bak:     (missing)\n# tpm device:       \u002Fdev\u002Ftpmrm0\n# allow-pair:       (absent)\n```\n\nKernel updates, initrd updates, `fwupd` UEFI firmware updates, and grub2\nupdates do **not** change PCR7 and do not require a reseal. PCR7 only\nchanges on Secure Boot policy edits, MOK enrollments, or moving the disk\nto a different host — at which point the daemon refuses to start with\n`TPM_RC_POLICY_FAIL` and `dist\u002Freseal-tpm.sh` recovers in ~90 seconds.\nSee [Recovery: PCR7 changed](#recovery-pcr7-changed-need-to-reseal).\n\n### 5. Enroll & verify\n\n```bash\n# Enroll a finger (use KDE Settings → Users → Fingerprint Auth for a GUI):\nfprintd-enroll mat\n\n# Verify:\nfprintd-verify mat\n\n# sudo with finger:\nsudo whoami\n```\n\nBoth KDE Settings (Plasma 6) and GNOME Control Center's user-account\nfingerprint dialogs drive `r503d` exactly as they drive upstream `fprintd`.\n\n### Re-pair \u002F key rotation\n\nIf you want a fresh key (key compromised, planned hardware swap, paranoia):\n\n```bash\nsudo systemctl stop r503d\nsudo r503d --unpair                        # framed; wipes Nano EEPROM + host key\nsudo touch \u002Fetc\u002Fr503d\u002Fallow-pair\nsudo r503d --pair                          # plaintext-key rotation\n#  - or -\nsudo r503d --pair --seal-tpm               # TPM-sealed rotation\nsudo systemctl start r503d\n```\n\n**Match your original pairing path.** If you originally used `--seal-tpm`,\nrotate with `--seal-tpm` — otherwise the rotation silently downgrades you\nto a plaintext key on disk.\n\n### Recovery: PCR7 changed, need to reseal\n\nIf you used `--pair --seal-tpm` and later changed something that PCR7\nmeasures (Secure Boot turned off\u002Fon, new MOK enrolled, disk moved to\nanother box), the daemon will refuse to start with a journal message\nabout `TPM_RC_POLICY_FAIL`. Recovery is one command:\n\n```bash\nsudo bash pcside\u002Fdaemon\u002Fdist\u002Freseal-tpm.sh\n```\n\nThe script stops `r503d`, reflashes `firmware\u002Fr503fp_wipe\u002F` to wipe the\nNano EEPROM, reflashes the main firmware, creates `\u002Fetc\u002Fr503d\u002Fallow-pair`,\nruns `r503d --reseal-tpm` to generate a fresh key sealed to the *current*\nPCR7, and starts the daemon back up. Wall-clock: ~90 seconds. Enrolled\nfingers are preserved — templates live on the R503 sensor's flash, not\nthe Nano.\n\nThe script needs `arduino-cli` available. If it's installed in your\nuser's `$HOME\u002F.local\u002Fbin` it's auto-detected via `$SUDO_USER`; otherwise\nset `ARDUINO_CLI=\u002Ffull\u002Fpath\u002Fto\u002Farduino-cli` before running.\n\n### Recovery: lost `state.json` (counter desync)\n\nIf the host key is intact but `\u002Fvar\u002Flib\u002Fr503d\u002Fstate.json` is gone or rolled\nback (restored an old backup, accidental rm), the daemon's counter falls\nbehind the Nano's `last_seen` and every framed command bounces off\n`ERR replay`. `r503d --status` flags this; the fix is one command:\n\n```bash\nsudo systemctl stop r503d\nsudo r503d --resync                         # reads Nano last_seen, sets host counter to last_seen+1\nsudo systemctl start r503d\n```\n\nNo re-pair, no reflash — the key never moves. The `status` query `--resync`\nrelies on is unauthenticated, but it can only move the host counter *forward*\nto match what the Nano already committed, so it can never make an old frame\nreplayable (worst case a lying MITM forces another `ERR replay`, which it\ncould already do by garbling frames). See [`SPEC.md` §13.11](SPEC.md).\n\n### Recovery: lost the host key entirely\n\nThe authenticated `--unpair` needs the key to authorize. If all the\non-disk copies are gone (disk crash, accidental rm, both `key` + `key.bak`\ndeleted, or `key.tpm` blob lost), you need the **reflash-to-wipe** escape\nhatch — same procedure that `dist\u002Freseal-tpm.sh` automates for the\nPCR7-changed case above:\n\n```bash\nsudo systemctl stop r503d\n# \u002Fdev\u002Fr503 is root:root 0600 since install (audit H1), so the uploads need root.\nsudo arduino-cli upload --fqbn arduino:avr:nano:cpu=atmega328 --port \u002Fdev\u002Fr503 firmware\u002Fr503fp_wipe\u002F\n# Wait ~1s for the wipe to complete (LED starts blinking — that's the wipe sketch).\nsudo arduino-cli upload --fqbn arduino:avr:nano:cpu=atmega328 --port \u002Fdev\u002Fr503 firmware\u002Fr503fp\u002F\nsudo touch \u002Fetc\u002Fr503d\u002Fallow-pair\nsudo r503d --pair\nsudo systemctl start r503d\n```\n\nIf `sudo arduino-cli` reports command-not-found (arduino-cli lives in your\n`~\u002F.local\u002Fbin`, not on root's `PATH`), run it as\n`sudo env \"PATH=$PATH\" arduino-cli …` or give the absolute path.\n\nThis isn't a backdoor an attacker can use: re-pairing requires root on\nthe host (the opt-in file and the `--pair` CLI both need root), so a\nreflashed Nano can't be brought into trust without you already being\nroot.\n\n### Uninstall\n\n```bash\nsudo bash pcside\u002Fdaemon\u002Fdist\u002Funinstall.sh\n```\n\nReverts everything, unmasks `fprintd`, leaves `\u002Fvar\u002Flib\u002Fr503d\u002F` (key,\nstate, users) in place in case you want to reinstall later. Delete that\ndirectory manually if you want a true clean slate.\n\n## How it works\n\nThe Arduino runs a small ASCII-protocol firmware (`firmware\u002Fr503fp\u002F`)\nthat talks the R503's native R30x (\"Sync Word\") binary protocol on its\nUART side and exchanges line-oriented text commands with the host over\nUSB-CDC: `ping`, `info`, `enroll N`, `verify`, `delete N`, `clear`,\n`led off`. Full v1 protocol in [`SPEC.md` §5](SPEC.md).\n\nSince `fw=1.0` (Milestone E of the v2 authenticated-channel work), every\ncommand and response is wrapped in a `C \u003Ccounter> \u003Cbody> M \u003Cmac>` \u002F\n`R \u003Ccounter> \u003Cseq> \u003Cbody> M \u003Cmac>` frame, MAC'd with SipHash-2-4 over a\nTOFU-paired 128-bit key. The Nano keeps a wear-leveled monotonic counter\nin EEPROM; the daemon keeps a matching counter in `\u002Fvar\u002Flib\u002Fr503d\u002Fstate.json`.\nReplay attempts (firmware-side `incoming \u003C= last_seen`) get rejected as\n`ERR replay`; tampered frames get `ERR mac_invalid`. Full spec, threat\nmodel, and known limitations in [`SPEC.md` §13](SPEC.md).\n\nThe Rust daemon (`r503d`) speaks D-Bus on `net.reactivated.Fprint` — bit-for-bit\nthe same interface upstream `fprintd` exposes — so every `fprintd` client\nworks unmodified. A JSON sidecar at `\u002Fvar\u002Flib\u002Fr503d\u002Fusers.json` maps\n(user, finger) to slot indices in the R503's internal flash.\n\nLayout:\n\n```\nfirmware\u002Fr503fp\u002F             Arduino firmware (v2 framed ASCII protocol)\nfirmware\u002Fr503fp_wipe\u002F        Emergency one-shot EEPROM wipe (lost-key recovery)\nfirmware\u002F*                   Diagnostic \u002F development sketches (ping, loopback, ...)\npcside\u002Fdaemon\u002F               Rust daemon (the fprintd replacement)\npcside\u002Fdaemon\u002Fsrc\u002F{crypto,framing,keystore,state,pairing}.rs\n                             v2 wire protocol implementation\npcside\u002Fdaemon\u002Fsrc\u002Fauth.rs    caller-identity gating for D-Bus methods\npcside\u002Fdaemon\u002Fdist\u002F          udev rule, systemd unit, polkit + bus policy,\n                             install scripts\ndocs\u002F                        Decision logs + troubleshooting\nSPEC.md                      Full architecture + protocol spec (§13 = v2 auth)\n```\n\n## Security model — quick summary\n\nThe wire-level authentication targets a specific threat —\n**\"evil maid with five minutes and a spare Nano\"** plus a hostile local\nprocess on `\u002Fdev\u002Fr503` — not nation-states or hardware attackers with\nlabs. Single-user desktop deployment with a documented out-of-scope list.\nThe full threat model lives in [`SPEC.md` §13.1](SPEC.md); the\nimplementation-and-review evidence lives in\n[`docs\u002FREVIEW-2026-05-28.md`](docs\u002FREVIEW-2026-05-28.md). A separate\nadversarial privilege-escalation audit (2026-05-28) and its per-claim\nvalidation\u002Fremediation pass are at\n[`docs\u002FSECURITY-AUDIT-2026-05-28.html`](docs\u002FSECURITY-AUDIT-2026-05-28.html)\nand\n[`docs\u002FSECURITY-AUDIT-2026-05-28-VALIDATION.html`](docs\u002FSECURITY-AUDIT-2026-05-28-VALIDATION.html).\n\n**Defended:**\n- Hot-swap of the Nano with a hostile unit (no key → all frames fail MAC).\n- Local process injecting fake match responses on `\u002Fdev\u002Fr503`. Two layers:\n  the device node is `root:root 0600` (udev rule) and the daemon holds it\n  with `TIOCEXCL`, so a non-root process can't open it — and even if it\n  could, it has no key, so the frame fails MAC verify.\n- Replay of recorded `OK match=...` frames in a future session.\n- Bit-flip tampering of any frame field (constant-time MAC compare).\n- Counter-exhaustion brick: a peer (or a one-shot MITM during `--resync`)\n  driving the monotonic counter to `u64::MAX` and permanently wedging the\n  channel is blocked by a reserved counter ceiling enforced on both ends\n  (`fw=1.1+`; SPEC §13.4 \u002F 2026-05-28 audit DoS-2).\n- Local-user denial-of-service of the sensor: a single capture-slot gate\n  caps in-flight enroll\u002Fverify work and the delete paths are action-gated,\n  so a `Start`\u002F`Stop` (or concurrent-delete) flood can't wedge auth.\n- Cross-user fingerprint plant \u002F wipe \u002F enumeration by a local non-root\n  user (e.g. `mallory` calling `Claim \"root\"` then enrolling her own\n  finger) — caller identity is checked on every `username`-taking D-Bus\n  method, and the system bus policy denies non-`wheel` callers at the\n  broker layer.\n- **Offline-disk attacks on the host key** *when paired with `--seal-tpm`*:\n  the key on disk is TPM2-sealed to PCR7, so `dd` of an unmounted\n  partition or SSD swap into a hostile host yields ciphertext only.\n  Unwraps only on the same machine under the same Secure Boot policy.\n  See [SPEC §13.12](SPEC.md).\n\n**Not defended:**\n- Host root compromise (key is in `\u002Fvar\u002Flib\u002Fr503d\u002Fkey`, `0600 root:root`).\n  Root on a running host can unseal the TPM-sealed variant too — sealing\n  blunts *offline* attacks, not online ones.\n- Physical attack on the Nano (EEPROM readback ~30 sec with ISP; chip decap; etc.).\n- Firmware-reflash attack (the Arduino bootloader has no signing — but\n  re-pairing requires root on the host, so a reflashed Nano can't be\n  brought into trust without host compromise anyway).\n- R503-side compromise (R30x protocol has no auth at all; out of our scope).\n- **Crypto posture.** SipHash-2-4 MACs, 128-bit shared key, 64-bit MAC\n  output, domain-separated MAC inputs. Two independent implementations\n  (hand-rolled C++ on the AVR with boot-time KAT self-test; hand-rolled\n  Rust on the host, bit-for-bit cross-validated against the third-party\n  `siphasher` crate on 1024 random vectors in CI). Host MAC compare uses\n  `subtle::ConstantTimeEq`. Wire parsers property-fuzzed on every CI run\n  (~135 000 inputs). `cargo audit` clean. SipHash key wrapped in\n  `zeroize::Zeroizing\u003C...>` so it scrubs on drop (as are the per-frame\n  MAC-input buffers). A `cargo fuzz`\n  libFuzzer target ships at `pcside\u002Fdaemon\u002Ffuzz\u002F` for long-corpus runs on\n  nightly. No paid third-party human audit — that would still be\n  valuable, PRs welcome.\n\nFull threat model with rationale: [`SPEC.md` §13.1](SPEC.md).\n\n## Limitations\n\n- **Multi-user works, but only for `wheel` members.** Caller identity is\n  checked on every D-Bus method that takes a `username` (`Claim`,\n  `EnrollStart`, `VerifyStart`, `ListEnrolledFingers`,\n  `DeleteEnrolledFingers`); self-requests and `uid 0` (PAM) succeed\n  silently, cross-user from a non-root caller is denied with\n  `net.reactivated.Fprint.Error.PermissionDenied`. The system bus policy\n  further restricts which accounts can even start a conversation: only\n  `root` and `wheel` members reach the daemon, everyone else gets\n  `org.freedesktop.DBus.Error.AccessDenied` at the broker layer.\n  Need cross-user enroll? Become root: `sudo fprintd-enroll target-user`.\n  Need to loosen the cross-user gate for a kiosk \u002F multi-user lab? Drop\n  a JS rule into `\u002Fetc\u002Fpolkit-1\u002Frules.d\u002F` targeting\n  [`net.reactivated.fprint.device.setusername`](https:\u002F\u002Fgitlab.freedesktop.org\u002Flibfprint\u002Ffprintd\u002F-\u002Fblob\u002Fmaster\u002Fsrc\u002Fnet.reactivated.fprint.device.policy.in)\n  — the action name mirrors upstream fprintd verbatim.\n- **One reader.** The daemon exposes a single Device object on D-Bus.\n  Multi-reader setups need an extension to the Manager.\n- **No `PropertiesChanged` emit** for the `finger-present` \u002F `finger-needed`\n  hint properties. Every common fprintd client (PAM, KDE Settings, GNOME)\n  drives off `EnrollStatus` \u002F `VerifyStatus` signals (which are emitted),\n  not those polled hints — but a strict client that does\n  `Get + PropertiesChanged` will see stale values.\n- **Single Nano = single point of failure.** If the Nano dies, fingerprint\n  login is gone until you reflash a spare and re-pair. Keep a password\n  auth method enabled as backup.\n- **State.json loss is recoverable in one command.** If `state.json` is lost\n  while the firmware still has a high `last_seen`, the daemon hits `ERR replay`\n  on first send. Run `sudo r503d --resync` to read the Nano's counter and\n  realign the host — no re-pair needed. See [`SPEC.md` §13.11](SPEC.md).\n\n## Troubleshooting\n\n```bash\n# Daemon logs:\nsudo journalctl -u r503d.service -f\n\n# Confirm the sensor enumerates correctly:\nls -l \u002Fdev\u002Fr503\nbusctl --system call net.reactivated.Fprint \u002Fnet\u002Freactivated\u002FFprint\u002FDevice\u002F0 \\\n    net.reactivated.Fprint.Device ListEnrolledFingers s \"\"\n\n# Confirm fprintd is masked and r503d owns the bus name:\nsystemctl is-enabled fprintd  # should print \"masked\"\nbusctl --system list | grep -i fprint\n```\n\nIf the daemon won't start or the sensor never responds, the most common\nfix is the wiring — see [`SPEC.md` §3](SPEC.md), particularly the\n**\"no voltage divider\"** note in §3.1. There's a more detailed runbook\nin [`docs\u002FTROUBLESHOOTING.md`](docs\u002FTROUBLESHOOTING.md).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Credits\n\n- [Adafruit_Fingerprint](https:\u002F\u002Fgithub.com\u002Fadafruit\u002FAdafruit-Fingerprint-Sensor-Library)\n  — Arduino-side R30x protocol implementation.\n- [zbus](https:\u002F\u002Fgithub.com\u002Fdbus2\u002Fzbus),\n  [serialport-rs](https:\u002F\u002Fgithub.com\u002Fserialport\u002Fserialport-rs),\n  [tokio](https:\u002F\u002Ftokio.rs\u002F) — the Rust D-Bus \u002F serial \u002F async stack.\n- The `fprintd` project — for designing a clean D-Bus interface that\n  this daemon could implement against without ever reading\n  `libfprint`'s source.\n","该项目实现了使用Grow R503指纹传感器和Arduino通过Rust编写的fprintd替换守护进程在Linux桌面系统上进行指纹登录。其核心功能包括支持PAM、KDE设置、GNOME设置、fprintd-verify、sudo以及屏幕解锁等，同时提供了一个成本低于15美元的USB指纹读取器解决方案。技术特点方面，项目利用了公开协议的R503传感器，并且通过D-Bus与libfprint兼容层通信，确保了从硬件到底层软件的完全开源可审计性。适用于需要低成本、高安全性指纹认证解决方案的个人或组织，特别是对现有昂贵或不稳定指纹读取器不满意的用户。","2026-06-11 04:06:48","CREATED_QUERY"]