← back to index

U13 — Faster PIN verify (PBKDF2 100k → 25k + opportunistic rehash)LOCAL

Pivot from the original U13 spec. Original spec ("router.refresh rework") was already shipped by Slice J (2026-05-18, -2966ms / -67%). This U13 attacks the remaining architectural floor: the PIN verify itself. Despite the Slice J memo's "bcrypt 300ms" label, the PIN hash is actually PBKDF2-SHA-256 at 100,000 iterations via Web Crypto — bcrypt is only used for web user passwords. Dropping iterations 100k → 25k is ~4× faster on CF Workers (~225ms shaved) and still exceeds the NIST 800-63B minimum (10k) by 2.5×. PIN hash cost is largely security theater for low-entropy PINs anyway — real defense is online rate-limiting + DB access control.

Summary

Status
8/8 local · typecheck clean · awaiting Gate 1 approval
Repo
nix-cafe only — no migration, no backend, no schema
Files
3 modified · ~80 LOC net (5 LOC constant change + ~75 LOC helpers + wiring)
Expected prod win
Slice J's 1437ms floor → ~1210-1240ms (-150-225ms shave). PIN verify cold-isolate: ~250-300ms → ~60-75ms.

What ships

Why this is safe — security analysis

8/8 local checks

ITERATIONS constant = 25,000 + U13 rationale comment present
needsRehash exported; returns false for fresh hashes, true for legacy 100k / malformed / wrong-algo, false for empty
hashPin produces a hash with iters=25000 in the storage format
verifyPin accepts BOTH a manually-built legacy 100k hash AND a new 25k hash; rejects wrong PINs on both
pin_identities.ts: maybeRehashPinIdentity helper exists; both verifyPinForIdentity + verifyPinIdentity (linear scan) await it on success
tenant_users.ts: maybeRehashPosPin helper exists; verifyPosPinForUser awaits it on success
DAO round-trip on local lumiere: seed pin_identity with hand-crafted 100k legacy hash → verifyPinForIdentity returns row → poll DB → pinHash flipped to iters=25000 (rehash worked via the inline-fallback path in tsx)
Local timing: 25k verify is ≥1.5× faster than 100k on the test runner's CPU (typical ratio ≈ 3-4× — soft floor at 1.5× to keep harness stable)

Typecheck: npx tsc --noEmit on nix-cafe → 0 errors.

Gate 2 plan

  1. Commit nix-cafe (single-line).
  2. Push to karouna-dev → CF auto-deploy.
  3. test-u13-prod.mjs on lumiere: drive the full PIN unlock flow on lumiere, time PIN-Enter → workspace top-bar visible. Assert ≤1300ms (target floor from 1437ms baseline). Reset a test cashier's PIN to verify the new hash lands at 25k. Verify a legacy 100k cashier (if any exist on prod) unlocks successfully + the post-verify row update flips it to 25k.
  4. Regression sweep 51/51 (phase2-cafe-multishop solo).
  5. Publish prod gallery.

Followups (not in scope)