Skip to content

Auto-lock: re-anchor hash-key hard-expiry on activity instead of sign-in #924

Description

@piyalbasu

Summary

Re-anchor the hash-key hard-expiry backstop on user activity/foreground instead of on sign-in, so an actively-used wallet never hits a surprise full re-authentication.

Follow-up to #905 (configurable auto-lock).

Background

#905 added two independent lock timers:

  • Soft auto-lock (AUTO_LOCK_TIMER): user-configurable (1m–24h, default 12h); activity-based (resets on interaction / foreground within the window) with a fast unlock. This is the primary mechanism.
  • Hash-key hard-expiry (HASH_KEY_EXPIRATION_MS): a coarser security backstop that discards cached derived-key material and forces a full password re-auth (HASH_KEY_EXPIRED, not the fast path). It is currently anchored at sign-in and only re-stamped on credential-verified moments.

Problem

Because the hard-expiry counts from the last unlock (not last activity), a loosely-active user on the 24h preset — whose usage gaps stay just under 24h, so the soft-lock never fires and never resets the hard-expiry clock — can hit a surprise full re-auth. At the current 72h value that requires ~3 continuous days of that pattern (we widened it from 48h → 72h, i.e. 3× the max preset, as a stopgap in #905), but it is not fully eliminated for the most regular 24h users, and it can in principle fire mid-session (the expiry is checked on every auth tick).

Proposed change

Re-anchor expiresAt on foreground/activity so:

  • An actively-used wallet keeps pushing the deadline out → never hard-expires from normal use.
  • A genuinely idle/away device (no foreground for the backstop window) still forces a full re-auth on next open — preserving the security bound as "N hours of inactivity" (matches the Freighter extension's session model).

Once re-anchored, the soft and hard timers become two thresholds on the same clock, and no fixed HASH_KEY_EXPIRATION_MS margin above the max preset is needed.

Design decisions to settle (the real cost is here, not LOC)

  1. Anchor granularity. Refreshing on every gesture is too many secure-storage writes. Practical anchor is the background→foreground transition (or a throttled "refresh if expiresAt is older than X"). Decide + document.
  2. Write throttling. getAuthStatus runs ~every 5s while foregrounded — the refresh must be gated to the transition (or throttled) so it doesn't hammer the keychain.
  3. Product/security sign-off on the posture change. Re-anchoring means an actively-used wallet never hard-expires; the bound becomes "N hours of no use." Confirm this is the intended posture (it matches the extension).

Scope / effort estimate

Small–medium, contained; most plumbing already exists (foreground detection in useAuthCheck, secure-storage timestamp read/write like backgroundedAt, and the clock-rollback generatedAt guard added in #905):

  • Core: re-stamp expiresAt/generatedAt on foreground (throttled) and remove the current "deliberately NOT refreshed here" logic in getAuthStatus.
  • Files: src/ducks/auth.ts, src/hooks/useAuthCheck.ts, a small src/services/autoLock.ts helper.
  • Tests: active-stays-valid, idle-N-hours-expires, cold-start, clock-rollback.
  • Main gating cost is security review, since incorrect refresh placement could inadvertently extend the backstop.

Estimate: ~0.5–1 day implementation + tests; no UI or new user-facing surface.

Acceptance criteria

  • An actively-used wallet (regular foregrounding) does not hit HASH_KEY_EXPIRED from normal use, on any preset including 24h.
  • A device left idle for the backstop window forces a full password re-auth on next foreground/cold-start.
  • Cold-start and backward-clock (rollback) cases still force expiry (don't regress the generatedAt guard from App Configurable Auto lock #905).
  • Secure-storage writes for the refresh are throttled to lifecycle transitions, not per-gesture.
  • Tests cover the above.

References

  • PR App Configurable Auto lock #905
  • HASH_KEY_EXPIRATION_MS:
    // Hard-expiry backstop: after this the persisted derived key is discarded and
    // the session fully re-authenticates from the password (HASH_KEY_EXPIRED)
    // rather than taking the fast soft-lock unlock path. It is a separate, coarser
    // bound than the user-configurable soft auto-lock: the soft timer governs how
    // soon the wallet re-locks (fast unlock), while this caps how long key material
    // may live in secure storage regardless of that choice.
    //
    // Set above the largest AUTO_LOCK_TIMER preset (24h) so that preset's soft-lock
    // fast path stays reachable — if this were <= 24h, the hard expiry (anchored at
    // sign-in and checked before the soft timer) would fire first and the 24h
    // preset could never fast-unlock. 72h (3x the max preset) gives enough headroom
    // that even a loosely-active 24h-preset user — one whose gaps stay just under
    // 24h so the soft-lock never fires and never resets this clock — is unlikely to
    // hit a surprise full re-auth. (The complete fix is to re-anchor this on
    // activity rather than sign-in; tracked as a follow-up.)
    export const HASH_KEY_EXPIRATION_MS = 72 * 60 * 60 * 1000; // 72 hours
  • isHashKeyExpired (+ rollback guard) and the sign-in-anchored stamping / "not refreshed on foreground" logic in src/ducks/auth.ts.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions