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:
Ednunp
2026-07-27 12:03:49 +01:00
co-authored by Claude Fable 5
parent 9577fc9940
commit e82268f925
4 changed files with 84 additions and 4 deletions
+39
View File
@@ -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
+8 -1
View File
@@ -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
+36 -2
View File
@@ -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.";