Fix (NVDA-critical): weak-password state must never pop a modal at startup
Ed launched 5.6 with a profile carrying the old weak "Games" password and hit a show-stopper: a dialog he could neither read nor dismiss, and the window came up on the connectivity screen instead of minimised. Cause: ExplainWeakPasswordIfNeeded fired a modal TaskDialog automatically from RecomputeAudioCrypto on profile load / auto- connect, via ForegroundDialog.Show — which also drags the window to the foreground (hence the ignored "minimised"). A modal that steals focus mid-startup, before NVDA can reach it, locks a screen-reader user out completely. Fix: no automatic modal on the weak-password path. The state is surfaced NON-modally and persistently in the status line (UpdateStatus, via the new pure WeakPasswordBlocks- Audio) — NVDA reads it at the user's own pace, and it leads the line so it's spoken first. The guided modal prompt is kept ONLY for the user-INITIATED streaming tick (EnsureStreamingPassword), where the user just pressed a key so focus is clean and the dialog is reachable; that path is already suppressed during startup profile-apply, so there is now no startup modal at all. WeakPasswordBlocksAudio correctly stays false for a strong password still deriving off-thread (key null, Critique null), so that transient never shows the warning. Test: WeakPasswordBlocksAudio pinned (weak+no-key true; empty/strong-deriving/has-key false). Gate 71/71 + 7 relay tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7188,7 +7188,12 @@ public sealed partial class MainForm : Form
|
||||
: "not receiving";
|
||||
var peerCount = knownPeers.Count;
|
||||
var hbSummary = heartbeatService?.GetHealthSummary() ?? "no peers";
|
||||
statusLabel.Text = $"Connected for {since}. {peerCount} peer(s) known. {sendText}. {receiveText}. Heartbeat: {hbSummary}.";
|
||||
// Weak-password block surfaced here, NON-modally, so NVDA reads it without a startup dialog
|
||||
// trap (2026-07-27). Leads the status line so it's the first thing spoken.
|
||||
var weakPrefix = WeakPasswordBlocksAudio(currentProfilePassword, currentAudioKey is not null)
|
||||
? "No audio: this profile's password is too weak to protect it — change it in the File menu, “Change this profile's password”, on every machine you connect with. "
|
||||
: "";
|
||||
statusLabel.Text = $"{weakPrefix}Connected for {since}. {peerCount} peer(s) known. {sendText}. {receiveText}. Heartbeat: {hbSummary}.";
|
||||
bool streaming = connected && (sender.IsRunning || receiver.IsRunning);
|
||||
healthLabel.Text = connected
|
||||
? streaming ? "Health: streaming" : "Health: idle"
|
||||
@@ -8345,33 +8350,28 @@ public sealed partial class MainForm : Form
|
||||
receiver.AudioFingerprint = currentAudioFingerprint;
|
||||
}
|
||||
|
||||
/// <summary>Since 5.6 the derivation rule refuses a WEAK password (key comes back null), so a
|
||||
/// profile that auto-connects at startup with an old guessable password must not just sit
|
||||
/// silently dead — say why, once, and point at the fix. The interactive tick path has its own
|
||||
/// guided flow (EnsureStreamingPassword); this catches every other route in.</summary>
|
||||
/// <summary>Since 5.6 the derivation rule refuses a WEAK password (key comes back null). This runs
|
||||
/// automatically on profile load / auto-connect, so it must NOT pop a modal: a startup dialog that
|
||||
/// steals focus before NVDA can reach it — and drags the window out of the tray to show it — locked
|
||||
/// a blind user out completely (Ed, 2026-07-27, "it will fuck over all nvda users"). The weak state
|
||||
/// is surfaced NON-modally and persistently in the status line instead (see <see cref="UpdateStatus"/>
|
||||
/// and <see cref="WeakPasswordBlocksAudio"/>), which NVDA reads at the user's own pace. The guided
|
||||
/// modal prompt is reserved for the user-INITIATED streaming tick (<see cref="EnsureStreamingPassword"/>),
|
||||
/// where the user just pressed a key so focus is clean and the dialog is reachable. Here: log once.</summary>
|
||||
private void ExplainWeakPasswordIfNeeded(string? pw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pw) || currentAudioKey is not null || weakPasswordExplained) return;
|
||||
weakPasswordExplained = true;
|
||||
logFile.Event("audio crypto: profile password fails the 5.6 strength rule — no audio until it's changed");
|
||||
var advice = PasswordStrength.Critique(pw) ?? "";
|
||||
BeginInvoke(() =>
|
||||
{
|
||||
var page = new TaskDialogPage
|
||||
{
|
||||
Caption = "Password needs strengthening",
|
||||
Heading = "No audio until this profile's password is stronger",
|
||||
Text = "From this version, RemSound refuses to stream on a password that's easy to guess. "
|
||||
+ advice + " Change it via the File menu, “Change this profile's password” — on every machine that uses it.",
|
||||
Icon = TaskDialogIcon.Warning,
|
||||
Buttons = { TaskDialogButton.OK },
|
||||
DefaultButton = TaskDialogButton.OK,
|
||||
AllowCancel = true,
|
||||
};
|
||||
ForegroundDialog.Show(owner => TaskDialog.ShowDialog(owner, page));
|
||||
});
|
||||
logFile.Event("audio crypto: profile password fails the 5.6 strength rule — no audio until it's changed (shown in the status line)");
|
||||
}
|
||||
|
||||
/// <summary>Pure, testable: is audio blocked purely because the current profile password is too
|
||||
/// weak (as opposed to no password, or a strong password still deriving off-thread)? Drives the
|
||||
/// status-line warning. A strong password mid-derive has a null key too, but Critique returns null
|
||||
/// for it, so this stays false there — it fires only for a genuinely guessable password.</summary>
|
||||
internal static bool WeakPasswordBlocksAudio(string? password, bool haveKey) =>
|
||||
!string.IsNullOrEmpty(password) && !haveKey && PasswordStrength.Critique(password) is not null;
|
||||
|
||||
// Bumped on every password change so a slow background derive that finishes AFTER a newer change
|
||||
// knows to discard its now-stale result (see RecomputeAudioCrypto).
|
||||
private int cryptoGeneration;
|
||||
|
||||
@@ -2560,7 +2560,16 @@ internal static class SelfTest
|
||||
Check(RemSoundCrypto.IsCached("") && RemSoundCrypto.IsCached("Games"),
|
||||
"empty and weak passwords must count as 'no work' (they never trigger a blocking derive)");
|
||||
|
||||
return "weak+common refused with advice; derivation refuses weak everywhere; results cached (no repeat PBKDF2)";
|
||||
// The weak-password state must surface NON-modally at startup (no focus-trapping dialog that
|
||||
// NVDA can't reach — the 2026-07-27 launch bug). WeakPasswordBlocksAudio drives the status line:
|
||||
// true only for a genuinely weak password with no key; NOT for empty, NOT for a strong password
|
||||
// still deriving off-thread (key null but Critique null), NOT once the key is present.
|
||||
Check(MainForm.WeakPasswordBlocksAudio("Games", haveKey: false), "a weak password with no key must flag the status warning");
|
||||
Check(!MainForm.WeakPasswordBlocksAudio("", haveKey: false), "no password is not a 'weak password' block");
|
||||
Check(!MainForm.WeakPasswordBlocksAudio("kettle9tiger42moon", haveKey: false), "a STRONG password still deriving (key null, Critique null) must NOT show the weak warning");
|
||||
Check(!MainForm.WeakPasswordBlocksAudio("Games", haveKey: true), "once a key exists the warning clears");
|
||||
|
||||
return "weak+common refused with advice; derivation refuses weak everywhere; cached; weak-block surfaced non-modally";
|
||||
}
|
||||
|
||||
/// <summary>The relay address-proof (2026-07-27): an AddrCheck cookie arriving at the receiver
|
||||
|
||||
Reference in New Issue
Block a user