fix(security): re-lock biometric app-lock on background; persist edited password (#125)

Two issues from a security review of the credential/auth path (the review also
verified the fundamentals are solid — passwords in SecureStore, Sentry/analytics/
Chatwoot all scrub secrets).

1. HIGH: biometric app-lock never re-armed. authenticate() sets isAuthenticated
   =true once at cold start and lock() was never called (no AppState listener) —
   so 'Require Biometric to Open' was fully bypassable: after one unlock, anyone
   with brief physical access could reopen a backgrounded app straight into
   session history and connection details for the life of the JS process. Now an
   AppState 'background' listener calls lock() when the toggle is on. Fires on
   'background' only, so the biometric prompt / app switcher (transient
   'inactive') don't cause spurious re-locks.

2. Editing a connection's password did nothing: the edit screen's password field
   was never passed to updateConnection, which never wrote PASSWORDS_PREFIX — so
   a user rotating a server password silently kept using the old one. updateConnection
   now takes an optional password and writes it to SecureStore (blank = keep
   existing, since the field loads empty).

typecheck clean, 187/187 tests.


Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6

Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-18 08:49:42 -07:00
committed by GitHub
parent b78ee442cc
commit f86265aa7c
3 changed files with 42 additions and 10 deletions

View File

@@ -136,13 +136,19 @@ export default function EditConnectionScreen() {
}
setIsSaving(true)
await updateConnection(connection.id, {
name: name.trim(),
type,
url: url.trim(),
directory: directory.trim() || undefined,
username: username.trim() || undefined,
})
await updateConnection(
connection.id,
{
name: name.trim(),
type,
url: url.trim(),
directory: directory.trim() || undefined,
username: username.trim() || undefined,
},
// Empty = keep existing password (the field loads blank); a typed value
// rotates it in SecureStore.
password || undefined,
)
// If this was the active connection, the SSE loop may have stopped
// retrying after a prior 401 (see events.ts) — reconnect now with the
// freshly saved credentials instead of leaving the user stuck until