From 75f5994a3bd30f01716f371fc9e0b2d6db242152 Mon Sep 17 00:00:00 2001 From: dbuechel Date: Fri, 14 Dec 2018 09:50:10 +0100 Subject: [PATCH] SEBWIN-221: Extended loading algorithm with functionality required to save data as local client configuration. --- .../ConfigurationRepositoryTests.cs | 1 + .../ConfigurationRepository.cs | 189 ++++++++------ .../{ => Cryptography}/HashAlgorithm.cs | 4 +- .../Cryptography/PasswordEncryption.cs | 6 +- .../Cryptography/PublicKeyHashEncryption.cs | 6 +- ...PublicKeyHashWithSymmetricKeyEncryption.cs | 6 +- .../GZipCompressor.cs | 4 +- .../DataFormats/BinaryFormat.cs | 90 ++++--- .../DataFormats/XmlFormat.cs | 9 +- .../FileResource.cs} | 12 +- .../NetworkResource.cs} | 12 +- .../SafeExamBrowser.Configuration.csproj | 14 +- .../Cryptography/EncryptionParameters.cs | 17 ++ .../{ => Cryptography}/IHashAlgorithm.cs | 2 +- .../Cryptography/PasswordParameters.cs | 26 ++ .../Cryptography/PublicKeyHashParameters.cs | 33 +++ .../{ => DataCompression}/IDataCompressor.cs | 2 +- .../Configuration/DataFormats/Format.cs | 19 ++ .../{ => DataFormats}/IDataFormat.cs | 5 +- .../{ => DataFormats}/ParseResult.cs | 13 +- .../IDataResource.cs} | 13 +- .../Configuration/IConfigurationRepository.cs | 24 +- .../Configuration/LoadStatus.cs | 14 +- .../Configuration/PasswordInfo.cs | 31 --- .../Configuration/SaveStatus.cs | 18 ++ .../SafeExamBrowser.Contracts.csproj | 16 +- SafeExamBrowser.Runtime/CompositionRoot.cs | 11 +- .../Operations/ConfigurationOperation.cs | 244 +++++++++++++----- SafeExamBrowser.Runtime/RuntimeController.cs | 11 +- 29 files changed, 572 insertions(+), 280 deletions(-) rename SafeExamBrowser.Configuration/{ => Cryptography}/HashAlgorithm.cs (87%) rename SafeExamBrowser.Configuration/{DataFormats => }/Cryptography/PasswordEncryption.cs (96%) rename SafeExamBrowser.Configuration/{DataFormats => }/Cryptography/PublicKeyHashEncryption.cs (95%) rename SafeExamBrowser.Configuration/{DataFormats => }/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs (92%) rename SafeExamBrowser.Configuration/{Compression => DataCompression}/GZipCompressor.cs (96%) rename SafeExamBrowser.Configuration/{ResourceLoaders/FileResourceLoader.cs => DataResources/FileResource.cs} (79%) rename SafeExamBrowser.Configuration/{ResourceLoaders/NetworkResourceLoader.cs => DataResources/NetworkResource.cs} (93%) create mode 100644 SafeExamBrowser.Contracts/Configuration/Cryptography/EncryptionParameters.cs rename SafeExamBrowser.Contracts/Configuration/{ => Cryptography}/IHashAlgorithm.cs (90%) create mode 100644 SafeExamBrowser.Contracts/Configuration/Cryptography/PasswordParameters.cs create mode 100644 SafeExamBrowser.Contracts/Configuration/Cryptography/PublicKeyHashParameters.cs rename SafeExamBrowser.Contracts/Configuration/{ => DataCompression}/IDataCompressor.cs (93%) create mode 100644 SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs rename SafeExamBrowser.Contracts/Configuration/{ => DataFormats}/IDataFormat.cs (77%) rename SafeExamBrowser.Contracts/Configuration/{ => DataFormats}/ParseResult.cs (65%) rename SafeExamBrowser.Contracts/Configuration/{IResourceLoader.cs => DataResources/IDataResource.cs} (58%) delete mode 100644 SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs create mode 100644 SafeExamBrowser.Contracts/Configuration/SaveStatus.cs diff --git a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs index 0892f306..6ac30d6c 100644 --- a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs +++ b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs @@ -11,6 +11,7 @@ using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.UnitTests diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index ed4a959f..314f6339 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -10,8 +10,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography.X509Certificates; using SafeExamBrowser.Configuration.DataFormats; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration.DataFormats; +using SafeExamBrowser.Contracts.Configuration.DataResources; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; @@ -29,7 +33,7 @@ namespace SafeExamBrowser.Configuration private AppConfig appConfig; private IHashAlgorithm hashAlgorithm; private IList dataFormats; - private IList resourceLoaders; + private IList dataResources; private ILogger logger; public ConfigurationRepository( @@ -41,7 +45,7 @@ namespace SafeExamBrowser.Configuration string programVersion) { dataFormats = new List(); - resourceLoaders = new List(); + dataResources = new List(); this.hashAlgorithm = hashAlgorithm; this.logger = logger; @@ -51,6 +55,35 @@ namespace SafeExamBrowser.Configuration this.programVersion = programVersion ?? string.Empty; } + public void ConfigureClientWith(Uri resource, EncryptionParameters encryption = null) + { + logger.Info($"Attempting to configure local client settings from '{resource}'..."); + + try + { + TryLoadData(resource, out Stream stream); + + using (stream) + { + // TODO: + //TryParseData(stream, encryption, out _, out _, out var data); + //HandleIdentityCertificates(data); + + // Save configuration data as local client config under %APPDATA%! + // -> New key will determine whether to use default password or current settings password! + // -> "clientConfigEncryptUsingSettingsPassword" + // -> Default settings password for local client configuration appears to be string.Empty -> passwords.SettingsPassword + // -> Otherwise, the local client configuration must again be encrypted in the same way as the original file!! + } + + logger.Info($"Successfully configured local client settings with '{resource}'."); + } + catch (Exception e) + { + logger.Error($"Unexpected error while trying to configure local client settings '{resource}'!", e); + } + } + public AppConfig InitializeAppConfig() { var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(SafeExamBrowser)); @@ -128,34 +161,38 @@ namespace SafeExamBrowser.Configuration dataFormats.Add(dataFormat); } - public void Register(IResourceLoader resourceLoader) + public void Register(IDataResource dataResource) { - resourceLoaders.Add(resourceLoader); + dataResources.Add(dataResource); } - public LoadStatus TryLoadSettings(Uri resource, PasswordInfo passwordInfo, out Settings settings) + public LoadStatus TryLoadSettings(Uri resource, PasswordParameters password, out EncryptionParameters encryption, out Format format, out Settings settings) { logger.Info($"Attempting to load '{resource}'..."); + encryption = default(EncryptionParameters); + format = default(Format); settings = LoadDefaultSettings(); try { - var status = TryLoadData(resource, out Stream data); + var status = TryLoadData(resource, out Stream stream); - using (data) + using (stream) { - if (status == LoadStatus.LoadWithBrowser) - { - return HandleBrowserResource(resource, settings); - } - if (status != LoadStatus.Success) { return status; } - return TryParseData(data, passwordInfo, resource, settings); + status = TryParseData(stream, password, out encryption, out format, out var data); + + if (status == LoadStatus.Success) + { + data.MapTo(settings); + } + + return status; } } catch (Exception e) @@ -166,39 +203,65 @@ namespace SafeExamBrowser.Configuration } } - private void ExtractAndImportCertificates(IDictionary data) + public SaveStatus TrySaveSettings(Uri resource, Format format, Settings settings, EncryptionParameters encryption = null) { - // TODO + throw new NotImplementedException(); } - private LoadStatus HandleBrowserResource(Uri resource, Settings settings) + private void HandleIdentityCertificates(IDictionary data) { - settings.Browser.StartUrl = resource.AbsoluteUri; - logger.Info($"The resource needs authentication or is HTML data, loaded default settings with '{resource}' as startup URL."); + const int IDENTITY_CERTIFICATE = 1; + var hasCertificates = data.TryGetValue("embeddedCertificates", out object value); - 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) + if (hasCertificates && value is IList> certificates) { - result.Status = TryConfigureClient(result.RawData, settings, passwordInfo); + var toRemove = new List>(); + + foreach (var certificate in certificates) + { + var isIdentity = certificate.TryGetValue("type", out object t) && t is int type && type == IDENTITY_CERTIFICATE; + var hasData = certificate.TryGetValue("certificateData", out value); + + if (isIdentity && hasData && value is byte[] certificateData) + { + ImportIdentityCertificate(certificateData, new X509Store(StoreLocation.CurrentUser)); + ImportIdentityCertificate(certificateData, new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine)); + + toRemove.Add(certificate); + } + } + + toRemove.ForEach(c => certificates.Remove(c)); + } + } + + private void ImportIdentityCertificate(byte[] certificateData, X509Store store) + { + try + { + var certificate = new X509Certificate2(); + + certificate.Import(certificateData, "Di𝈭l𝈖Ch𝈒ah𝉇t𝈁a𝉈Hai1972", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet); + + store.Open(OpenFlags.ReadWrite); + store.Add(certificate); + + logger.Info($"Successfully imported identity certificate into {store.Location}.{store.Name}."); + } + catch (Exception e) + { + logger.Error($"Failed to import identity certificate into {store.Location}.{store.Name}!", e); + } + finally + { + store.Close(); } } private LoadStatus TryLoadData(Uri resource, out Stream data) { var status = LoadStatus.NotSupported; - var resourceLoader = resourceLoaders.FirstOrDefault(l => l.CanLoad(resource)); + var resourceLoader = dataResources.FirstOrDefault(l => l.CanLoad(resource)); data = default(Stream); @@ -215,23 +278,25 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus TryParseData(Stream data, PasswordInfo passwordInfo, Uri resource, Settings settings) + private LoadStatus TryParseData(Stream data, PasswordParameters password, out EncryptionParameters encryption, out Format format, out IDictionary rawData) { var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); var status = LoadStatus.NotSupported; + encryption = default(EncryptionParameters); + format = default(Format); + rawData = default(Dictionary); + if (dataFormat != null) { - 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); - } + var result = dataFormat.TryParse(data, password); + encryption = result.Encryption; + format = result.Format; + rawData = result.RawData; status = result.Status; + + logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}."); } else { @@ -241,44 +306,6 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus TryConfigureClient(IDictionary data, Settings settings, PasswordInfo passwordInfo) - { - logger.Info("Attempting to configure local client settings..."); - - 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() { appConfig.ClientId = Guid.NewGuid(); diff --git a/SafeExamBrowser.Configuration/HashAlgorithm.cs b/SafeExamBrowser.Configuration/Cryptography/HashAlgorithm.cs similarity index 87% rename from SafeExamBrowser.Configuration/HashAlgorithm.cs rename to SafeExamBrowser.Configuration/Cryptography/HashAlgorithm.cs index c1404394..48019b70 100644 --- a/SafeExamBrowser.Configuration/HashAlgorithm.cs +++ b/SafeExamBrowser.Configuration/Cryptography/HashAlgorithm.cs @@ -10,9 +10,9 @@ using System; using System.Linq; using System.Security.Cryptography; using System.Text; -using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; -namespace SafeExamBrowser.Configuration +namespace SafeExamBrowser.Configuration.Cryptography { public class HashAlgorithm : IHashAlgorithm { diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs similarity index 96% rename from SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs rename to SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs index 8b92b995..102acb5b 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs @@ -12,7 +12,7 @@ using System.Security.Cryptography; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; -namespace SafeExamBrowser.Configuration.DataFormats.Cryptography +namespace SafeExamBrowser.Configuration.Cryptography { internal class PasswordEncryption { @@ -37,7 +37,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography if (password == null) { - return LoadStatus.SettingsPasswordNeeded; + return LoadStatus.PasswordNeeded; } var (version, options) = ParseHeader(data); @@ -112,7 +112,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography { logger.Debug($"The authentication failed due to an invalid password or corrupted data!"); - return LoadStatus.SettingsPasswordNeeded; + return LoadStatus.PasswordNeeded; } private Stream Decrypt(Stream data, byte[] encryptionKey, int hmacLength) diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs similarity index 95% rename from SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs rename to SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs index 7a04cd5b..09708f61 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs @@ -13,7 +13,7 @@ using System.Security.Cryptography.X509Certificates; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; -namespace SafeExamBrowser.Configuration.DataFormats.Cryptography +namespace SafeExamBrowser.Configuration.Cryptography { internal class PublicKeyHashEncryption { @@ -26,10 +26,10 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography this.logger = logger; } - internal virtual LoadStatus Decrypt(Stream data, out Stream decrypted) + internal virtual LoadStatus Decrypt(Stream data, out Stream decrypted, out X509Certificate2 certificate) { var keyHash = ParsePublicKeyHash(data); - var found = TryGetCertificateWith(keyHash, out X509Certificate2 certificate); + var found = TryGetCertificateWith(keyHash, out certificate); decrypted = default(Stream); diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs similarity index 92% rename from SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs rename to SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs index e897d15d..f49d16d3 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs @@ -12,7 +12,7 @@ using System.Security.Cryptography.X509Certificates; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; -namespace SafeExamBrowser.Configuration.DataFormats.Cryptography +namespace SafeExamBrowser.Configuration.Cryptography { internal class PublicKeyHashWithSymmetricKeyEncryption : PublicKeyHashEncryption { @@ -25,10 +25,10 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography this.passwordEncryption = passwordEncryption; } - internal override LoadStatus Decrypt(Stream data, out Stream decrypted) + internal override LoadStatus Decrypt(Stream data, out Stream decrypted, out X509Certificate2 certificate) { var keyHash = ParsePublicKeyHash(data); - var found = TryGetCertificateWith(keyHash, out X509Certificate2 certificate); + var found = TryGetCertificateWith(keyHash, out certificate); decrypted = default(Stream); diff --git a/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs b/SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs similarity index 96% rename from SafeExamBrowser.Configuration/Compression/GZipCompressor.cs rename to SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs index cd762fc3..b695b610 100644 --- a/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs +++ b/SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs @@ -9,10 +9,10 @@ using System; using System.IO; using System.IO.Compression; -using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.DataCompression; using SafeExamBrowser.Contracts.Logging; -namespace SafeExamBrowser.Configuration.Compression +namespace SafeExamBrowser.Configuration.DataCompression { /// /// Data compression using the GNU-Zip format (see https://en.wikipedia.org/wiki/Gzip). diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs index f92fc767..184cb202 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs @@ -8,9 +8,13 @@ using System; using System.IO; +using System.Security.Cryptography.X509Certificates; using System.Text; -using SafeExamBrowser.Configuration.DataFormats.Cryptography; +using SafeExamBrowser.Configuration.Cryptography; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration.DataCompression; +using SafeExamBrowser.Contracts.Configuration.DataFormats; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats @@ -56,7 +60,7 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public ParseResult TryParse(Stream data, PasswordInfo passwordInfo) + public ParseResult TryParse(Stream data, PasswordParameters password) { var prefix = ParsePrefix(data); var success = TryDetermineFormat(prefix, out FormatType format); @@ -75,13 +79,13 @@ namespace SafeExamBrowser.Configuration.DataFormats { case FormatType.Password: case FormatType.PasswordConfigureClient: - return ParsePasswordBlock(data, passwordInfo, format); + return ParsePasswordBlock(data, format, password); case FormatType.PlainData: - return ParsePlainDataBlock(data, passwordInfo); + return ParsePlainDataBlock(data); case FormatType.PublicKeyHash: - return ParsePublicKeyHashBlock(data, passwordInfo); + return ParsePublicKeyHashBlock(data, password); case FormatType.PublicKeyHashWithSymmetricKey: - return ParsePublicKeyHashWithSymmetricKeyBlock(data, passwordInfo); + return ParsePublicKeyHashWithSymmetricKeyBlock(data, password); } } @@ -90,40 +94,35 @@ namespace SafeExamBrowser.Configuration.DataFormats return new ParseResult { Status = LoadStatus.InvalidData }; } - private ParseResult ParsePasswordBlock(Stream data, PasswordInfo passwordInfo, FormatType format) + private ParseResult ParsePasswordBlock(Stream data, FormatType format, PasswordParameters password) { - var decrypted = default(Stream); var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); - var status = default(LoadStatus); + var encryptionParams = new PasswordParameters(); + var result = new ParseResult(); if (format == FormatType.PasswordConfigureClient) { - status = encryption.Decrypt(data, string.Empty, out decrypted); - - if (status == LoadStatus.SettingsPasswordNeeded && passwordInfo.AdminPasswordHash != null) - { - 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); - } + encryptionParams.Password = password.IsHash ? password.Password : hashAlgorithm.GenerateHashFor(password.Password); + encryptionParams.IsHash = true; } else { - status = encryption.Decrypt(data, passwordInfo.SettingsPassword, out decrypted); + encryptionParams.Password = password.Password; + encryptionParams.IsHash = password.IsHash; } - if (status == LoadStatus.Success) + result.Status = encryption.Decrypt(data, encryptionParams.Password, out var decrypted); + + if (result.Status == LoadStatus.Success) { - return ParsePlainDataBlock(decrypted, passwordInfo); + result = ParsePlainDataBlock(decrypted); + result.Encryption = encryptionParams; } - return new ParseResult { Status = status }; + return result; } - private ParseResult ParsePlainDataBlock(Stream data, PasswordInfo passwordInfo) + private ParseResult ParsePlainDataBlock(Stream data) { var xmlFormat = new XmlFormat(logger.CloneFor(nameof(XmlFormat))); @@ -132,35 +131,50 @@ namespace SafeExamBrowser.Configuration.DataFormats data = compressor.Decompress(data); } - return xmlFormat.TryParse(data, passwordInfo); + return xmlFormat.TryParse(data); } - private ParseResult ParsePublicKeyHashBlock(Stream data, PasswordInfo passwordInfo) + private ParseResult ParsePublicKeyHashBlock(Stream data, PasswordParameters password) { var encryption = new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); - var status = encryption.Decrypt(data, out Stream decrypted); + var result = new ParseResult(); - if (status == LoadStatus.Success) + result.Status = encryption.Decrypt(data, out var decrypted, out var certificate); + + if (result.Status == LoadStatus.Success) { - return TryParse(decrypted, passwordInfo); + result = TryParse(decrypted, password); + result.Encryption = new PublicKeyHashParameters + { + Certificate = certificate, + InnerEncryption = result.Encryption as PasswordParameters, + SymmetricEncryption = false + }; } - return new ParseResult { Status = status }; + return result; } - private ParseResult ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, PasswordInfo passwordInfo) + private ParseResult ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, PasswordParameters password) { - var logger = this.logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)); var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); - var encryption = new PublicKeyHashWithSymmetricKeyEncryption(logger, passwordEncryption); - var status = encryption.Decrypt(data, out Stream decrypted); + var encryption = new PublicKeyHashWithSymmetricKeyEncryption(logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); + var result = new ParseResult(); - if (status == LoadStatus.Success) + result.Status = encryption.Decrypt(data, out Stream decrypted, out X509Certificate2 certificate); + + if (result.Status == LoadStatus.Success) { - return TryParse(decrypted, passwordInfo); + result = TryParse(decrypted, password); + result.Encryption = new PublicKeyHashParameters + { + Certificate = certificate, + InnerEncryption = result.Encryption as PasswordParameters, + SymmetricEncryption = true + }; } - return new ParseResult { Status = status }; + return result; } private string ParsePrefix(Stream data) diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs index b74bd63e..620675e9 100644 --- a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs @@ -13,6 +13,8 @@ using System.Text; using System.Xml; using System.Xml.Linq; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration.DataFormats; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats @@ -58,7 +60,7 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public ParseResult TryParse(Stream data, PasswordInfo passwordInfo) + public ParseResult TryParse(Stream data, PasswordParameters password = null) { var result = new ParseResult { Status = LoadStatus.InvalidData }; var xmlSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; @@ -218,6 +220,11 @@ namespace SafeExamBrowser.Configuration.DataFormats { value = null; + if (element.IsEmpty) + { + return LoadStatus.Success; + } + switch (element.Name.LocalName) { case DataTypes.DATA: diff --git a/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs b/SafeExamBrowser.Configuration/DataResources/FileResource.cs similarity index 79% rename from SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs rename to SafeExamBrowser.Configuration/DataResources/FileResource.cs index 177514e5..23d623e9 100644 --- a/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs +++ b/SafeExamBrowser.Configuration/DataResources/FileResource.cs @@ -9,15 +9,16 @@ using System; using System.IO; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.DataResources; using SafeExamBrowser.Contracts.Logging; -namespace SafeExamBrowser.Configuration.ResourceLoaders +namespace SafeExamBrowser.Configuration.DataResources { - public class FileResourceLoader : IResourceLoader + public class FileResource: IDataResource { private ILogger logger; - public FileResourceLoader(ILogger logger) + public FileResource(ILogger logger) { this.logger = logger; } @@ -46,5 +47,10 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders return LoadStatus.Success; } + + public SaveStatus TrySave(Uri resource, Stream data) + { + throw new NotImplementedException(); + } } } diff --git a/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs b/SafeExamBrowser.Configuration/DataResources/NetworkResource.cs similarity index 93% rename from SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs rename to SafeExamBrowser.Configuration/DataResources/NetworkResource.cs index 20a544fa..d2567682 100644 --- a/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs +++ b/SafeExamBrowser.Configuration/DataResources/NetworkResource.cs @@ -14,11 +14,12 @@ using System.Net.Http; using System.Net.Mime; using System.Threading.Tasks; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.DataResources; using SafeExamBrowser.Contracts.Logging; -namespace SafeExamBrowser.Configuration.ResourceLoaders +namespace SafeExamBrowser.Configuration.DataResources { - public class NetworkResourceLoader : IResourceLoader + public class NetworkResource : IDataResource { private AppConfig appConfig; private ILogger logger; @@ -40,7 +41,7 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders Uri.UriSchemeHttps }; - public NetworkResourceLoader(AppConfig appConfig, ILogger logger) + public NetworkResource(AppConfig appConfig, ILogger logger) { this.appConfig = appConfig; this.logger = logger; @@ -85,6 +86,11 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders return LoadStatus.Success; } + public SaveStatus TrySave(Uri resource, Stream data) + { + throw new NotImplementedException(); + } + private Uri BuildUriFor(Uri resource) { var scheme = GetSchemeFor(resource); diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index edf93671..ba9a8df2 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -58,18 +58,18 @@ - + - - - + + + - + - - + + diff --git a/SafeExamBrowser.Contracts/Configuration/Cryptography/EncryptionParameters.cs b/SafeExamBrowser.Contracts/Configuration/Cryptography/EncryptionParameters.cs new file mode 100644 index 00000000..42d686cf --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/Cryptography/EncryptionParameters.cs @@ -0,0 +1,17 @@ +/* + * 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.Cryptography +{ + /// + /// Holds the cryptographic parameters used to encrypt configuration data. + /// + public abstract class EncryptionParameters + { + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/IHashAlgorithm.cs b/SafeExamBrowser.Contracts/Configuration/Cryptography/IHashAlgorithm.cs similarity index 90% rename from SafeExamBrowser.Contracts/Configuration/IHashAlgorithm.cs rename to SafeExamBrowser.Contracts/Configuration/Cryptography/IHashAlgorithm.cs index 45d3148f..25315a5a 100644 --- a/SafeExamBrowser.Contracts/Configuration/IHashAlgorithm.cs +++ b/SafeExamBrowser.Contracts/Configuration/Cryptography/IHashAlgorithm.cs @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -namespace SafeExamBrowser.Contracts.Configuration +namespace SafeExamBrowser.Contracts.Configuration.Cryptography { /// /// Provides functionality to calculate hash codes of different objects. diff --git a/SafeExamBrowser.Contracts/Configuration/Cryptography/PasswordParameters.cs b/SafeExamBrowser.Contracts/Configuration/Cryptography/PasswordParameters.cs new file mode 100644 index 00000000..35666ab8 --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/Cryptography/PasswordParameters.cs @@ -0,0 +1,26 @@ +/* + * 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.Cryptography +{ + /// + /// Holds all parameters for data encryption by password. + /// + public class PasswordParameters : EncryptionParameters + { + /// + /// The password in plain text. + /// + public string Password { get; set; } + + /// + /// Indicates whether the password is a hash code. + /// + public bool IsHash { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/Cryptography/PublicKeyHashParameters.cs b/SafeExamBrowser.Contracts/Configuration/Cryptography/PublicKeyHashParameters.cs new file mode 100644 index 00000000..26d07eca --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/Cryptography/PublicKeyHashParameters.cs @@ -0,0 +1,33 @@ +/* + * 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.Security.Cryptography.X509Certificates; + +namespace SafeExamBrowser.Contracts.Configuration.Cryptography +{ + /// + /// Holds all parameters for data encryption by certificate. + /// + public class PublicKeyHashParameters : EncryptionParameters + { + /// + /// The certificate holding the public key used for encryption. + /// + public X509Certificate2 Certificate { get; set; } + + /// + /// The encryption parameters of the inner data, if available. + /// + public PasswordParameters InnerEncryption { get; set; } + + /// + /// Determines the usage of symmetric vs. asymmetric encryption. + /// + public bool SymmetricEncryption { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/IDataCompressor.cs b/SafeExamBrowser.Contracts/Configuration/DataCompression/IDataCompressor.cs similarity index 93% rename from SafeExamBrowser.Contracts/Configuration/IDataCompressor.cs rename to SafeExamBrowser.Contracts/Configuration/DataCompression/IDataCompressor.cs index b3767c65..bfc97f1f 100644 --- a/SafeExamBrowser.Contracts/Configuration/IDataCompressor.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataCompression/IDataCompressor.cs @@ -8,7 +8,7 @@ using System.IO; -namespace SafeExamBrowser.Contracts.Configuration +namespace SafeExamBrowser.Contracts.Configuration.DataCompression { /// /// Defines the functionality for data compression and decompression. diff --git a/SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs new file mode 100644 index 00000000..a3b127e7 --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs @@ -0,0 +1,19 @@ +/* + * 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.DataFormats +{ + /// + /// Defines all supported data formats. + /// + public enum Format + { + Binary = 1, + Xml + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataFormat.cs similarity index 77% rename from SafeExamBrowser.Contracts/Configuration/IDataFormat.cs rename to SafeExamBrowser.Contracts/Configuration/DataFormats/IDataFormat.cs index 84e7e493..56656aaa 100644 --- a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataFormat.cs @@ -7,8 +7,9 @@ */ using System.IO; +using SafeExamBrowser.Contracts.Configuration.Cryptography; -namespace SafeExamBrowser.Contracts.Configuration +namespace SafeExamBrowser.Contracts.Configuration.DataFormats { /// /// Provides functionality to parse configuration data with a particular format. @@ -23,6 +24,6 @@ namespace SafeExamBrowser.Contracts.Configuration /// /// Tries to parse the given data. /// - ParseResult TryParse(Stream data, PasswordInfo passwordInfo); + ParseResult TryParse(Stream data, PasswordParameters password = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/ParseResult.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs similarity index 65% rename from SafeExamBrowser.Contracts/Configuration/ParseResult.cs rename to SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs index 9498408d..4532eb4b 100644 --- a/SafeExamBrowser.Contracts/Configuration/ParseResult.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs @@ -7,14 +7,25 @@ */ using System.Collections.Generic; +using SafeExamBrowser.Contracts.Configuration.Cryptography; -namespace SafeExamBrowser.Contracts.Configuration +namespace SafeExamBrowser.Contracts.Configuration.DataFormats { /// /// Defines the result of a data parsing operation by an . /// public class ParseResult { + /// + /// The encryption parameters which were used to decrypt the data, or null if it was not encrypted. + /// + public EncryptionParameters Encryption { get; set; } + + /// + /// The original format of the data. + /// + public Format Format { get; set; } + /// /// The parsed settings data. Might be null or in an undefinable state, depending on . /// diff --git a/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs b/SafeExamBrowser.Contracts/Configuration/DataResources/IDataResource.cs similarity index 58% rename from SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs rename to SafeExamBrowser.Contracts/Configuration/DataResources/IDataResource.cs index c63051e2..47f88931 100644 --- a/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataResources/IDataResource.cs @@ -9,15 +9,15 @@ using System; using System.IO; -namespace SafeExamBrowser.Contracts.Configuration +namespace SafeExamBrowser.Contracts.Configuration.DataResources { /// - /// Provides functionality to load configuration data from a particular resource. + /// Provides functionality to load and save configuration data from / as a particular resource. /// - public interface IResourceLoader + public interface IDataResource { /// - /// Indicates whether the resource loader is able to load data from the specified resource. + /// Indicates whether data can be loaded from the specified resource. /// bool CanLoad(Uri resource); @@ -25,5 +25,10 @@ namespace SafeExamBrowser.Contracts.Configuration /// Tries to load the configuration data from the specified resource. /// LoadStatus TryLoad(Uri resource, out Stream data); + + /// + /// Tries to save the given configuration data as the specified resource. + /// + SaveStatus TrySave(Uri resource, Stream data); } } diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index a9673d10..8929f668 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -7,6 +7,9 @@ */ using System; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration.DataFormats; +using SafeExamBrowser.Contracts.Configuration.DataResources; namespace SafeExamBrowser.Contracts.Configuration { @@ -15,6 +18,11 @@ namespace SafeExamBrowser.Contracts.Configuration /// public interface IConfigurationRepository { + /// + /// Saves the given resource as local client configuration. + /// + void ConfigureClientWith(Uri resource, EncryptionParameters encryption = null); + /// /// Initializes the global configuration information for the currently running application instance. /// @@ -31,19 +39,23 @@ namespace SafeExamBrowser.Contracts.Configuration Settings.Settings LoadDefaultSettings(); /// - /// Registers the specified as option to parse data from a configuration resource. + /// Registers the specified to be used when loading or saving configuration data. /// void Register(IDataFormat dataFormat); /// - /// Registers the specified as option to load data from a configuration resource. + /// Registers the specified to be used when loading or saving configuration data. /// - void Register(IResourceLoader resourceLoader); + void Register(IDataResource dataResource); /// - /// 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! + /// Attempts to load settings from the specified resource. /// - LoadStatus TryLoadSettings(Uri resource, PasswordInfo passwordInfo, out Settings.Settings settings); + LoadStatus TryLoadSettings(Uri resource, PasswordParameters password, out EncryptionParameters encryption, out Format format, out Settings.Settings settings); + + /// + /// Attempts to save settings according to the specified parameters. + /// + SaveStatus TrySaveSettings(Uri resource, Format format, Settings.Settings settings, EncryptionParameters encryption = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs index b679222b..1c0292f3 100644 --- a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs +++ b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs @@ -13,11 +13,6 @@ namespace SafeExamBrowser.Contracts.Configuration /// public enum LoadStatus { - /// - /// Indicates that the current administrator password is needed to be allowed to configure the local client. - /// - AdminPasswordNeeded = 1, - /// /// Indicates that a resource contains invalid data. /// @@ -34,20 +29,15 @@ namespace SafeExamBrowser.Contracts.Configuration NotSupported, /// - /// Indicates that the settings password is needed in order to decrypt the settings. + /// Indicates that a password is needed in order to decrypt the settings. /// - SettingsPasswordNeeded, + PasswordNeeded, /// /// 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/PasswordInfo.cs b/SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs deleted file mode 100644 index e2d188a1..00000000 --- a/SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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/Configuration/SaveStatus.cs b/SafeExamBrowser.Contracts/Configuration/SaveStatus.cs new file mode 100644 index 00000000..480787ef --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/SaveStatus.cs @@ -0,0 +1,18 @@ +/* + * 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 +{ + /// + /// Defines all possible results of an attempt to save an application configuration. + /// + public enum SaveStatus + { + + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 5403ce7c..ccc6ab3d 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -53,11 +53,15 @@ - - - - - + + + + + + + + + @@ -112,7 +116,7 @@ - + diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 0344dd96..66ed7781 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -13,9 +13,10 @@ using System.Reflection; using SafeExamBrowser.Communication.Hosts; using SafeExamBrowser.Communication.Proxies; using SafeExamBrowser.Configuration; -using SafeExamBrowser.Configuration.Compression; +using SafeExamBrowser.Configuration.Cryptography; +using SafeExamBrowser.Configuration.DataCompression; using SafeExamBrowser.Configuration.DataFormats; -using SafeExamBrowser.Configuration.ResourceLoaders; +using SafeExamBrowser.Configuration.DataResources; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Core; using SafeExamBrowser.Contracts.Core.OperationModel; @@ -75,7 +76,7 @@ namespace SafeExamBrowser.Runtime bootstrapOperations.Enqueue(new CommunicationHostOperation(runtimeHost, logger)); sessionOperations.Enqueue(new SessionInitializationOperation(configuration, logger, runtimeHost, sessionContext)); - sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, logger, sessionContext)); + sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, new HashAlgorithm(), logger, sessionContext)); sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, FIFTEEN_SECONDS)); sessionOperations.Enqueue(new KioskModeTerminationOperation(desktopFactory, explorerShell, logger, processFactory, sessionContext)); sessionOperations.Enqueue(new ServiceOperation(logger, serviceProxy, sessionContext)); @@ -121,8 +122,8 @@ namespace SafeExamBrowser.Runtime configuration.Register(new BinaryFormat(compressor, new HashAlgorithm(), new ModuleLogger(logger, nameof(BinaryFormat)))); configuration.Register(new XmlFormat(new ModuleLogger(logger, nameof(XmlFormat)))); - configuration.Register(new FileResourceLoader(new ModuleLogger(logger, nameof(FileResourceLoader)))); - configuration.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader)))); + configuration.Register(new FileResource(new ModuleLogger(logger, nameof(FileResource)))); + configuration.Register(new NetworkResource(appConfig, new ModuleLogger(logger, nameof(NetworkResource)))); } private void InitializeLogging() diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index dde27129..54ec32bb 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -10,6 +10,8 @@ using System; using System.IO; using SafeExamBrowser.Contracts.Communication.Data; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration.DataFormats; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Core.OperationModel; using SafeExamBrowser.Contracts.Core.OperationModel.Events; @@ -23,19 +25,32 @@ namespace SafeExamBrowser.Runtime.Operations { private string[] commandLineArgs; private IConfigurationRepository configuration; + private IHashAlgorithm hashAlgorithm; private ILogger logger; + private string AppDataFile + { + get { return Path.Combine(Context.Next.AppConfig.AppDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); } + } + + private string ProgramDataFile + { + get { return Path.Combine(Context.Next.AppConfig.ProgramDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); } + } + public override event ActionRequiredEventHandler ActionRequired; public override event StatusChangedEventHandler StatusChanged; public ConfigurationOperation( string[] commandLineArgs, IConfigurationRepository configuration, + IHashAlgorithm hashAlgorithm, ILogger logger, SessionContext sessionContext) : base(sessionContext) { this.commandLineArgs = commandLineArgs; this.configuration = configuration; + this.hashAlgorithm = hashAlgorithm; this.logger = logger; } @@ -98,53 +113,191 @@ namespace SafeExamBrowser.Runtime.Operations private OperationResult LoadSettings(Uri uri) { - var settings = default(Settings); - var status = default(LoadStatus); - var passwordInfo = new PasswordInfo { AdminPasswordHash = Context.Current?.Settings?.AdminPasswordHash }; + var passwordParams = new PasswordParameters { Password = string.Empty, IsHash = true }; + var status = configuration.TryLoadSettings(uri, passwordParams, out var encryption, out var format, out var settings); - for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;) + if (status == LoadStatus.PasswordNeeded && Context.Current?.Settings.AdminPasswordHash != null) { - status = configuration.TryLoadSettings(uri, passwordInfo, out settings); + passwordParams.Password = Context.Current.Settings.AdminPasswordHash; + passwordParams.IsHash = true; - if (status != LoadStatus.AdminPasswordNeeded && status != LoadStatus.SettingsPasswordNeeded) + status = configuration.TryLoadSettings(uri, passwordParams, out encryption, out format, out settings); + } + + for (int attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++) + { + var success = TryGetPassword(PasswordRequestPurpose.Settings, out var password); + + if (success) { - break; + passwordParams.Password = password; + passwordParams.IsHash = false; } - - var success = TryGetPassword(status, passwordInfo); - - if (!success) + else { return OperationResult.Aborted; } - adminAttempts += status == LoadStatus.AdminPasswordNeeded ? 1 : 0; - settingsAttempts += status == LoadStatus.SettingsPasswordNeeded ? 1 : 0; + status = configuration.TryLoadSettings(uri, passwordParams, out encryption, out format, out settings); } Context.Next.Settings = settings; + return HandleLoadResult(uri, settings, status, passwordParams, encryption, format); + } + + private OperationResult HandleLoadResult(Uri uri, Settings settings, LoadStatus status, PasswordParameters password, EncryptionParameters encryption, Format format) + { + if (status == LoadStatus.LoadWithBrowser) + { + return HandleBrowserResource(uri); + } + + if (status == LoadStatus.Success && settings.ConfigurationMode == ConfigurationMode.ConfigureClient) + { + return HandleClientConfiguration(uri, password, encryption, format); + } + if (status == LoadStatus.Success) { return OperationResult.Success; } - if (status == LoadStatus.SuccessConfigureClient) - { - return HandleClientConfiguration(); - } - ShowFailureMessage(status, uri); return OperationResult.Failed; } + private OperationResult HandleBrowserResource(Uri uri) + { + Context.Next.Settings.Browser.StartUrl = uri.AbsoluteUri; + logger.Info($"The configuration resource needs authentication or is a webpage, using '{uri}' as startup URL for the browser."); + + return OperationResult.Success; + } + + private OperationResult HandleClientConfiguration(Uri resource, PasswordParameters password, EncryptionParameters encryption, Format format) + { + var isAppDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(AppDataFile, StringComparison.OrdinalIgnoreCase); + var isProgramDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(ProgramDataFile, StringComparison.OrdinalIgnoreCase); + + if (!isAppDataFile && !isProgramDataFile) + { + var isFirstSession = Context.Current == null; + var requiresAuthentication = IsAuthenticationRequiredForClientConfiguration(password); + + logger.Info("Starting client configuration..."); + + if (requiresAuthentication) + { + var result = HandleClientConfigurationAuthentication(); + + if (result != OperationResult.Success) + { + return result; + } + } + else + { + logger.Info("Authentication is not required."); + } + + configuration.ConfigureClientWith(resource, encryption); + + if (isFirstSession) + { + var result = HandleClientConfigurationSuccess(); + + 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; + } + else + { + logger.Info("Authentication has failed!"); + + return OperationResult.Failed; + } + } + + private OperationResult HandleClientConfigurationSuccess() + { + var args = new ConfigurationCompletedEventArgs(); + + ActionRequired?.Invoke(args); + logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} after successful client configuration."); + + if (args.AbortStartup) + { + return OperationResult.Aborted; + } + + return OperationResult.Success; + } + private void ShowFailureMessage(LoadStatus status, Uri uri) { switch (status) { - case LoadStatus.AdminPasswordNeeded: - case LoadStatus.SettingsPasswordNeeded: + case LoadStatus.PasswordNeeded: ActionRequired?.Invoke(new InvalidPasswordMessageArgs()); break; case LoadStatus.InvalidData: @@ -159,31 +312,20 @@ namespace SafeExamBrowser.Runtime.Operations } } - private bool TryGetPassword(LoadStatus status, PasswordInfo passwordInfo) + private bool TryGetPassword(PasswordRequestPurpose purpose, out string password) { - var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings; var args = new PasswordRequiredEventArgs { Purpose = purpose }; ActionRequired?.Invoke(args); - - if (purpose == PasswordRequestPurpose.Administrator) - { - passwordInfo.AdminPassword = args.Password; - } - else - { - passwordInfo.SettingsPassword = args.Password; - } + password = args.Password; return args.Success; } private bool TryInitializeSettingsUri(out Uri uri) { - var path = string.Empty; + var path = default(string); var isValidUri = false; - var programDataSettings = Path.Combine(Context.Next.AppConfig.ProgramDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); - var appDataSettings = Path.Combine(Context.Next.AppConfig.AppDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); uri = null; @@ -194,16 +336,16 @@ namespace SafeExamBrowser.Runtime.Operations logger.Info($"Found command-line argument for configuration resource: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); } - if (!isValidUri && File.Exists(programDataSettings)) + if (!isValidUri && File.Exists(ProgramDataFile)) { - path = programDataSettings; + path = ProgramDataFile; isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); logger.Info($"Found configuration file in PROGRAMDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); } - if (!isValidUri && File.Exists(appDataSettings)) + if (!isValidUri && File.Exists(AppDataFile)) { - path = appDataSettings; + path = AppDataFile; isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); logger.Info($"Found configuration file in APPDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); } @@ -221,32 +363,6 @@ namespace SafeExamBrowser.Runtime.Operations return isValidUri; } - private OperationResult HandleClientConfiguration() - { - var firstSession = Context.Current == null; - - if (firstSession) - { - var args = new ConfigurationCompletedEventArgs(); - - ActionRequired?.Invoke(args); - logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} after successful client configuration."); - - if (args.AbortStartup) - { - 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) { switch (result) diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index 059fa4b2..1ceec82c 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -398,6 +398,8 @@ namespace SafeExamBrowser.Runtime private void ShowMessageBox(MessageEventArgs args) { + var isStartup = !SessionIsRunning; + var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.KioskMode == KioskMode.DisableExplorerShell; var message = text.Get(args.Message); var title = text.Get(args.Title); @@ -411,7 +413,14 @@ namespace SafeExamBrowser.Runtime title = title.Replace(placeholder.Key, placeholder.Value); } - messageBox.Show(message, title, MessageBoxAction.Confirm, args.Icon, runtimeWindow); + if (isStartup || isRunningOnDefaultDesktop) + { + messageBox.Show(message, title, MessageBoxAction.Confirm, args.Icon, runtimeWindow); + } + else + { + // TODO + } } private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args)