Files
amethyst/docs/plans/2026-06-30-privacy-lock-manual-testing.md
T
nrobi144 0ff4ef1e10 docs(privacylock): manual testing sheet, banner plan, security review
- 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.
2026-07-01 11:35:52 +03:00

11 KiB
Raw Blame History

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 — DesktopMessagesLockGate wrapping the Messages deck column; PBKDF2 password unlock (100k iterations, 16-byte salt); PrivacyLockSettingsScreen embedded in the Settings pane; app-global CompositionLocals provided in Main.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 DmRedactionLevel policy exists in commons/.../privacylock/; the actual write into Android's NotificationUtils.kt or a desktop notification path is deferred (no desktop notifications wired today).
  • Screen-capture window blocks — NSWindowSharingNone / SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) deferred. macOS 15+ broke sharingType anyway; 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

  1. Click Settings in the sidebar.
  2. Scroll to the "Lock the Messages tab" card.
    • ✅ Card shows a description and a Switch.
    • ✅ Switch is OFF by default.
  3. 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.
  4. Enter a password shorter than 4 characters, click Save.
    • ✅ Error: "New password must be at least 4 characters".
  5. Enter mismatched passwords, click Save.
    • ✅ Error: "Passwords don't match".
  6. 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

  1. 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.
  2. Click Unlock without typing.
    • ✅ Button is disabled.
  3. Type a wrong password, press Enter (or click Unlock).
    • ✅ "Wrong password" error under the field.
    • ✅ Chat still not visible.
  4. Type the correct password, press Enter.
    • ✅ Lock screen disappears.
    • ✅ Full Messages column visible with conversations + chat pane.

Path C — Auto re-lock triggers

  1. Unlocked. Set inactivity timer to 1 min via Settings → Privacy lock → Auto-lock after.
  2. Return to Messages, don't interact for 60 seconds.
    • ✅ Column re-locks; password field reappears.
  3. Unlock. Scroll a chat back and forth for 30 seconds.
    • ✅ Column stays unlocked (user interaction resets timer).
  4. Unlock. Navigate to Feed or Discover column.
    • ✅ Return to Messages → re-locked immediately (leave-route trigger via DisposableEffect(onDispose)).
  5. Set timer to Never.
    • ✅ Wait 2 minutes without input → column stays unlocked.
    • ✅ Navigating away still re-locks (leave-route independent).

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:

  1. Enable lock, quit the app.
  2. Cold-start (./gradlew :desktopApp:run).
  3. 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

  1. Lock enabled. Settings → Privacy lock → Change password.
  2. Dialog opens with: Current password field, New password, Confirm.
  3. Enter wrong current password.
    • ✅ "Current password is wrong" error.
  4. Enter correct current, new, confirm → Save.
    • ✅ Old password no longer works on the lock screen.
    • ✅ New password unlocks.
  5. Toggle lock OFF, then back ON.
    • ✅ Password persists — you're NOT re-prompted to set one, since the hash is still stored (passwordHashed is not cleared on disable).

Path F — Fallback (no password set)

Rare: user manually clears passwordHashed from ~/.java/.userPrefs/com/vitorpamplona/amethyst/privacylock/ while lockEnabled = true. To simulate:

  1. Quit the app.
  2. Open the prefs file for the node and remove password_hashed=<value> while keeping lock_enabled=true.
  3. Restart.
  4. Navigate to Messages.
    • ✅ Lock screen shows: "No password is set yet. Open Settings → Privacy lock to set one." + a Disable lock button.
  5. Click Disable lock.
    • ✅ Lock is disabled globally; Messages column visible.

Path G — Multi-account switch

  1. Lock enabled, unlocked. In Messages, viewing a conversation.
  2. 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

  1. Feed / Discover / Wallet / Videos columns still function normally, no gate anywhere else.
  2. Sending a DM (after unlock) works — the compose pane is inside the gated content, so unlock allows send.
  3. Settings pane still shows all other sections (Media servers, Local Relay, Namecoin, Logout).
  4. 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.

  1. Fresh state — quit app, delete ~/.java/.userPrefs/com/vitorpamplona/amethyst/privacylock/prefs.xml (or edit to clear first_run_card_seen + lock_enabled).
  2. 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).
  3. Click Not now.
    • ✅ Banner animates out (shrinkVertically + fadeOut).
    • ✅ Navigate away and back → banner does NOT return.
    • ✅ Quit + relaunch → banner still does not return.
  4. 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).
  5. Continue browsing chats without unlock prompt.
  6. Navigate to Feed → back to Messages.
    • ✅ Lock screen appears (leave-route trigger fired after enable).
    • ✅ Unlock with the password you just set.
  7. Reset state. In Settings, enable the lock first (via the toggle card). Then open Messages.
    • ✅ Banner does NOT appear (lockEnabled == true suppresses it).
  8. 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 firstRunCardSeen flag persists across enable/disable cycles).

Path I — Cross-run persistence

  1. Set a password, enable lock, set timer to 15 min, redaction to Hidden.
  2. Quit the app.
  3. 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.prefs node 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 via SecureKeyStorage as it is today — the lock does not touch it.

Sign-off checklist

  • Paths A–I executed on macOS
  • ./gradlew :commons:jvmTest green
  • ./gradlew :desktopApp:compileKotlin green
  • ./gradlew spotlessApply clean
  • 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)