From d34ce15e3f17337b186261cabb10f94bbe522d36 Mon Sep 17 00:00:00 2001 From: dbuechel Date: Fri, 21 Dec 2018 11:36:20 +0100 Subject: [PATCH] SEBWIN-221: Finished saving algorithm for client configuration (including password encryption - public key encryption remains to do). --- .../ConfigurationRepositoryTests.cs | 2 +- .../ConfigurationData/Certificates.cs | 76 +++++ .../ConfigurationData/DataMapper.cs | 64 ++++ .../ConfigurationData/DataValues.cs | 113 +++++++ .../ConfigurationData/Keys.cs | 71 +++++ .../ConfigurationRepository.cs | 275 +++++++----------- .../Cryptography/PasswordEncryption.cs | 90 +++++- .../Cryptography/PublicKeyHashEncryption.cs | 8 + ...PublicKeyHashWithSymmetricKeyEncryption.cs | 7 + .../DataCompression/GZipCompressor.cs | 29 +- .../DataFormats/BinaryBlock.cs | 19 ++ .../DataFormats/BinaryFormat.cs | 239 --------------- .../DataFormats/BinaryParser.cs | 207 +++++++++++++ .../DataFormats/BinarySerializer.cs | 168 +++++++++++ .../DataFormats/DataMapper.cs | 65 ----- .../DataFormats/XmlElement.cs | 25 ++ .../{XmlFormat.cs => XmlParser.cs} | 81 +++--- .../DataFormats/XmlSerializer.cs | 199 +++++++++++++ ...{FileResource.cs => FileResourceLoader.cs} | 13 +- .../DataResources/FileResourceSaver.cs | 65 +++++ ...rkResource.cs => NetworkResourceLoader.cs} | 9 +- .../SafeExamBrowser.Configuration.csproj | 18 +- .../DataFormats/{Format.cs => FormatType.cs} | 2 +- .../{IDataFormat.cs => IDataParser.cs} | 2 +- .../DataFormats/IDataSerializer.cs | 29 ++ .../Configuration/DataFormats/ParseResult.cs | 6 +- .../DataFormats/SerializeResult.cs | 28 ++ .../{IDataResource.cs => IResourceLoader.cs} | 9 +- .../DataResources/IResourceSaver.cs | 29 ++ .../Configuration/IConfigurationRepository.cs | 20 +- .../Configuration/SaveStatus.cs | 12 +- .../Configuration/Settings/Settings.cs | 2 +- .../SafeExamBrowser.Contracts.csproj | 9 +- SafeExamBrowser.Runtime/CompositionRoot.cs | 11 +- .../Operations/ConfigurationOperation.cs | 8 +- SafeExamBrowser.Runtime/RuntimeController.cs | 22 +- .../LogWindow.xaml.cs | 10 +- 37 files changed, 1457 insertions(+), 585 deletions(-) create mode 100644 SafeExamBrowser.Configuration/ConfigurationData/Certificates.cs create mode 100644 SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs create mode 100644 SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs create mode 100644 SafeExamBrowser.Configuration/ConfigurationData/Keys.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs delete mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs delete mode 100644 SafeExamBrowser.Configuration/DataFormats/DataMapper.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/XmlElement.cs rename SafeExamBrowser.Configuration/DataFormats/{XmlFormat.cs => XmlParser.cs} (71%) create mode 100644 SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs rename SafeExamBrowser.Configuration/DataResources/{FileResource.cs => FileResourceLoader.cs} (74%) create mode 100644 SafeExamBrowser.Configuration/DataResources/FileResourceSaver.cs rename SafeExamBrowser.Configuration/DataResources/{NetworkResource.cs => NetworkResourceLoader.cs} (95%) rename SafeExamBrowser.Contracts/Configuration/DataFormats/{Format.cs => FormatType.cs} (95%) rename SafeExamBrowser.Contracts/Configuration/DataFormats/{IDataFormat.cs => IDataParser.cs} (96%) create mode 100644 SafeExamBrowser.Contracts/Configuration/DataFormats/IDataSerializer.cs create mode 100644 SafeExamBrowser.Contracts/Configuration/DataFormats/SerializeResult.cs rename SafeExamBrowser.Contracts/Configuration/DataResources/{IDataResource.cs => IResourceLoader.cs} (71%) create mode 100644 SafeExamBrowser.Contracts/Configuration/DataResources/IResourceSaver.cs diff --git a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs index 6ac30d6c..a4204831 100644 --- a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs +++ b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs @@ -26,7 +26,7 @@ namespace SafeExamBrowser.Configuration.UnitTests { var executablePath = Assembly.GetExecutingAssembly().Location; var hashAlgorithm = new Mock(); - var logger = new Mock(); + var logger = new Mock(); sut = new ConfigurationRepository(hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty); } diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Certificates.cs b/SafeExamBrowser.Configuration/ConfigurationData/Certificates.cs new file mode 100644 index 00000000..8228bafa --- /dev/null +++ b/SafeExamBrowser.Configuration/ConfigurationData/Certificates.cs @@ -0,0 +1,76 @@ +/* + * 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; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.ConfigurationData +{ + internal class Certificates + { + private ILogger logger; + + internal Certificates(ILogger logger) + { + this.logger = logger; + } + + internal void ExtractAndImportIdentityCertificates(IDictionary data) + { + const int IDENTITY_CERTIFICATE = 1; + var hasCertificates = data.TryGetValue(Keys.Network.Certificates.EmbeddedCertificates, out var value); + + if (hasCertificates && value is IList> certificates) + { + var toRemove = new List>(); + + foreach (var certificate in certificates) + { + var hasData = certificate.TryGetValue(Keys.Network.Certificates.CertificateData, out var dataValue); + var hasType = certificate.TryGetValue(Keys.Network.Certificates.CertificateType, out var typeValue); + var isIdentity = typeValue is int type && type == IDENTITY_CERTIFICATE; + + if (hasData && hasType && isIdentity && dataValue 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)); + } + } + + internal 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(); + } + } + } +} diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs new file mode 100644 index 00000000..93d68c7c --- /dev/null +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs @@ -0,0 +1,64 @@ +/* + * 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; +using SafeExamBrowser.Contracts.Configuration.Settings; + +namespace SafeExamBrowser.Configuration.ConfigurationData +{ + internal class DataMapper + { + internal void MapRawDataToSettings(IDictionary rawData, Settings settings) + { + foreach (var item in rawData) + { + Map(item.Key, item.Value, settings); + } + } + + private void Map(string key, object value, Settings settings) + { + switch (key) + { + case Keys.General.AdminPasswordHash: + MapAdminPasswordHash(settings, value); + break; + case Keys.General.StartUrl: + MapStartUrl(settings, value); + break; + case Keys.ConfigurationFile.ConfigurationPurpose: + MapConfigurationMode(settings, value); + break; + } + } + + private void MapAdminPasswordHash(Settings settings, object value) + { + if (value is string hash) + { + settings.AdminPasswordHash = hash; + } + } + + private void MapConfigurationMode(Settings settings, object value) + { + if (value is int mode) + { + settings.ConfigurationMode = mode == 1 ? ConfigurationMode.ConfigureClient : ConfigurationMode.Exam; + } + } + + private void MapStartUrl(Settings settings, object value) + { + if (value is string url) + { + settings.Browser.StartUrl = url; + } + } + } +} diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs new file mode 100644 index 00000000..0e18f4ed --- /dev/null +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs @@ -0,0 +1,113 @@ +/* + * 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; +using System.IO; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.ConfigurationData +{ + internal class DataValues + { + private const string BASE_ADDRESS = "net.pipe://localhost/safeexambrowser"; + + private AppConfig appConfig; + private string executablePath; + private string programCopyright; + private string programTitle; + private string programVersion; + + internal DataValues(string executablePath, string programCopyright, string programTitle, string programVersion) + { + this.executablePath = executablePath ?? string.Empty; + this.programCopyright = programCopyright ?? string.Empty; + this.programTitle = programTitle ?? string.Empty; + this.programVersion = programVersion ?? string.Empty; + } + + internal string GetAppDataFilePath() + { + return Path.Combine(appConfig.AppDataFolder, appConfig.DefaultSettingsFileName); + } + + internal AppConfig InitializeAppConfig() + { + var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(SafeExamBrowser)); + var startTime = DateTime.Now; + var logFolder = Path.Combine(appDataFolder, "Logs"); + var logFilePrefix = startTime.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s"); + + appConfig = new AppConfig(); + appConfig.ApplicationStartTime = startTime; + appConfig.AppDataFolder = appDataFolder; + appConfig.BrowserCachePath = Path.Combine(appDataFolder, "Cache"); + appConfig.BrowserLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Browser.log"); + appConfig.ClientId = Guid.NewGuid(); + appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}"; + appConfig.ClientExecutablePath = Path.Combine(Path.GetDirectoryName(executablePath), $"{nameof(SafeExamBrowser)}.Client.exe"); + appConfig.ClientLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Client.log"); + appConfig.ConfigurationFileExtension = ".seb"; + appConfig.DefaultSettingsFileName = "SebClientSettings.seb"; + appConfig.DownloadDirectory = Path.Combine(appDataFolder, "Downloads"); + appConfig.LogLevel = LogLevel.Debug; + appConfig.ProgramCopyright = programCopyright; + appConfig.ProgramDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), nameof(SafeExamBrowser)); + appConfig.ProgramTitle = programTitle; + appConfig.ProgramVersion = programVersion; + appConfig.RuntimeId = Guid.NewGuid(); + appConfig.RuntimeAddress = $"{BASE_ADDRESS}/runtime/{Guid.NewGuid()}"; + appConfig.RuntimeLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.log"); + appConfig.SebUriScheme = "seb"; + appConfig.SebUriSchemeSecure = "sebs"; + appConfig.ServiceAddress = $"{BASE_ADDRESS}/service"; + + return appConfig; + } + + internal ISessionConfiguration InitializeSessionConfiguration() + { + var configuration = new SessionConfiguration(); + + appConfig.ClientId = Guid.NewGuid(); + appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}"; + + configuration.AppConfig = appConfig.Clone(); + configuration.Id = Guid.NewGuid(); + configuration.StartupToken = Guid.NewGuid(); + + return configuration; + } + + internal Settings LoadDefaultSettings() + { + var settings = new Settings(); + + // TODO: Specify default settings + + settings.KioskMode = KioskMode.None; + settings.ServicePolicy = ServicePolicy.Optional; + + settings.Browser.StartUrl = "https://www.safeexambrowser.org/testing"; + settings.Browser.AllowAddressBar = true; + settings.Browser.AllowBackwardNavigation = true; + settings.Browser.AllowConfigurationDownloads = true; + settings.Browser.AllowDeveloperConsole = true; + settings.Browser.AllowDownloads = true; + settings.Browser.AllowForwardNavigation = true; + settings.Browser.AllowReloading = true; + + settings.Taskbar.AllowApplicationLog = true; + settings.Taskbar.AllowKeyboardLayout = true; + settings.Taskbar.AllowWirelessNetwork = true; + + return settings; + } + } +} diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs new file mode 100644 index 00000000..6d9a86ff --- /dev/null +++ b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs @@ -0,0 +1,71 @@ +/* + * 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.Configuration.ConfigurationData +{ + internal static class Keys + { + internal static class General + { + internal const string AdminPasswordHash = "hashedAdminPassword"; + internal const string StartUrl = "startURL"; + } + + internal static class ConfigurationFile + { + internal const string ConfigurationPurpose = "sebConfigPurpose"; + internal const string KeepClientConfigEncryption = "clientConfigKeepEncryption"; + } + + internal static class UserInterface + { + } + + internal static class Browser + { + } + + internal static class DownUploads + { + } + + internal static class Exam + { + } + + internal static class Applications + { + } + + internal static class AdditionalResources + { + } + + internal static class Network + { + internal static class Certificates + { + internal const string CertificateData = "certificateData"; + internal const string CertificateType = "type"; + internal const string EmbeddedCertificates = "embeddedCertificates"; + } + } + + internal static class Security + { + } + + internal static class Registry + { + } + + internal static class HookedKeys + { + } + } +} diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index 19cd57de..5625d027 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -10,8 +10,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Security.Cryptography.X509Certificates; -using SafeExamBrowser.Configuration.DataFormats; +using SafeExamBrowser.Configuration.ConfigurationData; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Cryptography; using SafeExamBrowser.Contracts.Configuration.DataFormats; @@ -23,65 +22,67 @@ namespace SafeExamBrowser.Configuration { public class ConfigurationRepository : IConfigurationRepository { - private const string BASE_ADDRESS = "net.pipe://localhost/safeexambrowser"; - - private readonly string executablePath; - private readonly string programCopyright; - private readonly string programTitle; - private readonly string programVersion; - - private AppConfig appConfig; + private Certificates certificates; + private IList dataParsers; + private IList dataSerializers; + private DataMapper dataMapper; + private DataValues dataValues; private IHashAlgorithm hashAlgorithm; - private IList dataFormats; - private IList dataResources; private ILogger logger; + private IList resourceLoaders; + private IList resourceSavers; public ConfigurationRepository( IHashAlgorithm hashAlgorithm, - ILogger logger, + IModuleLogger logger, string executablePath, string programCopyright, string programTitle, string programVersion) { - dataFormats = new List(); - dataResources = new List(); + certificates = new Certificates(logger.CloneFor(nameof(Certificates))); + dataParsers = new List(); + dataSerializers = new List(); + dataMapper = new DataMapper(); + dataValues = new DataValues(executablePath, programCopyright, programTitle, programVersion); + resourceLoaders = new List(); + resourceSavers = new List(); this.hashAlgorithm = hashAlgorithm; this.logger = logger; - this.executablePath = executablePath ?? string.Empty; - this.programCopyright = programCopyright ?? string.Empty; - this.programTitle = programTitle ?? string.Empty; - this.programVersion = programVersion ?? string.Empty; } public SaveStatus ConfigureClientWith(Uri resource, PasswordParameters password = null) { - logger.Info($"Attempting to configure local client settings from '{resource}'..."); + logger.Info($"Attempting to configure local client with '{resource}'..."); try { - TryLoadData(resource, out var stream); + TryLoadData(resource, out var data); - using (stream) + using (data) { - TryParseData(stream, out var encryption, out var format, out var data, password); - HandleIdentityCertificates(data); + TryParseData(data, out var encryption, out var format, out var rawData, password); - // TODO: Encrypt and 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!! + certificates.ExtractAndImportIdentityCertificates(rawData); + encryption = DetermineEncryptionForClientConfiguration(rawData, encryption); + + var status = TrySerializeData(rawData, format, out var serialized, encryption); + + using (serialized) + { + if (status == SaveStatus.Success) + { + status = TrySaveData(new Uri(dataValues.GetAppDataFilePath()), serialized); + } + + return status; + } } - - logger.Info($"Successfully configured local client settings with '{resource}'."); - - return SaveStatus.Success; } catch (Exception e) { - logger.Error($"Unexpected error while trying to configure local client settings '{resource}'!", e); + logger.Error($"Unexpected error while trying to configure local client with '{resource}'!", e); return SaveStatus.UnexpectedError; } @@ -89,84 +90,37 @@ namespace SafeExamBrowser.Configuration public AppConfig InitializeAppConfig() { - var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(SafeExamBrowser)); - var startTime = DateTime.Now; - var logFolder = Path.Combine(appDataFolder, "Logs"); - var logFilePrefix = startTime.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s"); - - appConfig = new AppConfig(); - appConfig.ApplicationStartTime = startTime; - appConfig.AppDataFolder = appDataFolder; - appConfig.BrowserCachePath = Path.Combine(appDataFolder, "Cache"); - appConfig.BrowserLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Browser.log"); - appConfig.ClientId = Guid.NewGuid(); - appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}"; - appConfig.ClientExecutablePath = Path.Combine(Path.GetDirectoryName(executablePath), $"{nameof(SafeExamBrowser)}.Client.exe"); - appConfig.ClientLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Client.log"); - appConfig.ConfigurationFileExtension = ".seb"; - appConfig.DefaultSettingsFileName = "SebClientSettings.seb"; - appConfig.DownloadDirectory = Path.Combine(appDataFolder, "Downloads"); - appConfig.LogLevel = LogLevel.Debug; - appConfig.ProgramCopyright = programCopyright; - appConfig.ProgramDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), nameof(SafeExamBrowser)); - appConfig.ProgramTitle = programTitle; - appConfig.ProgramVersion = programVersion; - appConfig.RuntimeId = Guid.NewGuid(); - appConfig.RuntimeAddress = $"{BASE_ADDRESS}/runtime/{Guid.NewGuid()}"; - appConfig.RuntimeLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.log"); - appConfig.SebUriScheme = "seb"; - appConfig.SebUriSchemeSecure = "sebs"; - appConfig.ServiceAddress = $"{BASE_ADDRESS}/service"; - - return appConfig; + return dataValues.InitializeAppConfig(); } public ISessionConfiguration InitializeSessionConfiguration() { - var configuration = new SessionConfiguration(); - - UpdateAppConfig(); - - configuration.AppConfig = appConfig.Clone(); - configuration.Id = Guid.NewGuid(); - configuration.StartupToken = Guid.NewGuid(); - - return configuration; + return dataValues.InitializeSessionConfiguration(); } public Settings LoadDefaultSettings() { - var settings = new Settings(); - - // TODO: Specify default settings - - settings.KioskMode = KioskMode.None; - settings.ServicePolicy = ServicePolicy.Optional; - - settings.Browser.StartUrl = "https://www.safeexambrowser.org/testing"; - settings.Browser.AllowAddressBar = true; - settings.Browser.AllowBackwardNavigation = true; - settings.Browser.AllowConfigurationDownloads = true; - settings.Browser.AllowDeveloperConsole = true; - settings.Browser.AllowDownloads = true; - settings.Browser.AllowForwardNavigation = true; - settings.Browser.AllowReloading = true; - - settings.Taskbar.AllowApplicationLog = true; - settings.Taskbar.AllowKeyboardLayout = true; - settings.Taskbar.AllowWirelessNetwork = true; - - return settings; + return dataValues.LoadDefaultSettings(); } - public void Register(IDataFormat dataFormat) + public void Register(IDataParser parser) { - dataFormats.Add(dataFormat); + dataParsers.Add(parser); } - public void Register(IDataResource dataResource) + public void Register(IDataSerializer serializer) { - dataResources.Add(dataResource); + dataSerializers.Add(serializer); + } + + public void Register(IResourceLoader loader) + { + resourceLoaders.Add(loader); + } + + public void Register(IResourceSaver saver) + { + resourceSavers.Add(saver); } public LoadStatus TryLoadSettings(Uri resource, out Settings settings, PasswordParameters password = null) @@ -181,16 +135,14 @@ namespace SafeExamBrowser.Configuration using (stream) { - if (status != LoadStatus.Success) - { - return status; - } - - status = TryParseData(stream, out _, out _, out var data, password); - if (status == LoadStatus.Success) { - data.MapTo(settings); + status = TryParseData(stream, out _, out _, out var data, password); + + if (status == LoadStatus.Success) + { + dataMapper.MapRawDataToSettings(data, settings); + } } return status; @@ -204,65 +156,28 @@ namespace SafeExamBrowser.Configuration } } - public SaveStatus TrySaveSettings(Uri resource, Format format, Settings settings, EncryptionParameters encryption = null) + public SaveStatus TrySaveSettings(Uri destination, FormatType format, Settings settings, EncryptionParameters encryption = null) { - throw new NotImplementedException(); + throw new NotImplementedException("This functionality is not part of version 3.0 Alpha!"); } - private void HandleIdentityCertificates(IDictionary data) + private EncryptionParameters DetermineEncryptionForClientConfiguration(IDictionary data, EncryptionParameters encryption) { - const int IDENTITY_CERTIFICATE = 1; - var hasCertificates = data.TryGetValue("embeddedCertificates", out object value); + var hasKey = data.TryGetValue(Keys.ConfigurationFile.KeepClientConfigEncryption, out var value); + var useDefaultEncryption = value is bool keepEncryption && !keepEncryption; - if (hasCertificates && value is IList> certificates) + if (!hasKey || (hasKey && useDefaultEncryption)) { - var toRemove = new List>(); - - foreach (var certificate in certificates) - { - var isIdentity = certificate.TryGetValue("type", out var o) && o 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)); + encryption = new PasswordParameters { Password = string.Empty, IsHash = true }; } - } - 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(); - } + return encryption; } private LoadStatus TryLoadData(Uri resource, out Stream data) { var status = LoadStatus.NotSupported; - var resourceLoader = dataResources.FirstOrDefault(l => l.CanLoad(resource)); + var resourceLoader = resourceLoaders.FirstOrDefault(l => l.CanLoad(resource)); data = default(Stream); @@ -279,38 +194,74 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus TryParseData(Stream data, out EncryptionParameters encryption, out Format format, out IDictionary rawData, PasswordParameters password = null) + private LoadStatus TryParseData(Stream data, out EncryptionParameters encryption, out FormatType format, out IDictionary rawData, PasswordParameters password = null) { - var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); + var parser = dataParsers.FirstOrDefault(p => p.CanParse(data)); var status = LoadStatus.NotSupported; encryption = default(EncryptionParameters); - format = default(Format); + format = default(FormatType); rawData = default(Dictionary); - if (dataFormat != null) + if (parser != null) { - var result = dataFormat.TryParse(data, password); + var result = parser.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}."); + logger.Info($"Tried to parse data from '{data}' using {parser.GetType().Name} -> Result: {status}."); } else { - logger.Warn($"No data format found for '{data}'!"); + logger.Warn($"No data parser found which can parse '{data}'!"); } return status; } - private void UpdateAppConfig() + private SaveStatus TrySaveData(Uri destination, Stream data) { - appConfig.ClientId = Guid.NewGuid(); - appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}"; + var status = SaveStatus.NotSupported; + var resourceSaver = resourceSavers.FirstOrDefault(s => s.CanSave(destination)); + + if (resourceSaver != null) + { + status = resourceSaver.TrySave(destination, data); + logger.Info($"Tried to save data as '{destination}' using {resourceSaver.GetType().Name} -> Result: {status}."); + } + else + { + logger.Warn($"No resource saver found for '{destination}'!"); + } + + return status; + } + + private SaveStatus TrySerializeData(IDictionary data, FormatType format, out Stream serialized, EncryptionParameters encryption = null) + { + var serializer = dataSerializers.FirstOrDefault(s => s.CanSerialize(format)); + var status = SaveStatus.NotSupported; + + serialized = default(Stream); + + if (serializer != null) + { + var result = serializer.TrySerialize(data, encryption); + + serialized = result.Data; + status = result.Status; + + logger.Info($"Tried to serialize data as '{format}' using {serializer.GetType().Name} -> Result: {status}."); + } + else + { + logger.Error($"No data serializer found which can serialize '{format}'!"); + } + + return status; } } } diff --git a/SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs index 102acb5b..be0df0bf 100644 --- a/SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs @@ -41,8 +41,8 @@ namespace SafeExamBrowser.Configuration.Cryptography } var (version, options) = ParseHeader(data); - var (authenticationKey, encryptionKey) = GenerateKeys(data, password); - var (originalHmac, computedHmac) = GenerateHmac(data, authenticationKey); + var (authenticationKey, encryptionKey) = GenerateKeysForDecryption(data, password); + var (originalHmac, computedHmac) = GenerateHmacForDecryption(authenticationKey, data); if (!computedHmac.SequenceEqual(originalHmac)) { @@ -54,6 +54,16 @@ namespace SafeExamBrowser.Configuration.Cryptography return LoadStatus.Success; } + internal SaveStatus Encrypt(Stream data, string password, out Stream encrypted) + { + var (authKey, authSalt, encrKey, encrSalt) = GenerateKeysForEncryption(password); + + encrypted = Encrypt(data, encrKey, out var initVector); + encrypted = WriteEncryptionParameters(authKey, authSalt, encrSalt, initVector, encrypted); + + return SaveStatus.Success; + } + private (int version, int options) ParseHeader(Stream data) { data.Seek(0, SeekOrigin.Begin); @@ -70,7 +80,7 @@ namespace SafeExamBrowser.Configuration.Cryptography return (version, options); } - private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password) + private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeysForDecryption(Stream data, string password) { var authenticationSalt = new byte[SALT_SIZE]; var encryptionSalt = new byte[SALT_SIZE]; @@ -91,7 +101,23 @@ namespace SafeExamBrowser.Configuration.Cryptography } } - private (byte[] originalHmac, byte[] computedHmac) GenerateHmac(Stream data, byte[] authenticationKey) + private (byte[] authKey, byte[] authSalt, byte[] encrKey, byte[] encrSalt) GenerateKeysForEncryption(string password) + { + logger.Debug("Generating keys for authentication and encryption..."); + + using (var authenticationGenerator = new Rfc2898DeriveBytes(password, SALT_SIZE, ITERATIONS)) + using (var encryptionGenerator = new Rfc2898DeriveBytes(password, SALT_SIZE, ITERATIONS)) + { + var authenticationSalt = authenticationGenerator.Salt; + var authenticationKey = authenticationGenerator.GetBytes(KEY_SIZE); + var encryptionSalt = encryptionGenerator.Salt; + var encryptionKey = encryptionGenerator.GetBytes(KEY_SIZE); + + return (authenticationKey, authenticationSalt, encryptionKey, encryptionSalt); + } + } + + private (byte[] originalHmac, byte[] computedHmac) GenerateHmacForDecryption(byte[] authenticationKey, Stream data) { logger.Debug("Generating HMACs for authentication..."); @@ -108,6 +134,17 @@ namespace SafeExamBrowser.Configuration.Cryptography } } + private byte[] GenerateHmacForEncryption(byte[] authenticationKey, Stream data) + { + data.Seek(0, SeekOrigin.Begin); + logger.Debug("Generating HMAC for authentication..."); + + using (var algorithm = new HMACSHA256(authenticationKey)) + { + return algorithm.ComputeHash(data); + } + } + private LoadStatus FailForInvalidHmac() { logger.Debug($"The authentication failed due to an invalid password or corrupted data!"); @@ -136,5 +173,50 @@ namespace SafeExamBrowser.Configuration.Cryptography return decryptedData; } + + private Stream Encrypt(Stream data, byte[] encryptionKey, out byte[] initializationVector) + { + var encryptedData = new MemoryStream(); + + logger.Debug("Encrypting data..."); + + using (var algorithm = new AesManaged { KeySize = KEY_SIZE * 8, BlockSize = BLOCK_SIZE * 8, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }) + { + algorithm.GenerateIV(); + data.Seek(0, SeekOrigin.Begin); + initializationVector = algorithm.IV; + + using (var encryptor = algorithm.CreateEncryptor(encryptionKey, initializationVector)) + using (var cryptoStream = new CryptoStream(data, encryptor, CryptoStreamMode.Read)) + { + cryptoStream.CopyTo(encryptedData); + } + + return encryptedData; + } + } + + private Stream WriteEncryptionParameters(byte[] authKey, byte[] authSalt, byte[] encrSalt, byte[] initVector, Stream encryptedData) + { + var data = new MemoryStream(); + var header = new byte[] { VERSION, OPTIONS }; + + logger.Debug("Writing encryption parameters..."); + + data.Write(header, 0, header.Length); + data.Write(encrSalt, 0, encrSalt.Length); + data.Write(authSalt, 0, authSalt.Length); + data.Write(initVector, 0, initVector.Length); + + encryptedData.Seek(0, SeekOrigin.Begin); + encryptedData.CopyTo(data); + + var hmac = GenerateHmacForEncryption(authKey, data); + + data.Seek(0, SeekOrigin.End); + data.Write(hmac, 0, hmac.Length); + + return data; + } } } diff --git a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs index 09708f61..e6a94f60 100644 --- a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -43,6 +44,13 @@ namespace SafeExamBrowser.Configuration.Cryptography return LoadStatus.Success; } + internal virtual SaveStatus Encrypt(Stream data, X509Certificate2 certificate, out Stream encrypted) + { + // TODO: Don't forget to write encryption parameters! + + throw new NotImplementedException(); + } + protected byte[] ParsePublicKeyHash(Stream data) { var keyHash = new byte[PUBLIC_KEY_HASH_SIZE]; diff --git a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs index f49d16d3..7dbd8c62 100644 --- a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs @@ -44,6 +44,13 @@ namespace SafeExamBrowser.Configuration.Cryptography return status; } + internal override SaveStatus Encrypt(Stream data, X509Certificate2 certificate, out Stream encrypted) + { + // TODO: Don't forget to write encryption parameters! + + throw new NotImplementedException(); + } + private string ParseSymmetricKey(Stream data, X509Certificate2 certificate) { var keyLengthData = new byte[KEY_LENGTH_SIZE]; diff --git a/SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs b/SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs index b695b610..d52a4c16 100644 --- a/SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs +++ b/SafeExamBrowser.Configuration/DataCompression/GZipCompressor.cs @@ -34,29 +34,36 @@ namespace SafeExamBrowser.Configuration.DataCompression public Stream Compress(Stream data) { - throw new NotImplementedException(); + var compressed = new MemoryStream(); + var originalSize = data.Length / 1000.0; + + logger.Debug($"Starting compression of '{data}' with {originalSize} KB data..."); + data.Seek(0, SeekOrigin.Begin); + + using (var stream = new GZipStream(compressed, CompressionMode.Compress, true)) + { + data.CopyTo(stream); + } + + logger.Debug($"Successfully compressed {originalSize} KB to {compressed.Length / 1000.0} KB data."); + + return compressed; } public Stream Decompress(Stream data) { var decompressed = new MemoryStream(); + var originalSize = data.Length / 1000.0; - logger.Debug($"Starting decompression of '{data}' with {data.Length / 1000.0} KB data..."); + logger.Debug($"Starting decompression of '{data}' with {originalSize} KB data..."); data.Seek(0, SeekOrigin.Begin); using (var stream = new GZipStream(data, CompressionMode.Decompress)) { - var buffer = new byte[4096]; - var bytesRead = 0; - - do - { - bytesRead = stream.Read(buffer, 0, buffer.Length); - decompressed.Write(buffer, 0, bytesRead); - } while (bytesRead > 0); + stream.CopyTo(decompressed); } - logger.Debug($"Successfully decompressed {decompressed.Length / 1000.0} KB data into '{decompressed}'."); + logger.Debug($"Successfully decompressed {originalSize} KB to {decompressed.Length / 1000.0} KB data."); return decompressed; } diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs new file mode 100644 index 00000000..7a8dd67c --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryBlock.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.Configuration.DataFormats +{ + internal static class BinaryBlock + { + internal const string Password = "pswd"; + internal const string PasswordConfigureClient = "pwcc"; + internal const string PlainData = "plnd"; + internal const string PublicKeyHash = "pkhs"; + internal const string PublicKeyHashWithSymmetricKey = "phsk"; + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs deleted file mode 100644 index 93a9f381..00000000 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs +++ /dev/null @@ -1,239 +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/. - */ - -using System; -using System.IO; -using System.Security.Cryptography.X509Certificates; -using System.Text; -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 -{ - public partial class BinaryFormat : IDataFormat - { - private const int PREFIX_LENGTH = 4; - - private IDataCompressor compressor; - private IHashAlgorithm hashAlgorithm; - private IModuleLogger logger; - - public BinaryFormat(IDataCompressor compressor, IHashAlgorithm hashAlgorithm, IModuleLogger logger) - { - this.compressor = compressor; - this.hashAlgorithm = hashAlgorithm; - this.logger = logger; - } - - public bool CanParse(Stream data) - { - try - { - var longEnough = data.Length > PREFIX_LENGTH; - - if (longEnough) - { - var prefix = ParsePrefix(data); - var success = TryDetermineFormat(prefix, out FormatType format); - - logger.Debug($"'{data}' starting with '{prefix}' does {(success ? string.Empty : "not ")}match the binary format."); - - return success; - } - - logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to match the binary format."); - } - catch (Exception e) - { - logger.Error($"Failed to determine whether '{data}' with {data.Length / 1000.0} KB data matches the binary format!", e); - } - - return false; - } - - public ParseResult TryParse(Stream data, PasswordParameters password = null) - { - var prefix = ParsePrefix(data); - var success = TryDetermineFormat(prefix, out FormatType format); - - if (success) - { - if (compressor.IsCompressed(data)) - { - data = compressor.Decompress(data); - } - - logger.Debug($"Attempting to parse '{data}' with format '{prefix}'..."); - data = new SubStream(data, PREFIX_LENGTH, data.Length - PREFIX_LENGTH); - - switch (format) - { - case FormatType.Password: - case FormatType.PasswordConfigureClient: - return ParsePasswordBlock(data, format, password); - case FormatType.PlainData: - return ParsePlainDataBlock(data); - case FormatType.PublicKeyHash: - return ParsePublicKeyHashBlock(data, password); - case FormatType.PublicKeyHashWithSymmetricKey: - return ParsePublicKeyHashWithSymmetricKeyBlock(data, password); - } - } - - logger.Error($"'{data}' starting with '{prefix}' does not match the binary format!"); - - return new ParseResult { Status = LoadStatus.InvalidData }; - } - - private ParseResult ParsePasswordBlock(Stream data, FormatType format, PasswordParameters password = null) - { - var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); - var encryptionParams = new PasswordParameters(); - var result = new ParseResult(); - - if (password != null) - { - if (format == FormatType.PasswordConfigureClient) - { - encryptionParams.Password = password.IsHash ? password.Password : hashAlgorithm.GenerateHashFor(password.Password); - encryptionParams.IsHash = true; - } - else - { - encryptionParams.Password = password.Password; - encryptionParams.IsHash = password.IsHash; - } - - result.Status = encryption.Decrypt(data, encryptionParams.Password, out var decrypted); - - if (result.Status == LoadStatus.Success) - { - result = ParsePlainDataBlock(decrypted); - result.Encryption = encryptionParams; - } - } - else - { - result.Status = LoadStatus.PasswordNeeded; - } - - return result; - } - - private ParseResult ParsePlainDataBlock(Stream data) - { - var xmlFormat = new XmlFormat(logger.CloneFor(nameof(XmlFormat))); - - if (compressor.IsCompressed(data)) - { - data = compressor.Decompress(data); - } - - return xmlFormat.TryParse(data); - } - - private ParseResult ParsePublicKeyHashBlock(Stream data, PasswordParameters password = null) - { - var encryption = new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); - var result = new ParseResult(); - - result.Status = encryption.Decrypt(data, out var decrypted, out var certificate); - - if (result.Status == LoadStatus.Success) - { - result = TryParse(decrypted, password); - result.Encryption = new PublicKeyHashParameters - { - Certificate = certificate, - InnerEncryption = result.Encryption as PasswordParameters, - SymmetricEncryption = false - }; - } - - return result; - } - - private ParseResult ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, PasswordParameters password = null) - { - var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); - var encryption = new PublicKeyHashWithSymmetricKeyEncryption(logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); - var result = new ParseResult(); - - result.Status = encryption.Decrypt(data, out Stream decrypted, out X509Certificate2 certificate); - - if (result.Status == LoadStatus.Success) - { - result = TryParse(decrypted, password); - result.Encryption = new PublicKeyHashParameters - { - Certificate = certificate, - InnerEncryption = result.Encryption as PasswordParameters, - SymmetricEncryption = true - }; - } - - return result; - } - - private string ParsePrefix(Stream data) - { - var prefixData = new byte[PREFIX_LENGTH]; - - if (compressor.IsCompressed(data)) - { - prefixData = compressor.Peek(data, PREFIX_LENGTH); - } - else - { - data.Seek(0, SeekOrigin.Begin); - data.Read(prefixData, 0, PREFIX_LENGTH); - } - - return Encoding.UTF8.GetString(prefixData); - } - - private bool TryDetermineFormat(string prefix, out FormatType format) - { - format = default(FormatType); - - switch (prefix) - { - case "pswd": - format = FormatType.Password; - return true; - case "pwcc": - format = FormatType.PasswordConfigureClient; - return true; - case "plnd": - format = FormatType.PlainData; - return true; - case "pkhs": - format = FormatType.PublicKeyHash; - return true; - case "phsk": - format = FormatType.PublicKeyHashWithSymmetricKey; - return true; - } - - return false; - } - - private enum FormatType - { - Password = 1, - PasswordConfigureClient, - PlainData, - PublicKeyHash, - PublicKeyHashWithSymmetricKey - } - } -} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs new file mode 100644 index 00000000..c081f781 --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs @@ -0,0 +1,207 @@ +/* + * 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; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +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 +{ + public class BinaryParser : IDataParser + { + private const int PREFIX_LENGTH = 4; + + private IDataCompressor compressor; + private IHashAlgorithm hashAlgorithm; + private IModuleLogger logger; + + public BinaryParser(IDataCompressor compressor, IHashAlgorithm hashAlgorithm, IModuleLogger logger) + { + this.compressor = compressor; + this.hashAlgorithm = hashAlgorithm; + this.logger = logger; + } + + public bool CanParse(Stream data) + { + try + { + var longEnough = data.Length > PREFIX_LENGTH; + + if (longEnough) + { + var prefix = ReadPrefix(data); + var isValid = IsValid(prefix); + + logger.Debug($"'{data}' starting with '{prefix}' does {(isValid ? string.Empty : "not ")}match the {FormatType.Binary} format."); + + return isValid; + } + + logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to match the {FormatType.Binary} format."); + } + catch (Exception e) + { + logger.Error($"Failed to determine whether '{data}' with {data.Length / 1000.0} KB data matches the {FormatType.Binary} format!", e); + } + + return false; + } + + public ParseResult TryParse(Stream data, PasswordParameters password = null) + { + var prefix = ReadPrefix(data); + var isValid = IsValid(prefix); + + if (isValid) + { + data = compressor.IsCompressed(data) ? compressor.Decompress(data) : data; + data = new SubStream(data, PREFIX_LENGTH, data.Length - PREFIX_LENGTH); + + switch (prefix) + { + case BinaryBlock.Password: + case BinaryBlock.PasswordConfigureClient: + return ParsePasswordBlock(data, prefix, password); + case BinaryBlock.PlainData: + return ParsePlainDataBlock(data); + case BinaryBlock.PublicKeyHash: + case BinaryBlock.PublicKeyHashWithSymmetricKey: + return ParsePublicKeyHashBlock(data, prefix, password); + } + } + + logger.Error($"'{data}' starting with '{prefix}' does not match the {FormatType.Binary} format!"); + + return new ParseResult { Status = LoadStatus.InvalidData }; + } + + private ParseResult ParsePasswordBlock(Stream data, string prefix, PasswordParameters password = null) + { + var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); + var result = new ParseResult(); + + if (password != null) + { + var encryptionParameters = DetermineEncryptionParametersFor(prefix, password); + + logger.Debug($"Attempting to parse password block with prefix '{prefix}'..."); + result.Status = encryption.Decrypt(data, encryptionParameters.Password, out var decrypted); + + if (result.Status == LoadStatus.Success) + { + result = ParsePlainDataBlock(decrypted); + result.Encryption = encryptionParameters; + } + } + else + { + result.Status = LoadStatus.PasswordNeeded; + } + + return result; + } + + private ParseResult ParsePlainDataBlock(Stream data) + { + var xmlFormat = new XmlParser(logger.CloneFor(nameof(XmlParser))); + + data = compressor.IsCompressed(data) ? compressor.Decompress(data) : data; + logger.Debug("Attempting to parse plain data block..."); + + var result = xmlFormat.TryParse(data); + result.Format = FormatType.Binary; + + return result; + } + + private ParseResult ParsePublicKeyHashBlock(Stream data, string prefix, PasswordParameters password = null) + { + var encryption = DetermineEncryptionForPublicKeyHashBlock(prefix); + var result = new ParseResult(); + + logger.Debug($"Attempting to parse public key hash block with prefix '{prefix}'..."); + result.Status = encryption.Decrypt(data, out var decrypted, out var certificate); + + if (result.Status == LoadStatus.Success) + { + result = TryParse(decrypted, password); + result.Encryption = new PublicKeyHashParameters + { + Certificate = certificate, + InnerEncryption = result.Encryption as PasswordParameters, + SymmetricEncryption = prefix == BinaryBlock.PublicKeyHashWithSymmetricKey + }; + } + + return result; + } + + private PublicKeyHashEncryption DetermineEncryptionForPublicKeyHashBlock(string prefix) + { + var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); + + if (prefix == BinaryBlock.PublicKeyHash) + { + return new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); + } + + return new PublicKeyHashWithSymmetricKeyEncryption(logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); + } + + private PasswordParameters DetermineEncryptionParametersFor(string prefix, PasswordParameters password) + { + var parameters = new PasswordParameters(); + + if (prefix == BinaryBlock.PasswordConfigureClient) + { + parameters.Password = password.IsHash ? password.Password : hashAlgorithm.GenerateHashFor(password.Password); + parameters.IsHash = true; + } + else + { + parameters.Password = password.Password; + parameters.IsHash = password.IsHash; + } + + return parameters; + } + + private string ReadPrefix(Stream data) + { + var prefixData = new byte[PREFIX_LENGTH]; + + if (compressor.IsCompressed(data)) + { + prefixData = compressor.Peek(data, PREFIX_LENGTH); + } + else + { + data.Seek(0, SeekOrigin.Begin); + data.Read(prefixData, 0, PREFIX_LENGTH); + } + + return Encoding.UTF8.GetString(prefixData); + } + + private bool IsValid(string prefix) + { + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static; + + return typeof(BinaryBlock).GetFields(bindingFlags).Any(f => f.GetRawConstantValue() as string == prefix); + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs b/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs new file mode 100644 index 00000000..23e40631 --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs @@ -0,0 +1,168 @@ +/* + * 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; +using System.IO; +using System.Text; +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 +{ + public class BinarySerializer : IDataSerializer + { + private IDataCompressor compressor; + private IModuleLogger logger; + + public BinarySerializer(IDataCompressor compressor, IModuleLogger logger) + { + this.compressor = compressor; + this.logger = logger; + } + + public bool CanSerialize(FormatType format) + { + return format == FormatType.Binary; + } + + public SerializeResult TrySerialize(IDictionary data, EncryptionParameters encryption = null) + { + var result = new SerializeResult(); + + switch (encryption) + { + case PasswordParameters p: + result = SerializePasswordBlock(data, p); + break; + case PublicKeyHashParameters p: + result = SerializePublicKeyHashBlock(data, p); + break; + default: + result = SerializePlainDataBlock(data, true); + break; + } + + if (result.Status == SaveStatus.Success) + { + result.Data = compressor.Compress(result.Data); + } + + return result; + } + + private SerializeResult SerializePasswordBlock(IDictionary data, PasswordParameters password) + { + var result = SerializePlainDataBlock(data); + + if (result.Status == SaveStatus.Success) + { + var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); + var prefix = password.IsHash ? BinaryBlock.PasswordConfigureClient : BinaryBlock.Password; + + logger.Debug("Attempting to serialize password block..."); + + var status = encryption.Encrypt(result.Data, password.Password, out var encrypted); + + if (status == SaveStatus.Success) + { + result.Data = WritePrefix(prefix, encrypted); + } + + result.Status = status; + } + + return result; + } + + private SerializeResult SerializePlainDataBlock(IDictionary data, bool writePrefix = false) + { + logger.Debug("Attempting to serialize plain data block..."); + + var xmlSerializer = new XmlSerializer(logger.CloneFor(nameof(XmlSerializer))); + var result = xmlSerializer.TrySerialize(data); + + if (result.Status == SaveStatus.Success) + { + if (writePrefix) + { + result.Data = WritePrefix(BinaryBlock.PlainData, result.Data); + } + + result.Data = compressor.Compress(result.Data); + } + + return result; + } + + private SerializeResult SerializePublicKeyHashBlock(IDictionary data, PublicKeyHashParameters parameters) + { + var result = SerializePlainDataBlock(data); + + if (result.Status == SaveStatus.Success) + { + result = SerializePublicKeyHashInnerBlock(data, parameters); + + if (result.Status == SaveStatus.Success) + { + var encryption = DetermineEncryptionForPublicKeyHashBlock(parameters); + var prefix = parameters.SymmetricEncryption ? BinaryBlock.PublicKeyHashWithSymmetricKey : BinaryBlock.PublicKeyHash; + + logger.Debug("Attempting to serialize public key hash block..."); + + var status = encryption.Encrypt(result.Data, parameters.Certificate, out var encrypted); + + if (status == SaveStatus.Success) + { + result.Data = WritePrefix(prefix, encrypted); + } + } + } + + return result; + } + + private SerializeResult SerializePublicKeyHashInnerBlock(IDictionary data, PublicKeyHashParameters parameters) + { + if (parameters.InnerEncryption is PasswordParameters password) + { + return SerializePasswordBlock(data, password); + } + + return SerializePlainDataBlock(data, true); + } + + private PublicKeyHashEncryption DetermineEncryptionForPublicKeyHashBlock(PublicKeyHashParameters parameters) + { + var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); + + if (parameters.SymmetricEncryption) + { + return new PublicKeyHashWithSymmetricKeyEncryption(logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); + } + + return new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); + } + + private Stream WritePrefix(string prefix, Stream data) + { + var prefixBytes = Encoding.UTF8.GetBytes(prefix); + var stream = new MemoryStream(); + + stream.Write(prefixBytes, 0, prefixBytes.Length); + + data.Seek(0, SeekOrigin.Begin); + data.CopyTo(stream); + + return stream; + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs b/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs deleted file mode 100644 index 63af9af9..00000000 --- a/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs +++ /dev/null @@ -1,65 +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/. - */ - -using System; -using System.Collections.Generic; -using SafeExamBrowser.Contracts.Configuration.Settings; - -namespace SafeExamBrowser.Configuration.DataFormats -{ - internal static class DataMapper - { - internal static void MapTo(this IDictionary rawData, Settings settings) - { - foreach (var kvp in rawData) - { - Map(kvp.Key, kvp.Value, settings); - } - } - - private static void Map(string key, object value, Settings settings) - { - switch (key) - { - case "hashedAdminPassword": - settings.MapAdminPasswordHash(value); - break; - case "sebConfigPurpose": - settings.MapConfigurationMode(value); - break; - case "startURL": - settings.MapStartUrl(value); - break; - } - } - - 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) - { - settings.ConfigurationMode = mode == 1 ? ConfigurationMode.ConfigureClient : ConfigurationMode.Exam; - } - } - - private static void MapStartUrl(this Settings settings, object value) - { - if (value is string url) - { - settings.Browser.StartUrl = url; - } - } - } -} diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlElement.cs b/SafeExamBrowser.Configuration/DataFormats/XmlElement.cs new file mode 100644 index 00000000..d4c807d1 --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/XmlElement.cs @@ -0,0 +1,25 @@ +/* + * 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.Configuration.DataFormats +{ + internal static class XmlElement + { + public const string Array = "array"; + public const string Data = "data"; + public const string Date = "date"; + public const string Dictionary = "dict"; + public const string False = "false"; + public const string Integer = "integer"; + public const string Key = "key"; + public const string Real = "real"; + public const string Root = "plist"; + public const string String = "string"; + public const string True = "true"; + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlParser.cs similarity index 71% rename from SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs rename to SafeExamBrowser.Configuration/DataFormats/XmlParser.cs index 620675e9..63476b91 100644 --- a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/XmlParser.cs @@ -19,14 +19,13 @@ using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats { - public class XmlFormat : IDataFormat + public class XmlParser : IDataParser { - private const string ROOT_NODE = "plist"; private const string XML_PREFIX = "(); if (hasRoot && hasDictionary) @@ -84,7 +84,7 @@ namespace SafeExamBrowser.Configuration.DataFormats } else { - logger.Error($"Could not find root {(!hasRoot ? $"node '{ROOT_NODE}'" : $"dictionary '{DataTypes.DICTIONARY}'")}!"); + logger.Error($"Could not find root {(!hasRoot ? $"node '{XmlElement.Root}'" : $"dictionary '{XmlElement.Dictionary}'")}!"); } } @@ -118,13 +118,13 @@ namespace SafeExamBrowser.Configuration.DataFormats reader.MoveToContent(); } - if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DataTypes.ARRAY) + if (reader.NodeType == XmlNodeType.EndElement && reader.Name == XmlElement.Array) { return LoadStatus.Success; } else { - logger.Error($"Expected closing tag for '{DataTypes.ARRAY}', but found '{reader.Name}{reader.Value}'!"); + logger.Error($"Expected closing tag for '{XmlElement.Array}', but found '{reader.Name}{reader.Value}'!"); return LoadStatus.InvalidData; } @@ -153,13 +153,13 @@ namespace SafeExamBrowser.Configuration.DataFormats reader.MoveToContent(); } - if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DataTypes.DICTIONARY) + if (reader.NodeType == XmlNodeType.EndElement && reader.Name == XmlElement.Dictionary) { return LoadStatus.Success; } else { - logger.Error($"Expected closing tag for '{DataTypes.DICTIONARY}', but found '{reader.Name}{reader.Value}'!"); + logger.Error($"Expected closing tag for '{XmlElement.Dictionary}', but found '{reader.Name}{reader.Value}'!"); return LoadStatus.InvalidData; } @@ -169,9 +169,9 @@ namespace SafeExamBrowser.Configuration.DataFormats { var key = XNode.ReadFrom(reader) as XElement; - if (key.Name.LocalName != DataTypes.KEY) + if (key.Name.LocalName != XmlElement.Key) { - logger.Error($"Expected element '{DataTypes.KEY}', but found '{key}'!"); + logger.Error($"Expected element '{XmlElement.Key}', but found '{key}'!"); return LoadStatus.InvalidData; } @@ -196,12 +196,12 @@ namespace SafeExamBrowser.Configuration.DataFormats var status = default(LoadStatus); var value = default(object); - if (reader.Name == DataTypes.ARRAY) + if (reader.Name == XmlElement.Array) { array = new List(); status = ParseArray(reader, array); } - else if (reader.Name == DataTypes.DICTIONARY) + else if (reader.Name == XmlElement.Dictionary) { dictionary = new Dictionary(); status = ParseDictionary(reader, dictionary); @@ -218,60 +218,49 @@ namespace SafeExamBrowser.Configuration.DataFormats private LoadStatus ParseSimpleType(XElement element, out object value) { + var status = LoadStatus.Success; + value = null; if (element.IsEmpty) { - return LoadStatus.Success; + return status; } switch (element.Name.LocalName) { - case DataTypes.DATA: + case XmlElement.Data: value = Convert.FromBase64String(element.Value); break; - case DataTypes.DATE: + case XmlElement.Date: value = XmlConvert.ToDateTime(element.Value, XmlDateTimeSerializationMode.Utc); break; - case DataTypes.FALSE: + case XmlElement.False: value = false; break; - case DataTypes.INTEGER: + case XmlElement.Integer: value = Convert.ToInt32(element.Value); break; - case DataTypes.REAL: + case XmlElement.Real: value = Convert.ToDouble(element.Value); break; - case DataTypes.STRING: + case XmlElement.String: value = element.Value; break; - case DataTypes.TRUE: + case XmlElement.True: value = true; break; + default: + status = LoadStatus.InvalidData; + break; } - if (value == null) + if (status != LoadStatus.Success) { logger.Error($"Element '{element}' is not supported!"); - - return LoadStatus.InvalidData; } - return LoadStatus.Success; - } - - private struct DataTypes - { - public const string ARRAY = "array"; - public const string DATA = "data"; - public const string DATE = "date"; - public const string DICTIONARY = "dict"; - public const string FALSE = "false"; - public const string INTEGER = "integer"; - public const string KEY = "key"; - public const string REAL = "real"; - public const string STRING = "string"; - public const string TRUE = "true"; + return status; } } } diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs b/SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs new file mode 100644 index 00000000..6a5246fa --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs @@ -0,0 +1,199 @@ +/* + * 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; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration.DataFormats; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public class XmlSerializer : IDataSerializer + { + private ILogger logger; + + public XmlSerializer(ILogger logger) + { + this.logger = logger; + } + + public bool CanSerialize(FormatType format) + { + return format == FormatType.Xml; + } + + public SerializeResult TrySerialize(IDictionary data, EncryptionParameters encryption = null) + { + var result = new SerializeResult(); + var settings = new XmlWriterSettings { Encoding = new UTF8Encoding(), Indent = true }; + var stream = new MemoryStream(); + + logger.Debug("Starting to serialize data..."); + + using (var writer = XmlWriter.Create(stream, settings)) + { + writer.WriteStartDocument(); + writer.WriteDocType("plist", "-//Apple Computer//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null); + writer.WriteStartElement(XmlElement.Root); + writer.WriteAttributeString("version", "1.0"); + + result.Status = WriteDictionary(writer, data); + result.Data = stream; + + writer.WriteEndElement(); + writer.WriteEndDocument(); + writer.Flush(); + writer.Close(); + } + + logger.Debug($"Finished serialization -> Result: {result.Status}."); + + return result; + } + + private SaveStatus WriteArray(XmlWriter writer, List array) + { + var status = SaveStatus.Success; + + writer.WriteStartElement(XmlElement.Array); + + foreach (var item in array) + { + status = WriteElement(writer, item); + + if (status != SaveStatus.Success) + { + break; + } + } + + writer.WriteEndElement(); + + return status; + } + + private SaveStatus WriteDictionary(XmlWriter writer, IDictionary data) + { + var status = SaveStatus.Success; + + writer.WriteStartElement(XmlElement.Dictionary); + + foreach (var item in data.OrderBy(i => i.Key)) + { + status = WriteKeyValuePair(writer, item); + + if (status != SaveStatus.Success) + { + break; + } + } + + writer.WriteEndElement(); + + return status; + } + + private SaveStatus WriteKeyValuePair(XmlWriter writer, KeyValuePair item) + { + var status = SaveStatus.InvalidData; + + if (item.Key != null) + { + writer.WriteElementString(XmlElement.Key, item.Key); + status = WriteElement(writer, item.Value); + } + else + { + logger.Error($"Key of item '{item}' is null!"); + } + + return status; + } + + private SaveStatus WriteElement(XmlWriter writer, object element) + { + var status = default(SaveStatus); + + if (element is List array) + { + status = WriteArray(writer, array); + } + else if (element is Dictionary dictionary) + { + status = WriteDictionary(writer, dictionary); + } + else + { + status = WriteSimpleType(writer, element); + } + + return status; + } + + private SaveStatus WriteSimpleType(XmlWriter writer, object element) + { + var name = default(string); + var value = default(string); + var status = SaveStatus.Success; + + switch (element) + { + case byte[] data: + name = XmlElement.Data; + value = Convert.ToBase64String(data); + break; + case DateTime date: + name = XmlElement.Date; + value = XmlConvert.ToString(date, XmlDateTimeSerializationMode.Utc); + break; + case bool boolean when boolean == false: + name = XmlElement.False; + break; + case bool boolean when boolean == true: + name = XmlElement.True; + break; + case int integer: + name = XmlElement.Integer; + value = integer.ToString(NumberFormatInfo.InvariantInfo); + break; + case double real: + name = XmlElement.Real; + value = real.ToString(NumberFormatInfo.InvariantInfo); + break; + case string text: + name = XmlElement.String; + value = text; + break; + case null: + name = XmlElement.String; + break; + default: + status = SaveStatus.InvalidData; + break; + } + + if (status == SaveStatus.Success) + { + writer.WriteElementString(name, value); + } + else + { + logger.Error($"Element '{element}' is not supported!"); + } + + return status; + } + } +} diff --git a/SafeExamBrowser.Configuration/DataResources/FileResource.cs b/SafeExamBrowser.Configuration/DataResources/FileResourceLoader.cs similarity index 74% rename from SafeExamBrowser.Configuration/DataResources/FileResource.cs rename to SafeExamBrowser.Configuration/DataResources/FileResourceLoader.cs index 23d623e9..14abd539 100644 --- a/SafeExamBrowser.Configuration/DataResources/FileResource.cs +++ b/SafeExamBrowser.Configuration/DataResources/FileResourceLoader.cs @@ -14,11 +14,11 @@ using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataResources { - public class FileResource: IDataResource + public class FileResourceLoader : IResourceLoader { private ILogger logger; - public FileResource(ILogger logger) + public FileResourceLoader(ILogger logger) { this.logger = logger; } @@ -29,11 +29,11 @@ namespace SafeExamBrowser.Configuration.DataResources if (exists) { - logger.Debug($"Can load '{resource}' as it references an existing file."); + logger.Debug($"Can load '{resource}' as it is an existing file."); } else { - logger.Debug($"Can't load '{resource}' since it isn't a file URI or no file exists at the specified path."); + logger.Debug($"Can't load '{resource}' as it isn't an existing file."); } return exists; @@ -47,10 +47,5 @@ namespace SafeExamBrowser.Configuration.DataResources return LoadStatus.Success; } - - public SaveStatus TrySave(Uri resource, Stream data) - { - throw new NotImplementedException(); - } } } diff --git a/SafeExamBrowser.Configuration/DataResources/FileResourceSaver.cs b/SafeExamBrowser.Configuration/DataResources/FileResourceSaver.cs new file mode 100644 index 00000000..dfd0253a --- /dev/null +++ b/SafeExamBrowser.Configuration/DataResources/FileResourceSaver.cs @@ -0,0 +1,65 @@ +/* + * 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; +using System.IO; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.DataResources; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.DataResources +{ + public class FileResourceSaver : IResourceSaver + { + private ILogger logger; + + public FileResourceSaver(ILogger logger) + { + this.logger = logger; + } + + public bool CanSave(Uri destination) + { + var isFullPath = destination.IsFile && Path.IsPathRooted(destination.LocalPath); + + if (isFullPath) + { + logger.Debug($"Can save data as '{destination}' since it defines an absolute file path."); + } + else + { + logger.Debug($"Can't save data as '{destination}' since it doesn't define an absolute file path."); + } + + return isFullPath; + } + + public SaveStatus TrySave(Uri destination, Stream data) + { + var directory = Path.GetDirectoryName(destination.AbsolutePath); + + logger.Debug($"Attempting to save '{data}' with {data.Length / 1000.0} KB data as '{destination}'..."); + + if (!Directory.Exists(directory)) + { + logger.Debug($"Creating directory '{directory}'..."); + Directory.CreateDirectory(directory); + } + + using (var fileStream = new FileStream(destination.AbsolutePath, FileMode.Create)) + { + data.Seek(0, SeekOrigin.Begin); + data.CopyTo(fileStream); + } + + logger.Debug($"Successfully saved data as '{destination}'."); + + return SaveStatus.Success; + } + } +} diff --git a/SafeExamBrowser.Configuration/DataResources/NetworkResource.cs b/SafeExamBrowser.Configuration/DataResources/NetworkResourceLoader.cs similarity index 95% rename from SafeExamBrowser.Configuration/DataResources/NetworkResource.cs rename to SafeExamBrowser.Configuration/DataResources/NetworkResourceLoader.cs index d2567682..3553a2c9 100644 --- a/SafeExamBrowser.Configuration/DataResources/NetworkResource.cs +++ b/SafeExamBrowser.Configuration/DataResources/NetworkResourceLoader.cs @@ -19,7 +19,7 @@ using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataResources { - public class NetworkResource : IDataResource + public class NetworkResourceLoader : IResourceLoader { private AppConfig appConfig; private ILogger logger; @@ -41,7 +41,7 @@ namespace SafeExamBrowser.Configuration.DataResources Uri.UriSchemeHttps }; - public NetworkResource(AppConfig appConfig, ILogger logger) + public NetworkResourceLoader(AppConfig appConfig, ILogger logger) { this.appConfig = appConfig; this.logger = logger; @@ -86,11 +86,6 @@ namespace SafeExamBrowser.Configuration.DataResources 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 ba9a8df2..88e71186 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -58,18 +58,26 @@ + + + - - - + + + + + + + + - - + + diff --git a/SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/FormatType.cs similarity index 95% rename from SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs rename to SafeExamBrowser.Contracts/Configuration/DataFormats/FormatType.cs index a3b127e7..a4fbab30 100644 --- a/SafeExamBrowser.Contracts/Configuration/DataFormats/Format.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/FormatType.cs @@ -11,7 +11,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats /// /// Defines all supported data formats. /// - public enum Format + public enum FormatType { Binary = 1, Xml diff --git a/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataParser.cs similarity index 96% rename from SafeExamBrowser.Contracts/Configuration/DataFormats/IDataFormat.cs rename to SafeExamBrowser.Contracts/Configuration/DataFormats/IDataParser.cs index 56656aaa..d283ba3d 100644 --- a/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataFormat.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataParser.cs @@ -14,7 +14,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats /// /// Provides functionality to parse configuration data with a particular format. /// - public interface IDataFormat + public interface IDataParser { /// /// Indicates whether the given data complies with the required format. diff --git a/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataSerializer.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataSerializer.cs new file mode 100644 index 00000000..6b796f10 --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/IDataSerializer.cs @@ -0,0 +1,29 @@ +/* + * 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; +using SafeExamBrowser.Contracts.Configuration.Cryptography; + +namespace SafeExamBrowser.Contracts.Configuration.DataFormats +{ + /// + /// Provides functionality to serialize configuration data to a particular format. + /// + public interface IDataSerializer + { + /// + /// Indicates whether data can be serialized to the given format. + /// + bool CanSerialize(FormatType format); + + /// + /// Tries to serialize the given data. + /// + SerializeResult TrySerialize(IDictionary data, EncryptionParameters encryption = null); + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs index 4532eb4b..1a60f099 100644 --- a/SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/ParseResult.cs @@ -12,7 +12,7 @@ using SafeExamBrowser.Contracts.Configuration.Cryptography; namespace SafeExamBrowser.Contracts.Configuration.DataFormats { /// - /// Defines the result of a data parsing operation by an . + /// Defines the result of a data parsing operation by an . /// public class ParseResult { @@ -24,7 +24,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats /// /// The original format of the data. /// - public Format Format { get; set; } + public FormatType Format { get; set; } /// /// The parsed settings data. Might be null or in an undefinable state, depending on . @@ -32,7 +32,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats public IDictionary RawData { get; set; } /// - /// The status result of a parsing operation. + /// The status result of the parsing operation. /// public LoadStatus Status { get; set; } } diff --git a/SafeExamBrowser.Contracts/Configuration/DataFormats/SerializeResult.cs b/SafeExamBrowser.Contracts/Configuration/DataFormats/SerializeResult.cs new file mode 100644 index 00000000..410c9053 --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/DataFormats/SerializeResult.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.IO; + +namespace SafeExamBrowser.Contracts.Configuration.DataFormats +{ + /// + /// Defines the result of a data serialization operation by an . + /// + public class SerializeResult + { + /// + /// The serialized data. Might be null or in an undefinable state, depending on . + /// + public Stream Data { get; set; } + + /// + /// The status result of the serialization operation. + /// + public SaveStatus Status { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/DataResources/IDataResource.cs b/SafeExamBrowser.Contracts/Configuration/DataResources/IResourceLoader.cs similarity index 71% rename from SafeExamBrowser.Contracts/Configuration/DataResources/IDataResource.cs rename to SafeExamBrowser.Contracts/Configuration/DataResources/IResourceLoader.cs index 47f88931..cbcca5f4 100644 --- a/SafeExamBrowser.Contracts/Configuration/DataResources/IDataResource.cs +++ b/SafeExamBrowser.Contracts/Configuration/DataResources/IResourceLoader.cs @@ -12,9 +12,9 @@ using System.IO; namespace SafeExamBrowser.Contracts.Configuration.DataResources { /// - /// Provides functionality to load and save configuration data from / as a particular resource. + /// Provides functionality to load configuration data from a particular resource type. /// - public interface IDataResource + public interface IResourceLoader { /// /// Indicates whether data can be loaded from the specified resource. @@ -25,10 +25,5 @@ namespace SafeExamBrowser.Contracts.Configuration.DataResources /// 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/DataResources/IResourceSaver.cs b/SafeExamBrowser.Contracts/Configuration/DataResources/IResourceSaver.cs new file mode 100644 index 00000000..842459e2 --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/DataResources/IResourceSaver.cs @@ -0,0 +1,29 @@ +/* + * 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; +using System.IO; + +namespace SafeExamBrowser.Contracts.Configuration.DataResources +{ + /// + /// Provides functionality to save configuration data as a particular resource type. + /// + public interface IResourceSaver + { + /// + /// Indicates whether data can be saved as the specified resource. + /// + bool CanSave(Uri destination); + + /// + /// Tries to save the configuration data as the specified resource. + /// + SaveStatus TrySave(Uri destination, Stream data); + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index 5d05d47b..033fe114 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -39,14 +39,24 @@ namespace SafeExamBrowser.Contracts.Configuration Settings.Settings LoadDefaultSettings(); /// - /// Registers the specified to be used when loading or saving configuration data. + /// Registers the specified to be used to parse configuration data. /// - void Register(IDataFormat dataFormat); + void Register(IDataParser parser); /// - /// Registers the specified to be used when loading or saving configuration data. + /// Registers the specified to be used to serialize configuration data. /// - void Register(IDataResource dataResource); + void Register(IDataSerializer serializer); + + /// + /// Registers the specified to be used to load configuration resources. + /// + void Register(IResourceLoader loader); + + /// + /// Registers the specified to be used to save configuration resources. + /// + void Register(IResourceSaver saver); /// /// Attempts to load settings from the specified resource. @@ -56,6 +66,6 @@ namespace SafeExamBrowser.Contracts.Configuration /// /// Attempts to save settings according to the specified parameters. /// - SaveStatus TrySaveSettings(Uri resource, Format format, Settings.Settings settings, EncryptionParameters encryption = null); + SaveStatus TrySaveSettings(Uri destination, FormatType format, Settings.Settings settings, EncryptionParameters encryption = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/SaveStatus.cs b/SafeExamBrowser.Contracts/Configuration/SaveStatus.cs index 86b12e6e..02a954a7 100644 --- a/SafeExamBrowser.Contracts/Configuration/SaveStatus.cs +++ b/SafeExamBrowser.Contracts/Configuration/SaveStatus.cs @@ -9,10 +9,20 @@ namespace SafeExamBrowser.Contracts.Configuration { /// - /// Defines all possible results of an attempt to save an application configuration. + /// Defines all possible results of an attempt to save a configuration resource. /// public enum SaveStatus { + /// + /// The configuration data is invalid or contains invalid elements. + /// + InvalidData, + + /// + /// The configuration format or resource type is not supported. + /// + NotSupported, + /// /// The configuration was saved successfully. /// diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/Settings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/Settings.cs index ba11b4e3..75d191bb 100644 --- a/SafeExamBrowser.Contracts/Configuration/Settings/Settings.cs +++ b/SafeExamBrowser.Contracts/Configuration/Settings/Settings.cs @@ -63,7 +63,7 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings Mouse = new MouseSettings(); Taskbar = new TaskbarSettings(); - // TODO: For version 3.0 alpha only, remove for final release! + // TODO: For version 3.0 Alpha only, remove for final release! ServicePolicy = ServicePolicy.Optional; Taskbar.AllowApplicationLog = true; } diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 87a5303d..4081dbd7 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -60,11 +60,15 @@ - + - + + + + + @@ -120,7 +124,6 @@ - diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 66ed7781..849fc7e9 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -120,10 +120,13 @@ namespace SafeExamBrowser.Runtime 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)))); - configuration.Register(new XmlFormat(new ModuleLogger(logger, nameof(XmlFormat)))); - configuration.Register(new FileResource(new ModuleLogger(logger, nameof(FileResource)))); - configuration.Register(new NetworkResource(appConfig, new ModuleLogger(logger, nameof(NetworkResource)))); + configuration.Register(new BinaryParser(compressor, new HashAlgorithm(), new ModuleLogger(logger, nameof(BinaryParser)))); + configuration.Register(new BinarySerializer(compressor, new ModuleLogger(logger, nameof(BinarySerializer)))); + configuration.Register(new XmlParser(new ModuleLogger(logger, nameof(XmlParser)))); + configuration.Register(new XmlSerializer(new ModuleLogger(logger, nameof(XmlSerializer)))); + configuration.Register(new FileResourceLoader(new ModuleLogger(logger, nameof(FileResourceLoader)))); + configuration.Register(new FileResourceSaver(new ModuleLogger(logger, nameof(FileResourceSaver)))); + configuration.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader)))); } private void InitializeLogging() diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index c5c3c5d4..5ce71b6e 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -179,10 +179,10 @@ namespace SafeExamBrowser.Runtime.Operations { var isAppDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(AppDataFile, StringComparison.OrdinalIgnoreCase); var isProgramDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(ProgramDataFile, StringComparison.OrdinalIgnoreCase); + var isFirstSession = Context.Current == null; if (!isAppDataFile && !isProgramDataFile) { - var isFirstSession = Context.Current == null; var requiresAuthentication = IsAuthenticationRequiredForClientConfiguration(password); logger.Info("Starting client configuration..."); @@ -203,7 +203,11 @@ namespace SafeExamBrowser.Runtime.Operations var status = configuration.ConfigureClientWith(resource, password); - if (status != SaveStatus.Success) + if (status == SaveStatus.Success) + { + logger.Info("Client configuration was successful."); + } + else { logger.Error($"Client configuration failed with status '{status}'!"); ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs()); diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index 7ef4bd65..981ad79a 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -158,7 +158,7 @@ namespace SafeExamBrowser.Runtime runtimeWindow.Show(); runtimeWindow.BringToForeground(); runtimeWindow.ShowProgressBar(); - logger.Info("### --- Session Start Procedure --- ###"); + logger.Info(AppendDivider("Session Start Procedure")); if (SessionIsRunning) { @@ -169,19 +169,19 @@ namespace SafeExamBrowser.Runtime if (result == OperationResult.Success) { - logger.Info("### --- Session Running --- ###"); + logger.Info(AppendDivider("Session Running")); HandleSessionStartSuccess(); } else if (result == OperationResult.Failed) { - logger.Info("### --- Session Start Failed --- ###"); + logger.Info(AppendDivider("Session Start Failed")); HandleSessionStartFailure(); } else if (result == OperationResult.Aborted) { - logger.Info("### --- Session Start Aborted --- ###"); + logger.Info(AppendDivider("Session Start Aborted")); HandleSessionStartAbortion(); } @@ -232,7 +232,7 @@ namespace SafeExamBrowser.Runtime runtimeWindow.Show(); runtimeWindow.BringToForeground(); runtimeWindow.ShowProgressBar(); - logger.Info("### --- Session Stop Procedure --- ###"); + logger.Info(AppendDivider("Session Stop Procedure")); DeregisterSessionEvents(); @@ -240,11 +240,11 @@ namespace SafeExamBrowser.Runtime if (success) { - logger.Info("### --- Session Terminated --- ###"); + logger.Info(AppendDivider("Session Terminated")); } else { - logger.Info("### --- Session Stop Failed --- ###"); + logger.Info(AppendDivider("Session Stop Failed")); } } @@ -532,5 +532,13 @@ namespace SafeExamBrowser.Runtime progressIndicator?.Regress(); } } + + private string AppendDivider(string message) + { + var dashesLeft = new String('-', 48 - message.Length / 2 - message.Length % 2); + var dashesRight = new String('-', 48 - message.Length / 2); + + return $"### {dashesLeft} {message} {dashesRight} ###"; + } } } diff --git a/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs b/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs index a1be03ea..ef136289 100644 --- a/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs @@ -40,7 +40,15 @@ namespace SafeExamBrowser.UserInterface.Classic public void BringToForeground() { - Dispatcher.Invoke(Activate); + Dispatcher.Invoke(() => + { + if (WindowState == WindowState.Minimized) + { + WindowState = WindowState.Normal; + } + + Activate(); + }); } public new void Close()