clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

main
lofi / docs / decisions / passkey-backup-alpha53.md
passkey-backup-alpha53.md 5.8K rendered
1# Jazz `BrowserPasskeyBackup` security review23Date: 2026-07-17\4Reviewed dependency: `jazz-tools@2.0.0-alpha.53`\5Decision: retain the pin for Wave 267## Contract reviewed89The current Jazz local-first-auth documentation describes `jazz-tools/passkey-backup` as a10browser-only, encrypted-at-rest vault for the same 32-byte local-first secret used by11`BrowserAuthSecretStore`. Restore must return that secret after a user-verifying WebAuthn ceremony;12the live Jazz client must then be replaced. The recovery phrase remains the portable bearer-secret13fallback.1415The exact installed implementation was reviewed at16`jazz-tools/2.0.0-alpha.53/dist/runtime/passkey-backup.js` and its packaged tests, not inferred from17a newer release.1819| Assumption | Exact alpha.53 behavior | Lofi boundary |20| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |21| At-rest protection | The 32-byte secret is supplied as the resident credential user handle. It is released by `credentials.get()` after the authenticator assertion; Jazz does not add application-level wrapping. | We rely on the browser/platform passkey provider to protect credential material at rest. We do not claim lofi controls or can audit provider encryption. |22| RP-ID | `appHostname` becomes both `rp.id` during backup and `rpId` during restore. | Production apps should pin one canonical hostname. A changed or preview hostname cannot see the old passkey and is reported as an RP-ID mismatch when the browser exposes that distinction. |23| Resident credential | `residentKey: "required"` and `requireResidentKey: true`. Restore uses discoverable lookup without an allow-list. | A non-resident authenticator does not satisfy the contract. |24| User verification | Backup requests `userVerification: "required"` and `credentialProtectionPolicy: "userVerificationRequired"`. Restore again requires verification and checks both UP and UV bits before reading `userHandle`. | Cancellation and failed verification are non-secret, retryable UI errors. Lofi never treats credential presence alone as authority. |25| Provider sync | The implementation requests `authenticatorAttachment: "platform"`; whether the passkey roams is decided by the OS/browser/provider. | Same-provider recovery is expected where that provider syncs passkeys. Universal iOS/Android/browser/provider portability is not promised. Cross-device QR flows may require the original device and are not loss recovery. |2627## Decision2829Alpha.53 implements the documented resident, RP-scoped, user-verifying secret round trip and its30packaged tests cover secret length, credential options, RP-ID, UP/UV enforcement, invalid handles,31and round-trip restore. No dependency upgrade is required for the reviewed contract.3233Lofi exposes this as a **recoverable account backup**, distinct from its earlier **guard-only34credential**. The earlier credential proves user presence before showing a locally stored recovery35phrase; it does not contain the Jazz secret and cannot restore an account. Existing guard-only36credentials remain usable for phrase reveal but are labeled as legacy/local and never presented as37account backups.3839## Trust and fallback4041- The passkey provider and browser are trusted to protect and, where supported, sync the resident42 credential.43- The canonical RP-ID is part of the recovery boundary and must remain stable.44- The recovery phrase is the portable fallback. It is the account secret encoded as words; anyone45 holding it can act as the account.46- Restoring a different secret requires explicit confirmation while the current account is47 local-only, because unsynced local rows cannot be recovered from Jazz sync.48- Principal replacement detaches lofi stores/subscriptions and shuts down the old Jazz client and49 workers via `db.shutdown()` — deliberately not `logout()`, which would clear local account state50 that must carry forward. It then saves the restored secret for the app ID, commits the sync51 election and managed-namespace record only after that save succeeds (a failed replacement leaves52 the previous local account bootable), reopens the runtime on the restored principal, and exposes53 the restored `session.user_id` for verification.5455## Evidence sources5657- Jazz, “Local-first auth”: <https://jazz.tools/docs/auth/local-first-auth>58- Jazz, “Lifecycle”: <https://jazz.tools/docs/auth/lifecycle>59- Pinned package source and tests in the resolved Deno npm cache for `jazz-tools@2.0.0-alpha.53`