diff --git a/DialogManager.cs b/DialogManager.cs
index 7d4deeb..1ee0f2b 100644
--- a/DialogManager.cs
+++ b/DialogManager.cs
@@ -317,12 +317,43 @@ private void LaunchDialog(List argList)
CreateNoWindow = false,
};
- _dialogProcess = Process.Start(startInfo);
+ // A shell-executed child cannot be given its own environment, only inherit
+ // ours, so the key is set on this process for the launch and removed again.
+ // It never goes on the command line or into the log.
+ var authKey = BootstrapMate.Core.DialogAuthKey.Resolve(
+ BootstrapMate.Core.ConfigManager.Instance.Config.DialogAuthKeyPath);
+ var callerKey = Environment.GetEnvironmentVariable(BootstrapMate.Core.DialogAuthKey.EnvironmentVariable);
+ try
+ {
+ if (authKey != null)
+ Environment.SetEnvironmentVariable(BootstrapMate.Core.DialogAuthKey.EnvironmentVariable, authKey);
+ _dialogProcess = Process.Start(startInfo);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(BootstrapMate.Core.DialogAuthKey.EnvironmentVariable, callerKey);
+ }
+
+ // A dialog that refuses the key exits at once with 30. Catch that here so the
+ // run carries on headless instead of writing to a dialog that is not there.
+ if (_dialogProcess != null && _dialogProcess.WaitForExit(3000) &&
+ _dialogProcess.ExitCode == BootstrapMate.Core.DialogAuthKey.KeyRequiredExitCode)
+ {
+ Logger.Warning(authKey == null
+ ? "csharpdialog requires an authorisation key and none was found; continuing without the dialog"
+ : "csharpdialog rejected the authorisation key; continuing without the dialog");
+ _dialogProcess.Dispose();
+ _dialogProcess = null;
+ _isAvailable = false;
+ return;
+ }
if (_dialogProcess != null)
{
_isRunning = true;
- Logger.Info("csharpdialog launched successfully");
+ Logger.Info(authKey != null
+ ? "csharpdialog launched successfully (authorisation key supplied)"
+ : "csharpdialog launched successfully");
}
else
{
diff --git a/README.md b/README.md
index 55ecda3..daedba8 100644
--- a/README.md
+++ b/README.md
@@ -317,6 +317,17 @@ no simulated install, so a policy-set `DryRun` exits `1` without installing anyt
sets the dialog's icon. `SilentMode` and `VerboseMode` from policy or settings turn
those modes on, and a CLI switch cannot turn them off.
+## csharpDialog authorisation key
+
+When csharpDialog is managed with an authorisation key, it opens only for a caller that
+passes the matching plain key in the `DIALOG_AUTH_KEY` environment variable, and otherwise
+exits `30`. BootstrapMate passes its own `DIALOG_AUTH_KEY` through when it has one, and
+otherwise reads the key from the file named by `DialogAuthKeyPath` (policy or machine
+settings; default `C:\ProgramData\ManagedNotifications\authkey`), which should be readable
+only by SYSTEM and Administrators. The key is never put on the command line or logged. With
+no key, or a wrong one, the dialog does not open, a warning is logged, and the run carries
+on without it.
+
## Registry Status Contract
BootstrapMate tracks completion status in both 64-bit and 32-bit registry views:
diff --git a/resources/BootstrapMate.admx b/resources/BootstrapMate.admx
index 18c703c..0f1d543 100644
--- a/resources/BootstrapMate.admx
+++ b/resources/BootstrapMate.admx
@@ -166,6 +166,19 @@
+
+
+
+
+
+
+
+
Leave empty to use the default icon.
+ Dialog Authorisation Key File
+ Path to a file holding the plain csharpDialog authorisation key. When csharpDialog is managed with an authorisation key, BootstrapMate reads this file and passes the key to the dialog in the DIALOG_AUTH_KEY environment variable. The file should be readable only by SYSTEM and Administrators.
+
+If the key is missing or wrong the dialog does not open and the run continues without it.
+
+Default: C:\ProgramData\ManagedNotifications\authkey
+
Blur Screen Behind DialogWhen enabled, the screen behind the progress dialog is blurred to prevent user interaction during setup.
@@ -162,6 +169,12 @@ Default: Disabled
+
+
+
+
+
+
diff --git a/src/BootstrapMate.Core/BootstrapMateConfig.cs b/src/BootstrapMate.Core/BootstrapMateConfig.cs
index 5091bc2..19b571f 100644
--- a/src/BootstrapMate.Core/BootstrapMateConfig.cs
+++ b/src/BootstrapMate.Core/BootstrapMateConfig.cs
@@ -22,6 +22,8 @@ public sealed class BootstrapMateConfig
public string? DialogIcon { get; set; }
public bool BlurScreen { get; set; }
public bool NoDialog { get; set; }
+ /// File holding the plain csharpDialog authorisation key; null means the default path.
+ public string? DialogAuthKeyPath { get; set; }
// Reporting: vendor-neutral run-summary POST
public string? ReportingUrl { get; set; }
diff --git a/src/BootstrapMate.Core/BootstrapMateConstants.cs b/src/BootstrapMate.Core/BootstrapMateConstants.cs
index 732d8b5..5a3e686 100644
--- a/src/BootstrapMate.Core/BootstrapMateConstants.cs
+++ b/src/BootstrapMate.Core/BootstrapMateConstants.cs
@@ -47,6 +47,12 @@ public static class BootstrapMateConstants
/// Default dialog message.
public const string DefaultDialogMessage = "Please wait while we install required software...";
+ ///
+ /// Where the csharpDialog authorisation key is read from when no DialogAuthKeyPath is set.
+ /// The file should be readable only by SYSTEM and Administrators.
+ ///
+ public const string DefaultDialogAuthKeyPath = @"C:\ProgramData\ManagedNotifications\authkey";
+
/// Named pipe prefix for GUI ↔ CLI communication.
public const string PipeNamePrefix = "BootstrapMate_";
diff --git a/src/BootstrapMate.Core/ConfigManager.cs b/src/BootstrapMate.Core/ConfigManager.cs
index 13af99c..d9af2f2 100644
--- a/src/BootstrapMate.Core/ConfigManager.cs
+++ b/src/BootstrapMate.Core/ConfigManager.cs
@@ -179,6 +179,7 @@ void WriteInt(string key, int value)
WriteString("DialogMessage", settings.DialogMessage);
WriteString("DialogIcon", settings.DialogIcon);
WriteBool("BlurScreen", settings.BlurScreen);
+ WriteString("DialogAuthKeyPath", settings.DialogAuthKeyPath);
WriteString("CustomInstallPath", settings.CustomInstallPath);
WriteInt("NetworkTimeout", settings.NetworkTimeout);
WriteInt("BaselineMinIntervalHours", settings.BaselineMinIntervalHours);
@@ -265,6 +266,9 @@ private void LoadFromManagement(ManagementDetector management)
if (management.GetManagedString("DialogIcon") is { Length: > 0 } icon)
Config.DialogIcon = icon;
+ if (management.GetManagedString("DialogAuthKeyPath") is { Length: > 0 } keyPath)
+ Config.DialogAuthKeyPath = keyPath;
+
if (management.GetManagedBool("BlurScreen") is { } blur)
Config.BlurScreen = blur;
@@ -330,6 +334,7 @@ private void LoadFromHive(RegistryHive hive, ConfigSource source, RegistryView v
Config.DialogMessage = ReadString(settingsKey, "DialogMessage") ?? Config.DialogMessage;
Config.DialogIcon = ReadString(settingsKey, "DialogIcon") ?? Config.DialogIcon;
Config.BlurScreen = ReadBool(settingsKey, "BlurScreen") ?? Config.BlurScreen;
+ Config.DialogAuthKeyPath = ReadString(settingsKey, "DialogAuthKeyPath") ?? Config.DialogAuthKeyPath;
Config.CustomInstallPath = ReadString(settingsKey, "CustomInstallPath") ?? Config.CustomInstallPath;
Config.NetworkTimeout = ReadInt(settingsKey, "NetworkTimeout") ?? Config.NetworkTimeout;
Config.BaselineMinIntervalHours = ReadInt(settingsKey, "BaselineMinIntervalHours") ?? Config.BaselineMinIntervalHours;
diff --git a/src/BootstrapMate.Core/DialogAuthKey.cs b/src/BootstrapMate.Core/DialogAuthKey.cs
new file mode 100644
index 0000000..b3f213d
--- /dev/null
+++ b/src/BootstrapMate.Core/DialogAuthKey.cs
@@ -0,0 +1,41 @@
+namespace BootstrapMate.Core;
+
+///
+/// The csharpDialog authorisation key. When policy sets an AuthorisationKey for csharpDialog,
+/// the dialog refuses to open (exit 30) unless its caller passes the matching plain key in the
+/// DIALOG_AUTH_KEY environment variable. The key reaches this process either already in that
+/// variable or in a file only SYSTEM and Administrators can read.
+///
+public static class DialogAuthKey
+{
+ /// The environment variable csharpDialog reads the key from.
+ public const string EnvironmentVariable = "DIALOG_AUTH_KEY";
+
+ /// The exit code csharpDialog returns when the key is missing or wrong.
+ public const int KeyRequiredExitCode = 30;
+
+ ///
+ /// The key to hand csharpDialog, or null to launch it without one: the caller's own
+ /// DIALOG_AUTH_KEY if set, otherwise the trimmed contents of
+ /// (the default path when null or empty). A missing or unreadable file is not an error.
+ ///
+ public static string? Resolve(string? path, Func? readEnvironment = null)
+ {
+ var fromEnvironment = (readEnvironment ?? Environment.GetEnvironmentVariable)(EnvironmentVariable);
+ if (!string.IsNullOrWhiteSpace(fromEnvironment))
+ return fromEnvironment.Trim();
+
+ var file = string.IsNullOrWhiteSpace(path) ? BootstrapMateConstants.DefaultDialogAuthKeyPath : path;
+ try
+ {
+ if (!File.Exists(file))
+ return null;
+ var key = File.ReadAllText(file).Trim();
+ return key.Length > 0 ? key : null;
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/BootstrapMate.Core/ManagementDetector.cs b/src/BootstrapMate.Core/ManagementDetector.cs
index 5f6f42a..ccd768c 100644
--- a/src/BootstrapMate.Core/ManagementDetector.cs
+++ b/src/BootstrapMate.Core/ManagementDetector.cs
@@ -33,6 +33,7 @@ public sealed class ManagementDetector
["DialogMessage"] = ["DialogMessage"],
["DialogIcon"] = ["DialogIcon"],
["BlurScreen"] = ["BlurScreen"],
+ ["DialogAuthKeyPath"] = ["DialogAuthKeyPath"],
["CustomInstallPath"] = ["CustomInstallPath", "InstallPath", "iapath"],
["NetworkTimeout"] = ["NetworkTimeout"],
["BaselineMinIntervalHours"] = ["BaselineMinIntervalHours"],
diff --git a/tests/BootstrapMate.Core.Tests/DialogAuthKeyTests.cs b/tests/BootstrapMate.Core.Tests/DialogAuthKeyTests.cs
new file mode 100644
index 0000000..284005e
--- /dev/null
+++ b/tests/BootstrapMate.Core.Tests/DialogAuthKeyTests.cs
@@ -0,0 +1,43 @@
+using BootstrapMate.Core;
+using Xunit;
+
+namespace BootstrapMate.Core.Tests;
+
+public class DialogAuthKeyTests : IDisposable
+{
+ private readonly string _dir = Path.Combine(Path.GetTempPath(), "bm-authkey-" + Guid.NewGuid().ToString("N"));
+
+ public DialogAuthKeyTests() => Directory.CreateDirectory(_dir);
+
+ public void Dispose() => Directory.Delete(_dir, recursive: true);
+
+ private static string? NoEnvironment(string _) => null;
+
+ [Fact]
+ public void CallerEnvironmentWins()
+ {
+ var file = Path.Combine(_dir, "authkey");
+ File.WriteAllText(file, "from-file");
+ Assert.Equal("from-env", DialogAuthKey.Resolve(file, _ => " from-env "));
+ }
+
+ [Fact]
+ public void FileIsReadAndTrimmed()
+ {
+ var file = Path.Combine(_dir, "authkey");
+ File.WriteAllText(file, " secret-key\r\n");
+ Assert.Equal("secret-key", DialogAuthKey.Resolve(file, NoEnvironment));
+ }
+
+ [Fact]
+ public void MissingFileMeansNoKey() =>
+ Assert.Null(DialogAuthKey.Resolve(Path.Combine(_dir, "absent"), NoEnvironment));
+
+ [Fact]
+ public void EmptyFileMeansNoKey()
+ {
+ var file = Path.Combine(_dir, "authkey");
+ File.WriteAllText(file, "\r\n");
+ Assert.Null(DialogAuthKey.Resolve(file, NoEnvironment));
+ }
+}