SEBWIN-221: Finished saving algorithm for client configuration (including password encryption - public key encryption remains to do).
This commit is contained in:
parent
140abe789e
commit
d34ce15e3f
37 changed files with 1457 additions and 585 deletions
|
@ -26,7 +26,7 @@ namespace SafeExamBrowser.Configuration.UnitTests
|
||||||
{
|
{
|
||||||
var executablePath = Assembly.GetExecutingAssembly().Location;
|
var executablePath = Assembly.GetExecutingAssembly().Location;
|
||||||
var hashAlgorithm = new Mock<IHashAlgorithm>();
|
var hashAlgorithm = new Mock<IHashAlgorithm>();
|
||||||
var logger = new Mock<ILogger>();
|
var logger = new Mock<IModuleLogger>();
|
||||||
|
|
||||||
sut = new ConfigurationRepository(hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty);
|
sut = new ConfigurationRepository(hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<string, object> data)
|
||||||
|
{
|
||||||
|
const int IDENTITY_CERTIFICATE = 1;
|
||||||
|
var hasCertificates = data.TryGetValue(Keys.Network.Certificates.EmbeddedCertificates, out var value);
|
||||||
|
|
||||||
|
if (hasCertificates && value is IList<IDictionary<string, object>> certificates)
|
||||||
|
{
|
||||||
|
var toRemove = new List<IDictionary<string, object>>();
|
||||||
|
|
||||||
|
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𝈒aht𝈁aHai1972", 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string, object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
Normal file
113
SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
Normal file
71
SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
Normal file
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,8 +10,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using SafeExamBrowser.Configuration.ConfigurationData;
|
||||||
using SafeExamBrowser.Configuration.DataFormats;
|
|
||||||
using SafeExamBrowser.Contracts.Configuration;
|
using SafeExamBrowser.Contracts.Configuration;
|
||||||
using SafeExamBrowser.Contracts.Configuration.Cryptography;
|
using SafeExamBrowser.Contracts.Configuration.Cryptography;
|
||||||
using SafeExamBrowser.Contracts.Configuration.DataFormats;
|
using SafeExamBrowser.Contracts.Configuration.DataFormats;
|
||||||
|
@ -23,65 +22,67 @@ namespace SafeExamBrowser.Configuration
|
||||||
{
|
{
|
||||||
public class ConfigurationRepository : IConfigurationRepository
|
public class ConfigurationRepository : IConfigurationRepository
|
||||||
{
|
{
|
||||||
private const string BASE_ADDRESS = "net.pipe://localhost/safeexambrowser";
|
private Certificates certificates;
|
||||||
|
private IList<IDataParser> dataParsers;
|
||||||
private readonly string executablePath;
|
private IList<IDataSerializer> dataSerializers;
|
||||||
private readonly string programCopyright;
|
private DataMapper dataMapper;
|
||||||
private readonly string programTitle;
|
private DataValues dataValues;
|
||||||
private readonly string programVersion;
|
|
||||||
|
|
||||||
private AppConfig appConfig;
|
|
||||||
private IHashAlgorithm hashAlgorithm;
|
private IHashAlgorithm hashAlgorithm;
|
||||||
private IList<IDataFormat> dataFormats;
|
|
||||||
private IList<IDataResource> dataResources;
|
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
|
private IList<IResourceLoader> resourceLoaders;
|
||||||
|
private IList<IResourceSaver> resourceSavers;
|
||||||
|
|
||||||
public ConfigurationRepository(
|
public ConfigurationRepository(
|
||||||
IHashAlgorithm hashAlgorithm,
|
IHashAlgorithm hashAlgorithm,
|
||||||
ILogger logger,
|
IModuleLogger logger,
|
||||||
string executablePath,
|
string executablePath,
|
||||||
string programCopyright,
|
string programCopyright,
|
||||||
string programTitle,
|
string programTitle,
|
||||||
string programVersion)
|
string programVersion)
|
||||||
{
|
{
|
||||||
dataFormats = new List<IDataFormat>();
|
certificates = new Certificates(logger.CloneFor(nameof(Certificates)));
|
||||||
dataResources = new List<IDataResource>();
|
dataParsers = new List<IDataParser>();
|
||||||
|
dataSerializers = new List<IDataSerializer>();
|
||||||
|
dataMapper = new DataMapper();
|
||||||
|
dataValues = new DataValues(executablePath, programCopyright, programTitle, programVersion);
|
||||||
|
resourceLoaders = new List<IResourceLoader>();
|
||||||
|
resourceSavers = new List<IResourceSaver>();
|
||||||
|
|
||||||
this.hashAlgorithm = hashAlgorithm;
|
this.hashAlgorithm = hashAlgorithm;
|
||||||
this.logger = logger;
|
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)
|
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
|
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);
|
TryParseData(data, out var encryption, out var format, out var rawData, password);
|
||||||
HandleIdentityCertificates(data);
|
|
||||||
|
|
||||||
// TODO: Encrypt and save configuration data as local client config under %APPDATA%!
|
certificates.ExtractAndImportIdentityCertificates(rawData);
|
||||||
// -> New key will determine whether to use default password or current settings password!
|
encryption = DetermineEncryptionForClientConfiguration(rawData, encryption);
|
||||||
// -> "clientConfigEncryptUsingSettingsPassword"
|
|
||||||
// -> Default settings password for local client configuration appears to be string.Empty -> passwords.SettingsPassword
|
var status = TrySerializeData(rawData, format, out var serialized, encryption);
|
||||||
// -> Otherwise, the local client configuration must again be encrypted in the same way as the original file!!
|
|
||||||
|
using (serialized)
|
||||||
|
{
|
||||||
|
if (status == SaveStatus.Success)
|
||||||
|
{
|
||||||
|
status = TrySaveData(new Uri(dataValues.GetAppDataFilePath()), serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info($"Successfully configured local client settings with '{resource}'.");
|
return status;
|
||||||
|
}
|
||||||
return SaveStatus.Success;
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
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;
|
return SaveStatus.UnexpectedError;
|
||||||
}
|
}
|
||||||
|
@ -89,84 +90,37 @@ namespace SafeExamBrowser.Configuration
|
||||||
|
|
||||||
public AppConfig InitializeAppConfig()
|
public AppConfig InitializeAppConfig()
|
||||||
{
|
{
|
||||||
var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(SafeExamBrowser));
|
return dataValues.InitializeAppConfig();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ISessionConfiguration InitializeSessionConfiguration()
|
public ISessionConfiguration InitializeSessionConfiguration()
|
||||||
{
|
{
|
||||||
var configuration = new SessionConfiguration();
|
return dataValues.InitializeSessionConfiguration();
|
||||||
|
|
||||||
UpdateAppConfig();
|
|
||||||
|
|
||||||
configuration.AppConfig = appConfig.Clone();
|
|
||||||
configuration.Id = Guid.NewGuid();
|
|
||||||
configuration.StartupToken = Guid.NewGuid();
|
|
||||||
|
|
||||||
return configuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Settings LoadDefaultSettings()
|
public Settings LoadDefaultSettings()
|
||||||
{
|
{
|
||||||
var settings = new Settings();
|
return dataValues.LoadDefaultSettings();
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
public LoadStatus TryLoadSettings(Uri resource, out Settings settings, PasswordParameters password = null)
|
||||||
|
@ -181,16 +135,14 @@ namespace SafeExamBrowser.Configuration
|
||||||
|
|
||||||
using (stream)
|
using (stream)
|
||||||
{
|
{
|
||||||
if (status != LoadStatus.Success)
|
if (status == LoadStatus.Success)
|
||||||
{
|
{
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = TryParseData(stream, out _, out _, out var data, password);
|
status = TryParseData(stream, out _, out _, out var data, password);
|
||||||
|
|
||||||
if (status == LoadStatus.Success)
|
if (status == LoadStatus.Success)
|
||||||
{
|
{
|
||||||
data.MapTo(settings);
|
dataMapper.MapRawDataToSettings(data, settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return status;
|
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<string, object> data)
|
private EncryptionParameters DetermineEncryptionForClientConfiguration(IDictionary<string, object> data, EncryptionParameters encryption)
|
||||||
{
|
{
|
||||||
const int IDENTITY_CERTIFICATE = 1;
|
var hasKey = data.TryGetValue(Keys.ConfigurationFile.KeepClientConfigEncryption, out var value);
|
||||||
var hasCertificates = data.TryGetValue("embeddedCertificates", out object value);
|
var useDefaultEncryption = value is bool keepEncryption && !keepEncryption;
|
||||||
|
|
||||||
if (hasCertificates && value is IList<IDictionary<string, object>> certificates)
|
if (!hasKey || (hasKey && useDefaultEncryption))
|
||||||
{
|
{
|
||||||
var toRemove = new List<IDictionary<string, object>>();
|
encryption = new PasswordParameters { Password = string.Empty, IsHash = true };
|
||||||
|
|
||||||
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));
|
return encryption;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ImportIdentityCertificate(byte[] certificateData, X509Store store)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var certificate = new X509Certificate2();
|
|
||||||
|
|
||||||
certificate.Import(certificateData, "Di𝈭l𝈖Ch𝈒aht𝈁aHai1972", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet);
|
|
||||||
|
|
||||||
store.Open(OpenFlags.ReadWrite);
|
|
||||||
store.Add(certificate);
|
|
||||||
|
|
||||||
logger.Info($"Successfully imported identity certificate into {store.Location}.{store.Name}.");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.Error($"Failed to import identity certificate into {store.Location}.{store.Name}!", e);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
store.Close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoadStatus TryLoadData(Uri resource, out Stream data)
|
private LoadStatus TryLoadData(Uri resource, out Stream data)
|
||||||
{
|
{
|
||||||
var status = LoadStatus.NotSupported;
|
var status = LoadStatus.NotSupported;
|
||||||
var resourceLoader = dataResources.FirstOrDefault(l => l.CanLoad(resource));
|
var resourceLoader = resourceLoaders.FirstOrDefault(l => l.CanLoad(resource));
|
||||||
|
|
||||||
data = default(Stream);
|
data = default(Stream);
|
||||||
|
|
||||||
|
@ -279,38 +194,74 @@ namespace SafeExamBrowser.Configuration
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoadStatus TryParseData(Stream data, out EncryptionParameters encryption, out Format format, out IDictionary<string, object> rawData, PasswordParameters password = null)
|
private LoadStatus TryParseData(Stream data, out EncryptionParameters encryption, out FormatType format, out IDictionary<string, object> rawData, PasswordParameters password = null)
|
||||||
{
|
{
|
||||||
var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data));
|
var parser = dataParsers.FirstOrDefault(p => p.CanParse(data));
|
||||||
var status = LoadStatus.NotSupported;
|
var status = LoadStatus.NotSupported;
|
||||||
|
|
||||||
encryption = default(EncryptionParameters);
|
encryption = default(EncryptionParameters);
|
||||||
format = default(Format);
|
format = default(FormatType);
|
||||||
rawData = default(Dictionary<string, object>);
|
rawData = default(Dictionary<string, object>);
|
||||||
|
|
||||||
if (dataFormat != null)
|
if (parser != null)
|
||||||
{
|
{
|
||||||
var result = dataFormat.TryParse(data, password);
|
var result = parser.TryParse(data, password);
|
||||||
|
|
||||||
encryption = result.Encryption;
|
encryption = result.Encryption;
|
||||||
format = result.Format;
|
format = result.Format;
|
||||||
rawData = result.RawData;
|
rawData = result.RawData;
|
||||||
status = result.Status;
|
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
|
else
|
||||||
{
|
{
|
||||||
logger.Warn($"No data format found for '{data}'!");
|
logger.Warn($"No data parser found which can parse '{data}'!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAppConfig()
|
private SaveStatus TrySaveData(Uri destination, Stream data)
|
||||||
{
|
{
|
||||||
appConfig.ClientId = Guid.NewGuid();
|
var status = SaveStatus.NotSupported;
|
||||||
appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}";
|
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<string, object> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@ namespace SafeExamBrowser.Configuration.Cryptography
|
||||||
}
|
}
|
||||||
|
|
||||||
var (version, options) = ParseHeader(data);
|
var (version, options) = ParseHeader(data);
|
||||||
var (authenticationKey, encryptionKey) = GenerateKeys(data, password);
|
var (authenticationKey, encryptionKey) = GenerateKeysForDecryption(data, password);
|
||||||
var (originalHmac, computedHmac) = GenerateHmac(data, authenticationKey);
|
var (originalHmac, computedHmac) = GenerateHmacForDecryption(authenticationKey, data);
|
||||||
|
|
||||||
if (!computedHmac.SequenceEqual(originalHmac))
|
if (!computedHmac.SequenceEqual(originalHmac))
|
||||||
{
|
{
|
||||||
|
@ -54,6 +54,16 @@ namespace SafeExamBrowser.Configuration.Cryptography
|
||||||
return LoadStatus.Success;
|
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)
|
private (int version, int options) ParseHeader(Stream data)
|
||||||
{
|
{
|
||||||
data.Seek(0, SeekOrigin.Begin);
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
@ -70,7 +80,7 @@ namespace SafeExamBrowser.Configuration.Cryptography
|
||||||
return (version, options);
|
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 authenticationSalt = new byte[SALT_SIZE];
|
||||||
var encryptionSalt = 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...");
|
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()
|
private LoadStatus FailForInvalidHmac()
|
||||||
{
|
{
|
||||||
logger.Debug($"The authentication failed due to an invalid password or corrupted data!");
|
logger.Debug($"The authentication failed due to an invalid password or corrupted data!");
|
||||||
|
@ -136,5 +173,50 @@ namespace SafeExamBrowser.Configuration.Cryptography
|
||||||
|
|
||||||
return decryptedData;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
@ -43,6 +44,13 @@ namespace SafeExamBrowser.Configuration.Cryptography
|
||||||
return LoadStatus.Success;
|
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)
|
protected byte[] ParsePublicKeyHash(Stream data)
|
||||||
{
|
{
|
||||||
var keyHash = new byte[PUBLIC_KEY_HASH_SIZE];
|
var keyHash = new byte[PUBLIC_KEY_HASH_SIZE];
|
||||||
|
|
|
@ -44,6 +44,13 @@ namespace SafeExamBrowser.Configuration.Cryptography
|
||||||
return status;
|
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)
|
private string ParseSymmetricKey(Stream data, X509Certificate2 certificate)
|
||||||
{
|
{
|
||||||
var keyLengthData = new byte[KEY_LENGTH_SIZE];
|
var keyLengthData = new byte[KEY_LENGTH_SIZE];
|
||||||
|
|
|
@ -34,29 +34,36 @@ namespace SafeExamBrowser.Configuration.DataCompression
|
||||||
|
|
||||||
public Stream Compress(Stream data)
|
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)
|
public Stream Decompress(Stream data)
|
||||||
{
|
{
|
||||||
var decompressed = new MemoryStream();
|
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);
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
using (var stream = new GZipStream(data, CompressionMode.Decompress))
|
using (var stream = new GZipStream(data, CompressionMode.Decompress))
|
||||||
{
|
{
|
||||||
var buffer = new byte[4096];
|
stream.CopyTo(decompressed);
|
||||||
var bytesRead = 0;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
bytesRead = stream.Read(buffer, 0, buffer.Length);
|
|
||||||
decompressed.Write(buffer, 0, bytesRead);
|
|
||||||
} while (bytesRead > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return decompressed;
|
||||||
}
|
}
|
||||||
|
|
19
SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs
Normal file
19
SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs
Normal file
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
207
SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs
Normal file
207
SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
168
SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs
Normal file
168
SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs
Normal file
|
@ -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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string, object> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
25
SafeExamBrowser.Configuration/DataFormats/XmlElement.cs
Normal file
25
SafeExamBrowser.Configuration/DataFormats/XmlElement.cs
Normal file
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,14 +19,13 @@ using SafeExamBrowser.Contracts.Logging;
|
||||||
|
|
||||||
namespace SafeExamBrowser.Configuration.DataFormats
|
namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
{
|
{
|
||||||
public class XmlFormat : IDataFormat
|
public class XmlParser : IDataParser
|
||||||
{
|
{
|
||||||
private const string ROOT_NODE = "plist";
|
|
||||||
private const string XML_PREFIX = "<?xm";
|
private const string XML_PREFIX = "<?xm";
|
||||||
|
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
|
|
||||||
public XmlFormat(ILogger logger)
|
public XmlParser(ILogger logger)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -42,19 +41,20 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
{
|
{
|
||||||
data.Seek(0, SeekOrigin.Begin);
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
data.Read(prefixData, 0, prefixData.Length);
|
data.Read(prefixData, 0, prefixData.Length);
|
||||||
|
|
||||||
var prefix = Encoding.UTF8.GetString(prefixData);
|
var prefix = Encoding.UTF8.GetString(prefixData);
|
||||||
var success = prefix == XML_PREFIX;
|
var success = prefix == XML_PREFIX;
|
||||||
|
|
||||||
logger.Debug($"'{data}' starting with '{prefix}' does {(success ? string.Empty : "not ")}match the XML format.");
|
logger.Debug($"'{data}' starting with '{prefix}' does {(success ? string.Empty : "not ")}match the {FormatType.Xml} format.");
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to match the XML format.");
|
logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to match the {FormatType.Xml} format.");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to determine whether '{data}' with {data.Length / 1000.0} KB data matches the XML format!", e);
|
logger.Error($"Failed to determine whether '{data}' with {data.Length / 1000.0} KB data matches the {FormatType.Xml} format!", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -62,15 +62,15 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
|
|
||||||
public ParseResult TryParse(Stream data, PasswordParameters password = null)
|
public ParseResult TryParse(Stream data, PasswordParameters password = null)
|
||||||
{
|
{
|
||||||
var result = new ParseResult { Status = LoadStatus.InvalidData };
|
var result = new ParseResult { Format = FormatType.Xml, Status = LoadStatus.InvalidData };
|
||||||
var xmlSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
|
var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
|
||||||
|
|
||||||
data.Seek(0, SeekOrigin.Begin);
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
using (var reader = XmlReader.Create(data, xmlSettings))
|
using (var reader = XmlReader.Create(data, settings))
|
||||||
{
|
{
|
||||||
var hasRoot = reader.ReadToFollowing(ROOT_NODE);
|
var hasRoot = reader.ReadToFollowing(XmlElement.Root);
|
||||||
var hasDictionary = reader.ReadToDescendant(DataTypes.DICTIONARY);
|
var hasDictionary = reader.ReadToDescendant(XmlElement.Dictionary);
|
||||||
var rawData = new Dictionary<string, object>();
|
var rawData = new Dictionary<string, object>();
|
||||||
|
|
||||||
if (hasRoot && hasDictionary)
|
if (hasRoot && hasDictionary)
|
||||||
|
@ -84,7 +84,7 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
}
|
}
|
||||||
else
|
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();
|
reader.MoveToContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DataTypes.ARRAY)
|
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == XmlElement.Array)
|
||||||
{
|
{
|
||||||
return LoadStatus.Success;
|
return LoadStatus.Success;
|
||||||
}
|
}
|
||||||
else
|
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;
|
return LoadStatus.InvalidData;
|
||||||
}
|
}
|
||||||
|
@ -153,13 +153,13 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
reader.MoveToContent();
|
reader.MoveToContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == DataTypes.DICTIONARY)
|
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == XmlElement.Dictionary)
|
||||||
{
|
{
|
||||||
return LoadStatus.Success;
|
return LoadStatus.Success;
|
||||||
}
|
}
|
||||||
else
|
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;
|
return LoadStatus.InvalidData;
|
||||||
}
|
}
|
||||||
|
@ -169,9 +169,9 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
{
|
{
|
||||||
var key = XNode.ReadFrom(reader) as XElement;
|
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;
|
return LoadStatus.InvalidData;
|
||||||
}
|
}
|
||||||
|
@ -196,12 +196,12 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
var status = default(LoadStatus);
|
var status = default(LoadStatus);
|
||||||
var value = default(object);
|
var value = default(object);
|
||||||
|
|
||||||
if (reader.Name == DataTypes.ARRAY)
|
if (reader.Name == XmlElement.Array)
|
||||||
{
|
{
|
||||||
array = new List<object>();
|
array = new List<object>();
|
||||||
status = ParseArray(reader, array);
|
status = ParseArray(reader, array);
|
||||||
}
|
}
|
||||||
else if (reader.Name == DataTypes.DICTIONARY)
|
else if (reader.Name == XmlElement.Dictionary)
|
||||||
{
|
{
|
||||||
dictionary = new Dictionary<string, object>();
|
dictionary = new Dictionary<string, object>();
|
||||||
status = ParseDictionary(reader, dictionary);
|
status = ParseDictionary(reader, dictionary);
|
||||||
|
@ -218,60 +218,49 @@ namespace SafeExamBrowser.Configuration.DataFormats
|
||||||
|
|
||||||
private LoadStatus ParseSimpleType(XElement element, out object value)
|
private LoadStatus ParseSimpleType(XElement element, out object value)
|
||||||
{
|
{
|
||||||
|
var status = LoadStatus.Success;
|
||||||
|
|
||||||
value = null;
|
value = null;
|
||||||
|
|
||||||
if (element.IsEmpty)
|
if (element.IsEmpty)
|
||||||
{
|
{
|
||||||
return LoadStatus.Success;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (element.Name.LocalName)
|
switch (element.Name.LocalName)
|
||||||
{
|
{
|
||||||
case DataTypes.DATA:
|
case XmlElement.Data:
|
||||||
value = Convert.FromBase64String(element.Value);
|
value = Convert.FromBase64String(element.Value);
|
||||||
break;
|
break;
|
||||||
case DataTypes.DATE:
|
case XmlElement.Date:
|
||||||
value = XmlConvert.ToDateTime(element.Value, XmlDateTimeSerializationMode.Utc);
|
value = XmlConvert.ToDateTime(element.Value, XmlDateTimeSerializationMode.Utc);
|
||||||
break;
|
break;
|
||||||
case DataTypes.FALSE:
|
case XmlElement.False:
|
||||||
value = false;
|
value = false;
|
||||||
break;
|
break;
|
||||||
case DataTypes.INTEGER:
|
case XmlElement.Integer:
|
||||||
value = Convert.ToInt32(element.Value);
|
value = Convert.ToInt32(element.Value);
|
||||||
break;
|
break;
|
||||||
case DataTypes.REAL:
|
case XmlElement.Real:
|
||||||
value = Convert.ToDouble(element.Value);
|
value = Convert.ToDouble(element.Value);
|
||||||
break;
|
break;
|
||||||
case DataTypes.STRING:
|
case XmlElement.String:
|
||||||
value = element.Value;
|
value = element.Value;
|
||||||
break;
|
break;
|
||||||
case DataTypes.TRUE:
|
case XmlElement.True:
|
||||||
value = true;
|
value = true;
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
status = LoadStatus.InvalidData;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == null)
|
if (status != LoadStatus.Success)
|
||||||
{
|
{
|
||||||
logger.Error($"Element '{element}' is not supported!");
|
logger.Error($"Element '{element}' is not supported!");
|
||||||
|
|
||||||
return LoadStatus.InvalidData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoadStatus.Success;
|
return status;
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
199
SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs
Normal file
199
SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs
Normal file
|
@ -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<string, object> 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<object> 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<string, object> 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<string, object> 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<object> array)
|
||||||
|
{
|
||||||
|
status = WriteArray(writer, array);
|
||||||
|
}
|
||||||
|
else if (element is Dictionary<string, object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,11 @@ using SafeExamBrowser.Contracts.Logging;
|
||||||
|
|
||||||
namespace SafeExamBrowser.Configuration.DataResources
|
namespace SafeExamBrowser.Configuration.DataResources
|
||||||
{
|
{
|
||||||
public class FileResource: IDataResource
|
public class FileResourceLoader : IResourceLoader
|
||||||
{
|
{
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
|
|
||||||
public FileResource(ILogger logger)
|
public FileResourceLoader(ILogger logger)
|
||||||
{
|
{
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,11 @@ namespace SafeExamBrowser.Configuration.DataResources
|
||||||
|
|
||||||
if (exists)
|
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
|
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;
|
return exists;
|
||||||
|
@ -47,10 +47,5 @@ namespace SafeExamBrowser.Configuration.DataResources
|
||||||
|
|
||||||
return LoadStatus.Success;
|
return LoadStatus.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SaveStatus TrySave(Uri resource, Stream data)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ using SafeExamBrowser.Contracts.Logging;
|
||||||
|
|
||||||
namespace SafeExamBrowser.Configuration.DataResources
|
namespace SafeExamBrowser.Configuration.DataResources
|
||||||
{
|
{
|
||||||
public class NetworkResource : IDataResource
|
public class NetworkResourceLoader : IResourceLoader
|
||||||
{
|
{
|
||||||
private AppConfig appConfig;
|
private AppConfig appConfig;
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
|
@ -41,7 +41,7 @@ namespace SafeExamBrowser.Configuration.DataResources
|
||||||
Uri.UriSchemeHttps
|
Uri.UriSchemeHttps
|
||||||
};
|
};
|
||||||
|
|
||||||
public NetworkResource(AppConfig appConfig, ILogger logger)
|
public NetworkResourceLoader(AppConfig appConfig, ILogger logger)
|
||||||
{
|
{
|
||||||
this.appConfig = appConfig;
|
this.appConfig = appConfig;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
@ -86,11 +86,6 @@ namespace SafeExamBrowser.Configuration.DataResources
|
||||||
return LoadStatus.Success;
|
return LoadStatus.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SaveStatus TrySave(Uri resource, Stream data)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri BuildUriFor(Uri resource)
|
private Uri BuildUriFor(Uri resource)
|
||||||
{
|
{
|
||||||
var scheme = GetSchemeFor(resource);
|
var scheme = GetSchemeFor(resource);
|
|
@ -58,18 +58,26 @@
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="ConfigurationData\Certificates.cs" />
|
||||||
|
<Compile Include="ConfigurationData\Keys.cs" />
|
||||||
|
<Compile Include="ConfigurationData\DataValues.cs" />
|
||||||
<Compile Include="DataCompression\GZipCompressor.cs" />
|
<Compile Include="DataCompression\GZipCompressor.cs" />
|
||||||
<Compile Include="DataFormats\BinaryFormat.cs" />
|
|
||||||
<Compile Include="Cryptography\PasswordEncryption.cs" />
|
<Compile Include="Cryptography\PasswordEncryption.cs" />
|
||||||
<Compile Include="Cryptography\PublicKeyHashEncryption.cs" />
|
<Compile Include="Cryptography\PublicKeyHashEncryption.cs" />
|
||||||
<Compile Include="Cryptography\PublicKeyHashWithSymmetricKeyEncryption.cs" />
|
<Compile Include="Cryptography\PublicKeyHashWithSymmetricKeyEncryption.cs" />
|
||||||
<Compile Include="DataFormats\XmlFormat.cs" />
|
<Compile Include="DataFormats\BinaryParser.cs" />
|
||||||
<Compile Include="DataFormats\DataMapper.cs" />
|
<Compile Include="DataFormats\BinarySerializer.cs" />
|
||||||
|
<Compile Include="DataFormats\BinaryBlock.cs" />
|
||||||
|
<Compile Include="ConfigurationData\DataMapper.cs" />
|
||||||
<Compile Include="Cryptography\HashAlgorithm.cs" />
|
<Compile Include="Cryptography\HashAlgorithm.cs" />
|
||||||
|
<Compile Include="DataFormats\XmlElement.cs" />
|
||||||
|
<Compile Include="DataFormats\XmlParser.cs" />
|
||||||
|
<Compile Include="DataFormats\XmlSerializer.cs" />
|
||||||
|
<Compile Include="DataResources\FileResourceSaver.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="ConfigurationRepository.cs" />
|
<Compile Include="ConfigurationRepository.cs" />
|
||||||
<Compile Include="DataResources\FileResource.cs" />
|
<Compile Include="DataResources\FileResourceLoader.cs" />
|
||||||
<Compile Include="DataResources\NetworkResource.cs" />
|
<Compile Include="DataResources\NetworkResourceLoader.cs" />
|
||||||
<Compile Include="SessionConfiguration.cs" />
|
<Compile Include="SessionConfiguration.cs" />
|
||||||
<Compile Include="SubStream.cs" />
|
<Compile Include="SubStream.cs" />
|
||||||
<Compile Include="SystemInfo.cs" />
|
<Compile Include="SystemInfo.cs" />
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines all supported data formats.
|
/// Defines all supported data formats.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum Format
|
public enum FormatType
|
||||||
{
|
{
|
||||||
Binary = 1,
|
Binary = 1,
|
||||||
Xml
|
Xml
|
|
@ -14,7 +14,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides functionality to parse configuration data with a particular format.
|
/// Provides functionality to parse configuration data with a particular format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDataFormat
|
public interface IDataParser
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether the given data complies with the required format.
|
/// Indicates whether the given data complies with the required format.
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides functionality to serialize configuration data to a particular format.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDataSerializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether data can be serialized to the given format.
|
||||||
|
/// </summary>
|
||||||
|
bool CanSerialize(FormatType format);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to serialize the given data.
|
||||||
|
/// </summary>
|
||||||
|
SerializeResult TrySerialize(IDictionary<string, object> data, EncryptionParameters encryption = null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ using SafeExamBrowser.Contracts.Configuration.Cryptography;
|
||||||
namespace SafeExamBrowser.Contracts.Configuration.DataFormats
|
namespace SafeExamBrowser.Contracts.Configuration.DataFormats
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the result of a data parsing operation by an <see cref="IDataFormat"/>.
|
/// Defines the result of a data parsing operation by an <see cref="IDataParser"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ParseResult
|
public class ParseResult
|
||||||
{
|
{
|
||||||
|
@ -24,7 +24,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original format of the data.
|
/// The original format of the data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Format Format { get; set; }
|
public FormatType Format { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The parsed settings data. Might be <c>null</c> or in an undefinable state, depending on <see cref="Status"/>.
|
/// The parsed settings data. Might be <c>null</c> or in an undefinable state, depending on <see cref="Status"/>.
|
||||||
|
@ -32,7 +32,7 @@ namespace SafeExamBrowser.Contracts.Configuration.DataFormats
|
||||||
public IDictionary<string, object> RawData { get; set; }
|
public IDictionary<string, object> RawData { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The status result of a parsing operation.
|
/// The status result of the parsing operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LoadStatus Status { get; set; }
|
public LoadStatus Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the result of a data serialization operation by an <see cref="IDataSerializer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SerializeResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The serialized data. Might be <c>null</c> or in an undefinable state, depending on <see cref="Status"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Stream Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The status result of the serialization operation.
|
||||||
|
/// </summary>
|
||||||
|
public SaveStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,9 +12,9 @@ using System.IO;
|
||||||
namespace SafeExamBrowser.Contracts.Configuration.DataResources
|
namespace SafeExamBrowser.Contracts.Configuration.DataResources
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides functionality to load and save configuration data from / as a particular resource.
|
/// Provides functionality to load configuration data from a particular resource type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDataResource
|
public interface IResourceLoader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether data can be loaded from the specified resource.
|
/// 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.
|
/// Tries to load the configuration data from the specified resource.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
LoadStatus TryLoad(Uri resource, out Stream data);
|
LoadStatus TryLoad(Uri resource, out Stream data);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to save the given configuration data as the specified resource.
|
|
||||||
/// </summary>
|
|
||||||
SaveStatus TrySave(Uri resource, Stream data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides functionality to save configuration data as a particular resource type.
|
||||||
|
/// </summary>
|
||||||
|
public interface IResourceSaver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether data can be saved as the specified resource.
|
||||||
|
/// </summary>
|
||||||
|
bool CanSave(Uri destination);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to save the configuration data as the specified resource.
|
||||||
|
/// </summary>
|
||||||
|
SaveStatus TrySave(Uri destination, Stream data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,14 +39,24 @@ namespace SafeExamBrowser.Contracts.Configuration
|
||||||
Settings.Settings LoadDefaultSettings();
|
Settings.Settings LoadDefaultSettings();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers the specified <see cref="IDataFormat"/> to be used when loading or saving configuration data.
|
/// Registers the specified <see cref="IDataParser"/> to be used to parse configuration data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Register(IDataFormat dataFormat);
|
void Register(IDataParser parser);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers the specified <see cref="IDataResource"/> to be used when loading or saving configuration data.
|
/// Registers the specified <see cref="IDataSerializer"/> to be used to serialize configuration data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Register(IDataResource dataResource);
|
void Register(IDataSerializer serializer);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the specified <see cref="IResourceLoader"/> to be used to load configuration resources.
|
||||||
|
/// </summary>
|
||||||
|
void Register(IResourceLoader loader);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the specified <see cref="IResourceSaver"/> to be used to save configuration resources.
|
||||||
|
/// </summary>
|
||||||
|
void Register(IResourceSaver saver);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to load settings from the specified resource.
|
/// Attempts to load settings from the specified resource.
|
||||||
|
@ -56,6 +66,6 @@ namespace SafeExamBrowser.Contracts.Configuration
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to save settings according to the specified parameters.
|
/// Attempts to save settings according to the specified parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SaveStatus TrySaveSettings(Uri resource, Format format, Settings.Settings settings, EncryptionParameters encryption = null);
|
SaveStatus TrySaveSettings(Uri destination, FormatType format, Settings.Settings settings, EncryptionParameters encryption = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,20 @@
|
||||||
namespace SafeExamBrowser.Contracts.Configuration
|
namespace SafeExamBrowser.Contracts.Configuration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines all possible results of an attempt to save an application configuration.
|
/// Defines all possible results of an attempt to save a configuration resource.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SaveStatus
|
public enum SaveStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration data is invalid or contains invalid elements.
|
||||||
|
/// </summary>
|
||||||
|
InvalidData,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration format or resource type is not supported.
|
||||||
|
/// </summary>
|
||||||
|
NotSupported,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The configuration was saved successfully.
|
/// The configuration was saved successfully.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -63,7 +63,7 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings
|
||||||
Mouse = new MouseSettings();
|
Mouse = new MouseSettings();
|
||||||
Taskbar = new TaskbarSettings();
|
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;
|
ServicePolicy = ServicePolicy.Optional;
|
||||||
Taskbar.AllowApplicationLog = true;
|
Taskbar.AllowApplicationLog = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,11 +60,15 @@
|
||||||
<Compile Include="Configuration\Cryptography\EncryptionParameters.cs" />
|
<Compile Include="Configuration\Cryptography\EncryptionParameters.cs" />
|
||||||
<Compile Include="Configuration\Cryptography\PasswordParameters.cs" />
|
<Compile Include="Configuration\Cryptography\PasswordParameters.cs" />
|
||||||
<Compile Include="Configuration\Cryptography\PublicKeyHashParameters.cs" />
|
<Compile Include="Configuration\Cryptography\PublicKeyHashParameters.cs" />
|
||||||
<Compile Include="Configuration\DataFormats\Format.cs" />
|
<Compile Include="Configuration\DataFormats\FormatType.cs" />
|
||||||
<Compile Include="Configuration\DataCompression\IDataCompressor.cs" />
|
<Compile Include="Configuration\DataCompression\IDataCompressor.cs" />
|
||||||
<Compile Include="Configuration\DataFormats\IDataFormat.cs" />
|
|
||||||
<Compile Include="Configuration\Cryptography\IHashAlgorithm.cs" />
|
<Compile Include="Configuration\Cryptography\IHashAlgorithm.cs" />
|
||||||
|
<Compile Include="Configuration\DataFormats\IDataParser.cs" />
|
||||||
|
<Compile Include="Configuration\DataFormats\IDataSerializer.cs" />
|
||||||
<Compile Include="Configuration\DataFormats\ParseResult.cs" />
|
<Compile Include="Configuration\DataFormats\ParseResult.cs" />
|
||||||
|
<Compile Include="Configuration\DataFormats\SerializeResult.cs" />
|
||||||
|
<Compile Include="Configuration\DataResources\IResourceLoader.cs" />
|
||||||
|
<Compile Include="Configuration\DataResources\IResourceSaver.cs" />
|
||||||
<Compile Include="Configuration\SaveStatus.cs" />
|
<Compile Include="Configuration\SaveStatus.cs" />
|
||||||
<Compile Include="Core\Events\InstanceTerminatedEventHandler.cs" />
|
<Compile Include="Core\Events\InstanceTerminatedEventHandler.cs" />
|
||||||
<Compile Include="Core\Events\NameChangedEventHandler.cs" />
|
<Compile Include="Core\Events\NameChangedEventHandler.cs" />
|
||||||
|
@ -120,7 +124,6 @@
|
||||||
<Compile Include="Communication\Data\SimpleResponsePurport.cs" />
|
<Compile Include="Communication\Data\SimpleResponsePurport.cs" />
|
||||||
<Compile Include="Communication\Data\SimpleResponse.cs" />
|
<Compile Include="Communication\Data\SimpleResponse.cs" />
|
||||||
<Compile Include="Configuration\ClientConfiguration.cs" />
|
<Compile Include="Configuration\ClientConfiguration.cs" />
|
||||||
<Compile Include="Configuration\DataResources\IDataResource.cs" />
|
|
||||||
<Compile Include="Configuration\LoadStatus.cs" />
|
<Compile Include="Configuration\LoadStatus.cs" />
|
||||||
<Compile Include="Configuration\AppConfig.cs" />
|
<Compile Include="Configuration\AppConfig.cs" />
|
||||||
<Compile Include="Configuration\ISessionConfiguration.cs" />
|
<Compile Include="Configuration\ISessionConfiguration.cs" />
|
||||||
|
|
|
@ -120,10 +120,13 @@ namespace SafeExamBrowser.Runtime
|
||||||
configuration = new ConfigurationRepository(new HashAlgorithm(), repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
|
configuration = new ConfigurationRepository(new HashAlgorithm(), repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
|
||||||
appConfig = configuration.InitializeAppConfig();
|
appConfig = configuration.InitializeAppConfig();
|
||||||
|
|
||||||
configuration.Register(new BinaryFormat(compressor, new HashAlgorithm(), new ModuleLogger(logger, nameof(BinaryFormat))));
|
configuration.Register(new BinaryParser(compressor, new HashAlgorithm(), new ModuleLogger(logger, nameof(BinaryParser))));
|
||||||
configuration.Register(new XmlFormat(new ModuleLogger(logger, nameof(XmlFormat))));
|
configuration.Register(new BinarySerializer(compressor, new ModuleLogger(logger, nameof(BinarySerializer))));
|
||||||
configuration.Register(new FileResource(new ModuleLogger(logger, nameof(FileResource))));
|
configuration.Register(new XmlParser(new ModuleLogger(logger, nameof(XmlParser))));
|
||||||
configuration.Register(new NetworkResource(appConfig, new ModuleLogger(logger, nameof(NetworkResource))));
|
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()
|
private void InitializeLogging()
|
||||||
|
|
|
@ -179,10 +179,10 @@ namespace SafeExamBrowser.Runtime.Operations
|
||||||
{
|
{
|
||||||
var isAppDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(AppDataFile, StringComparison.OrdinalIgnoreCase);
|
var isAppDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(AppDataFile, StringComparison.OrdinalIgnoreCase);
|
||||||
var isProgramDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(ProgramDataFile, StringComparison.OrdinalIgnoreCase);
|
var isProgramDataFile = Path.GetFullPath(resource.AbsolutePath).Equals(ProgramDataFile, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isFirstSession = Context.Current == null;
|
||||||
|
|
||||||
if (!isAppDataFile && !isProgramDataFile)
|
if (!isAppDataFile && !isProgramDataFile)
|
||||||
{
|
{
|
||||||
var isFirstSession = Context.Current == null;
|
|
||||||
var requiresAuthentication = IsAuthenticationRequiredForClientConfiguration(password);
|
var requiresAuthentication = IsAuthenticationRequiredForClientConfiguration(password);
|
||||||
|
|
||||||
logger.Info("Starting client configuration...");
|
logger.Info("Starting client configuration...");
|
||||||
|
@ -203,7 +203,11 @@ namespace SafeExamBrowser.Runtime.Operations
|
||||||
|
|
||||||
var status = configuration.ConfigureClientWith(resource, password);
|
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}'!");
|
logger.Error($"Client configuration failed with status '{status}'!");
|
||||||
ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs());
|
ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs());
|
||||||
|
|
|
@ -158,7 +158,7 @@ namespace SafeExamBrowser.Runtime
|
||||||
runtimeWindow.Show();
|
runtimeWindow.Show();
|
||||||
runtimeWindow.BringToForeground();
|
runtimeWindow.BringToForeground();
|
||||||
runtimeWindow.ShowProgressBar();
|
runtimeWindow.ShowProgressBar();
|
||||||
logger.Info("### --- Session Start Procedure --- ###");
|
logger.Info(AppendDivider("Session Start Procedure"));
|
||||||
|
|
||||||
if (SessionIsRunning)
|
if (SessionIsRunning)
|
||||||
{
|
{
|
||||||
|
@ -169,19 +169,19 @@ namespace SafeExamBrowser.Runtime
|
||||||
|
|
||||||
if (result == OperationResult.Success)
|
if (result == OperationResult.Success)
|
||||||
{
|
{
|
||||||
logger.Info("### --- Session Running --- ###");
|
logger.Info(AppendDivider("Session Running"));
|
||||||
|
|
||||||
HandleSessionStartSuccess();
|
HandleSessionStartSuccess();
|
||||||
}
|
}
|
||||||
else if (result == OperationResult.Failed)
|
else if (result == OperationResult.Failed)
|
||||||
{
|
{
|
||||||
logger.Info("### --- Session Start Failed --- ###");
|
logger.Info(AppendDivider("Session Start Failed"));
|
||||||
|
|
||||||
HandleSessionStartFailure();
|
HandleSessionStartFailure();
|
||||||
}
|
}
|
||||||
else if (result == OperationResult.Aborted)
|
else if (result == OperationResult.Aborted)
|
||||||
{
|
{
|
||||||
logger.Info("### --- Session Start Aborted --- ###");
|
logger.Info(AppendDivider("Session Start Aborted"));
|
||||||
|
|
||||||
HandleSessionStartAbortion();
|
HandleSessionStartAbortion();
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ namespace SafeExamBrowser.Runtime
|
||||||
runtimeWindow.Show();
|
runtimeWindow.Show();
|
||||||
runtimeWindow.BringToForeground();
|
runtimeWindow.BringToForeground();
|
||||||
runtimeWindow.ShowProgressBar();
|
runtimeWindow.ShowProgressBar();
|
||||||
logger.Info("### --- Session Stop Procedure --- ###");
|
logger.Info(AppendDivider("Session Stop Procedure"));
|
||||||
|
|
||||||
DeregisterSessionEvents();
|
DeregisterSessionEvents();
|
||||||
|
|
||||||
|
@ -240,11 +240,11 @@ namespace SafeExamBrowser.Runtime
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
logger.Info("### --- Session Terminated --- ###");
|
logger.Info(AppendDivider("Session Terminated"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.Info("### --- Session Stop Failed --- ###");
|
logger.Info(AppendDivider("Session Stop Failed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,5 +532,13 @@ namespace SafeExamBrowser.Runtime
|
||||||
progressIndicator?.Regress();
|
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} ###";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,15 @@ namespace SafeExamBrowser.UserInterface.Classic
|
||||||
|
|
||||||
public void BringToForeground()
|
public void BringToForeground()
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(Activate);
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
WindowState = WindowState.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
Activate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public new void Close()
|
public new void Close()
|
||||||
|
|
Loading…
Reference in a new issue