Closes#2285.
Adds an opt-in 4-digit PIN that gates the library and reader on app
launch. Threat model: casual physical/browser access by another person
on a shared device — peace of mind, not defense against an attacker
with filesystem access. The PIN is stored as a salted PBKDF2-SHA256
hash (100k iterations) in settings.json; the plaintext PIN is never
persisted.
Configured from Settings → Advanced Settings → "Set PIN…" (and
"Change PIN…" / "Disable PIN…" once enabled). The lock screen and the
set/change/disable dialog share a single 4-dot input component
(PinInput) for a consistent UI; the dialog auto-advances focus from
Current → New → Confirm. Lock-on-resume, biometric unlock, and
account-based reset are out of scope for this MVP — disable for now is
"clear app data".
Bundles the previously-missed sync-passphrase i18n strings (PR #4090)
across all 33 locales so no `__STRING_NOT_TRANSLATED__` placeholders
remain in the tree.
New
- src/libs/crypto/applock.ts (PBKDF2 hash/verify; reuses derivePbkdf2Key)
- src/store/appLockStore.ts (gate + dialog state)
- src/components/PinInput.tsx (shared 4-dot input)
- src/components/AppLockScreen.tsx (full-screen lock gate)
- src/components/settings/AppLockDialog.tsx (set/change/disable)
- src/__tests__/libs/crypto/applock.test.ts
Modified
- src/types/settings.ts (pinCodeEnabled / pinCodeHash / pinCodeSalt)
- src/services/constants.ts (default off)
- src/components/Providers.tsx (mount gate + dialog above app shell)
- src/app/library/components/SettingsMenu.tsx (Advanced submenu entries)
- src/styles/globals.css (animate-pin-shake keyframe)
- public/locales/*/translation.json (21 PIN keys + 17 leftover passphrase keys × 33 locales)
Verified
- pnpm test (4018 pass)
- pnpm lint (clean)
- Manual web smoke: Set/Reload-locks/Wrong-PIN/Unlock/Change/Disable
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>