- docs/plans/2026-06-30-privacy-lock-manual-testing.md — 10-path manual QA sheet covering setup, lock behavior, timer, deep-link race, change/remove password, banner discovery, cross-run persistence. - docs/plans/2026-07-01-feat-messages-first-run-lock-banner-plan.md — design doc for the discovery banner shipped in aadbf3601. - docs/plans/2026-07-01-privacy-lock-security-review.md — honest review of PasswordHasher (PBKDF2 100k / 16B salt) + prefs storage. Flags 4 medium-severity items (iterations below OWASP 2023 rec; no versioned hash format; no UI-layer rate-limiting; unencrypted prefs storage) with concrete fixes. All within accepted threat model but P0 items are cheap and high-value follow-ups.
11 KiB
title, type, status, date
| title | type | status | date |
|---|---|---|---|
| Messaging Privacy Lock — Desktop Manual Testing Sheet | test | active | 2026-06-30 |
Messaging Privacy Lock — Desktop Manual Testing Sheet
Companion to 2026-06-30-feat-messaging-privacy-lock-plan.md and
2026-06-30-feat-messaging-privacy-lock-brainstorm.md.
What ships on the worktree-brainstorm-messaging-privacy-lock branch
- ✅ Commons foundation — headless cross-platform state machine,
preferences with password-hashed field, gate composable primitives,
idle-timer modifier, 8 unit tests all green
(
./gradlew :commons:jvmTest --tests "*MessagesLockStateTest*"). - ✅ Desktop wiring —
DesktopMessagesLockGatewrapping the Messages deck column; PBKDF2 password unlock (100k iterations, 16-byte salt);PrivacyLockSettingsScreenembedded in the Settings pane; app-global CompositionLocals provided inMain.kt.
What is NOT in this branch
- Android app module changes — reverted. Foundation code in
commons/still compiles for Android; wiring is a separate future workstream. - macOS Touch ID / Windows Hello / Linux biometrics — Desktop v1
uses a PBKDF2 password gate. Native biometric shims (Swift
LAContext, Windows Hello, polkit) remain in Future Considerations per the plan. - Notification redaction call site — the
DmRedactionLevelpolicy exists incommons/.../privacylock/; the actual write into Android'sNotificationUtils.ktor a desktop notification path is deferred (no desktop notifications wired today). - Screen-capture window blocks —
NSWindowSharingNone/SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)deferred. macOS 15+ brokesharingTypeanyway; Windows can revisit as a v1.1 patch. - Blur-on-unfocus — Desktop deferred to v1.1; the gate's onLeaveRoute-fires-on-column-navigation already covers the "left Messages" scenario.
- Marmot group chats — Marmot UI is Android-only in this repo. Desktop only gates the primary Messages column.
- Security hardening H2–H6 — crash-report scrubber, DM-content log audit, MessagingStyle history flush, NotificationListener redaction of the main builder, bunker decrypt queue drain. Each is independent scope; see the plan's §Security Hardening Additions.
Prerequisites
- macOS host recommended (the primary Amethyst Desktop dev target). Also runs on Windows and Linux (untested for this feature).
- Java 17+ (Compose Desktop bundles its own JBR).
- A Nostr account with at least one DM conversation for meaningful gating.
Automated verification
# Unit tests for the state machine + settings interface
./gradlew :commons:jvmTest --tests "com.vitorpamplona.amethyst.commons.privacylock.MessagesLockStateTest"
# Full commons + desktop compile
./gradlew :commons:compileKotlinJvm
./gradlew :desktopApp:compileKotlin
# Formatter
./gradlew spotlessApply
Expected: green.
Manual testing paths
Launch
./gradlew :desktopApp:run
Log in with an account that has one or more DM conversations. Add a Messages column to your deck via the "+" button → Messages, if not already present.
Path A — Enable the lock
- Click Settings in the sidebar.
- Scroll to the "Lock the Messages tab" card.
- ✅ Card shows a description and a Switch.
- ✅ Switch is OFF by default.
- Click the Switch to ON.
- ✅ A Set a password dialog appears (because no password is set yet).
- ✅ Dialog has: "New password" field, "Confirm new password" field, Cancel + Save buttons.
- Enter a password shorter than 4 characters, click Save.
- ✅ Error: "New password must be at least 4 characters".
- Enter mismatched passwords, click Save.
- ✅ Error: "Passwords don't match".
- Enter matching passwords ≥ 4 chars, click Save.
- ✅ Dialog closes.
- ✅ Switch is now ON.
- ✅ Below the toggle: "Change password" button appears.
- ✅ Two new cards appear: "Auto-lock after" (default: 5 min) and "DM notification preview" (default: Full).
Path B — Lock behavior
- Lock enabled with a password set. Navigate to the Messages deck
column.
- ✅ Lock screen renders with a padlock icon, "Messages locked" title, "Enter your privacy-lock password" subtitle, a password field, and an Unlock button.
- ✅ The chat list is NOT visible behind the lock screen.
- Click Unlock without typing.
- ✅ Button is disabled.
- Type a wrong password, press Enter (or click Unlock).
- ✅ "Wrong password" error under the field.
- ✅ Chat still not visible.
- Type the correct password, press Enter.
- ✅ Lock screen disappears.
- ✅ Full Messages column visible with conversations + chat pane.
Path C — Auto re-lock triggers
- Unlocked. Set inactivity timer to 1 min via Settings → Privacy lock → Auto-lock after.
- Return to Messages, don't interact for 60 seconds.
- ✅ Column re-locks; password field reappears.
- Unlock. Scroll a chat back and forth for 30 seconds.
- ✅ Column stays unlocked (user interaction resets timer).
- Unlock. Navigate to Feed or Discover column.
- ✅ Return to Messages → re-locked immediately (leave-route
trigger via
DisposableEffect(onDispose)).
- ✅ Return to Messages → re-locked immediately (leave-route
trigger via
- Set timer to Never.
- ✅ Wait 2 minutes without input → column stays unlocked.
- ✅ Navigating away still re-locks (leave-route independent).
Path D — Deep-link race (security H1)
Not directly testable in this branch — Desktop deck columns navigate
via the sidebar, so there's no true "cold-start-to-chatroom deep link"
flow. However, the invariant still applies: the gate reads
MessagesLockState.state synchronously in composition (no
LaunchedEffect guard). Verify by:
- Enable lock, quit the app.
- Cold-start (
./gradlew :desktopApp:run). - Add the Messages column immediately.
- ✅ Lock screen appears from the first frame — never see chat content flash before the lock overlay paints.
Path E — Change / clear password
- Lock enabled. Settings → Privacy lock → Change password.
- Dialog opens with: Current password field, New password, Confirm.
- Enter wrong current password.
- ✅ "Current password is wrong" error.
- Enter correct current, new, confirm → Save.
- ✅ Old password no longer works on the lock screen.
- ✅ New password unlocks.
- Toggle lock OFF, then back ON.
- ✅ Password persists — you're NOT re-prompted to set one, since
the hash is still stored (
passwordHashedis not cleared on disable).
- ✅ Password persists — you're NOT re-prompted to set one, since
the hash is still stored (
Path F — Fallback (no password set)
Rare: user manually clears passwordHashed from
~/.java/.userPrefs/com/vitorpamplona/amethyst/privacylock/ while
lockEnabled = true. To simulate:
- Quit the app.
- Open the prefs file for the node and remove
password_hashed=<value>while keepinglock_enabled=true. - Restart.
- Navigate to Messages.
- ✅ Lock screen shows: "No password is set yet. Open Settings → Privacy lock to set one." + a Disable lock button.
- Click Disable lock.
- ✅ Lock is disabled globally; Messages column visible.
Path G — Multi-account switch
- Lock enabled, unlocked. In Messages, viewing a conversation.
- Switch account via the sidebar / account switcher.
- ✅ Return to Messages → column re-locked (account switch counts as leave-route because the deck column composable is disposed and re-composed).
Path H — Regression sweep
- Feed / Discover / Wallet / Videos columns still function normally, no gate anywhere else.
- Sending a DM (after unlock) works — the compose pane is inside the gated content, so unlock allows send.
- Settings pane still shows all other sections (Media servers, Local Relay, Namecoin, Logout).
- Long-press-and-drag column reordering still works.
Path J — First-run discovery banner
Added by docs/plans/2026-07-01-feat-messages-first-run-lock-banner-plan.md.
- Fresh state — quit app, delete
~/.java/.userPrefs/com/vitorpamplona/amethyst/privacylock/prefs.xml(or edit to clearfirst_run_card_seen+lock_enabled). - Launch app, open Messages column.
- ✅ Inline banner at top of Messages column: padlock icon + "Lock the Messages tab?" + description + Not now + Enable buttons.
- ✅ Conversation list + chat pane still visible below (banner is ~50dp, doesn't dominate).
- Click Not now.
- ✅ Banner animates out (shrinkVertically + fadeOut).
- ✅ Navigate away and back → banner does NOT return.
- ✅ Quit + relaunch → banner still does not return.
- Reset state again. Click Enable on the banner.
- ✅ Set a password dialog opens (same dialog as Settings).
- ✅ Enter matching password ≥ 4 chars → Save.
- ✅ Banner animates out.
- ✅ Column STAYS interactive — no lock-screen flash after
save (verifies the
onUnlockSuccess()leniency).
- Continue browsing chats without unlock prompt.
- Navigate to Feed → back to Messages.
- ✅ Lock screen appears (leave-route trigger fired after enable).
- ✅ Unlock with the password you just set.
- Reset state. In Settings, enable the lock first (via the toggle
card). Then open Messages.
- ✅ Banner does NOT appear (
lockEnabled == truesuppresses it).
- ✅ Banner does NOT appear (
- Reset state. Show the banner. Dismiss with Not now. Now
enable the lock via Settings. Later disable it via Settings.
Return to Messages.
- ✅ Banner does NOT reappear (dismissal is sticky — the
firstRunCardSeenflag persists across enable/disable cycles).
- ✅ Banner does NOT reappear (dismissal is sticky — the
Path I — Cross-run persistence
- Set a password, enable lock, set timer to 15 min, redaction to Hidden.
- Quit the app.
- Restart via
./gradlew :desktopApp:run.- ✅ Lock is still enabled; navigating to Messages requires password.
- ✅ Timer setting persists.
- ✅ Redaction persists.
Known-honest limitations
Copy in the Settings pane's Limitations card matches the actual threat model:
- Filesystem-level attacker can read the
java.util.prefsnode and see the hash — cannot recover the password without brute-force, but 4-char passwords are weak. Recommend ≥ 8 chars, but v1 min is 4. - Memory dumps expose the plaintext password briefly during verification. Not defended.
- The Nostr private key (
nsec) is stored viaSecureKeyStorageas it is today — the lock does not touch it.
Sign-off checklist
- Paths A–I executed on macOS
./gradlew :commons:jvmTestgreen./gradlew :desktopApp:compileKotlingreen./gradlew spotlessApplyclean- Follow-up issues filed for:
- Windows / Linux manual QA
- Screen-capture window blocks (macOS 15+ caveat)
- Notification redaction call site (desktop notifications don't exist yet — deferred to when they do)
- Native biometrics (Touch ID / Windows Hello / polkit)
- Security hardening H2–H6 (crash scrub, log audit, MessagingStyle history, NotificationListener redaction, bunker queue drain)
- Marmot group chat gating (once Marmot desktop UI lands)