I have a Mac mini.
Apple's official path to fingerprint authentication on a Mac mini is buying their $199 Magic Keyboard with Touch ID. The new MacBook Neo charges $100 extra for the variant with the sensor. Mac Studio and Mac Pro never ship with one at all.
The fingerprint sensor I actually use 50 times a day already lives in the phone in my pocket.
So I spent two weekends building TouchBridge — an open-source macOS daemon and PAM module that lets any phone, watch, or browser act as Touch ID for any Mac.
This post is the technical write-up. Repo: github.com/HMAKT99/UnTouchID.
What it actually does
After a one-line install, this just works:
$ sudo rm -rf ~/.cache
TouchBridge: check your phone…
[phone vibrates, you press the sensor, the prompt clears]
$ _
sudo got authenticated by the Secure Enclave on my iPhone over local Bluetooth. No password. No cloud round-trip. No Apple ID. The same flow works for the screensaver unlock and (soon) the App Store / System Settings prompts that go through Authorization Services.
If my phone is dead, out of range, or refuses biometric, the PAM stack falls through to the normal password prompt. You're never locked out.
The architecture
flowchart LR
A[sudo / loginwindow] --> B[/etc/pam.d]
B --> C[pam_touchbridge.so]
C -- Unix socket --> D[touchbridged]
D <-- BLE: ECDH + AES-256-GCM --> E[TouchBridge app]
E --> F[Secure Enclave / StrongBox]
Three pieces:
-
pam_touchbridge.so— a tiny PAM module written in C. Drop a single line into/etc/pam.d/sudoandsudonow consults it before falling through to the password module. -
touchbridged— a Swift launchd daemon that owns the BLE connection, the paired-device list, and the audit log. PAM talks to it over a Unix socket. - Companion apps — Swift on iOS / Apple Watch, Kotlin on Android / Wear OS, and a JavaScript fallback for any browser that supports WebAuthn. Each one holds a non-extractable ECDSA P-256 keypair in secure hardware.
The handshake, end to end
When you type sudo:
- PAM invokes
pam_sm_authenticateinpam_touchbridge.so. The module connects to the daemon's Unix socket and asks: anyone paired and online? - The daemon picks the highest-priority paired device (your iPhone first, watch as fallback) and generates a fresh 32-byte cryptographic nonce.
- It encrypts the nonce under a session key derived via ECDH-on-pair (rotated periodically) with AES-256-GCM, then sends it to the device over BLE.
- The companion app shows a system biometric prompt — Face ID, Touch ID, fingerprint sensor, whatever the device has.
- On approval, the device signs the nonce with its non-extractable ECDSA P-256 key in the Secure Enclave or StrongBox / TEE Keystore. The private key never leaves secure hardware.
- The signed nonce comes back over the same encrypted BLE channel. The daemon verifies the signature against the public key it cached during pairing.
- If verification passes, the daemon writes a one-line entry to
~/.touchbridge/audit.logand returnsPAM_SUCCESS. PAM letssudoproceed.
End-to-end latency on a recent iPhone over BLE: 80–250 ms. Faster than typing my password.
What pam_touchbridge.so actually looks like
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
const char *user = NULL;
if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || !user)
return PAM_AUTH_ERR;
int sock = tb_connect_daemon();
if (sock < 0)
return PAM_AUTHINFO_UNAVAIL; // PAM falls through
if (tb_send_request(sock, user, getpid(), tb_surface(argv)) < 0) {
close(sock);
return PAM_AUTHINFO_UNAVAIL;
}
pam_info(pamh, "TouchBridge: check your phone…");
tb_response_t resp;
int rc = tb_recv_response(sock, &resp, /*timeout_ms=*/30000);
close(sock);
if (rc < 0) return PAM_AUTHINFO_UNAVAIL;
if (resp.status == TB_OK) return PAM_SUCCESS;
if (resp.status == TB_DENY) return PAM_AUTH_ERR;
return PAM_AUTHINFO_UNAVAIL;
}
Returning PAM_AUTHINFO_UNAVAIL instead of PAM_AUTH_ERR when the daemon isn't reachable tells PAM to keep walking the stack — so if the daemon's down, the password prompt still works. This is what makes it safe to install.
The Swift signing path
func sign(nonce: Data) async throws -> Data {
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
nil
)!
let key = try SecureEnclave.P256.Signing.PrivateKey(
accessControl: access
)
let sig = try key.signature(for: nonce)
return sig.derRepresentation
}
biometryCurrentSet hardware-enforces biometric at signing time. If the enrolled fingerprint or face changes, the key invalidates — so a stolen phone can't be re-enrolled to keep using TouchBridge.
What it can't do — being honest
FileVault unlock happens before any user-space daemon is alive. No PAM hook there.
The macOS login screen — same reason: loginwindow boots before launchd user agents.
Apple Pay runs on a dedicated hardware path that only Apple's signed components can touch.
Keychain biometric items — the crypto wall is enforced in the kernel and binds to the local Secure Enclave.
1Password biometric unlock — SIP sandboxes prevent any third-party module from injecting into 1Password's prompt path. (Bitwarden works because its CLI uses the OS PAM stack.)
If you see a tool that claims any of those, be skeptical and read its source.
Try it in 60 seconds (no phone needed)
brew tap HMAKT99/touchbridge
brew install --cask touchbridge
# in one terminal
touchbridged serve --simulator
# in another
sudo echo "this just prompted touchbridge instead of my password"
The cask is signed with a Developer ID and notarized; the formula pins SHA256.
What I'd love help with
Linux PAM port. The protocol is portable; pam_touchbridge.so builds on Linux, but the daemon needs a BlueZ adapter. Friendly first contribution if you've poked at BlueZ.
MDM / fleet pairing. Provisioning the paired-device key over MDM for shared lab Macs. Open RFC.
Hardware test reports. Comment with Mac model + paired device + result so the README's compatibility table grows.
Why I'm posting here
DEV.to has the largest concentration of people who actually understand the difference between PAM, PolicyKit, and Authorization Services. The threat model is in SECURITY.md and I'd love to have it picked apart.
If TouchBridge saves you the price of a Magic Keyboard, a star on the repo is the cheapest way to say thanks.
Repo: github.com/HMAKT99/UnTouchID
License: MIT
Built because I refused to pay $199 for a sensor my phone already has.
Top comments (0)