SEBWIN-220: Corrected configuration algorithm to also verify the current administrator password when reconfiguring during application startup.

This commit is contained in:
dbuechel 2019-01-30 14:43:41 +01:00
parent 5641dc3e4b
commit 7173109d05
10 changed files with 304 additions and 176 deletions

View file

@ -129,7 +129,7 @@ namespace SafeExamBrowser.Client.UnitTests.Communication
public void MustHandlePasswordRequestCorrectly()
{
var passwordRequested = false;
var purpose = PasswordRequestPurpose.Administrator;
var purpose = PasswordRequestPurpose.LocalAdministrator;
var requestId = Guid.NewGuid();
var resetEvent = new AutoResetEvent(false);

View file

@ -285,13 +285,28 @@ namespace SafeExamBrowser.Client
private void ClientHost_PasswordRequested(PasswordRequestEventArgs args)
{
var isAdmin = args.Purpose == PasswordRequestPurpose.Administrator;
var message = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequired : TextKey.PasswordDialog_SettingsPasswordRequired;
var title = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequiredTitle : TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
var message = default(TextKey);
var title = default(TextKey);
logger.Info($"Received input request with id '{args.RequestId}' for the {args.Purpose.ToString().ToLower()} password.");
switch (args.Purpose)
{
case PasswordRequestPurpose.LocalAdministrator:
message = TextKey.PasswordDialog_LocalAdminPasswordRequired;
title = TextKey.PasswordDialog_LocalAdminPasswordRequiredTitle;
break;
case PasswordRequestPurpose.LocalSettings:
message = TextKey.PasswordDialog_LocalSettingsPasswordRequired;
title = TextKey.PasswordDialog_LocalSettingsPasswordRequiredTitle;
break;
case PasswordRequestPurpose.Settings:
message = TextKey.PasswordDialog_SettingsPasswordRequired;
title = TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
break;
}
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
var result = dialog.Show();
runtime.SubmitPassword(args.RequestId, result.Success, result.Password);

View file

@ -252,7 +252,7 @@ namespace SafeExamBrowser.Configuration.DataFormats
if (status != LoadStatus.Success)
{
logger.Error($"Element '{element}' is not supported!");
logger.Error($"Element '{element}' is not a supported value type!");
}
return status;

View file

@ -14,9 +14,14 @@ namespace SafeExamBrowser.Contracts.Communication.Data
public enum PasswordRequestPurpose
{
/// <summary>
/// The password is to be used as administrator password for an application configuration.
/// The password is to be used as administrator password for the local client configuration.
/// </summary>
Administrator,
LocalAdministrator,
/// <summary>
/// The password is to be used as settings password for the local client configuration.
/// </summary>
LocalSettings,
/// <summary>
/// The password is to be used as settings password for an application configuration.

View file

@ -85,10 +85,12 @@ namespace SafeExamBrowser.Contracts.I18n
OperationStatus_WaitExplorerStartup,
OperationStatus_WaitExplorerTermination,
OperationStatus_WaitRuntimeDisconnection,
PasswordDialog_AdminPasswordRequired,
PasswordDialog_AdminPasswordRequiredTitle,
PasswordDialog_Cancel,
PasswordDialog_Confirm,
PasswordDialog_LocalAdminPasswordRequired,
PasswordDialog_LocalAdminPasswordRequiredTitle,
PasswordDialog_LocalSettingsPasswordRequired,
PasswordDialog_LocalSettingsPasswordRequiredTitle,
PasswordDialog_QuitPasswordRequired,
PasswordDialog_QuitPasswordRequiredTitle,
PasswordDialog_SettingsPasswordRequired,

View file

@ -213,18 +213,24 @@
<Entry key="OperationStatus_WaitRuntimeDisconnection">
Waiting for the runtime to disconnect
</Entry>
<Entry key="PasswordDialog_AdminPasswordRequired">
Please enter the administrator password for the application configuration:
</Entry>
<Entry key="PasswordDialog_AdminPasswordRequiredTitle">
Administrator Password Required
</Entry>
<Entry key="PasswordDialog_Cancel">
Cancel
</Entry>
<Entry key="PasswordDialog_Confirm">
Confirm
</Entry>
<Entry key="PasswordDialog_LocalAdminPasswordRequired">
Please enter the administrator password for the local client configuration:
</Entry>
<Entry key="PasswordDialog_LocalAdminPasswordRequiredTitle">
Administrator Password Required
</Entry>
<Entry key="PasswordDialog_LocalSettingsPasswordRequired">
Please enter the settings password for the local client configuration:
</Entry>
<Entry key="PasswordDialog_LocalSettingsPasswordRequiredTitle">
Settings Password Required
</Entry>
<Entry key="PasswordDialog_QuitPasswordRequired">
Please enter the quit password in order to terminate the application:
</Entry>
@ -232,7 +238,7 @@
Quit Password Required
</Entry>
<Entry key="PasswordDialog_SettingsPasswordRequired">
Please enter the settings password for the application configuration:
Please enter the settings password for the selected application configuration:
</Entry>
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
Settings Password Required

View file

@ -102,7 +102,7 @@ namespace SafeExamBrowser.Runtime.Operations
var clientExecutable = Context.Next.AppConfig.ClientExecutablePath;
var clientLogFile = $"{'"' + Context.Next.AppConfig.ClientLogFile + '"'}";
var clientLogLevel = logger.LogLevel.ToString();
var clientLogLevel = Context.Next.Settings.LogLevel.ToString();
var runtimeHostUri = Context.Next.AppConfig.RuntimeAddress;
var startupToken = Context.Next.StartupToken.ToString("D");

View file

@ -59,11 +59,11 @@ namespace SafeExamBrowser.Runtime.Operations
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
var isValidUri = TryInitializeSettingsUri(out Uri uri);
var isValidUri = TryInitializeSettingsUri(out var uri, out var source);
if (isValidUri)
{
result = LoadSettings(uri);
result = LoadSettingsForStartup(uri, source);
}
else
{
@ -81,11 +81,11 @@ namespace SafeExamBrowser.Runtime.Operations
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
var isValidUri = TryValidateSettingsUri(Context.ReconfigurationFilePath, out Uri uri);
var isValidUri = TryValidateSettingsUri(Context.ReconfigurationFilePath, out var uri);
if (isValidUri)
{
result = LoadSettings(uri);
result = LoadSettingsForReconfiguration(uri);
}
else
{
@ -104,66 +104,88 @@ namespace SafeExamBrowser.Runtime.Operations
private OperationResult LoadDefaultSettings()
{
logger.Info("No valid configuration resource specified nor found in PROGRAMDATA or APPDATA - loading default settings...");
logger.Info("No valid configuration resource specified and no local client configuration found - loading default settings...");
Context.Next.Settings = configuration.LoadDefaultSettings();
return OperationResult.Success;
}
private OperationResult LoadSettings(Uri uri)
private OperationResult LoadSettingsForStartup(Uri uri, UriSource source)
{
var passwordParams = new PasswordParameters { Password = string.Empty, IsHash = true };
var status = configuration.TryLoadSettings(uri, out var settings, passwordParams);
var currentPassword = default(string);
var passwordParams = default(PasswordParameters);
var settings = default(Settings);
var status = default(LoadStatus?);
if (status == LoadStatus.PasswordNeeded && Context.Current?.Settings.AdminPasswordHash != null)
if (source == UriSource.CommandLine)
{
passwordParams.Password = Context.Current.Settings.AdminPasswordHash;
passwordParams.IsHash = true;
var hasAppDataFile = File.Exists(AppDataFile);
var hasProgramDataFile = File.Exists(ProgramDataFile);
status = configuration.TryLoadSettings(uri, out settings, passwordParams);
}
for (int attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++)
{
var success = TryGetPassword(PasswordRequestPurpose.Settings, out var password);
if (success)
if (hasProgramDataFile)
{
passwordParams.Password = password;
passwordParams.IsHash = false;
status = TryLoadSettings(new Uri(ProgramDataFile, UriKind.Absolute), UriSource.ProgramData, out _, out settings);
}
else
else if (hasAppDataFile)
{
return OperationResult.Aborted;
status = TryLoadSettings(new Uri(AppDataFile, UriKind.Absolute), UriSource.AppData, out _, out settings);
}
status = configuration.TryLoadSettings(uri, out settings, passwordParams);
if ((!hasProgramDataFile && !hasAppDataFile) || status == LoadStatus.Success)
{
currentPassword = settings?.AdminPasswordHash;
status = TryLoadSettings(uri, source, out passwordParams, out settings, currentPassword);
}
}
Context.Next.Settings = settings;
if (settings != null)
else
{
logger.LogLevel = settings.LogLevel;
status = TryLoadSettings(uri, source, out passwordParams, out settings);
}
return HandleLoadResult(uri, settings, status, passwordParams);
if (status.HasValue)
{
return DetermineLoadResult(uri, source, settings, status.Value, passwordParams, currentPassword);
}
else
{
return OperationResult.Aborted;
}
}
private OperationResult HandleLoadResult(Uri uri, Settings settings, LoadStatus status, PasswordParameters password)
private OperationResult LoadSettingsForReconfiguration(Uri uri)
{
if (status == LoadStatus.LoadWithBrowser)
{
return HandleBrowserResource(uri);
}
var currentPassword = Context.Current.Settings.AdminPasswordHash;
var source = UriSource.Reconfiguration;
var status = TryLoadSettings(uri, source, out var passwordParams, out var settings, currentPassword);
if (status == LoadStatus.Success && settings.ConfigurationMode == ConfigurationMode.ConfigureClient)
if (status.HasValue)
{
return HandleClientConfiguration(uri, password);
return DetermineLoadResult(uri, source, settings, status.Value, passwordParams, currentPassword);
}
if (status == LoadStatus.Success)
else
{
return OperationResult.Aborted;
}
}
private OperationResult DetermineLoadResult(Uri uri, UriSource source, Settings settings, LoadStatus status, PasswordParameters passwordParams, string currentPassword = default(string))
{
if (status == LoadStatus.LoadWithBrowser || status == LoadStatus.Success)
{
var isNewConfiguration = source == UriSource.CommandLine || source == UriSource.Reconfiguration;
Context.Next.Settings = settings;
if (status == LoadStatus.LoadWithBrowser)
{
return HandleBrowserResource(uri);
}
if (isNewConfiguration && settings.ConfigurationMode == ConfigurationMode.ConfigureClient)
{
return HandleClientConfiguration(uri, passwordParams, currentPassword);
}
return OperationResult.Success;
}
@ -180,132 +202,171 @@ namespace SafeExamBrowser.Runtime.Operations
return OperationResult.Success;
}
private OperationResult HandleClientConfiguration(Uri resource, PasswordParameters password)
private OperationResult HandleClientConfiguration(Uri uri, PasswordParameters passwordParams, string currentPassword = default(string))
{
var isAppDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(AppDataFile, StringComparison.OrdinalIgnoreCase);
var isProgramDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(ProgramDataFile, StringComparison.OrdinalIgnoreCase);
var isFirstSession = Context.Current == null;
var success = TryConfigureClient(uri, passwordParams, currentPassword);
if (!isAppDataFile && !isProgramDataFile)
if (success == true)
{
var requiresAuthentication = IsAuthenticationRequiredForClientConfiguration(password);
logger.Info("Starting client configuration...");
if (requiresAuthentication)
if (isFirstSession && AbortAfterClientConfiguration())
{
var result = HandleClientConfigurationAuthentication();
if (result != OperationResult.Success)
{
return result;
}
}
else
{
logger.Info("Authentication is not required.");
}
var status = configuration.ConfigureClientWith(resource, password);
if (status == SaveStatus.Success)
{
logger.Info("Client configuration was successful.");
}
else
{
logger.Error($"Client configuration failed with status '{status}'!");
ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs());
return OperationResult.Failed;
}
if (isFirstSession)
{
var result = HandleClientConfigurationOnStartup();
if (result != OperationResult.Success)
{
return result;
}
}
}
return OperationResult.Success;
}
private bool IsAuthenticationRequiredForClientConfiguration(PasswordParameters password)
{
var requiresAuthentication = Context.Current?.Settings.AdminPasswordHash != null;
if (requiresAuthentication)
{
var currentPassword = Context.Current.Settings.AdminPasswordHash;
var nextPassword = Context.Next.Settings.AdminPasswordHash;
var hasSettingsPassword = password.Password != null;
var sameAdminPassword = currentPassword.Equals(nextPassword, StringComparison.OrdinalIgnoreCase);
requiresAuthentication = !sameAdminPassword;
if (requiresAuthentication && hasSettingsPassword)
{
var settingsPassword = password.IsHash ? password.Password : hashAlgorithm.GenerateHashFor(password.Password);
var knowsAdminPassword = currentPassword.Equals(settingsPassword, StringComparison.OrdinalIgnoreCase);
requiresAuthentication = !knowsAdminPassword;
}
}
return requiresAuthentication;
}
private OperationResult HandleClientConfigurationAuthentication()
{
var currentPassword = Context.Current.Settings.AdminPasswordHash;
var isSamePassword = false;
for (int attempts = 0; attempts < 5 && !isSamePassword; attempts++)
{
var success = TryGetPassword(PasswordRequestPurpose.Administrator, out var password);
if (success)
{
isSamePassword = currentPassword.Equals(hashAlgorithm.GenerateHashFor(password), StringComparison.OrdinalIgnoreCase);
}
else
{
logger.Info("Authentication was aborted.");
return OperationResult.Aborted;
}
}
if (isSamePassword)
{
logger.Info("Authentication was successful.");
return OperationResult.Success;
}
logger.Info("Authentication has failed!");
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
if (!success.HasValue)
{
return OperationResult.Aborted;
}
return OperationResult.Failed;
}
private OperationResult HandleClientConfigurationOnStartup()
private LoadStatus? TryLoadSettings(Uri uri, UriSource source, out PasswordParameters passwordParams, out Settings settings, string currentPassword = default(string))
{
passwordParams = new PasswordParameters { Password = string.Empty, IsHash = true };
var status = configuration.TryLoadSettings(uri, out settings, passwordParams);
if (status == LoadStatus.PasswordNeeded && currentPassword != default(string))
{
passwordParams.Password = currentPassword;
passwordParams.IsHash = true;
status = configuration.TryLoadSettings(uri, out settings, passwordParams);
}
for (int attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++)
{
var isLocalConfig = source == UriSource.AppData || source == UriSource.ProgramData;
var purpose = isLocalConfig ? PasswordRequestPurpose.LocalSettings : PasswordRequestPurpose.Settings;
var success = TryGetPassword(purpose, out var password);
if (success)
{
passwordParams.Password = password;
passwordParams.IsHash = false;
}
else
{
return null;
}
status = configuration.TryLoadSettings(uri, out settings, passwordParams);
}
return status;
}
private bool? TryConfigureClient(Uri uri, PasswordParameters passwordParams, string currentPassword = default(string))
{
var mustAuthenticate = IsRequiredToAuthenticateForClientConfiguration(passwordParams, currentPassword);
logger.Info("Starting client configuration...");
if (mustAuthenticate)
{
var authenticated = AuthenticateForClientConfiguration(currentPassword);
if (authenticated == true)
{
logger.Info("Authentication was successful.");
}
if (authenticated == false)
{
logger.Info("Authentication has failed!");
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
return false;
}
if (!authenticated.HasValue)
{
logger.Info("Authentication was aborted.");
return null;
}
}
else
{
logger.Info("Authentication is not required.");
}
var status = configuration.ConfigureClientWith(uri, passwordParams);
var success = status == SaveStatus.Success;
if (success)
{
logger.Info("Client configuration was successful.");
}
else
{
logger.Error($"Client configuration failed with status '{status}'!");
ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs());
}
return success;
}
private bool IsRequiredToAuthenticateForClientConfiguration(PasswordParameters passwordParams, string currentPassword = default(string))
{
var mustAuthenticate = currentPassword != default(string);
if (mustAuthenticate)
{
var nextPassword = Context.Next.Settings.AdminPasswordHash;
var hasSettingsPassword = passwordParams.Password != null;
var sameAdminPassword = currentPassword.Equals(nextPassword, StringComparison.OrdinalIgnoreCase);
if (sameAdminPassword)
{
mustAuthenticate = false;
}
else if (hasSettingsPassword)
{
var settingsPassword = passwordParams.IsHash ? passwordParams.Password : hashAlgorithm.GenerateHashFor(passwordParams.Password);
var knowsAdminPassword = currentPassword.Equals(settingsPassword, StringComparison.OrdinalIgnoreCase);
mustAuthenticate = !knowsAdminPassword;
}
}
return mustAuthenticate;
}
private bool? AuthenticateForClientConfiguration(string currentPassword)
{
var authenticated = false;
for (int attempts = 0; attempts < 5 && !authenticated; attempts++)
{
var success = TryGetPassword(PasswordRequestPurpose.LocalAdministrator, out var password);
if (success)
{
authenticated = currentPassword.Equals(hashAlgorithm.GenerateHashFor(password), StringComparison.OrdinalIgnoreCase);
}
else
{
return null;
}
}
return authenticated;
}
private bool AbortAfterClientConfiguration()
{
var args = new ConfigurationCompletedEventArgs();
ActionRequired?.Invoke(args);
logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} startup after successful client configuration.");
if (args.AbortStartup)
{
return OperationResult.Aborted;
}
return OperationResult.Success;
return args.AbortStartup;
}
private void ShowFailureMessage(LoadStatus status, Uri uri)
@ -337,32 +398,32 @@ namespace SafeExamBrowser.Runtime.Operations
return args.Success;
}
private bool TryInitializeSettingsUri(out Uri uri)
private bool TryInitializeSettingsUri(out Uri uri, out UriSource source)
{
var path = default(string);
var isValidUri = false;
uri = null;
source = default(UriSource);
if (commandLineArgs?.Length > 1)
{
path = commandLineArgs[1];
isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri);
logger.Info($"Found command-line argument for configuration resource: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}.");
isValidUri = Uri.TryCreate(commandLineArgs[1], UriKind.Absolute, out uri);
source = UriSource.CommandLine;
logger.Info($"Found command-line argument for configuration resource: '{uri}', the URI is {(isValidUri ? "valid" : "invalid")}.");
}
if (!isValidUri && File.Exists(ProgramDataFile))
{
path = ProgramDataFile;
isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri);
logger.Info($"Found configuration file in PROGRAMDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}.");
isValidUri = Uri.TryCreate(ProgramDataFile, UriKind.Absolute, out uri);
source = UriSource.ProgramData;
logger.Info($"Found configuration file in PROGRAMDATA directory: '{uri}'.");
}
if (!isValidUri && File.Exists(AppDataFile))
{
path = AppDataFile;
isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri);
logger.Info($"Found configuration file in APPDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}.");
isValidUri = Uri.TryCreate(AppDataFile, UriKind.Absolute, out uri);
source = UriSource.AppData;
logger.Info($"Found configuration file in APPDATA directory: '{uri}'.");
}
return isValidUri;
@ -393,5 +454,14 @@ namespace SafeExamBrowser.Runtime.Operations
break;
}
}
private enum UriSource
{
Undefined,
AppData,
CommandLine,
ProgramData,
Reconfiguration
}
}
}

View file

@ -26,6 +26,7 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Perform()
{
SwitchLogSeverity();
ActivateNewSession();
return OperationResult.Success;
@ -33,6 +34,7 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Repeat()
{
SwitchLogSeverity();
ActivateNewSession();
return OperationResult.Success;
@ -43,6 +45,18 @@ namespace SafeExamBrowser.Runtime.Operations
return OperationResult.Success;
}
private void SwitchLogSeverity()
{
if (logger.LogLevel != Context.Next.Settings.LogLevel)
{
var current = logger.LogLevel.ToString().ToUpper();
var next = Context.Next.Settings.LogLevel.ToString().ToUpper();
logger.Info($"Switching from log severity '{current}' to '{next}' for new session.");
logger.LogLevel = Context.Next.Settings.LogLevel;
}
}
private void ActivateNewSession()
{
var isFirstSession = Context.Current == null;

View file

@ -452,9 +452,25 @@ namespace SafeExamBrowser.Runtime
private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args)
{
var isAdmin = args.Purpose == PasswordRequestPurpose.Administrator;
var message = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequired : TextKey.PasswordDialog_SettingsPasswordRequired;
var title = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequiredTitle : TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
var message = default(TextKey);
var title = default(TextKey);
switch (args.Purpose)
{
case PasswordRequestPurpose.LocalAdministrator:
message = TextKey.PasswordDialog_LocalAdminPasswordRequired;
title = TextKey.PasswordDialog_LocalAdminPasswordRequiredTitle;
break;
case PasswordRequestPurpose.LocalSettings:
message = TextKey.PasswordDialog_LocalSettingsPasswordRequired;
title = TextKey.PasswordDialog_LocalSettingsPasswordRequiredTitle;
break;
case PasswordRequestPurpose.Settings:
message = TextKey.PasswordDialog_SettingsPasswordRequired;
title = TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
break;
}
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
var result = dialog.Show(runtimeWindow);