Weak SERVICE password: warn from the app, since the headless service can't ask
Ed's point: the send-only service auto-updates itself to 5.6, and a pre-existing weak service-profile password then leaves it silently not streaming — the service is headless (SYSTEM, no window) so it can't prompt, and nobody watches its log. Users would never know. Closed from the two places a user WILL see: 1. App launch (MaybeWarnWeakServicePassword, in the settled RunStartupNotices sequence): if a service is installed and its profile password is set-but-weak, a readable, focus-clean dialog warns and offers to open the service settings to fix it. Self-resolving — stops once the service password is strengthened. The person running the service almost always has the app (that's how it's configured), and the app updates around the same time the service does, so this fires right when it matters. Decision extracted to the pure ServicePasswordNeedsStrengthening (tested: installed+weak warns; not-installed / unset / strong don't). 2. Saving the service profile (ServiceProfileDialog): Save now validates first — a weak password pops a warning offering to set a stronger one inline (requireStrong), Save-anyway, or Cancel. The save button no longer auto-closes; it validates then closes explicitly. readme: a sentence in the service section explaining the service password follows the same rule and how RemSound surfaces a weak one. Gate 71/71 + 7 relay tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1668,8 +1668,47 @@ public sealed partial class MainForm : Form
|
||||
MaybeWarnMicBlockedOnStartup();
|
||||
if (IsDisposed) return;
|
||||
MaybeWarnWeakPassword();
|
||||
if (IsDisposed) return;
|
||||
MaybeWarnWeakServicePassword();
|
||||
}
|
||||
|
||||
/// <summary>The send-only service is headless — it CANNOT ask for a password. So when it
|
||||
/// auto-updates to 5.6 with a pre-existing weak service-profile password, it silently stops
|
||||
/// streaming and only a log line explains why (nobody watches a service log). This closes that
|
||||
/// gap from the one place a user WILL see: the interactive app's launch. If a service is installed
|
||||
/// and its profile password is too weak, warn here (readable, focus-clean, via the same settled
|
||||
/// notice sequence as the other startup warnings) and offer to open the service settings to fix it
|
||||
/// (Ed, 2026-07-27 — "people won't know to change the service password"). Self-resolving: once the
|
||||
/// service password is strengthened, this never fires again.</summary>
|
||||
private void MaybeWarnWeakServicePassword()
|
||||
{
|
||||
if (IsDisposed || CuePlayer.GloballyMuted) return;
|
||||
try
|
||||
{
|
||||
var installed = ServiceControl.Query() is not (ServiceState.NotInstalled or ServiceState.Unknown);
|
||||
var sp = ServiceStore.LoadProfile();
|
||||
var pw = string.IsNullOrEmpty(sp?.Password) ? "" : RemSoundCrypto.Deobfuscate(sp!.Password);
|
||||
if (!ServicePasswordNeedsStrengthening(installed, pw)) return;
|
||||
}
|
||||
catch { return; } // service state unreadable (e.g. Win7 without the feature) — nothing to warn about
|
||||
var open = ForegroundDialog.Show(owner => MessageBox.Show(owner,
|
||||
"Your RemSound background service is using a password that's too weak to meet the new "
|
||||
+ "security rules, so the service won't stream until the password is changed. (The "
|
||||
+ "service can't ask you itself, because it runs in the background with no window.)\n\n"
|
||||
+ "Use at least 8 characters — three unrelated words with a number, like kettle9tiger42moon, works well.\n\n"
|
||||
+ "Would you like to open the service settings now to change it?",
|
||||
"RemSound — service password needs strengthening",
|
||||
MessageBoxButtons.YesNo, MessageBoxIcon.Warning));
|
||||
if (open == DialogResult.Yes && !IsDisposed) ConfigureServiceProfile();
|
||||
}
|
||||
|
||||
/// <summary>Pure, testable: should the app nag about the SERVICE profile's password? Only when a
|
||||
/// service is installed AND its password is set-but-weak — the regression case (a service that
|
||||
/// used to stream goes silent after the 5.6 auto-update). An unset password is a never-configured
|
||||
/// service, not a regression, so it's left alone here.</summary>
|
||||
internal static bool ServicePasswordNeedsStrengthening(bool serviceInstalled, string? servicePlainPassword) =>
|
||||
serviceInstalled && !string.IsNullOrEmpty(servicePlainPassword) && PasswordStrength.Critique(servicePlainPassword) is not null;
|
||||
|
||||
/// <summary>Startup warning for a profile whose password is too weak to stream under the 5.6 rule.
|
||||
/// Runs from the SETTLED post-launch notice sequence — after the window is fully shown and
|
||||
/// activated, one dialog at a time — NOT from the mid-connect crypto path where an earlier version
|
||||
|
||||
@@ -2569,7 +2569,14 @@ internal static class SelfTest
|
||||
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";
|
||||
// The headless service can't ask for a password, so the app nags on launch when an INSTALLED
|
||||
// service has a set-but-weak password (the silent-death-after-auto-update case, Ed 2026-07-27).
|
||||
Check(MainForm.ServicePasswordNeedsStrengthening(serviceInstalled: true, "Games"), "installed service + weak password → the app must warn");
|
||||
Check(!MainForm.ServicePasswordNeedsStrengthening(serviceInstalled: false, "Games"), "no service installed → nothing to warn about");
|
||||
Check(!MainForm.ServicePasswordNeedsStrengthening(serviceInstalled: true, ""), "an unset service password is 'not configured', not a weak-password regression");
|
||||
Check(!MainForm.ServicePasswordNeedsStrengthening(serviceInstalled: true, "kettle9tiger42moon"), "a strong service password needs no nag");
|
||||
|
||||
return "weak+common refused with advice; derivation refuses weak everywhere; cached; app+service weak surfaced";
|
||||
}
|
||||
|
||||
/// <summary>The relay address-proof (2026-07-27): an AddrCheck cookie arriving at the receiver
|
||||
|
||||
@@ -42,7 +42,9 @@ internal sealed class ServiceProfileDialog : Form
|
||||
private const SendRate ServiceSendRate = ServiceAudioDefaults.Rate;
|
||||
|
||||
// --- Button row ---
|
||||
private readonly Button saveButton = new() { Text = "&Save and Close", AutoSize = true, DialogResult = DialogResult.OK };
|
||||
// No auto-close DialogResult: the Click handler validates the password strength first (a weak
|
||||
// service password means the headless service silently won't stream), then closes explicitly.
|
||||
private readonly Button saveButton = new() { Text = "&Save and Close", AutoSize = true };
|
||||
private readonly Button cancelButton = new() { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||
private readonly Button additionalButton = new() { Text = "Additional &options...", AutoSize = true, AccessibleName = "Additional options" };
|
||||
|
||||
@@ -80,7 +82,13 @@ internal sealed class ServiceProfileDialog : Form
|
||||
|
||||
AcceptButton = saveButton;
|
||||
CancelButton = cancelButton;
|
||||
saveButton.Click += (_, _) => SaveToProfile();
|
||||
saveButton.Click += (_, _) =>
|
||||
{
|
||||
if (!ConfirmOrFixWeakPassword()) return; // user chose to keep editing / cancel the fix
|
||||
SaveToProfile();
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
};
|
||||
additionalButton.Click += (_, _) => ShowAdditionalOptions();
|
||||
}
|
||||
|
||||
@@ -329,6 +337,32 @@ internal sealed class ServiceProfileDialog : Form
|
||||
UpdatePasswordStatus();
|
||||
}
|
||||
|
||||
/// <summary>On Save: if the service password is too weak the headless service will silently not
|
||||
/// stream, so warn and offer to strengthen it right now. Returns true to proceed with the save
|
||||
/// (either the password is fine, was just strengthened, or the user chose to save anyway), false
|
||||
/// to stay in the dialog. Empty is left alone — that's an un-configured profile, not a regression.</summary>
|
||||
private bool ConfirmOrFixWeakPassword()
|
||||
{
|
||||
var current = string.IsNullOrEmpty(working.Password) ? "" : RemSoundCrypto.Deobfuscate(working.Password);
|
||||
if (string.IsNullOrEmpty(current) || PasswordStrength.Critique(current) is null) return true;
|
||||
|
||||
var choice = ForegroundDialog.Show(owner => MessageBox.Show(owner,
|
||||
"This service password is too weak to meet the new security rules, so the service won't "
|
||||
+ "stream until it's strengthened.\n\n"
|
||||
+ "Use at least 8 characters — three unrelated words with a number, like kettle9tiger42moon, works well.\n\n"
|
||||
+ "Set a stronger password now?",
|
||||
"RemSound — service password needs strengthening",
|
||||
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning));
|
||||
if (choice == DialogResult.Cancel) return false; // back to the dialog, nothing saved
|
||||
if (choice == DialogResult.No) return true; // save the weak one anyway (warned)
|
||||
// Yes → enter a stronger one now (requireStrong disables the unchanged-exemption).
|
||||
var stronger = ProfilePasswordDialog.Show(ServiceControl.ServiceProfileTitle, current, requireNonEmpty: true, requireStrong: true);
|
||||
if (string.IsNullOrEmpty(stronger)) return false; // cancelled the change — stay put
|
||||
working.Password = RemSoundCrypto.Obfuscate(stronger);
|
||||
UpdatePasswordStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdatePasswordStatus()
|
||||
=> passwordStatus.Text = string.IsNullOrEmpty(working.Password) ? "No password set." : "Password set.";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user