Restore SEBPatch
This commit is contained in:
19
SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs
Normal file
19
SafeExamBrowser.Configuration/DataFormats/BinaryBlock.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 PublicKey = "pkhs";
|
||||
internal const string PublicKeySymmetric = "phsk";
|
||||
}
|
||||
}
|
211
SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs
Normal file
211
SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataCompression;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataFormats;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.DataFormats
|
||||
{
|
||||
public class BinaryParser : IDataParser
|
||||
{
|
||||
private const int PREFIX_LENGTH = 4;
|
||||
|
||||
private IDataCompressor compressor;
|
||||
private IHashAlgorithm hashAlgorithm;
|
||||
private ILogger logger;
|
||||
private IPasswordEncryption passwordEncryption;
|
||||
private IPublicKeyEncryption publicKeyEncryption;
|
||||
private IPublicKeyEncryption publicKeySymmetricEncryption;
|
||||
private readonly IDataParser xmlParser;
|
||||
|
||||
public BinaryParser(
|
||||
IDataCompressor compressor,
|
||||
IHashAlgorithm hashAlgorithm,
|
||||
ILogger logger,
|
||||
IPasswordEncryption passwordEncryption,
|
||||
IPublicKeyEncryption publicKeyEncryption,
|
||||
IPublicKeyEncryption publicKeySymmetricEncryption,
|
||||
IDataParser xmlParser)
|
||||
{
|
||||
this.compressor = compressor;
|
||||
this.hashAlgorithm = hashAlgorithm;
|
||||
this.logger = logger;
|
||||
this.passwordEncryption = passwordEncryption;
|
||||
this.publicKeyEncryption = publicKeyEncryption;
|
||||
this.publicKeySymmetricEncryption = publicKeySymmetricEncryption;
|
||||
this.xmlParser = xmlParser;
|
||||
}
|
||||
|
||||
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}' {(isValid ? "matches" : "does 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);
|
||||
var result = new ParseResult { Status = LoadStatus.InvalidData };
|
||||
|
||||
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:
|
||||
result = ParsePasswordBlock(data, prefix, password);
|
||||
break;
|
||||
case BinaryBlock.PlainData:
|
||||
result = ParsePlainDataBlock(data);
|
||||
break;
|
||||
case BinaryBlock.PublicKey:
|
||||
case BinaryBlock.PublicKeySymmetric:
|
||||
result = ParsePublicKeyBlock(data, prefix, password);
|
||||
break;
|
||||
}
|
||||
|
||||
result.Format = FormatType.Binary;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"'{data}' starting with '{prefix}' does not match the {FormatType.Binary} format!");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ParseResult ParsePasswordBlock(Stream data, string prefix, PasswordParameters password = null)
|
||||
{
|
||||
var result = new ParseResult();
|
||||
|
||||
if (password != null)
|
||||
{
|
||||
var parameters = DetermineEncryptionParametersFor(prefix, password);
|
||||
|
||||
logger.Debug($"Attempting to parse password block with prefix '{prefix}'...");
|
||||
result.Status = passwordEncryption.Decrypt(data, parameters.Password, out var decrypted);
|
||||
|
||||
if (result.Status == LoadStatus.Success)
|
||||
{
|
||||
result = ParsePlainDataBlock(decrypted);
|
||||
result.Encryption = parameters;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Status = LoadStatus.PasswordNeeded;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ParseResult ParsePlainDataBlock(Stream data)
|
||||
{
|
||||
data = compressor.IsCompressed(data) ? compressor.Decompress(data) : data;
|
||||
logger.Debug("Attempting to parse plain data block...");
|
||||
|
||||
return xmlParser.TryParse(data);
|
||||
}
|
||||
|
||||
private ParseResult ParsePublicKeyBlock(Stream data, string prefix, PasswordParameters password = null)
|
||||
{
|
||||
var encryption = prefix == BinaryBlock.PublicKey ? publicKeyEncryption : publicKeySymmetricEncryption;
|
||||
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 PublicKeyParameters
|
||||
{
|
||||
Certificate = certificate,
|
||||
InnerEncryption = result.Encryption as PasswordParameters,
|
||||
SymmetricEncryption = prefix == BinaryBlock.PublicKeySymmetric
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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 prefix = new byte[PREFIX_LENGTH];
|
||||
|
||||
if (compressor.IsCompressed(data))
|
||||
{
|
||||
prefix = compressor.Peek(data, PREFIX_LENGTH);
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
data.Read(prefix, 0, PREFIX_LENGTH);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(prefix);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
162
SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs
Normal file
162
SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataCompression;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataFormats;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.DataFormats
|
||||
{
|
||||
public class BinarySerializer : IDataSerializer
|
||||
{
|
||||
private IDataCompressor compressor;
|
||||
private ILogger logger;
|
||||
private IPasswordEncryption passwordEncryption;
|
||||
private IPublicKeyEncryption publicKeyEncryption;
|
||||
private IPublicKeyEncryption symmetricEncryption;
|
||||
private IDataSerializer xmlSerializer;
|
||||
|
||||
public BinarySerializer(
|
||||
IDataCompressor compressor,
|
||||
ILogger logger,
|
||||
IPasswordEncryption passwordEncryption,
|
||||
IPublicKeyEncryption publicKeyEncryption,
|
||||
IPublicKeyEncryption symmetricEncryption,
|
||||
IDataSerializer xmlSerializer)
|
||||
{
|
||||
this.compressor = compressor;
|
||||
this.logger = logger;
|
||||
this.passwordEncryption = passwordEncryption;
|
||||
this.publicKeyEncryption = publicKeyEncryption;
|
||||
this.symmetricEncryption = symmetricEncryption;
|
||||
this.xmlSerializer = xmlSerializer;
|
||||
}
|
||||
|
||||
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 PublicKeyParameters 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 prefix = password.IsHash ? BinaryBlock.PasswordConfigureClient : BinaryBlock.Password;
|
||||
|
||||
logger.Debug("Attempting to serialize password block...");
|
||||
|
||||
var status = passwordEncryption.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 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, PublicKeyParameters parameters)
|
||||
{
|
||||
var result = SerializePublicKeyHashInnerBlock(data, parameters);
|
||||
|
||||
if (result.Status == SaveStatus.Success)
|
||||
{
|
||||
var encryption = parameters.SymmetricEncryption ? symmetricEncryption : publicKeyEncryption;
|
||||
var prefix = parameters.SymmetricEncryption ? BinaryBlock.PublicKeySymmetric : BinaryBlock.PublicKey;
|
||||
|
||||
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, PublicKeyParameters parameters)
|
||||
{
|
||||
if (parameters.InnerEncryption is PasswordParameters password)
|
||||
{
|
||||
return SerializePasswordBlock(data, password);
|
||||
}
|
||||
|
||||
return SerializePlainDataBlock(data, true);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
25
SafeExamBrowser.Configuration/DataFormats/XmlElement.cs
Normal file
25
SafeExamBrowser.Configuration/DataFormats/XmlElement.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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";
|
||||
}
|
||||
}
|
301
SafeExamBrowser.Configuration/DataFormats/XmlParser.cs
Normal file
301
SafeExamBrowser.Configuration/DataFormats/XmlParser.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataCompression;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataFormats;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.DataFormats
|
||||
{
|
||||
public class XmlParser : IDataParser
|
||||
{
|
||||
private const string XML_PREFIX = "<?xm";
|
||||
|
||||
private readonly IDataCompressor compressor;
|
||||
private readonly ILogger logger;
|
||||
|
||||
public XmlParser(IDataCompressor compressor, ILogger logger)
|
||||
{
|
||||
this.compressor = compressor;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public bool CanParse(Stream data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var longEnough = data.Length > XML_PREFIX.Length;
|
||||
|
||||
if (longEnough)
|
||||
{
|
||||
var prefix = ReadPrefix(data);
|
||||
var isValid = XML_PREFIX.Equals(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
logger.Debug($"'{data}' starting with '{prefix}' {(isValid ? "matches" : "does not match")} the {FormatType.Xml} format.");
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to match the {FormatType.Xml} format.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to determine whether '{data}' with {data?.Length / 1000.0} KB data matches the {FormatType.Xml} format!", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ParseResult TryParse(Stream data, PasswordParameters password = null)
|
||||
{
|
||||
var prefix = ReadPrefix(data);
|
||||
var isValid = XML_PREFIX.Equals(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
var result = new ParseResult { Status = LoadStatus.InvalidData };
|
||||
|
||||
if (isValid)
|
||||
{
|
||||
data = compressor.IsCompressed(data) ? compressor.Decompress(data) : data;
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (var reader = XmlReader.Create(data, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }))
|
||||
{
|
||||
var hasRoot = reader.ReadToFollowing(XmlElement.Root);
|
||||
var hasDictionary = reader.ReadToDescendant(XmlElement.Dictionary);
|
||||
|
||||
if (hasRoot && hasDictionary)
|
||||
{
|
||||
logger.Debug($"Found root node, starting to parse data...");
|
||||
|
||||
result.Status = ParseDictionary(reader, out var rawData);
|
||||
result.RawData = rawData;
|
||||
|
||||
logger.Debug($"Finished parsing -> Result: {result.Status}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Could not find root {(!hasRoot ? $"node '{XmlElement.Root}'" : $"dictionary '{XmlElement.Dictionary}'")}!");
|
||||
}
|
||||
}
|
||||
|
||||
result.Format = FormatType.Xml;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"'{data}' starting with '{prefix}' does not match the {FormatType.Xml} format!");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private LoadStatus ParseArray(XmlReader reader, out List<object> array)
|
||||
{
|
||||
array = new List<object>();
|
||||
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
reader.Read();
|
||||
reader.MoveToContent();
|
||||
|
||||
return LoadStatus.Success;
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
reader.MoveToContent();
|
||||
|
||||
while (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
var status = ParseElement(reader, out object element);
|
||||
|
||||
if (status == LoadStatus.Success)
|
||||
{
|
||||
array.Add(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
reader.MoveToContent();
|
||||
}
|
||||
|
||||
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == XmlElement.Array)
|
||||
{
|
||||
reader.Read();
|
||||
reader.MoveToContent();
|
||||
|
||||
return LoadStatus.Success;
|
||||
}
|
||||
|
||||
logger.Error($"Expected closing tag for '{XmlElement.Array}', but found '{reader.Name}{reader.Value}'!");
|
||||
|
||||
return LoadStatus.InvalidData;
|
||||
}
|
||||
|
||||
private LoadStatus ParseDictionary(XmlReader reader, out Dictionary<string, object> dictionary)
|
||||
{
|
||||
dictionary = new Dictionary<string, object>();
|
||||
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
reader.Read();
|
||||
reader.MoveToContent();
|
||||
|
||||
return LoadStatus.Success;
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
reader.MoveToContent();
|
||||
|
||||
while (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
var status = ParseKeyValuePair(reader, out var pair);
|
||||
|
||||
if (status == LoadStatus.Success)
|
||||
{
|
||||
dictionary[pair.Key] = pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
reader.MoveToContent();
|
||||
}
|
||||
|
||||
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == XmlElement.Dictionary)
|
||||
{
|
||||
reader.Read();
|
||||
reader.MoveToContent();
|
||||
|
||||
return LoadStatus.Success;
|
||||
}
|
||||
|
||||
logger.Error($"Expected closing tag for '{XmlElement.Dictionary}', but found '{reader.Name}{reader.Value}'!");
|
||||
|
||||
return LoadStatus.InvalidData;
|
||||
}
|
||||
|
||||
private LoadStatus ParseKeyValuePair(XmlReader reader, out KeyValuePair<string, object> pair)
|
||||
{
|
||||
var status = LoadStatus.InvalidData;
|
||||
var key = XNode.ReadFrom(reader) as XElement;
|
||||
|
||||
pair = default(KeyValuePair<string, object>);
|
||||
|
||||
if (key.Name.LocalName == XmlElement.Key)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
status = ParseElement(reader, out object value);
|
||||
|
||||
if (status == LoadStatus.Success)
|
||||
{
|
||||
pair = new KeyValuePair<string, object>(key.Value, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Expected element '{XmlElement.Key}', but found '{key}'!");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private LoadStatus ParseElement(XmlReader reader, out object element)
|
||||
{
|
||||
var array = default(List<object>);
|
||||
var dictionary = default(Dictionary<string, object>);
|
||||
var status = default(LoadStatus);
|
||||
var value = default(object);
|
||||
|
||||
if (reader.Name == XmlElement.Array)
|
||||
{
|
||||
status = ParseArray(reader, out array);
|
||||
}
|
||||
else if (reader.Name == XmlElement.Dictionary)
|
||||
{
|
||||
status = ParseDictionary(reader, out dictionary);
|
||||
}
|
||||
else
|
||||
{
|
||||
status = ParseSimpleType(XNode.ReadFrom(reader) as XElement, out value);
|
||||
}
|
||||
|
||||
element = array ?? dictionary ?? value;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private LoadStatus ParseSimpleType(XElement element, out object value)
|
||||
{
|
||||
var status = LoadStatus.Success;
|
||||
|
||||
value = null;
|
||||
|
||||
switch (element.Name.LocalName)
|
||||
{
|
||||
case XmlElement.Data:
|
||||
value = Convert.FromBase64String(element.Value);
|
||||
break;
|
||||
case XmlElement.Date:
|
||||
value = XmlConvert.ToDateTime(element.Value, XmlDateTimeSerializationMode.Utc);
|
||||
break;
|
||||
case XmlElement.False:
|
||||
value = false;
|
||||
break;
|
||||
case XmlElement.Integer:
|
||||
value = Convert.ToInt32(element.Value);
|
||||
break;
|
||||
case XmlElement.Real:
|
||||
value = Convert.ToDouble(element.Value, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case XmlElement.String:
|
||||
value = element.IsEmpty ? null : element.Value;
|
||||
break;
|
||||
case XmlElement.True:
|
||||
value = true;
|
||||
break;
|
||||
default:
|
||||
status = LoadStatus.InvalidData;
|
||||
break;
|
||||
}
|
||||
|
||||
if (status != LoadStatus.Success)
|
||||
{
|
||||
logger.Error($"Element '{element}' is not a supported value type!");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private string ReadPrefix(Stream data)
|
||||
{
|
||||
var prefixData = new byte[XML_PREFIX.Length];
|
||||
|
||||
if (compressor.IsCompressed(data))
|
||||
{
|
||||
prefixData = compressor.Peek(data, XML_PREFIX.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
data.Read(prefixData, 0, XML_PREFIX.Length);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(prefixData);
|
||||
}
|
||||
}
|
||||
}
|
199
SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs
Normal file
199
SafeExamBrowser.Configuration/DataFormats/XmlSerializer.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Configuration.Contracts.DataFormats;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.DataFormats
|
||||
{
|
||||
public class XmlSerializer : IDataSerializer
|
||||
{
|
||||
private readonly 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", "https://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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user