Closing the coverage gaps the review flagged as blind spots we'd be relying on at
release:
4. RELAY LOGIC TESTS. server/test_relay.py (stdlib unittest + a FakeSocket, no network)
covers the address-proof end to end: cookie issued on join, wrong cookie rejected,
right cookie verifies once; enforce mode WITHHOLDS forwarding from an unverified
address then delivers after it proves itself; watch-only forwards but records
would-block; the per-IP cap counts across BOTH v1 and v2; a NAT-rebind clears
verification (spoof-takeover guard); a forged BYE from another address can't evict
the victim; and bad/short/unknown-version headers are refused. Wired into
run-tests.ps1 (Start-Process from server\, SKIPs loudly if no Python) so a relay
change can no longer ship past the gate untested. The relay had ZERO automated
coverage before and auto-updates every user.
5. UPDATER SIGNATURE ENFORCEMENT. Extracted the two refusal branches into a pure
VerifyStagedRelease gate and added UpdaterRefusesUnsignedRelease: no-sig refused,
wrong-key refused, garbage refused, tamper (good sig over changed bytes) refused,
genuine release accepted. ReleaseSigning only proved the crypto; this proves the
updater actually REFUSES - the hijacked-release-stream threat.
6. STREAMING PASSWORD STRENGTHENING. The accept decision is now a pure
ProfilePasswordDialog.RejectionAdviceFor shared by BOTH password dialogs (also
fixes the App-review trim inconsistency - manager dialog compared untrimmed). Test
pins the load-bearing rule: requireStrong DISABLES the unchanged-exemption so an
existing weak "Games" can't keep streaming, while casual mode still grandfathers an
unchanged password and blocks a new weak one, trim-safe.
Plus the NVDA-hang cache assertions in PasswordRules (miss->hit, same-instance repeat,
Prewarm, empty/weak = no work).
Gate 71/71 + 7 relay tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The everyone-must-update release. Four coordinated changes, each from the security
discussion Ed approved 2026-07-27, plus the remembered-apps polish:
1. SIGNED RELEASES. build-release.ps1 now signs the release zip (ECDSA P-256 /
SHA-256, --sign-update verb) with a private key that lives ONLY at Ed's chosen
location outside the repo; the matching public key is embedded (UpdateSignature)
and the updater REFUSES any release whose .sig asset is missing or does not
verify - a compromised GitHub account can no longer ship code to users. The
signing verb self-checks against the embedded key so a key/embed mismatch fails
the pipeline, and the gate proves the on-disk key matches the embed when present.
2. STRONGER PASSWORDS, ENFORCED (BREAKING). PBKDF2 raised 100k -> 600k (both peers
must derive the same key, so 5.6 cannot stream with pre-5.6 AT ALL - release
notes lead with it). New PasswordStrength rule (>= 8 chars, not an infamous
password) enforced at EVERY door: both password dialogs block weak NEW entries
with concrete plain-English advice; the streaming gate walks an existing weak
password through strengthening; and ForPlainPassword - the single derivation
choke-point shared with the service - refuses weak outright, so no path streams
on a guessable password. Headless service logs the why. Per Ed: painful once,
and this coordinated-update release is the cheapest moment it will ever have.
3. RELAY ADDRESS-PROOF (watch-only). The relay sends every new client address a
random cookie and marks it verified when echoed - a forged source address can
never echo, killing the reflection attack. 5.6 clients echo automatically
(AddrCheck type 10, verbatim, self-limiting); the relay ships watch-only
(logs would-blocks) until the fleet updates, then one flag (--require-addr-check)
enforces. Per-IP entry cap (4) enforced immediately. Relay changes are committed
but NOT deployed to the Pi - they ride the v5.6 release moment.
4. Remembered-apps empty state teaches its lifecycle + manual sentence; About/
release notes written; version bumped to 5.6.
New gate steps: signing round-trip/tamper/wrong-key/embed-match; password rules incl.
the exact "Games" case; AddrCheck verbatim echo. Gate 69/69.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline-built dialogs were invisible to the accessibility audit - which is exactly how
mnemonic-less buttons kept slipping through (the single-instance dialog Andre caught,
and now TWO more found by this very change: ManualPeerPrompt's OK/Cancel and
ProfileSaveAsPrompt's Cancel had no mnemonics - both fixed). Each inline dialog's
construction is split from its ShowDialog into a Build seam, and the audit now covers
15 dialog surfaces (was 8): manual peer prompt, quick profile switch, change password,
password manager, profile name prompt, and the service Additional-options window join
the eight Form dialogs. Behaviour unchanged.
The encoder-boundary clamp is now ONE shared rule (Core SampleClamp) instead of three
private copies in MixingEngine / AsioCaptureBackend / PushModeWasapiBackend - and the
ASIO copy's up-to-four interlocked increments per frame became one batched add per
buffer on the RT thread. Pinned by a new self-test (over-range clamps to exactly +/-1
and counts; +/-1 exactly passes untouched).
Gate 61/61.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UPnP close hang:
- RouterPortMapper.Stop()/Dispose() does a SYNCHRONOUS DeletePortMap call to the router
(plus StopDiscovery). When the router is slow or unresponsive that blocks — and it ran on
the UI thread in FormClosing, so enabling UPnP could make RemSound impossible to close
(Andre, this morning; his logs show UPnP teardown taking several seconds even on a good
run). Moved the router teardown into the same bounded background-task pattern already used
for the ASIO audio dispose: UPnP + audio now tear down off the UI thread, in parallel, under
one 3s cap. Anything unfinished is reclaimed on process exit, so the window always closes.
Mnemonics on dialog buttons:
- Andre flagged the "RemSound is already running" dialog (single-instance) having no shortcut
keys. Its three TaskDialog buttons had no & mnemonics; added Alt+S / Alt+F / Alt+C.
- Swept every dialog. Custom TaskDialog buttons and Form OK/Cancel/Close buttons that lacked
mnemonics now have them: single-instance (Switch/Force/Cancel), save-onto-read-only
(Save/Cancel), Add EQ band, Change/Manage profile password, Quick profile switch (Close),
Rename peer (OK), About (Close). Cancel stays on Esc in the few dialogs where Alt+C is
already taken (Rename peer's Clear, Service profile's peers list) — by design, not a miss.
- The rest already had mnemonics (Keyboard-shortcut import, Update-install notice, Profile
selection, Recording settings). The dialog-accessibility gate (mnemonic uniqueness + names)
stays green, so nothing collides.
Gate 46/46.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RemSound's password boxes are deliberately NOT PasswordChar-masked (a screen-reader user can't see
a mask), so the key-click hook's "is this a password field?" check (UseSystemPasswordChar /
PasswordChar) was always false and the distinct passkey.wav never played. Password fields now mark
themselves with Tag = KeyClickService.PasswordFieldTag, and the hook checks that (keeping the
masking-flag check as a fallback). Tagged both password fields - ProfilePasswordDialog (which all
password entry routes through, including the send/receive streaming gate) and
ProfilePasswordManagerDialog. So a key click + passkey now layer together on every password keystroke.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Headline: all audio is now encrypted (AES-256-GCM), keyed by a per-profile
password. Mandatory — v3.3 only interoperates with v3.3+.
Encryption
- RemSoundCrypto (Core): PBKDF2 key derivation, AES-GCM encrypt/decrypt
(low-alloc, into-span), password fingerprint, light on-disk obfuscation.
- Wire: SenderLane encrypts the audio payload (PCM split across parts when the
+28 overhead crosses MTU); AudioReceiver/StreamSession decrypt via a shared
single-thread AudioDecryptor. Fingerprint piggybacks on the Format packet
(offset 36, backward-compatible) so a peer can detect a password mismatch.
- Profile.Password (scrambled), carried through BuildCurrentProfile; MainForm
derives + pushes the key/fingerprint to sender + receiver (RecomputeAudioCrypto).
- UX: ask-for-password on profile create; File -> Change this profile's password
(ProfilePasswordDialog); Options -> Profile passwords (manager); a gate that
prompts before streaming without a password; and a clear "passwords don't
match" / "peer needs to update" message driven by the fingerprint.
Cue fixes
- CuePlayer (NAudio) replaces System.Media.SoundPlayer, which silently failed
on the 96 kHz/24-bit cue WAVs (and any custom file) — cues now play reliably,
resampled to 48 kHz/16-bit. Also fixes the Preferences preview button.
- Connect/disconnect cues now audio-gated with hysteresis: connected when audio
flows OR heartbeat healthy; lost only when audio stops AND heartbeat
unreachable. Kills false disconnects and the receive-only "no cues" case.
- Honest cue logging (played / muted / not loaded).
Smaller
- Endpoint stickiness: keep the audio target pinned to the heartbeat-proven
address instead of chasing a multi-homed peer's other (unreachable) address.
- "Online/offline" label now audio+heartbeat aware, not discovery-only.
- "Show what's new after each update" preference (on by default).
Docs: About v3.3 block, RELEASE_NOTES, README (encryption as a headline),
manual section 12 "Passwords and encryption" (+ renumber), MANUAL.md regenerated.
Version 3.2.0 -> 3.3.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>