Skip to content

Biometric unlock (Touch ID / Face ID) via WebAuthn PRF - #63

Merged
xumaple merged 1 commit into
mainfrom
xumaple/claude/biometric-unlock
Jul 16, 2026
Merged

Biometric unlock (Touch ID / Face ID) via WebAuthn PRF#63
xumaple merged 1 commit into
mainfrom
xumaple/claude/biometric-unlock

Conversation

@xumaple

@xumaple xumaple commented Jul 16, 2026

Copy link
Copy Markdown
Owner

What

Touch ID / Face ID unlock for MapoPass, implemented entirely client-side with the WebAuthn PRF extension — no server changes, no new endpoints.

How it works: enrollment creates a platform passkey whose PRF output (Secure-Enclave-backed) is run through HKDF into an AES-GCM key that encrypts the signed-in credentials (username, en_user, aesKey, en_pw) into localStorage. Unlocking re-evaluates the PRF — that's the OS biometric prompt — and decrypts. The passkey is never registered with the backend; the master password is never stored; the localStorage blob is useless without a biometric assertion. The plaintext username lives only inside the ciphertext (usernames are client-side-hashed secrets, so they must not leak into passkey metadata or storage).

UX

  • Sign-in: "Use Touch ID" (Face ID on iPhone) button when enrolled. Auto-fires once per page load — never after logout or a cancelled prompt (cancel means "I want to type"). Stale enrollments (master password changed elsewhere, passkey deleted) self-clear with an explanatory message; network failures don't destroy the enrollment.
  • Settings: biometric control sits with the username, separated from the password form by a divider. Un-enrolled it's a "Use Touch ID" button; enrolled it flips to a disabled green-check "Touch ID enabled" with an X (tooltip "Turn off Touch ID"). Back/Save row replaced by a corner X to close and a full-width "Change Password" button owned by the password form.
  • Post-login offer: top-center snackbar (never collides with the bottom copy snackbars) offering enrollment; "Not now" opts out per device, and the offer retires itself if you enroll from settings while it's up.
  • Changing the master password clears the enrollment (cached credentials would be stale) and says so.

Security notes

  • Biometrics gate a cached secret; they don't replace the master password. First login always requires the password.
  • The biometric check is the OS's: any fingerprint/face enrolled on the device can unlock the enrolled account. Trust boundary = the OS user account (same model as 1Password/Bitwarden).
  • Single enrollment slot per browser profile; enrolling overwrites.
  • Requires PRF support: Chrome, or Safari on macOS 15 / iOS 18+. Unsupported browsers get a clean error and everything else keeps working.

Testing

  • 6 unit tests simulate a PRF authenticator (HMAC over a device secret): round-trip, tamper rejection, wrong-authenticator rejection, no plaintext secrets in storage.
  • 8-scenario e2e journey via Chromium's CDP virtual authenticator (hasPrf): sign-up offer, settings enroll/disable, manual unlock, no-auto-fire-after-logout, reload auto-unlock with zero clicks, password change clearing enrollment, offer Enable path, offer retiring after settings enroll.
  • Full suites green: 41/41 unit, 24/24 e2e, clean CI=true build.

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website Ready Ready Preview, Comment Jul 16, 2026 4:05am

A platform passkey's PRF output wraps the signed-in credentials
(AES-GCM via HKDF) in localStorage; unlocking re-evaluates the PRF
behind the OS biometric prompt. Purely client-side — the passkey is
never registered with the backend and the master password is never
stored.

- Sign-in: "Use Touch ID / Face ID" button when enrolled; auto-fires
  once per page load (never after logout/cancel). Stale or dead
  enrollments self-clear with an explanatory message.
- Settings: enroll button next to the username flips to a checked
  disabled state with an X to turn off. Master password changes clear
  the enrollment.
- Post-login snackbar (top-center, away from the copy snackbars)
  offers enrollment to un-enrolled users; "Not now" opts out per
  device.
- Unit tests simulate the PRF authenticator; e2e drives the full
  journey through Chromium's virtual authenticator.
@xumaple
xumaple force-pushed the xumaple/claude/biometric-unlock branch from 2074aff to d9b0221 Compare July 16, 2026 04:04
@xumaple
xumaple merged commit 653b20e into main Jul 16, 2026
5 checks passed
@xumaple
xumaple deleted the xumaple/claude/biometric-unlock branch July 16, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant