From 9b639b0c533a2ec35f362f5ad57c3d249409e04b Mon Sep 17 00:00:00 2001 From: dbuechel Date: Fri, 30 Nov 2018 14:50:28 +0100 Subject: [PATCH] SEBWIN-221: Improved configuration loading / parsing algorithm and implemented scaffolding for data mapping. --- .../ConfigurationRepository.cs | 22 +- .../DataFormats/BinaryFormat.cs | 60 ++--- .../Cryptography/PasswordEncryption.cs | 18 +- .../DataFormats/DataMapper.cs | 54 +++++ .../DataFormats/XmlFormat.cs | 206 ++++++++++++------ .../SafeExamBrowser.Configuration.csproj | 1 + .../Configuration/IDataFormat.cs | 4 +- .../Operations/ConfigurationOperation.cs | 9 +- 8 files changed, 240 insertions(+), 134 deletions(-) create mode 100644 SafeExamBrowser.Configuration/DataFormats/DataMapper.cs diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index f746e212..89053846 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -126,10 +126,10 @@ namespace SafeExamBrowser.Configuration public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string password = null, bool passwordIsHash = false) { - settings = default(Settings); - logger.Info($"Attempting to load '{resource}'..."); + settings = LoadDefaultSettings(); + try { var status = TryLoadData(resource, out Stream data); @@ -139,13 +139,13 @@ namespace SafeExamBrowser.Configuration switch (status) { case LoadStatus.LoadWithBrowser: - return HandleBrowserResource(resource, out settings); + return HandleBrowserResource(resource, settings); case LoadStatus.Success: - return TryParseData(data, out settings, password, passwordIsHash); + return TryParseData(data, settings, password, passwordIsHash); } - } - return status; + return status; + } } catch (Exception e) { @@ -175,16 +175,14 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus TryParseData(Stream data, out Settings settings, string password = null, bool passwordIsHash = false) + private LoadStatus TryParseData(Stream data, Settings settings, string password = null, bool passwordIsHash = false) { var status = LoadStatus.NotSupported; var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); - settings = default(Settings); - if (dataFormat != null) { - status = dataFormat.TryParse(data, out settings, password, passwordIsHash); + status = dataFormat.TryParse(data, settings, password, passwordIsHash); logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}."); } else @@ -195,11 +193,9 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus HandleBrowserResource(Uri resource, out Settings settings) + private LoadStatus HandleBrowserResource(Uri resource, Settings settings) { - settings = LoadDefaultSettings(); settings.Browser.StartUrl = resource.AbsoluteUri; - logger.Info($"The resource needs authentication or is HTML data, loaded default settings with '{resource}' as startup URL."); return LoadStatus.Success; diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs index 006b12d9..0654e71e 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs @@ -57,13 +57,11 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public LoadStatus TryParse(Stream data, out Settings settings, string password = null, bool passwordIsHash = false) + public LoadStatus TryParse(Stream data, Settings settings, string password = null, bool passwordIsHash = false) { var prefix = ParsePrefix(data); var success = TryDetermineFormat(prefix, out FormatType format); - settings = default(Settings); - if (success) { if (compressor.IsCompressed(data)) @@ -71,20 +69,20 @@ namespace SafeExamBrowser.Configuration.DataFormats data = compressor.Decompress(data); } - data = new SubStream(data, PREFIX_LENGTH, data.Length - PREFIX_LENGTH); 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, out settings, password, passwordIsHash); + return ParsePasswordBlock(data, format, settings, password, passwordIsHash); case FormatType.PlainData: - return ParsePlainDataBlock(data, out settings); + return ParsePlainDataBlock(data, settings); case FormatType.PublicKeyHash: - return ParsePublicKeyHashBlock(data, out settings, password, passwordIsHash); + return ParsePublicKeyHashBlock(data, settings, password, passwordIsHash); case FormatType.PublicKeyHashWithSymmetricKey: - return ParsePublicKeyHashWithSymmetricKeyBlock(data, out settings, password, passwordIsHash); + return ParsePublicKeyHashWithSymmetricKeyBlock(data, settings, password, passwordIsHash); } } @@ -93,28 +91,36 @@ namespace SafeExamBrowser.Configuration.DataFormats return LoadStatus.InvalidData; } - private LoadStatus ParsePasswordBlock(Stream data, FormatType format, out Settings settings, string password, bool passwordIsHash) + private LoadStatus ParsePasswordBlock(Stream data, FormatType format, Settings settings, string password, bool passwordIsHash) { - - settings = default(Settings); - - if (format == FormatType.PasswordConfigureClient && !passwordIsHash) - { - password = hashAlgorithm.GenerateHashFor(password); - } - + var decrypted = default(Stream); var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); - var status = encryption.Decrypt(data, out Stream decrypted, password); + var status = default(LoadStatus); + + if (format == FormatType.PasswordConfigureClient) + { + password = password == null ? string.Empty : (passwordIsHash ? password : hashAlgorithm.GenerateHashFor(password)); + status = encryption.Decrypt(data, out decrypted, password); + + if (status == LoadStatus.PasswordNeeded && passwordIsHash && password != string.Empty) + { + status = encryption.Decrypt(data, out decrypted, string.Empty); + } + } + else + { + status = encryption.Decrypt(data, out decrypted, password); + } if (status == LoadStatus.Success) { - return ParsePlainDataBlock(decrypted, out settings); + return ParsePlainDataBlock(decrypted, settings); } return status; } - private LoadStatus ParsePlainDataBlock(Stream data, out Settings settings) + private LoadStatus ParsePlainDataBlock(Stream data, Settings settings) { var xmlFormat = new XmlFormat(logger.CloneFor(nameof(XmlFormat))); @@ -123,36 +129,32 @@ namespace SafeExamBrowser.Configuration.DataFormats data = compressor.Decompress(data); } - return xmlFormat.TryParse(data, out settings); + return xmlFormat.TryParse(data, settings); } - private LoadStatus ParsePublicKeyHashBlock(Stream data, out Settings settings, string password, bool passwordIsHash) + private LoadStatus ParsePublicKeyHashBlock(Stream data, Settings settings, string password, bool passwordIsHash) { var encryption = new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); var status = encryption.Decrypt(data, out Stream decrypted); - settings = default(Settings); - if (status == LoadStatus.Success) { - return TryParse(decrypted, out settings, password, passwordIsHash); + return TryParse(decrypted, settings, password, passwordIsHash); } return status; } - private LoadStatus ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, out Settings settings, string password, bool passwordIsHash) + private LoadStatus ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, Settings settings, string password, bool passwordIsHash) { 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); - settings = default(Settings); - if (status == LoadStatus.Success) { - return TryParse(decrypted, out settings, password, passwordIsHash); + return TryParse(decrypted, settings, password, passwordIsHash); } return status; diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs index 12876d38..7a9c88f1 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs +++ b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs @@ -41,12 +41,6 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography } var (version, options) = ParseHeader(data); - - if (version != VERSION || options != OPTIONS) - { - return FailForInvalidHeader(version, options); - } - var (authenticationKey, encryptionKey) = GenerateKeys(data, password); var (originalHmac, computedHmac) = GenerateHmac(data, authenticationKey); @@ -68,16 +62,14 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography var version = data.ReadByte(); var options = data.ReadByte(); + if (version != VERSION || options != OPTIONS) + { + logger.Warn($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); + } + return (version, options); } - private LoadStatus FailForInvalidHeader(int version, int options) - { - logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); - - return LoadStatus.InvalidData; - } - private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password) { var authenticationSalt = new byte[SALT_SIZE]; diff --git a/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs b/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs new file mode 100644 index 00000000..daf9b2fc --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/DataMapper.cs @@ -0,0 +1,54 @@ +/* + * 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 Dictionary 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 "sebConfigPurpose": + settings.MapConfigurationMode(value); + break; + case "startURL": + settings.MapStartUrl(value); + break; + } + } + + 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 startUrl) + { + settings.Browser.StartUrl = startUrl; + } + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs index 04ef2552..a2830bce 100644 --- a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs @@ -7,6 +7,7 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; @@ -19,9 +20,6 @@ namespace SafeExamBrowser.Configuration.DataFormats { public class XmlFormat : IDataFormat { - private const string ARRAY = "array"; - private const string DICTIONARY = "dict"; - private const string KEY = "key"; private const string ROOT_NODE = "plist"; private const string XML_PREFIX = "(); - if (hasRoot) + if (hasRoot && hasDictionary) { logger.Debug($"Found root node, starting to parse data..."); - Parse(reader, settings, out status); + status = ParseDictionary(reader, rawData); + + if (status == LoadStatus.Success) + { + logger.Debug("Mapping raw settings data..."); + rawData.MapTo(settings); + } + logger.Debug($"Finished parsing -> Result: {status}."); } else { - logger.Error($"Could not find root node '{ROOT_NODE}'!"); + logger.Error($"Could not find root {(!hasRoot ? $"node '{ROOT_NODE}'" : $"dictionary '{DataTypes.DICTIONARY}'")}!"); } } return status; } - private void Parse(XmlReader reader, Settings settings, out LoadStatus status) + private LoadStatus ParseArray(XmlReader reader, List array) { - status = LoadStatus.Success; - - if (reader.NodeType == XmlNodeType.Element) + if (reader.IsEmptyElement) { - switch (reader.Name) - { - case ARRAY: - ParseArray(reader, settings, out status); - break; - case DICTIONARY: - ParseDictionary(reader, settings, out status); - break; - case KEY: - ParseKeyValuePair(reader, settings, out status); - break; - case ROOT_NODE: - break; - default: - status = LoadStatus.InvalidData; - logger.Error($"Detected invalid element '{reader.Name}'!"); - break; - } + return LoadStatus.Success; } reader.Read(); reader.MoveToContent(); - if (!reader.EOF && status == LoadStatus.Success) - { - Parse(reader, settings, out status); - } - } - - private void ParseArray(XmlReader reader, Settings settings, out LoadStatus status) - { - reader.Read(); - reader.MoveToContent(); - while (reader.NodeType == XmlNodeType.Element) { - if (reader.Name == ARRAY) + var status = ParseElement(reader, out object element); + + if (status == LoadStatus.Success) { - ParseArray(reader, settings, out status); - } - else if (reader.Name == DICTIONARY) - { - ParseDictionary(reader, settings, out status); + array.Add(element); } else { - var item = XNode.ReadFrom(reader) as XElement; - - // TODO: Map data... + return status; } reader.Read(); reader.MoveToContent(); } - if (reader.NodeType == XmlNodeType.EndElement && reader.Name == ARRAY) + if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DataTypes.ARRAY) { - status = LoadStatus.Success; + return LoadStatus.Success; } else { - logger.Error($"Expected closing tag for '{ARRAY}', but found '{reader.Name}{reader.Value}'!"); - status = LoadStatus.InvalidData; + logger.Error($"Expected closing tag for '{DataTypes.ARRAY}', but found '{reader.Name}{reader.Value}'!"); + + return LoadStatus.InvalidData; } } - private void ParseDictionary(XmlReader reader, Settings settings, out LoadStatus status) + private LoadStatus ParseDictionary(XmlReader reader, Dictionary dictionary) { + if (reader.IsEmptyElement) + { + return LoadStatus.Success; + } + reader.Read(); reader.MoveToContent(); while (reader.NodeType == XmlNodeType.Element) { - ParseKeyValuePair(reader, settings, out status); + var status = ParseKeyValuePair(reader, dictionary); + + if (status != LoadStatus.Success) + { + return status; + } reader.Read(); reader.MoveToContent(); } - if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DICTIONARY) + if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DataTypes.DICTIONARY) { - status = LoadStatus.Success; + return LoadStatus.Success; } else { - logger.Error($"Expected closing tag for '{DICTIONARY}', but found '{reader.Name}{reader.Value}'!"); - status = LoadStatus.InvalidData; + logger.Error($"Expected closing tag for '{DataTypes.DICTIONARY}', but found '{reader.Name}{reader.Value}'!"); + + return LoadStatus.InvalidData; } } - private void ParseKeyValuePair(XmlReader reader, Settings settings, out LoadStatus status) + private LoadStatus ParseKeyValuePair(XmlReader reader, Dictionary dictionary) { var key = XNode.ReadFrom(reader) as XElement; + if (key.Name.LocalName != DataTypes.KEY) + { + logger.Error($"Expected element '{DataTypes.KEY}', but found '{key}'!"); + + return LoadStatus.InvalidData; + } + reader.Read(); reader.MoveToContent(); - if (reader.Name == ARRAY) + var status = ParseElement(reader, out object value); + + if (status == LoadStatus.Success) { - ParseArray(reader, settings, out status); + dictionary[key.Value] = value; } - else if (reader.Name == DICTIONARY) + + return status; + } + + private LoadStatus ParseElement(XmlReader reader, out object element) + { + var array = default(List); + var dictionary = default(Dictionary); + var status = default(LoadStatus); + var value = default(object); + + if (reader.Name == DataTypes.ARRAY) { - ParseDictionary(reader, settings, out status); + array = new List(); + status = ParseArray(reader, array); + } + else if (reader.Name == DataTypes.DICTIONARY) + { + dictionary = new Dictionary(); + status = ParseDictionary(reader, dictionary); } else { - var value = XNode.ReadFrom(reader) as XElement; - - // TODO: Map data... - - status = LoadStatus.Success; + status = ParseSimpleType(XNode.ReadFrom(reader) as XElement, out value); } + + element = array ?? dictionary ?? value; + + return status; + } + + private LoadStatus ParseSimpleType(XElement element, out object value) + { + value = null; + + switch (element.Name.LocalName) + { + case DataTypes.DATA: + value = Convert.FromBase64String(element.Value); + break; + case DataTypes.DATE: + value = XmlConvert.ToDateTime(element.Value, XmlDateTimeSerializationMode.Utc); + break; + case DataTypes.FALSE: + value = false; + break; + case DataTypes.INTEGER: + value = Convert.ToInt32(element.Value); + break; + case DataTypes.REAL: + value = Convert.ToDouble(element.Value); + break; + case DataTypes.STRING: + value = element.Value; + break; + case DataTypes.TRUE: + value = true; + break; + } + + if (value == null) + { + 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"; } } } diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index 1ee5ecdd..edf93671 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -64,6 +64,7 @@ + diff --git a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs index 621541cc..7d573a38 100644 --- a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs +++ b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs @@ -22,8 +22,8 @@ namespace SafeExamBrowser.Contracts.Configuration /// /// Tries to parse the given data, using the optional password. As long as the result is not , - /// the referenced settings may be null or in an undefinable state! + /// the referenced settings can be in an undefinable state! /// - LoadStatus TryParse(Stream data, out Settings.Settings settings, string password = null, bool passwordIsHash = false); + LoadStatus TryParse(Stream data, Settings.Settings settings, string password = null, bool passwordIsHash = false); } } diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index 12ed2973..1699bd7b 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -102,11 +102,6 @@ namespace SafeExamBrowser.Runtime.Operations { var status = configuration.TryLoadSettings(uri, out Settings settings, Context.Current?.Settings?.AdminPasswordHash, true); - if (status == LoadStatus.PasswordNeeded) - { - status = configuration.TryLoadSettings(uri, out settings, string.Empty); - } - for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++) { var result = TryGetPassword(); @@ -213,13 +208,15 @@ namespace SafeExamBrowser.Runtime.Operations // TODO: Save / overwrite configuration file in APPDATA directory! // -> Check whether current and new admin passwords are the same! If not, current needs to be verified before overwriting! + // -> Special case: If current admin password is same as new settings password, verification is not necessary! // -> Default settings password appears to be string.Empty for local client configuration // -> Any (new?) certificates need to be imported and REMOVED from the settings before the data is saved! + // -> DO NOT transform settings, just simply copy the given configuration file to %APPDATA%\SebClientSettings.seb !!! //configuration.SaveSettings(Context.Next.Settings, filePath); // TODO: If the client configuration happens while the application is already running, the new configuration should first // be loaded and then the user should have the option to terminate! - // -> Introduce flag in Context, e.g. AskForTermination + // -> Introduce flag in Context, e.g. AskForTermination? ActionRequired?.Invoke(args); logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} after successful client configuration.");