-
Eight PCB revisions for a tiny wireless fingerprint key
an hour ago • 0 commentsNotes from building immurok: a wrong MCU choice, a sensor swap, a tamper switch that only worked while powered, and a long fight to get standby current down to 40 uA.
![Eight immurok PCB revisions arranged as a hardware timeline]()
immurok is a small wireless fingerprint key: touch the sensor, approve the action, get back to work. It took eight PCB revisions to get there. These are the notes — the boards we scrapped, the parts that looked right until they met a battery, and the mechanical choices that worked in CAD and failed in plastic.
Four things drove most of the rework: keep the radio connected without draining the battery, make the sensor usable without it glowing like a toy, build an enclosure that survives assembly, and make the tamper switch actually do something.
Picking the MCU: ESP32 was too power-hungry
The first prototypes used ESP32-S3 and ESP32-C3. Familiar, well documented, easy to bring up.
Then we looked at the power budget. immurok sits on a desk for weeks, wakes on touch, and does nothing the rest of the time. The number that matters is idle current, not peak current during BLE traffic. The ESP32 direction projected over 200 uA static before the design was even finished. For a battery-powered key, that drains the battery while the user is doing nothing.
So we moved to CH592F. Tighter RAM, less convenient, but it gave us the sleep budget a desk key needs. The shipping design idles around 40 uA, after a lot of measurement and firmware tuning — BLE intervals, sensor rails, pull resistors, LEDs, wake paths, and every GPIO default.
Takeaway: for a device that’s supposed to disappear into the desk, the MCU choice is the product, not an implementation detail.
Choosing a fingerprint sensor
Our first working path used ZW3021-class modules. They worked: enroll a finger, match locally, report over UART. The problem was the built-in lighting — blue rings, green glows, decorative LEDs that make sense on an access-control panel and look wrong on a desk. The light also costs power and complicates the enclosure.
For a while we had a promising module shaped like a ping-pong paddle. Mechanically it fit a small handheld object well, and it skipped the built-in-light problem. Then the vendor told us they couldn’t support the volume we’d need for a real run. “Can I buy five?” and “Can I buy five thousand, repeatedly, without redesigning the product?” are different questions.
We settled on R559S: no light, on-module matching, fits the enclosure. It has one catch — after every power cycle, firmware must explicitly enable the touch interrupt switch. Most modules we tested do this automatically. On R559S, if firmware doesn’t send the setup command after power is restored, touch no longer wakes the system.
This matters in the failure path. If firmware crashes before re-enabling the touch interrupt, the user can touch the sensor forever and nothing happens. So we treat the command as part of the power system, not a feature toggle: bring up the sensor early, and re-send the touch-interrupt command after every power transition.
Why the board shape kept changing
![A pile of failed 3D-printed enclosure prototypes for immurok]()
The PCB revisions were half the story. The other half was a pile of failed enclosures: sensor holes a millimeter off, screw posts colliding with components, shapes that looked fine in CAD and felt wrong in the hand.
Each failed case pushed back on the board. The sensor opening set how far the module could sit from the edge. The screw posts set where copper couldn’t go. The USB connector needed room for a cable and fingers. The antenna needed air, not a wall of plastic and metal. Button and LED positions changed once the enclosure became something you could actually press.
- VER0 — proving the parts could talk: MCU, sensor, buttons, debug UART, BLE. A lab board, not a product.
- VER1–2 — real mechanical constraints: USB connector placement, sensor flex routing, room for a switch, antenna clearance, how the device sits in the hand.
- VER3–4 — consolidation. Components moved off the edges, power routing got cleaner, LED and button choices settled. Started to look like something that could survive assembly.
- VER5 — added the tamper switch. The firmware already had a security model for radio attackers; the hardware needed an answer for someone holding the device.
The first version of that answer was incomplete.
The tamper switch only worked while powered
VER5 could detect a case-open event while the device was powered and running. That sounds like the whole feature until you ask: what if the attacker turns the device off first?
With no power, the MCU never runs the tamper code. No flash erase, no red LED. A switch on a GPIO is useless if the chip isn’t alive to read it.
![The VER6 anti-tamper power bypass circuit]()
The VER6 fix: opening the case has to power the MCU long enough to enter the wipe path, not just signal it. The tamper switch now drives a small power-bypass path through a MOSFET arrangement — opening the enclosure forces the supply on even if the device was off. The MCU sees
ANTI_OPENand boots straight into the tamper handler.VER5 asked “can firmware notice the case opened?” VER6 asks “can opening the case create the electrical conditions firmware needs to wipe secrets?” Security features fail in the gap between those two questions.
Chasing standby current
Once the architecture settled, the rest was current measurement. We bought a microamp meter for this, because the LED being off and the logs being quiet tells you nothing — a pull-up, a sensor rail, or a BLE parameter can quietly eat the battery.
Status LED. Early boards used a WS2812-style single-wire RGB LED: cheap, one data pin, any color. Measured, the internal controller drew ~0.5–1 mA even when visually off — more than the rest of the sleeping system. We dropped it for plain LED channels driven straight from GPIO. Costs a few pins, removes an always-on controller.
Fingerprint UART. Leaving the port configured while the module slept kept current paths alive through the IO pins. Turning the sensor off wasn’t enough; the host-side UART pins also had to be parked so they didn’t back-power or bias the module. On a low-power board, serial ports are part of the power design.
Prompt behavior. My first
sudoprompt design flashed the LED, powered the fingerprint module, and waited for a touch. Wasteful — the user might hesitate or ignore the prompt while the sensor DSP is already awake. The current design keeps the heavy DSP unpowered during the prompt window; only the touch-detect path stays alive. We power the module only when the user actually touches it. That adds ~50 ms of wake latency but roughly halves the energy spent waiting for a finger.The full checklist, applied every revision:
- Turn the fingerprint module fully off when not needed.
- Keep the touch-detect path alive without powering the whole sensor stack.
- Park the fingerprint UART pins in sleep so they don’t leak current.
- Replace smart RGB LEDs with GPIO-driven LEDs.
- Increase BLE slave latency where possible, within Apple’s connection rules.
- Re-check every revision — a pin that was safe in VER3 may be expensive in VER5.
The target was the whole product — battery, regulator, sensor, radio, GPIOs, board leakage — not a heroic number on a bare MCU. Around 40 uA standby is where it stopped asking to be charged.
BLE connection parameters
BLE looked easy in the prototype: advertise, connect, expose a custom GATT service, send signed match events.
Shipping a battery-powered BLE device is fussier. Set interval, latency, and supervision timeout wrong and either the device stays too chatty and burns power, or the host decides the peripheral is misbehaving and drops it.
immurok presents as a BLE HID keyboard but keeps auth traffic on a private GATT service. HID gives the OS a reason to hold the connection; the custom GATT service handles pairing, challenge-response, and signed touch events without putting secrets in the keyboard channel.
Increasing slave latency is one of the most effective ways to cut current — the peripheral skips connection events and the radio sleeps longer. But Apple’s rule has to hold: supervision timeout > max interval × (latency + 1) × safety factor. Too much latency and the connection gets fragile; too little and the battery pays for needless radio wakeups. It never shows up in a product render, but it’s the difference between “there when I touch it” and “why is my key asleep again?”
Takeaways
- ESP32-S3/C3 — familiar chips can be wrong for the battery.
- ZW3021-class modules — a sensor can work and still feel wrong.
- The ping-pong-paddle sensor — supply chain is a design constraint.
- R559S — one power-cycle quirk can reshape firmware boot order.
- VER5 — a tamper switch isn’t a tamper response unless the MCU is powered to act.
- The microamp meter — don’t trust your feeling on power.
Eight boards later: small because the boards got smaller, quiet because the power budget became real, dark because the sensor choice mattered, and able to wipe itself because case-open is now an electrical event, not a firmware wish.
-
Why desktop fingerprint authentication is still awkward on Mac and Linux
an hour ago • 0 commentsTouch ID is excellent when it is built into the machine or keyboard. The awkward part starts everywhere else: Mac mini, Mac Studio, closed-lid MacBooks, external keyboards, Linux desktops, and terminal-heavy workflows.
immurok is my attempt to build a small wireless fingerprint key for those setups.
It is not meant to be “Apple Touch ID, but external.” Touch ID is Apple’s own secure platform, deeply fused into the Secure Enclave and the OS. immurok is something more modest and more honest about its scope: a local-first desktop authentication device with clear limits.
Where built-in fingerprint auth runs out
If you use a laptop with the sensor under your thumb, none of this is your problem. But a lot of desktop computing doesn’t look like that:
- Mac mini and Mac Studio ship with no fingerprint reader at all. Apple’s only first-party answer is a $199 Magic Keyboard with Touch ID.
- A MacBook in clamshell mode, driving an external display, can’t reach its own sensor — the lid is closed.
- External keyboards — and especially mechanical keyboards, which a lot of developers prefer — have no biometric story on macOS unless you buy Apple’s specific keyboard.
- Linux desktops can do fingerprint auth through
fprintd, but good, well-supported hardware is genuinely scarce, and setup is fiddly. - Terminal-heavy workflows are the worst case: you
sudoa dozen times an hour, sign Git commits, SSH into boxes — and every one of those is a password prompt.
The common thread is that biometric auth is welded to specific hardware. The moment your setup steps outside that hardware, you’re back to typing passwords.
What immurok actually is
A small wireless key with a capacitive fingerprint sensor that pairs to your computer over Bluetooth LE. Concretely, its scope is:
- Linux
sudoand system authentication through PAM. This is the cleanest integration — see below for why it matters. - A Linux CLI/TUI app for pairing, status, and local control — no GUI required, built for people who live in the terminal.
- macOS
sudo/ system-prompt support through PAM integration, for the auth prompts that do consult PAM. - Mac desk-setup workflows — Mac mini, Studio, clamshell, external keyboards — where Touch ID is missing or impractical.
- Fingerprint matching on the device. Your fingerprint template is enrolled and matched on the key itself; it never travels over Bluetooth, never lands on your disk, and there is no cloud to send it to.
- Open-source firmware and software. The macOS app, the PAM module, the Linux app, and the hardware design are open; the firmware will be opened up before the end of the year.
It is deliberately a single-purpose device. No screen, no account, no app store — a key that proves a fingerprint touch happened, and lets your OS act on it.
![immurok features overview]()
The distinction that matters: PAM, not a typed password
Here is the part I most want to be clear about, because it’s the easiest thing to get wrong when you build something like this.
For
sudoand system authentication, immurok is not just typing a stored password for you. A lot of “fingerprint unlock” gadgets are really just a biometric trigger wired to a password autotyper: they keep your password somewhere, and when you touch the sensor they replay it into the prompt. That works, but it means your password is sitting in storage, and anything that can see the keystrokes sees your password.On Linux, immurok integrates through PAM — the same authentication framework
sudo, login, andpolkitalready use. When you runsudo, the PAM stack asks immurok’s module, the module checks with your paired device over an authenticated channel, you touch the sensor, and PAM gets back a real success/failure result. There’s no stored password being replayed — the authentication decision flows through the OS’s own auth pipeline, the way a fingerprint should.On macOS the picture is split, and it’s worth being precise:
- For prompts that consult PAM —
sudoin a terminal, some system dialogs — immurok integrates through PAM the same way. - For the lock screen, macOS doesn’t let third-party PAM modules dismiss the login window or screensaver. So for that one flow, immurok falls back to keyboard simulation: it keeps your login password in the macOS Keychain and types it only after verifying a cryptographically signed match from your device. It’s the autotyper approach — but scoped to the single case the OS forces it into, gated behind a verified touch, and nowhere else.
That asymmetry isn’t a limitation we’re hiding; it’s a property of the platforms. Linux exposes a clean biometric path through PAM end to end. macOS exposes it for some flows and walls off the lock screen. immurok uses the real path wherever the OS offers one.
![immurok key with its leather case]()
Honest about the limits
immurok is local-first and deliberately narrow. It does not replace the Secure Enclave, it does not integrate with App Store purchases or
LAContextthe way Apple’s own biometrics do, and it does not pretend to be Touch ID. It is a wireless fingerprint key for the desktop setups Apple and the Linux ecosystem left without a good option — with the matching done on the device, the secrets kept off your disk, and the security model written up in full so you don’t have to take any of this on faith.If your desk is one of the awkward setups above, that’s exactly who I built it for. The macOS app, PAM module, and Linux app are on GitHub; if you want to try the device before launch, join the waitlist.
-
The immurok security model
2 hours ago • 0 commentsA fingerprint key is only as good as the trust it can prove. Here's how immurok keeps your biometrics on the device, authenticates every touch, and refuses to phone home — ECDH pairing, HMAC-signed events, and signed firmware, end to end.
immurok replaces your password with a fingerprint. That only works if the fingerprint touch is something your Mac can trust — not just a button press that any nearby radio could fake. This post is the full picture of how that trust is built: what the device proves, what the host verifies, where keys live, and the things we deliberately refuse to do.
The threat model
We designed against a concrete set of attackers:
- A passive radio eavesdropper sniffing Bluetooth LE traffic in the room.
- An active attacker who can connect to the device, spoof advertisements, or replay captured packets.
- A malicious or compromised peripheral pretending to be your immurok key.
- A thief who walks off with the device itself.
- A supply-chain attacker trying to push tampered firmware onto the device over the air.
And a set of principles we hold regardless of attacker:
- Your biometric never leaves the sensor. Not over BLE, not to disk, not to a cloud.
- Every authentication event is cryptographically authenticated — a touch the host accepts must have come from your paired device.
- No cloud, no account, no telemetry. The entire system works offline. There is no server to breach because there is no server.
- The host is the policy engine; the device is the proof. macOS decides what a touch unlocks; the device only proves that a real, authenticated touch happened.
Biometrics stay on the device
The fingerprint sensor (an R559S capacitive module) does matching on-chip. Enrollment templates are stored in the sensor’s own flash and are never read out over the wire. The CH592F microcontroller that runs immurok’s firmware never sees your fingerprint image or template — it only ever receives a match result: page N matched, or no match.
That means the BLE link, the companion app, your Mac’s disk, and certainly our infrastructure (which doesn’t exist for this) never hold anything that could reconstruct a fingerprint. The worst case for a stolen device is a stolen device — not a stolen identity.
Matching strictness matters here too. The sensor exposes a configurable score threshold, and the firmware sets it explicitly rather than trusting a vendor default. We learned this the hard way: an early build left the score level unset, fell back to the module’s lax default, and a second validation step was missing in firmware — which let unenrolled fingers occasionally pass. The fix (firmware 1.3.14) pins the score level and re-validates the match index in firmware before signing anything. Biometric thresholds are now part of the build, not an accident of defaults.
Pairing: ECDH, once
When you pair a device, the app and the device run an ECDH key agreement over NIST P-256:
- The app generates an ephemeral P-256 key pair and sends its compressed public key (33 bytes) to the device.
- The device does the same and sends its public key back.
- Both sides compute the shared secret, then run it through HKDF-SHA256 (with a fixed salt and info string) to derive a 32-byte symmetric shared key.
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: devicePubKey) let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey( using: SHA256.self, salt: "immurok-pairing-salt", sharedInfo: "immurok-shared-key", outputByteCount: 32 )The shared key never crosses the wire — each side derives it independently from the key agreement. The ephemeral private key is discarded the moment pairing completes. From then on, the app and the device share one secret that proves they paired with each other.
This is a single-device design on purpose: one key, one device, no roaming credentials to manage or leak.
Every touch is signed
A fingerprint match isn’t a bare “OK” byte that anyone could forge. When the device confirms a match, it emits a signed match notification over the custom GATT characteristic:
[0x21][page_id : 2 bytes][HMAC-SHA256(shared_key, 0x21‖page_id) : 8 bytes]
The app recomputes the HMAC with its copy of the shared key and rejects the notification unless the tag matches exactly. An attacker who can write arbitrary GATT values can’t produce a valid tag without the shared key — and the shared key was never transmitted. A forged or replayed-from-a-different-device “match” simply fails verification and is dropped.
On top of per-event signing, the app runs a challenge–response check when it (re)connects to a device, to make sure it’s still talking to the real key and not a clone or impostor advertising the same name:
- The app generates a fresh 8-byte random nonce.
- The device replies with
HMAC-SHA256(shared_key, nonce)[0:8]. - Only a device holding the shared key can answer correctly.
A device that passes is cached as verified (by its BLE identifier) so the check is cheap on subsequent connects, and the cache is cleared whenever you unpair.
Why a HID keyboard and a custom protocol
immurok presents itself to the system as a BLE HID keyboard. That’s not the security channel — it’s the connection anchor. Operating systems aggressively maintain connections to HID keyboards, which is what lets a tiny battery-powered key stay reliably reachable without a background daemon hammering the radio. This is the same on macOS and Linux — the host OS isn’t special-cased; both treat the device as a keyboard and keep it alive.
All the actual authentication traffic — pairing, signed match events, challenge–response — runs over a separate custom GATT service. We use purely random 128-bit UUIDs for that service, deliberately steering clear of the Bluetooth SIG’s reserved ranges and the Bluetooth Base UUID space, so our private protocol never collides with or impersonates a standardized service.
Splitting the roles this way means the convenient, always-connected HID surface carries no secrets, and the secret-bearing surface is a private protocol that a generic BLE client has no reason — and no schema — to poke at.
Where keys and secrets live
It’s worth being precise about which secrets live where, because it’s easy to assume “the app stores my secrets.” It doesn’t.
Your high-value secrets live on the device. SSH keys, OTP seeds, and API keys behind
imk://URIs are stored in the device’s own protected storage. They’re released only momentarily — into a child process’s environment atexec()time, under an active fingerprint cooldown — and are never written to the host’s disk, never held by the parent shell, and never land in an agent’s transcript. The device is the vault.The host only holds pairing material. On macOS, the shared key and the verified-device record live in the Keychain as generic password items, with
kSecAttrAccessibleAfterFirstUnlock— readable only after the user has unlocked the Mac once since boot, and never synced off the machine. That’s the pairing secret that proves this Mac paired with your device; it is not your SSH key or your API keys.The one host-stored password is the screen-unlock password — and only on macOS. This is where the two platforms diverge. On Linux, the lock screen goes through PAM like everything else, so screen unlock rides the same PAM path as
sudo— no stored password at all, just a verified touch satisfying the auth prompt. On macOS, the lock screen can’t be dismissed by PAM (the macOS login window and screensaver don’t consult third-party PAM modules), so for that one flow we fall back to keyboard simulation: the app keeps your login password in the Keychain and types it via simulated keystrokes after — and only after — it has verified a signed match from your device. Even in that fallback the password is at rest in the OS keystore, gated behind a cryptographically verified touch, and never crosses the wire. This applies to screen unlock alone — nothing else stashes a password on the host.The agent gate, briefly
The same trust primitives back immurok’s gate for AI coding agents: when an agent wraps a command with
imk run --agent --, a single verified fingerprint touch brokerssudo, SSH signing, and secret reads for the lifetime of that one subprocess, and a rejection cleanly kills it. The fingerprint is the human-in-the-loop signature. We wrote that flow up in detail in A fingerprint gate for your AI coding agent — the relevant point here is that it’s built on the very same signed-touch foundation, not a separate trust path.Firmware can’t be tampered with over the air
A wireless device that accepts wireless firmware updates is a juicy target. immurok’s OTA pipeline is built so that only firmware we built will ever boot:
- Each update image is encrypted with AES-128 and carries an HMAC-SHA256 signature plus a SHA-256 integrity hash.
- Those keys are generated per developer-machine (
generate_ota_keys.py) and never committed to git — they’re baked into the firmware build and the packaging tool. - Before committing an update, the device verifies the integrity hash and the signature. An image that fails either is rejected as unofficial firmware and discarded — it never gets promoted to the boot slot.
The update itself uses a dual-image (A/B) layout with a tiny separate bootloader (IAP): the new image is written to the inactive slot, verified, and only then swapped in. A failed or tampered update leaves the running firmware untouched.
When the attacker has the device in hand
Everything above assumes the attacker is on the radio. But a fingerprint key is a small object that lives on your desk, and the more interesting attacks happen once someone is holding it. We treat physical possession as its own threat, with two layers.
A stolen device can’t be re-paired. The moment a device has a saved shared key, the firmware flips its BLE bonding policy to refuse new bond requests. A thief can’t simply walk the device over to their own machine and pair it — the pairing slot is already claimed, and there’s no “forget and re-pair” path that doesn’t go through a wipe first.
Opening the case wipes the device. This is the one that matters if the attacker decides to get physical — desolder the flash, attach a debugger, probe the bus to extract keys. A spring contact inside the enclosure is wired to a tamper line on the microcontroller. Cracking the case open trips a hardware interrupt, and the firmware immediately erases everything that has value:
- the pairing shared key and the on-device keystore (your SSH keys, OTP seeds, and API secrets),
- all BLE bonds,
- every enrolled fingerprint template.
Then it lights a solid red LED, shuts off the radio, and halts until the device is power-cycled. By the time anyone has the case open far enough to touch the flash, there’s nothing left on it.
The part that took the most care is making the wipe un-interruptible. The obvious attack is to pop the case and immediately cut power, hoping to freeze the chip mid-erase with secrets still intact. So the wipe is a transaction: before erasing a single byte, the firmware writes a
case_openedmarker to a reserved flash page. If power is lost halfway through, the next boot sees that marker still set and resumes the wipe before doing anything else. The marker is cleared only after the erase fully completes. You cannot win the race by yanking the battery.Getting the detection itself reliable was its own small saga. The tamper line is read as a high-impedance signal (an open case pulls it to ~3 V through a high-value divider), so the pin has to be a true floating input — an early build that enabled the chip’s internal pull-down crushed the open-case signal down to 0.23 V and missed it entirely. Boot also deliberately checks only the stored marker, not the live line, so a case that’s open on the assembly bench during flashing still boots normally; it’s the act of opening a sealed unit that trips the wipe, not the static state.
This hardware tamper response ships on the latest revision (validated June 2026).
What we deliberately don’t do
Security is as much about absence as presence:
- No cloud and no account. There’s no backend that could be breached, no credential database, no password-reset flow to socially engineer.
- No telemetry. The device and app don’t report usage anywhere.
- We don’t touch
authorizationdb. macOS’s authorization database governs deeply sensitive system rights; immurok integrates through PAM and the screen-unlock path and leaves that subsystem alone entirely. - Open and auditable. The hardware design, the macOS app, the PAM module, and the Linux app are all open source. The firmware will be opened up before the end of the year — we’re holding it back only long enough to protect against cheap clones at launch, not to hide anything. The crypto described here isn’t a marketing claim you have to take on faith — it’s
CryptoKitcalls you can read inImmurokSecurity.swift.
The short version
Your fingerprint stays on the sensor. Pairing derives a shared key over ECDH that never crosses the wire. Every touch your Mac accepts carries an HMAC tag only your device could produce, and the device proves itself with a fresh challenge on every connect. Keys rest in the Keychain; firmware updates are encrypted and signed; nothing phones home. That’s the whole model — small enough to fit on a chip with 26 KB of RAM, and strong enough that the worst outcome of losing the device is buying another one.
We’ll keep publishing the details as the protocol evolves. If you want to read the code first, the hardware design, macOS app, PAM module, and Linux app are on GitHub— with the firmware to follow before year’s end.
-
Why we're building immurok
2 hours ago • 0 commentsMillions of developers use desktop Macs and Linux machines without any biometric auth. We're building a tiny wireless fingerprint key to fix that.
If you use a Mac mini, Mac Studio, or a MacBook in clamshell mode with an external keyboard, you know the pain: no Touch ID. Apple’s only solution is a $199 Magic Keyboard — and if you prefer a mechanical keyboard, you’re completely out of luck.
Linux users have it even worse. fprintd exists, but good hardware options are scarce.
The problem
Every time you:
- Unlock your screen
- Run sudo / ssh
- Approve a system dialog
- Sign a Git commit
…you type a password. Dozens of times a day.
Our solution
immurok is a tiny wireless device with a capacitive fingerprint sensor. It connects via Bluetooth LE and replaces passwords with a single touch:
- Screen unlock — touch the sensor, screen unlocks
- sudo & PAM — custom PAM module intercepts auth prompts
- SSH agent — sign commits and SSH into servers with your fingerprint
- Open source — the macOS app and PAM module are fully auditable
Security first
Your fingerprint template never leaves the device. Authentication uses ECDH P-256 key exchange and HMAC-SHA256 signed messages. There’s no cloud, no account, no telemetry — everything works offline over Bluetooth.
We’ll be sharing more technical deep-dives on the BLE protocol, PAM integration, and firmware architecture in upcoming posts. Stay tuned.
superdog



