diff --git a/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs b/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs index cb1dbb3d..7835f450 100644 --- a/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs @@ -42,7 +42,7 @@ namespace SafeExamBrowser.Browser.Handlers { var uri = new Uri(downloadItem.Url); var extension = Path.GetExtension(uri.AbsolutePath); - var isConfigFile = String.Equals(extension, appConfig.ConfigurationFileExtension, StringComparison.InvariantCultureIgnoreCase); + var isConfigFile = String.Equals(extension, appConfig.ConfigurationFileExtension, StringComparison.OrdinalIgnoreCase); logger.Debug($"Handling download request for '{uri}'."); diff --git a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs index ecab0473..0892f306 100644 --- a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs +++ b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs @@ -24,9 +24,10 @@ namespace SafeExamBrowser.Configuration.UnitTests public void Initialize() { var executablePath = Assembly.GetExecutingAssembly().Location; + var hashAlgorithm = new Mock(); var logger = new Mock(); - sut = new ConfigurationRepository(logger.Object, executablePath, string.Empty, string.Empty, string.Empty); + sut = new ConfigurationRepository(hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty); } [TestMethod] diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index 89053846..ed4a959f 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using SafeExamBrowser.Configuration.DataFormats; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; @@ -26,15 +27,23 @@ namespace SafeExamBrowser.Configuration private readonly string programVersion; private AppConfig appConfig; + private IHashAlgorithm hashAlgorithm; private IList dataFormats; - private ILogger logger; private IList resourceLoaders; + private ILogger logger; - public ConfigurationRepository(ILogger logger, string executablePath, string programCopyright, string programTitle, string programVersion) + public ConfigurationRepository( + IHashAlgorithm hashAlgorithm, + ILogger logger, + string executablePath, + string programCopyright, + string programTitle, + string programVersion) { dataFormats = new List(); resourceLoaders = new List(); + this.hashAlgorithm = hashAlgorithm; this.logger = logger; this.executablePath = executablePath ?? string.Empty; this.programCopyright = programCopyright ?? string.Empty; @@ -124,7 +133,7 @@ namespace SafeExamBrowser.Configuration resourceLoaders.Add(resourceLoader); } - public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string password = null, bool passwordIsHash = false) + public LoadStatus TryLoadSettings(Uri resource, PasswordInfo passwordInfo, out Settings settings) { logger.Info($"Attempting to load '{resource}'..."); @@ -136,15 +145,17 @@ namespace SafeExamBrowser.Configuration using (data) { - switch (status) + if (status == LoadStatus.LoadWithBrowser) { - case LoadStatus.LoadWithBrowser: - return HandleBrowserResource(resource, settings); - case LoadStatus.Success: - return TryParseData(data, settings, password, passwordIsHash); + return HandleBrowserResource(resource, settings); } - return status; + if (status != LoadStatus.Success) + { + return status; + } + + return TryParseData(data, passwordInfo, resource, settings); } } catch (Exception e) @@ -155,6 +166,35 @@ namespace SafeExamBrowser.Configuration } } + private void ExtractAndImportCertificates(IDictionary data) + { + // TODO + } + + private LoadStatus HandleBrowserResource(Uri resource, Settings settings) + { + settings.Browser.StartUrl = resource.AbsoluteUri; + logger.Info($"The resource needs authentication or is HTML data, loaded default settings with '{resource}' as startup URL."); + + return LoadStatus.Success; + } + + private void HandleParseSuccess(ParseResult result, Settings settings, PasswordInfo passwordInfo, Uri resource) + { + var appDataFile = new Uri(Path.Combine(appConfig.AppDataFolder, appConfig.DefaultSettingsFileName)); + var programDataFile = new Uri(Path.Combine(appConfig.ProgramDataFolder, appConfig.DefaultSettingsFileName)); + var isAppDataFile = resource.AbsolutePath.Equals(appDataFile.AbsolutePath, StringComparison.OrdinalIgnoreCase); + var isProgramDataFile = resource.AbsolutePath.Equals(programDataFile.AbsolutePath, StringComparison.OrdinalIgnoreCase); + + logger.Info("Mapping settings data..."); + result.RawData.MapTo(settings); + + if (settings.ConfigurationMode == ConfigurationMode.ConfigureClient && !isAppDataFile && !isProgramDataFile) + { + result.Status = TryConfigureClient(result.RawData, settings, passwordInfo); + } + } + private LoadStatus TryLoadData(Uri resource, out Stream data) { var status = LoadStatus.NotSupported; @@ -175,15 +215,23 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus TryParseData(Stream data, Settings settings, string password = null, bool passwordIsHash = false) + private LoadStatus TryParseData(Stream data, PasswordInfo passwordInfo, Uri resource, Settings settings) { - var status = LoadStatus.NotSupported; var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); + var status = LoadStatus.NotSupported; if (dataFormat != null) { - status = dataFormat.TryParse(data, settings, password, passwordIsHash); - logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}."); + var result = dataFormat.TryParse(data, passwordInfo); + + logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {result.Status}."); + + if (result.Status == LoadStatus.Success || result.Status == LoadStatus.SuccessConfigureClient) + { + HandleParseSuccess(result, settings, passwordInfo, resource); + } + + status = result.Status; } else { @@ -193,12 +241,42 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus HandleBrowserResource(Uri resource, Settings settings) + private LoadStatus TryConfigureClient(IDictionary data, Settings settings, PasswordInfo passwordInfo) { - settings.Browser.StartUrl = resource.AbsoluteUri; - logger.Info($"The resource needs authentication or is HTML data, loaded default settings with '{resource}' as startup URL."); + logger.Info("Attempting to configure local client settings..."); - return LoadStatus.Success; + if (passwordInfo.AdminPasswordHash != null) + { + var adminPasswordHash = passwordInfo.AdminPassword != null ? hashAlgorithm.GenerateHashFor(passwordInfo.AdminPassword) : null; + var settingsPasswordHash = passwordInfo.SettingsPassword != null ? hashAlgorithm.GenerateHashFor(passwordInfo.SettingsPassword) : null; + var enteredCorrectPassword = passwordInfo.AdminPasswordHash.Equals(adminPasswordHash, StringComparison.OrdinalIgnoreCase); + var sameAdminPassword = passwordInfo.AdminPasswordHash.Equals(settings.AdminPasswordHash, StringComparison.OrdinalIgnoreCase); + var knowsAdminPassword = passwordInfo.AdminPasswordHash.Equals(settingsPasswordHash, StringComparison.OrdinalIgnoreCase); + + if (sameAdminPassword || knowsAdminPassword || enteredCorrectPassword) + { + logger.Info("Authentication was successful."); + } + else + { + logger.Info("Authentication has failed!"); + + return LoadStatus.AdminPasswordNeeded; + } + } + else + { + logger.Info("Authentication is not required."); + } + + // -> Certificates need to be imported and REMOVED from the settings before the data is saved! + ExtractAndImportCertificates(data); + + // Save configuration data as local client config under %APPDATA%! + // -> Default settings password for local client configuration appears to be string.Empty + // -> Local client configuration needs to again be encrypted in the same way as the original file was!! + + return LoadStatus.SuccessConfigureClient; } private void UpdateAppConfig() diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs index 0654e71e..f92fc767 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs @@ -11,7 +11,6 @@ using System.IO; using System.Text; using SafeExamBrowser.Configuration.DataFormats.Cryptography; using SafeExamBrowser.Contracts.Configuration; -using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats @@ -57,7 +56,7 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public LoadStatus TryParse(Stream data, Settings settings, string password = null, bool passwordIsHash = false) + public ParseResult TryParse(Stream data, PasswordInfo passwordInfo) { var prefix = ParsePrefix(data); var success = TryDetermineFormat(prefix, out FormatType format); @@ -76,22 +75,22 @@ namespace SafeExamBrowser.Configuration.DataFormats { case FormatType.Password: case FormatType.PasswordConfigureClient: - return ParsePasswordBlock(data, format, settings, password, passwordIsHash); + return ParsePasswordBlock(data, passwordInfo, format); case FormatType.PlainData: - return ParsePlainDataBlock(data, settings); + return ParsePlainDataBlock(data, passwordInfo); case FormatType.PublicKeyHash: - return ParsePublicKeyHashBlock(data, settings, password, passwordIsHash); + return ParsePublicKeyHashBlock(data, passwordInfo); case FormatType.PublicKeyHashWithSymmetricKey: - return ParsePublicKeyHashWithSymmetricKeyBlock(data, settings, password, passwordIsHash); + return ParsePublicKeyHashWithSymmetricKeyBlock(data, passwordInfo); } } logger.Error($"'{data}' starting with '{prefix}' does not match the binary format!"); - return LoadStatus.InvalidData; + return new ParseResult { Status = LoadStatus.InvalidData }; } - private LoadStatus ParsePasswordBlock(Stream data, FormatType format, Settings settings, string password, bool passwordIsHash) + private ParseResult ParsePasswordBlock(Stream data, PasswordInfo passwordInfo, FormatType format) { var decrypted = default(Stream); var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); @@ -99,28 +98,32 @@ namespace SafeExamBrowser.Configuration.DataFormats if (format == FormatType.PasswordConfigureClient) { - password = password == null ? string.Empty : (passwordIsHash ? password : hashAlgorithm.GenerateHashFor(password)); - status = encryption.Decrypt(data, out decrypted, password); + status = encryption.Decrypt(data, string.Empty, out decrypted); - if (status == LoadStatus.PasswordNeeded && passwordIsHash && password != string.Empty) + if (status == LoadStatus.SettingsPasswordNeeded && passwordInfo.AdminPasswordHash != null) { - status = encryption.Decrypt(data, out decrypted, string.Empty); + status = encryption.Decrypt(data, passwordInfo.AdminPasswordHash, out decrypted); + } + + if (status == LoadStatus.SettingsPasswordNeeded && passwordInfo.SettingsPassword != null) + { + status = encryption.Decrypt(data, hashAlgorithm.GenerateHashFor(passwordInfo.SettingsPassword), out decrypted); } } else { - status = encryption.Decrypt(data, out decrypted, password); + status = encryption.Decrypt(data, passwordInfo.SettingsPassword, out decrypted); } if (status == LoadStatus.Success) { - return ParsePlainDataBlock(decrypted, settings); + return ParsePlainDataBlock(decrypted, passwordInfo); } - return status; + return new ParseResult { Status = status }; } - private LoadStatus ParsePlainDataBlock(Stream data, Settings settings) + private ParseResult ParsePlainDataBlock(Stream data, PasswordInfo passwordInfo) { var xmlFormat = new XmlFormat(logger.CloneFor(nameof(XmlFormat))); @@ -129,23 +132,23 @@ namespace SafeExamBrowser.Configuration.DataFormats data = compressor.Decompress(data); } - return xmlFormat.TryParse(data, settings); + return xmlFormat.TryParse(data, passwordInfo); } - private LoadStatus ParsePublicKeyHashBlock(Stream data, Settings settings, string password, bool passwordIsHash) + private ParseResult ParsePublicKeyHashBlock(Stream data, PasswordInfo passwordInfo) { var encryption = new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); var status = encryption.Decrypt(data, out Stream decrypted); if (status == LoadStatus.Success) { - return TryParse(decrypted, settings, password, passwordIsHash); + return TryParse(decrypted, passwordInfo); } - return status; + return new ParseResult { Status = status }; } - private LoadStatus ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, Settings settings, string password, bool passwordIsHash) + private ParseResult ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, PasswordInfo passwordInfo) { var logger = this.logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)); var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); @@ -154,10 +157,10 @@ namespace SafeExamBrowser.Configuration.DataFormats if (status == LoadStatus.Success) { - return TryParse(decrypted, settings, password, passwordIsHash); + return TryParse(decrypted, passwordInfo); } - return status; + return new ParseResult { Status = status }; } private string ParsePrefix(Stream data) diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs index 7a9c88f1..8b92b995 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs +++ b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs @@ -31,13 +31,13 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography this.logger = logger; } - internal LoadStatus Decrypt(Stream data, out Stream decrypted, string password) + internal LoadStatus Decrypt(Stream data, string password, out Stream decrypted) { decrypted = default(Stream); if (password == null) { - return LoadStatus.PasswordNeeded; + return LoadStatus.SettingsPasswordNeeded; } var (version, options) = ParseHeader(data); @@ -64,7 +64,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography if (version != VERSION || options != OPTIONS) { - logger.Warn($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); + logger.Debug($"Unknown encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); } return (version, options); @@ -110,9 +110,9 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography private LoadStatus FailForInvalidHmac() { - logger.Warn($"The authentication failed due to an invalid password or corrupted data!"); + logger.Debug($"The authentication failed due to an invalid password or corrupted data!"); - return LoadStatus.PasswordNeeded; + return LoadStatus.SettingsPasswordNeeded; } private Stream Decrypt(Stream data, byte[] encryptionKey, int hmacLength) diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs index 678aa5fd..e897d15d 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs +++ b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs @@ -39,7 +39,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography var symmetricKey = ParseSymmetricKey(data, certificate); var stream = new SubStream(data, data.Position, data.Length - data.Position); - var status = passwordEncryption.Decrypt(stream, out decrypted, symmetricKey); + var status = passwordEncryption.Decrypt(stream, symmetricKey, out decrypted); return status; } diff --git a/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs b/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs index daf9b2fc..63af9af9 100644 --- a/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs +++ b/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs @@ -14,7 +14,7 @@ namespace SafeExamBrowser.Configuration.DataFormats { internal static class DataMapper { - internal static void MapTo(this Dictionary rawData, Settings settings) + internal static void MapTo(this IDictionary rawData, Settings settings) { foreach (var kvp in rawData) { @@ -26,6 +26,9 @@ namespace SafeExamBrowser.Configuration.DataFormats { switch (key) { + case "hashedAdminPassword": + settings.MapAdminPasswordHash(value); + break; case "sebConfigPurpose": settings.MapConfigurationMode(value); break; @@ -35,6 +38,14 @@ namespace SafeExamBrowser.Configuration.DataFormats } } + private static void MapAdminPasswordHash(this Settings settings, object value) + { + if (value is string hash) + { + settings.AdminPasswordHash = hash; + } + } + private static void MapConfigurationMode(this Settings settings, object value) { if (value is Int32 mode) @@ -45,9 +56,9 @@ namespace SafeExamBrowser.Configuration.DataFormats private static void MapStartUrl(this Settings settings, object value) { - if (value is string startUrl) + if (value is string url) { - settings.Browser.StartUrl = startUrl; + settings.Browser.StartUrl = url; } } } diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs index a2830bce..b74bd63e 100644 --- a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs @@ -13,7 +13,6 @@ using System.Text; using System.Xml; using System.Xml.Linq; using SafeExamBrowser.Contracts.Configuration; -using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats @@ -59,9 +58,9 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public LoadStatus TryParse(Stream data, Settings settings, string password = null, bool passwordIsHash = false) + public ParseResult TryParse(Stream data, PasswordInfo passwordInfo) { - var status = LoadStatus.InvalidData; + var result = new ParseResult { Status = LoadStatus.InvalidData }; var xmlSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; data.Seek(0, SeekOrigin.Begin); @@ -75,15 +74,11 @@ namespace SafeExamBrowser.Configuration.DataFormats if (hasRoot && hasDictionary) { logger.Debug($"Found root node, starting to parse data..."); - status = ParseDictionary(reader, rawData); - if (status == LoadStatus.Success) - { - logger.Debug("Mapping raw settings data..."); - rawData.MapTo(settings); - } + result.Status = ParseDictionary(reader, rawData); + result.RawData = rawData; - logger.Debug($"Finished parsing -> Result: {status}."); + logger.Debug($"Finished parsing -> Result: {result.Status}."); } else { @@ -91,7 +86,7 @@ namespace SafeExamBrowser.Configuration.DataFormats } } - return status; + return result; } private LoadStatus ParseArray(XmlReader reader, List array) diff --git a/SafeExamBrowser.Contracts/Communication/Data/PasswordRequestPurpose.cs b/SafeExamBrowser.Contracts/Communication/Data/PasswordRequestPurpose.cs index 9cd49f82..0fc1e96d 100644 --- a/SafeExamBrowser.Contracts/Communication/Data/PasswordRequestPurpose.cs +++ b/SafeExamBrowser.Contracts/Communication/Data/PasswordRequestPurpose.cs @@ -13,8 +13,6 @@ namespace SafeExamBrowser.Contracts.Communication.Data /// public enum PasswordRequestPurpose { - Undefined = 0, - /// /// The password is to be used as administrator password for an application configuration. /// diff --git a/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs b/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs index 63d99315..6ffaf821 100644 --- a/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs +++ b/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs @@ -16,6 +16,11 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts /// public interface IRuntimeHost : ICommunicationHost { + /// + /// Determines whether another application component may establish a connection with the host. + /// + bool AllowConnection { get; set; } + /// /// The startup token used for initial authentication. /// diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index f686709c..a9673d10 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -11,7 +11,7 @@ using System; namespace SafeExamBrowser.Contracts.Configuration { /// - /// The repository which controls the loading and initializing of configuration data. + /// The repository which controls the loading and saving of configuration data. /// public interface IConfigurationRepository { @@ -41,9 +41,9 @@ namespace SafeExamBrowser.Contracts.Configuration void Register(IResourceLoader resourceLoader); /// - /// Attempts to load settings from the specified resource, using the optional password. As long as the result is not - /// , the referenced settings may be null or in an undefinable state! + /// Attempts to load settings from the specified resource. As long as the result is not , + /// the referenced settings may be null or in an undefinable state! /// - LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string password = null, bool passwordIsHash = false); + LoadStatus TryLoadSettings(Uri resource, PasswordInfo passwordInfo, out Settings.Settings settings); } } diff --git a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs index 7d573a38..84e7e493 100644 --- a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs +++ b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs @@ -21,9 +21,8 @@ namespace SafeExamBrowser.Contracts.Configuration bool CanParse(Stream data); /// - /// Tries to parse the given data, using the optional password. As long as the result is not , - /// the referenced settings can be in an undefinable state! + /// Tries to parse the given data. /// - LoadStatus TryParse(Stream data, Settings.Settings settings, string password = null, bool passwordIsHash = false); + ParseResult TryParse(Stream data, PasswordInfo passwordInfo); } } diff --git a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs index 295d661d..b679222b 100644 --- a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs +++ b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs @@ -14,9 +14,14 @@ namespace SafeExamBrowser.Contracts.Configuration public enum LoadStatus { /// - /// Indicates that a resource does not comply with the declared data format. + /// Indicates that the current administrator password is needed to be allowed to configure the local client. /// - InvalidData = 1, + AdminPasswordNeeded = 1, + + /// + /// Indicates that a resource contains invalid data. + /// + InvalidData, /// /// Indicates that a resource needs to be loaded with the browser. @@ -29,15 +34,20 @@ namespace SafeExamBrowser.Contracts.Configuration NotSupported, /// - /// Indicates that a password is needed in order to load the settings. + /// Indicates that the settings password is needed in order to decrypt the settings. /// - PasswordNeeded, + SettingsPasswordNeeded, /// /// The settings were loaded successfully. /// Success, + /// + /// The settings were loaded and the local client configuration was performed successfully. + /// + SuccessConfigureClient, + /// /// An unexpected error occurred while trying to load the settings. /// diff --git a/SafeExamBrowser.Contracts/Configuration/ParseResult.cs b/SafeExamBrowser.Contracts/Configuration/ParseResult.cs new file mode 100644 index 00000000..9498408d --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/ParseResult.cs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System.Collections.Generic; + +namespace SafeExamBrowser.Contracts.Configuration +{ + /// + /// Defines the result of a data parsing operation by an . + /// + public class ParseResult + { + /// + /// The parsed settings data. Might be null or in an undefinable state, depending on . + /// + public IDictionary RawData { get; set; } + + /// + /// The status result of a parsing operation. + /// + public LoadStatus Status { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs b/SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs new file mode 100644 index 00000000..e2d188a1 --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Configuration +{ + /// + /// Holds all password data necessary to load an application configuration. + /// + public class PasswordInfo + { + /// + /// The current administrator password in plain text. + /// + public string AdminPassword { get; set; } + + /// + /// The hash code of the current administrator password. + /// + public string AdminPasswordHash { get; set; } + + /// + /// The settings password of the configuration in plain text. + /// + public string SettingsPassword { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index 79287bfe..c9372abf 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -24,6 +24,8 @@ namespace SafeExamBrowser.Contracts.I18n MessageBox_ConfigurationDownloadErrorTitle, MessageBox_InvalidConfigurationData, MessageBox_InvalidConfigurationDataTitle, + MessageBox_InvalidPasswordError, + MessageBox_InvalidPasswordErrorTitle, MessageBox_NotSupportedConfigurationResource, MessageBox_NotSupportedConfigurationResourceTitle, MessageBox_Quit, diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 53e764c7..5403ce7c 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -56,6 +56,8 @@ + + diff --git a/SafeExamBrowser.I18n/Text.xml b/SafeExamBrowser.I18n/Text.xml index 8fbbc7d1..a0defa0c 100644 --- a/SafeExamBrowser.I18n/Text.xml +++ b/SafeExamBrowser.I18n/Text.xml @@ -30,6 +30,12 @@ Configuration Error + + You failed to enter the correct password within 5 attempts. The application will now terminate... + + + Invalid Password + The configuration resource '%%URI%%' is not supported! diff --git a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs index b937ca6c..7f5dbbed 100644 --- a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs +++ b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs @@ -17,8 +17,7 @@ namespace SafeExamBrowser.Runtime.Communication { internal class RuntimeHost : BaseHost, IRuntimeHost { - private bool allowConnection = true; - + public bool AllowConnection { get; set; } public Guid StartupToken { private get; set; } public event CommunicationEventHandler ClientDisconnected; @@ -35,11 +34,11 @@ namespace SafeExamBrowser.Runtime.Communication protected override bool OnConnect(Guid? token = null) { var authenticated = StartupToken == token; - var accepted = allowConnection && authenticated; + var accepted = AllowConnection && authenticated; if (accepted) { - allowConnection = false; + AllowConnection = false; } return accepted; @@ -48,13 +47,6 @@ namespace SafeExamBrowser.Runtime.Communication protected override void OnDisconnect() { ClientDisconnected?.Invoke(); - // TODO: Handle client crash scenario! - // If a client crashes or hangs when terminating (which should not happen!), it could be that it never gets to disconnect from - // the RuntimeHost - in that case, allowConnection prohibits restarting a new session as long as it's only set here! - // -> Move AllowConnection to interface and reset it in RuntimeController? - // -> Only possible as long as just the client connects, with service and client a more elaborate solution will be needed! - // -> E.g. ClientId and ServiceId, and then AllowClientConnection and AllowServiceConnection? - allowConnection = true; } protected override Response OnReceive(Message message) diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 29cb066c..0344dd96 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -116,7 +116,7 @@ namespace SafeExamBrowser.Runtime var compressor = new GZipCompressor(new ModuleLogger(logger, nameof(GZipCompressor))); var repositoryLogger = new ModuleLogger(logger, nameof(ConfigurationRepository)); - configuration = new ConfigurationRepository(repositoryLogger, executable.Location, programCopyright, programTitle, programVersion); + configuration = new ConfigurationRepository(new HashAlgorithm(), repositoryLogger, executable.Location, programCopyright, programTitle, programVersion); appConfig = configuration.InitializeAppConfig(); configuration.Register(new BinaryFormat(compressor, new HashAlgorithm(), new ModuleLogger(logger, nameof(BinaryFormat)))); diff --git a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs index 81815abf..ecd087c0 100644 --- a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs @@ -106,12 +106,14 @@ namespace SafeExamBrowser.Runtime.Operations var token = Context.Next.StartupToken.ToString("D"); logger.Info("Starting new client process..."); + runtimeHost.AllowConnection = true; runtimeHost.ClientReady += clientReadyEventHandler; ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, hostUri, token); logger.Info("Waiting for client to complete initialization..."); clientReady = clientReadyEvent.WaitOne(timeout_ms); runtimeHost.ClientReady -= clientReadyEventHandler; + runtimeHost.AllowConnection = false; if (!clientReady) { diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index 1699bd7b..dde27129 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -35,8 +35,8 @@ namespace SafeExamBrowser.Runtime.Operations SessionContext sessionContext) : base(sessionContext) { this.commandLineArgs = commandLineArgs; - this.logger = logger; this.configuration = configuration; + this.logger = logger; } public override OperationResult Perform() @@ -50,7 +50,6 @@ namespace SafeExamBrowser.Runtime.Operations if (isValidUri) { result = LoadSettings(uri); - HandleClientConfiguration(ref result, uri); } else { @@ -73,7 +72,6 @@ namespace SafeExamBrowser.Runtime.Operations if (isValidUri) { result = LoadSettings(uri); - HandleClientConfiguration(ref result, uri); } else { @@ -100,36 +98,55 @@ namespace SafeExamBrowser.Runtime.Operations private OperationResult LoadSettings(Uri uri) { - var status = configuration.TryLoadSettings(uri, out Settings settings, Context.Current?.Settings?.AdminPasswordHash, true); + var settings = default(Settings); + var status = default(LoadStatus); + var passwordInfo = new PasswordInfo { AdminPasswordHash = Context.Current?.Settings?.AdminPasswordHash }; - for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++) + for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;) { - var result = TryGetPassword(); + status = configuration.TryLoadSettings(uri, passwordInfo, out settings); - if (!result.Success) + if (status != LoadStatus.AdminPasswordNeeded && status != LoadStatus.SettingsPasswordNeeded) + { + break; + } + + var success = TryGetPassword(status, passwordInfo); + + if (!success) { return OperationResult.Aborted; } - status = configuration.TryLoadSettings(uri, out settings, result.Password); + adminAttempts += status == LoadStatus.AdminPasswordNeeded ? 1 : 0; + settingsAttempts += status == LoadStatus.SettingsPasswordNeeded ? 1 : 0; } + Context.Next.Settings = settings; + if (status == LoadStatus.Success) { - Context.Next.Settings = settings; - } - else - { - ShowFailureMessage(status, uri); + return OperationResult.Success; } - return status == LoadStatus.Success ? OperationResult.Success : OperationResult.Failed; + if (status == LoadStatus.SuccessConfigureClient) + { + return HandleClientConfiguration(); + } + + ShowFailureMessage(status, uri); + + return OperationResult.Failed; } private void ShowFailureMessage(LoadStatus status, Uri uri) { switch (status) { + case LoadStatus.AdminPasswordNeeded: + case LoadStatus.SettingsPasswordNeeded: + ActionRequired?.Invoke(new InvalidPasswordMessageArgs()); + break; case LoadStatus.InvalidData: ActionRequired?.Invoke(new InvalidDataMessageArgs(uri.ToString())); break; @@ -142,14 +159,23 @@ namespace SafeExamBrowser.Runtime.Operations } } - private PasswordRequiredEventArgs TryGetPassword() + private bool TryGetPassword(LoadStatus status, PasswordInfo passwordInfo) { - var purpose = PasswordRequestPurpose.Settings; + var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings; var args = new PasswordRequiredEventArgs { Purpose = purpose }; ActionRequired?.Invoke(args); - return args; + if (purpose == PasswordRequestPurpose.Administrator) + { + passwordInfo.AdminPassword = args.Password; + } + else + { + passwordInfo.SettingsPassword = args.Password; + } + + return args.Success; } private bool TryInitializeSettingsUri(out Uri uri) @@ -195,37 +221,30 @@ namespace SafeExamBrowser.Runtime.Operations return isValidUri; } - private void HandleClientConfiguration(ref OperationResult result, Uri uri) + private OperationResult HandleClientConfiguration() { - var configureMode = Context.Next.Settings?.ConfigurationMode == ConfigurationMode.ConfigureClient; - var loadWithBrowser = Context.Next.Settings?.Browser.StartUrl == uri.AbsoluteUri; - var successful = result == OperationResult.Success; + var firstSession = Context.Current == null; - if (successful && configureMode && !loadWithBrowser) + if (firstSession) { var args = new ConfigurationCompletedEventArgs(); - var filePath = Path.Combine(Context.Next.AppConfig.AppDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); - - // TODO: Save / overwrite configuration file in APPDATA directory! - // -> Check whether current and new admin passwords are the same! If not, current needs to be verified before overwriting! - // -> Special case: If current admin password is same as new settings password, verification is not necessary! - // -> Default settings password appears to be string.Empty for local client configuration - // -> Any (new?) certificates need to be imported and REMOVED from the settings before the data is saved! - // -> DO NOT transform settings, just simply copy the given configuration file to %APPDATA%\SebClientSettings.seb !!! - //configuration.SaveSettings(Context.Next.Settings, filePath); - - // TODO: If the client configuration happens while the application is already running, the new configuration should first - // be loaded and then the user should have the option to terminate! - // -> Introduce flag in Context, e.g. AskForTermination? + ActionRequired?.Invoke(args); - logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} after successful client configuration."); if (args.AbortStartup) { - result = OperationResult.Aborted; + return OperationResult.Aborted; } } + else + { + // TODO: If the client configuration happens while the application is already running, the new configuration should first + // be loaded and then the user should have the option to terminate! + // -> Introduce flag in Context, e.g. AskForTermination? + } + + return OperationResult.Success; } private void LogOperationResult(OperationResult result) diff --git a/SafeExamBrowser.Runtime/Operations/Events/InvalidPasswordMessageArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/InvalidPasswordMessageArgs.cs new file mode 100644 index 00000000..d630a8ea --- /dev/null +++ b/SafeExamBrowser.Runtime/Operations/Events/InvalidPasswordMessageArgs.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.UserInterface.MessageBox; + +namespace SafeExamBrowser.Runtime.Operations.Events +{ + internal class InvalidPasswordMessageArgs : MessageEventArgs + { + internal InvalidPasswordMessageArgs() + { + Icon = MessageBoxIcon.Error; + Message = TextKey.MessageBox_InvalidPasswordError; + Title = TextKey.MessageBox_InvalidPasswordErrorTitle; + } + } +} diff --git a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj index df5110e6..1c1f8682 100644 --- a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj +++ b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj @@ -91,6 +91,7 @@ +