Device-Bound Token Security — Design & Threat Model

Published with Vibe Sensei

Date: 2026-10-03 · Status: Draft · Scope: Desktop (Electron) + Mobile (React Native) + Backend (Express/WebSocket)

1. Executive Summary

Vibe Sensei's current auth tokens (15-min access JWTs, 30-day opaque refresh tokens) are portable — a stolen token works from any machine. OS-level encryption (safeStorage/Keychain) gives strong at-rest protection on desktop, but does not stop a token stolen from memory or via a privileged malware process from being replayed elsewhere.

This document gives the threat model, evaluates four binding strategies, and recommends a two-phase approach preceded by an immediate revocation fix:

2. Current State (verified in-repo)

TokenFileTTLStorageClaims
Access JWTback-end/src/auth/tokens.ts:1315 minMemory only (desktop)sub, email
Refresh tokenback-end/src/auth/tokens.ts:2630 daysDB (Session table)opaque randomBytes(32)
Mirror connect JWTback-end/src/realtime/mirrorConnectToken.ts:1360 sNever persistedaccountId, workspaceId, role, deviceId, jti

Desktop persistence (src/main/backend/tokenStorage.ts:59): access + refresh encrypted with safeStorage (Keychain/DPAPI/libsecret) before write to userData/backend-auth.json (0o600). Fail-closed: if safeStorage is unavailable, tokens are not persisted — in-memory only, re-login on next boot. No plaintext fallback.

What is NOT device-bound today: access JWTs carry {sub, email} only; refresh tokens are opaque random bytes; a stolen access+refresh pair works from any machine; the Device registry is a server record, not cryptographically linked to the token.

3. Threat Model

Attacker profiles A1–A6: disk thief, Keychain-capable malware, memory inspector, server-side DB breach, network MITM, rogue insider.

ThreatLikelihoodImpactCurrentAfter P1After P2
T1 Disk theftLowHighMediumLowLow
T2 Keychain malwareLowCriticalHighMediumLow
T3 Memory inspectionMediumHighHighMediumLow
T4 DB breach refreshLowCriticalHighMediumLow
T5 Mirror token interceptVery LowMediumLowLowLow
T6 Refresh token replayLowCriticalHighMediumLow
T7 Mobile token storageMediumHighUnknownMediumLow

4. Immediate Finding — Revocation Does Not Invalidate Sessions

POST /v1/devices/:deviceId/revoke (back-end/src/routes/devices.routes.ts:351) sets Device.revokedAt and closes the live WebSocket (close code 4408), but does not delete the Session rows for that device. Consequence: a revoked device's refresh token stays valid for up to 30 days.

Fix (backend-only, <1 day):

await prisma.session.deleteMany({ where: { deviceId } })

Should ship as its own standalone PR, independent of Phase 1/2.

5. Binding Strategy Evaluation

StrategyProof-of-possessionPlatform reachEffortVerdict
A — DPoP (RFC 9449)✅ asymmetricAll desktop (+ mobile)HighRecommended
B — Device claim❌ noneAllLowPhase 1 stopgap
C — Platform keys (Secure Enclave/TPM)✅ hardwareFragmentedVery highPhase 3 layer
D — Session-pinned HMAC⚠️ symmetricAllMediumRejected

Option A (DPoP): EC P-256 keypair per device at install, private key in safeStorage; every request carries a signed DPoP proof (htm, htu, iat, jti, jwk); server binds the access token to the key thumbprint (cnf.jkt). A stolen token is useless without the device private key. Fully mitigates T6/T4; substantially reduces T2/T3. Needs a nonce/replay store (RFC 9449 §8) and graceful degradation for pre-DPoP clients.

6. Recommendation & Roadmap

7. Open Questions

  1. Redis vs DB-backed store for DPoP jti replay prevention?
  2. Key rotation on re-install — invalidate old-key sessions immediately or let them expire?
  3. Mobile enforcement timeline — defer to Phase 3 with a firm deadline?
  4. Backward-compat gate — feature-flag strategy for requireDPoP() enforcement.

8. Feasibility Summary

PhaseEffortRiskImpact
0 — Revocation closure0.5 dayVery lowCloses post-revocation refresh window
1 — Device-claim bindingLowLowEliminates cross-device replay
2 — Desktop DPoP3–4 weeksMediumCloses T6; reduces T2, T3, T4
3 — Mobile + HW keys4–6 weeksHighEliminates disk/memory theft on supported HW

Phase 0 should ship immediately as a standalone backend PR, independent of the binding work.