Skip to main content

## Module pin

Source
Expand description

Profile PIN: hashing, and the lockout policy that decides what a guess costs.

The policy half is deliberately pure — it takes the stored counter state and the current time, and returns the decision plus the next state. That is what makes “five wrong guesses then a lockout that survives a restart” testable without a database, a clock, or a running app.

What this is not: at-rest protection. The PIN gates switching to a profile; it does not encrypt that profile’s access token, so anyone holding the database and the keyring has every token regardless. That trade is deliberate and its reasoning lives in DR-268 — a wrapped token would leave a locked profile unable to resume its own downloads or drain its own sync queue until somebody walked past and typed the code.

TRACES: UR-083 | DR-268

## Structs§

PinState
  Persisted counter state for one profile’s PIN.

## Enums§

PinDecision
  What the caller should do with an attempt.

## Constants§

BASE_LOCKOUT_SECS 🔒
  How long the first lockout lasts. Each subsequent failure doubles it.
MAX_ATTEMPTS
  Wrong guesses allowed before the first lockout.
MAX_LOCKOUT_SECS 🔒
  Ceiling on the doubling, so a forgotten PIN never bricks the tile — the password route is always there, and a lockout measured in hours would push people towards not setting a PIN at all.

## Functions§

evaluate
  Decide an attempt and produce the state to persist.
hash_pin
  Hash a PIN for storage. Returns a PHC string with the salt embedded.
validate_pin
  A PIN must be 4–8 digits. Rejecting non-digits here rather than in the pad keeps the rule where the rule is enforced.
verify_pin
  Compare a candidate PIN against a stored PHC string.