SEBWIN-221: Implemented basic password decryption of binary configuration files and finally found correct solution for multi-threading issues related to WPF.
This commit is contained in:
		
							parent
							
								
									b29fd8c2d7
								
							
						
					
					
						commit
						7f38c0b8c3
					
				
					 30 changed files with 589 additions and 164 deletions
				
			
		|  | @ -127,7 +127,7 @@ namespace SafeExamBrowser.Browser | |||
| 
 | ||||
| 		private void Button_OnClick(InstanceIdentifier id = null) | ||||
| 		{ | ||||
| 			if (id is null) | ||||
| 			if (id == null) | ||||
| 			{ | ||||
| 				CreateNewInstance(); | ||||
| 			} | ||||
|  |  | |||
|  | @ -71,9 +71,8 @@ | |||
|     <Reference Include="System.Threading.Tasks.Extensions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll</HintPath> | ||||
|     </Reference> | ||||
|     <Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll</HintPath> | ||||
|       <Private>True</Private> | ||||
|     <Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll</HintPath> | ||||
|     </Reference> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|  | @ -96,8 +95,12 @@ | |||
|     <Compile Include="ClientControllerTests.cs" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <None Include="app.config" /> | ||||
|     <None Include="packages.config" /> | ||||
|     <None Include="app.config"> | ||||
|       <SubType>Designer</SubType> | ||||
|     </None> | ||||
|     <None Include="packages.config"> | ||||
|       <SubType>Designer</SubType> | ||||
|     </None> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\SafeExamBrowser.Client\SafeExamBrowser.Client.csproj"> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|       </dependentAssembly> | ||||
|       <dependentAssembly> | ||||
|         <assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> | ||||
|         <bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0" /> | ||||
|         <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" /> | ||||
|       </dependentAssembly> | ||||
|     </assemblyBinding> | ||||
|   </runtime> | ||||
|  |  | |||
|  | @ -5,5 +5,5 @@ | |||
|   <package id="MSTest.TestAdapter" version="1.2.0" targetFramework="net461" /> | ||||
|   <package id="MSTest.TestFramework" version="1.2.0" targetFramework="net461" /> | ||||
|   <package id="System.Threading.Tasks.Extensions" version="4.4.0" targetFramework="net461" /> | ||||
|   <package id="System.ValueTuple" version="4.4.0" targetFramework="net461" /> | ||||
|   <package id="System.ValueTuple" version="4.5.0" targetFramework="net461" /> | ||||
| </packages> | ||||
|  | @ -229,7 +229,7 @@ namespace SafeExamBrowser.Communication.Proxies | |||
| 
 | ||||
| 		private void FailIfNull(Message message) | ||||
| 		{ | ||||
| 			if (message is null) | ||||
| 			if (message == null) | ||||
| 			{ | ||||
| 				throw new ArgumentNullException(nameof(message)); | ||||
| 			} | ||||
|  |  | |||
|  | @ -98,10 +98,11 @@ namespace SafeExamBrowser.Configuration.Compression | |||
| 
 | ||||
| 		public byte[] Peek(Stream data, int count) | ||||
| 		{ | ||||
| 			var stream = new GZipStream(data, CompressionMode.Decompress); | ||||
| 
 | ||||
| 			logger.Debug($"Peeking {count} bytes from '{data}'..."); | ||||
| 			data.Seek(0, SeekOrigin.Begin); | ||||
| 
 | ||||
| 			using (var stream = new GZipStream(data, CompressionMode.Decompress)) | ||||
| 			using (var decompressed = new MemoryStream()) | ||||
| 			{ | ||||
| 				var buffer = new byte[count]; | ||||
|  |  | |||
|  | @ -93,6 +93,8 @@ namespace SafeExamBrowser.Configuration | |||
| 		{ | ||||
| 			var settings = new Settings(); | ||||
| 
 | ||||
| 			// TODO: Specify default settings | ||||
| 
 | ||||
| 			settings.KioskMode = KioskMode.None; | ||||
| 			settings.ServicePolicy = ServicePolicy.Optional; | ||||
| 
 | ||||
|  | @ -122,7 +124,7 @@ namespace SafeExamBrowser.Configuration | |||
| 			resourceLoaders.Add(resourceLoader); | ||||
| 		} | ||||
| 
 | ||||
| 		public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string adminPassword = null, string settingsPassword = null) | ||||
| 		public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string password = null) | ||||
| 		{ | ||||
| 			settings = default(Settings); | ||||
| 
 | ||||
|  | @ -139,7 +141,7 @@ namespace SafeExamBrowser.Configuration | |||
| 						case LoadStatus.LoadWithBrowser: | ||||
| 							return HandleBrowserResource(resource, out settings); | ||||
| 						case LoadStatus.Success: | ||||
| 							return TryParseData(data, out settings, adminPassword, settingsPassword); | ||||
| 							return TryParseData(data, out settings, password); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
|  | @ -173,7 +175,7 @@ namespace SafeExamBrowser.Configuration | |||
| 			return status; | ||||
| 		} | ||||
| 
 | ||||
| 		private LoadStatus TryParseData(Stream data, out Settings settings, string adminPassword, string settingsPassword) | ||||
| 		private LoadStatus TryParseData(Stream data, out Settings settings, string password) | ||||
| 		{ | ||||
| 			var status = LoadStatus.NotSupported; | ||||
| 			var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); | ||||
|  | @ -182,7 +184,7 @@ namespace SafeExamBrowser.Configuration | |||
| 
 | ||||
| 			if (dataFormat != null) | ||||
| 			{ | ||||
| 				status = dataFormat.TryParse(data, out settings, adminPassword, settingsPassword); | ||||
| 				status = dataFormat.TryParse(data, out settings, password); | ||||
| 				logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}."); | ||||
| 			} | ||||
| 			else | ||||
|  |  | |||
|  | @ -0,0 +1,155 @@ | |||
| /* | ||||
|  * 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.Security.Cryptography; | ||||
| using System.Text; | ||||
| using SafeExamBrowser.Contracts.Configuration; | ||||
| using SafeExamBrowser.Contracts.Configuration.Settings; | ||||
| 
 | ||||
| namespace SafeExamBrowser.Configuration.DataFormats | ||||
| { | ||||
| 	public partial class BinaryFormat | ||||
| 	{ | ||||
| 		private const int BLOCK_SIZE = 16; | ||||
| 		private const int HEADER_SIZE = 2; | ||||
| 		private const int ITERATIONS = 10000; | ||||
| 		private const int KEY_SIZE = 32; | ||||
| 		private const int OPTIONS = 0x1; | ||||
| 		private const int SALT_SIZE = 8; | ||||
| 		private const int VERSION = 0x2; | ||||
| 
 | ||||
| 		private LoadStatus ParsePassword(Stream data, FormatType format, out Settings settings, string password = null) | ||||
| 		{ | ||||
| 			settings = default(Settings); | ||||
| 
 | ||||
| 			if (password == null) | ||||
| 			{ | ||||
| 				return LoadStatus.PasswordNeeded; | ||||
| 			} | ||||
| 
 | ||||
| 			var (version, options) = ParseHeader(data); | ||||
| 
 | ||||
| 			if (version != VERSION || options != OPTIONS) | ||||
| 			{ | ||||
| 				return FailForInvalidPasswordHeader(version, options); | ||||
| 			} | ||||
| 
 | ||||
| 			if (format == FormatType.PasswordConfigureClient) | ||||
| 			{ | ||||
| 				// TODO: Shouldn't this not only be done for admin password, and not settings password?!? | ||||
| 				password = GeneratePasswordHash(password); | ||||
| 			} | ||||
| 
 | ||||
| 			var (authenticationKey, encryptionKey) = GenerateKeys(data, password); | ||||
| 			var (originalHmac, computedHmac) = GenerateHmac(data, authenticationKey); | ||||
| 
 | ||||
| 			if (!computedHmac.SequenceEqual(originalHmac)) | ||||
| 			{ | ||||
| 				return FailForInvalidPasswordHmac(); | ||||
| 			} | ||||
| 
 | ||||
| 			using (var plainData = Decrypt(data, encryptionKey, originalHmac.Length)) | ||||
| 			{ | ||||
| 				return ParsePlainData(plainData, out settings); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private (int version, int options) ParseHeader(Stream data) | ||||
| 		{ | ||||
| 			data.Seek(0, SeekOrigin.Begin); | ||||
| 
 | ||||
| 			var version = data.ReadByte(); | ||||
| 			var options = data.ReadByte(); | ||||
| 
 | ||||
| 			return (version, options); | ||||
| 		} | ||||
| 
 | ||||
| 		private LoadStatus FailForInvalidPasswordHeader(int version, int options) | ||||
| 		{ | ||||
| 			logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); | ||||
| 
 | ||||
| 			return LoadStatus.InvalidData; | ||||
| 		} | ||||
| 
 | ||||
| 		private string GeneratePasswordHash(string input) | ||||
| 		{ | ||||
| 			using (var algorithm = new SHA256Managed()) | ||||
| 			{ | ||||
| 				var bytes = Encoding.UTF8.GetBytes(input); | ||||
| 				var hash = algorithm.ComputeHash(bytes); | ||||
| 				var @string = String.Join(String.Empty, hash.Select(b => b.ToString("x2"))); | ||||
| 
 | ||||
| 				return @string; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password) | ||||
| 		{ | ||||
| 			var authenticationSalt = new byte[SALT_SIZE]; | ||||
| 			var encryptionSalt = new byte[SALT_SIZE]; | ||||
| 
 | ||||
| 			data.Seek(HEADER_SIZE, SeekOrigin.Begin); | ||||
| 			data.Read(encryptionSalt, 0, SALT_SIZE); | ||||
| 			data.Read(authenticationSalt, 0, SALT_SIZE); | ||||
| 
 | ||||
| 			using (var authenticationGenerator = new Rfc2898DeriveBytes(password, authenticationSalt, ITERATIONS)) | ||||
| 			using (var encryptionGenerator = new Rfc2898DeriveBytes(password, encryptionSalt, ITERATIONS)) | ||||
| 			{ | ||||
| 				var authenticationKey = authenticationGenerator.GetBytes(KEY_SIZE); | ||||
| 				var encryptionKey = encryptionGenerator.GetBytes(KEY_SIZE); | ||||
| 
 | ||||
| 				return (authenticationKey, encryptionKey); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private (byte[] originalHmac, byte[] computedHmac) GenerateHmac(Stream data, byte[] authenticationKey) | ||||
| 		{ | ||||
| 			using (var hmac = new HMACSHA256(authenticationKey)) | ||||
| 			{ | ||||
| 				var originalHmac = new byte[hmac.HashSize / 8]; | ||||
| 				var hashStream = new SubStream(data, 0, data.Length - originalHmac.Length); | ||||
| 				var computedHmac = hmac.ComputeHash(hashStream); | ||||
| 
 | ||||
| 				data.Seek(originalHmac.Length, SeekOrigin.End); | ||||
| 				data.Read(originalHmac, 0, originalHmac.Length); | ||||
| 
 | ||||
| 				return (originalHmac, computedHmac); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private LoadStatus FailForInvalidPasswordHmac() | ||||
| 		{ | ||||
| 			logger.Warn($"The authentication failed due to an invalid password or corrupted data!"); | ||||
| 
 | ||||
| 			return LoadStatus.PasswordNeeded; | ||||
| 		} | ||||
| 
 | ||||
| 		private Stream Decrypt(Stream data, byte[] encryptionKey, int hmacLength) | ||||
| 		{ | ||||
| 			var initializationVector = new byte[BLOCK_SIZE]; | ||||
| 
 | ||||
| 			data.Seek(HEADER_SIZE + 2 * SALT_SIZE, SeekOrigin.Begin); | ||||
| 			data.Read(initializationVector, 0, BLOCK_SIZE); | ||||
| 
 | ||||
| 			var decryptedData = new MemoryStream(); | ||||
| 			var encryptedData = new SubStream(data, data.Position, data.Length - data.Position - hmacLength); | ||||
| 
 | ||||
| 			using (var aes = new AesManaged { KeySize = KEY_SIZE * 8, BlockSize = BLOCK_SIZE * 8, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }) | ||||
| 			using (var decryptor = aes.CreateDecryptor(encryptionKey, initializationVector)) | ||||
| 			using (var cryptoStream = new CryptoStream(encryptedData, decryptor, CryptoStreamMode.Read)) | ||||
| 			{ | ||||
| 				cryptoStream.CopyTo(decryptedData); | ||||
| 			} | ||||
| 
 | ||||
| 			return decryptedData; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,48 @@ | |||
| /* | ||||
|  * 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; | ||||
| using System.Text; | ||||
| using SafeExamBrowser.Contracts.Configuration; | ||||
| using SafeExamBrowser.Contracts.Configuration.Settings; | ||||
| 
 | ||||
| namespace SafeExamBrowser.Configuration.DataFormats | ||||
| { | ||||
| 	public partial class BinaryFormat | ||||
| 	{ | ||||
| 		private LoadStatus ParsePlainData(Stream data, out Settings settings) | ||||
| 		{ | ||||
| 			if (compressor.IsCompressed(data)) | ||||
| 			{ | ||||
| 				data = compressor.Decompress(data); | ||||
| 			} | ||||
| 
 | ||||
| 			var buffer = new byte[4096]; | ||||
| 			var bytesRead = 0; | ||||
| 			var xmlBuilder = new StringBuilder(); | ||||
| 
 | ||||
| 			data.Seek(0, SeekOrigin.Begin); | ||||
| 
 | ||||
| 			do | ||||
| 			{ | ||||
| 				bytesRead = data.Read(buffer, 0, buffer.Length); | ||||
| 				xmlBuilder.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); | ||||
| 			} while (bytesRead > 0); | ||||
| 
 | ||||
| 			var xml = xmlBuilder.ToString(); | ||||
| 
 | ||||
| 			// TODO: Parse XML data... | ||||
| 
 | ||||
| 			settings = new Settings(); | ||||
| 			settings.Browser.AllowAddressBar = true; | ||||
| 			settings.Browser.AllowConfigurationDownloads = true; | ||||
| 
 | ||||
| 			return LoadStatus.Success; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,23 @@ | |||
| /* | ||||
|  * 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; | ||||
| 
 | ||||
| namespace SafeExamBrowser.Configuration.DataFormats | ||||
| { | ||||
| 	public partial class BinaryFormat | ||||
| 	{ | ||||
| 		private LoadStatus ParsePublicKeyHash(Stream data, out Settings settings, string password) | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,23 @@ | |||
| /* | ||||
|  * 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; | ||||
| 
 | ||||
| namespace SafeExamBrowser.Configuration.DataFormats | ||||
| { | ||||
| 	public partial class BinaryFormat | ||||
| 	{ | ||||
| 		private LoadStatus ParsePublicKeyHashWithSymmetricKey(Stream data, out Settings settings, string password) | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -15,7 +15,7 @@ using SafeExamBrowser.Contracts.Logging; | |||
| 
 | ||||
| namespace SafeExamBrowser.Configuration.DataFormats | ||||
| { | ||||
| 	public class BinaryFormat : IDataFormat | ||||
| 	public partial class BinaryFormat : IDataFormat | ||||
| 	{ | ||||
| 		private const int PREFIX_LENGTH = 4; | ||||
| 
 | ||||
|  | @ -37,7 +37,7 @@ namespace SafeExamBrowser.Configuration.DataFormats | |||
| 				if (longEnough) | ||||
| 				{ | ||||
| 					var prefix = ParsePrefix(data); | ||||
| 					var success = TryDetermineFormat(prefix, out DataFormat format); | ||||
| 					var success = TryDetermineFormat(prefix, out FormatType format); | ||||
| 
 | ||||
| 					logger.Debug($"'{data}' starting with '{prefix}' does {(success ? string.Empty : "not ")}match the binary format."); | ||||
| 
 | ||||
|  | @ -54,14 +54,40 @@ namespace SafeExamBrowser.Configuration.DataFormats | |||
| 			return false; | ||||
| 		} | ||||
| 
 | ||||
| 		public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null) | ||||
| 		public LoadStatus TryParse(Stream data, out Settings settings, string password = null) | ||||
| 		{ | ||||
| 			settings = new Settings(); | ||||
| 			settings.Browser.AllowAddressBar = true; | ||||
| 			settings.Browser.StartUrl = "www.duckduckgo.com"; | ||||
| 			settings.Browser.AllowConfigurationDownloads = true; | ||||
| 			var prefix = ParsePrefix(data); | ||||
| 			var success = TryDetermineFormat(prefix, out FormatType format); | ||||
| 
 | ||||
| 			return LoadStatus.Success; | ||||
| 			settings = default(Settings); | ||||
| 
 | ||||
| 			if (success) | ||||
| 			{ | ||||
| 				if (compressor.IsCompressed(data)) | ||||
| 				{ | ||||
| 					data = compressor.Decompress(data); | ||||
| 				} | ||||
| 
 | ||||
| 				data = new SubStream(data, PREFIX_LENGTH, data.Length - PREFIX_LENGTH); | ||||
| 
 | ||||
| 				// TODO: Try to abstract (Parser -> Binary, Xml, ...; DataBlock -> Password, PlainData, ...) once fully implemented! | ||||
| 				switch (format) | ||||
| 				{ | ||||
| 					case FormatType.Password: | ||||
| 					case FormatType.PasswordConfigureClient: | ||||
| 						return ParsePassword(data, format, out settings, password); | ||||
| 					case FormatType.PlainData: | ||||
| 						return ParsePlainData(data, out settings); | ||||
| 					case FormatType.PublicKeyHash: | ||||
| 						return ParsePublicKeyHash(data, out settings, password); | ||||
| 					case FormatType.PublicKeyHashSymmetricKey: | ||||
| 						return ParsePublicKeyHashWithSymmetricKey(data, out settings, password); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			logger.Error($"'{data}' starting with '{prefix}' does not match the binary format!"); | ||||
| 
 | ||||
| 			return LoadStatus.InvalidData; | ||||
| 		} | ||||
| 
 | ||||
| 		private string ParsePrefix(Stream data) | ||||
|  | @ -81,39 +107,39 @@ namespace SafeExamBrowser.Configuration.DataFormats | |||
| 			return Encoding.UTF8.GetString(prefixData); | ||||
| 		} | ||||
| 
 | ||||
| 		private bool TryDetermineFormat(string prefix, out DataFormat format) | ||||
| 		private bool TryDetermineFormat(string prefix, out FormatType format) | ||||
| 		{ | ||||
| 			format = default(DataFormat); | ||||
| 			format = default(FormatType); | ||||
| 
 | ||||
| 			switch (prefix) | ||||
| 			{ | ||||
| 				case "pswd": | ||||
| 					format = DataFormat.Password; | ||||
| 					format = FormatType.Password; | ||||
| 					return true; | ||||
| 				case "pwcc": | ||||
| 					format = DataFormat.PasswordForConfigureClient; | ||||
| 					format = FormatType.PasswordConfigureClient; | ||||
| 					return true; | ||||
| 				case "plnd": | ||||
| 					format = DataFormat.PlainData; | ||||
| 					format = FormatType.PlainData; | ||||
| 					return true; | ||||
| 				case "pkhs": | ||||
| 					format = DataFormat.PublicKeyHash; | ||||
| 					format = FormatType.PublicKeyHash; | ||||
| 					return true; | ||||
| 				case "phsk": | ||||
| 					format = DataFormat.PublicKeyHashWithSymmetricKey; | ||||
| 					format = FormatType.PublicKeyHashSymmetricKey; | ||||
| 					return true; | ||||
| 			} | ||||
| 
 | ||||
| 			return false; | ||||
| 		} | ||||
| 
 | ||||
| 		private enum DataFormat | ||||
| 		private enum FormatType | ||||
| 		{ | ||||
| 			Password = 1, | ||||
| 			PasswordForConfigureClient, | ||||
| 			PasswordConfigureClient, | ||||
| 			PlainData, | ||||
| 			PublicKeyHash, | ||||
| 			PublicKeyHashWithSymmetricKey | ||||
| 			PublicKeyHashSymmetricKey | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ namespace SafeExamBrowser.Configuration.DataFormats | |||
| 			return false; | ||||
| 		} | ||||
| 
 | ||||
| 		public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null) | ||||
| 		public LoadStatus TryParse(Stream data, out Settings settings, string password = null) | ||||
| 		{ | ||||
| 			throw new System.NotImplementedException(); | ||||
| 		} | ||||
|  |  | |||
|  | @ -49,18 +49,34 @@ | |||
|   <ItemGroup> | ||||
|     <Reference Include="System" /> | ||||
|     <Reference Include="System.Net.Http" /> | ||||
|     <Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath> | ||||
|     </Reference> | ||||
|     <Reference Include="System.Windows.Forms" /> | ||||
|     <Reference Include="Microsoft.CSharp" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <Compile Include="Compression\GZipCompressor.cs" /> | ||||
|     <Compile Include="DataFormats\BinaryFormat.cs" /> | ||||
|     <Compile Include="DataFormats\BinaryFormat.Password.cs"> | ||||
|       <DependentUpon>BinaryFormat.cs</DependentUpon> | ||||
|     </Compile> | ||||
|     <Compile Include="DataFormats\BinaryFormat.PlainData.cs"> | ||||
|       <DependentUpon>BinaryFormat.cs</DependentUpon> | ||||
|     </Compile> | ||||
|     <Compile Include="DataFormats\BinaryFormat.PublicKeyHash.cs"> | ||||
|       <DependentUpon>BinaryFormat.cs</DependentUpon> | ||||
|     </Compile> | ||||
|     <Compile Include="DataFormats\BinaryFormat.PublicKeyHashWithSymmetricKey.cs"> | ||||
|       <DependentUpon>BinaryFormat.cs</DependentUpon> | ||||
|     </Compile> | ||||
|     <Compile Include="DataFormats\XmlFormat.cs" /> | ||||
|     <Compile Include="Properties\AssemblyInfo.cs" /> | ||||
|     <Compile Include="ConfigurationRepository.cs" /> | ||||
|     <Compile Include="ResourceLoaders\FileResourceLoader.cs" /> | ||||
|     <Compile Include="ResourceLoaders\NetworkResourceLoader.cs" /> | ||||
|     <Compile Include="SessionConfiguration.cs" /> | ||||
|     <Compile Include="SubStream.cs" /> | ||||
|     <Compile Include="SystemInfo.cs" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|  | @ -69,5 +85,8 @@ | |||
|       <Name>SafeExamBrowser.Contracts</Name> | ||||
|     </ProjectReference> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <None Include="packages.config" /> | ||||
|   </ItemGroup> | ||||
|   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | ||||
| </Project> | ||||
							
								
								
									
										141
									
								
								SafeExamBrowser.Configuration/SubStream.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								SafeExamBrowser.Configuration/SubStream.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,141 @@ | |||
| /* | ||||
|  * 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.Configuration | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// A read-only wrapper for a subsection of another, larger stream. | ||||
| 	/// TODO: Unit Test! | ||||
| 	/// </summary> | ||||
| 	internal class SubStream : Stream | ||||
| 	{ | ||||
| 		private long length; | ||||
| 		private long offset; | ||||
| 		private Stream original; | ||||
| 
 | ||||
| 		public override bool CanRead => original.CanRead; | ||||
| 		public override bool CanSeek => original.CanSeek; | ||||
| 		public override bool CanWrite => false; | ||||
| 		public override long Length => length; | ||||
| 		public override long Position { get; set; } | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Creates a new wrapper for the specified subsection of the given stream. | ||||
| 		/// </summary> | ||||
| 		/// <remarks> | ||||
| 		///  | ||||
| 		/// Below an example of a subsection within a stream: | ||||
| 		///  | ||||
| 		/// +==============+==============================================================+==============================+ | ||||
| 		/// |      ...     |####################### subsection ###########################|              ...             | | ||||
| 		/// +==============+==============================================================+==============================+ | ||||
| 		/// ^              ^                                                              ^                              ^ | ||||
| 		/// |              |                                                              |                              | | ||||
| 		/// |              + offset                                                       + length                       | | ||||
| 		/// |                                                                                                            | | ||||
| 		/// + start of original                                                                          end of original + | ||||
| 		///  | ||||
| 		/// </remarks> | ||||
| 		/// <exception cref="ArgumentException">In case the original stream does not support <see cref="Stream.CanRead"/>.</exception> | ||||
| 		/// <exception cref="ArgumentException">In case the original stream does not support <see cref="Stream.CanSeek"/>.</exception> | ||||
| 		/// <exception cref="ArgumentOutOfRangeException">In case the specified subsection is outside the bounds of the original stream.</exception> | ||||
| 		public SubStream(Stream original, long offset, long length) | ||||
| 		{ | ||||
| 			this.original = original; | ||||
| 			this.offset = offset; | ||||
| 			this.length = length; | ||||
| 
 | ||||
| 			if (!original.CanRead) | ||||
| 			{ | ||||
| 				throw new ArgumentException("The original stream must support reading!", nameof(original)); | ||||
| 			} | ||||
| 
 | ||||
| 			if (!original.CanSeek) | ||||
| 			{ | ||||
| 				throw new ArgumentException("The original stream must support seeking!", nameof(original)); | ||||
| 			} | ||||
| 
 | ||||
| 			if (original.Length < offset + length || offset < 0 || length < 1) | ||||
| 			{ | ||||
| 				throw new ArgumentOutOfRangeException($"Specified subsection is outside the bounds of the original stream!"); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public override void Flush() | ||||
| 		{ | ||||
| 			throw new NotSupportedException(); | ||||
| 		} | ||||
| 
 | ||||
| 		public override int Read(byte[] buffer, int offset, int count) | ||||
| 		{ | ||||
| 			var originalPosition = original.Position; | ||||
| 
 | ||||
| 			if (Position < 0 || Position >= Length) | ||||
| 			{ | ||||
| 				return 0; | ||||
| 			} | ||||
| 
 | ||||
| 			if (Position + count >= Length) | ||||
| 			{ | ||||
| 				count = Convert.ToInt32(Length - Position); | ||||
| 			} | ||||
| 
 | ||||
| 			original.Seek(this.offset + Position, SeekOrigin.Begin); | ||||
| 
 | ||||
| 			var bytesRead = original.Read(buffer, offset, count); | ||||
| 
 | ||||
| 			Position += bytesRead; | ||||
| 			original.Seek(originalPosition, SeekOrigin.Begin); | ||||
| 
 | ||||
| 			return bytesRead; | ||||
| 		} | ||||
| 
 | ||||
| 		public override int ReadByte() | ||||
| 		{ | ||||
| 			if (Position < 0 || Position >= Length) | ||||
| 			{ | ||||
| 				return -1; | ||||
| 			} | ||||
| 
 | ||||
| 			return base.ReadByte(); | ||||
| 		} | ||||
| 
 | ||||
| 		public override long Seek(long offset, SeekOrigin origin) | ||||
| 		{ | ||||
| 			switch (origin) | ||||
| 			{ | ||||
| 				case SeekOrigin.Begin: | ||||
| 					Position = offset; | ||||
| 					break; | ||||
| 				case SeekOrigin.Current: | ||||
| 					Position += offset; | ||||
| 					break; | ||||
| 				case SeekOrigin.End: | ||||
| 					Position = length - offset; | ||||
| 					break; | ||||
| 				default: | ||||
| 					throw new NotImplementedException($"Seeking from position '{origin}' is not implemented!"); | ||||
| 			} | ||||
| 
 | ||||
| 			return Position; | ||||
| 		} | ||||
| 
 | ||||
| 		public override void SetLength(long value) | ||||
| 		{ | ||||
| 			throw new NotSupportedException(); | ||||
| 		} | ||||
| 
 | ||||
| 		public override void Write(byte[] buffer, int offset, int count) | ||||
| 		{ | ||||
| 			throw new NotSupportedException(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										4
									
								
								SafeExamBrowser.Configuration/packages.config
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								SafeExamBrowser.Configuration/packages.config
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <packages> | ||||
|   <package id="System.ValueTuple" version="4.5.0" targetFramework="net452" /> | ||||
| </packages> | ||||
|  | @ -41,9 +41,9 @@ namespace SafeExamBrowser.Contracts.Configuration | |||
| 		void Register(IResourceLoader resourceLoader); | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Attempts to load settings from the specified resource, using the optional passwords. As long as the result is not | ||||
| 		/// Attempts to load settings from the specified resource, using the optional password. As long as the result is not | ||||
| 		/// <see cref="LoadStatus.Success"/>, the referenced settings may be <c>null</c> or in an undefinable state! | ||||
| 		/// </summary> | ||||
| 		LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); | ||||
| 		LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string password = null); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -21,9 +21,9 @@ namespace SafeExamBrowser.Contracts.Configuration | |||
| 		bool CanParse(Stream data); | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Tries to parse the given data, using the optional passwords. As long as the result is not <see cref="LoadStatus.Success"/>, | ||||
| 		/// Tries to parse the given data, using the optional password. As long as the result is not <see cref="LoadStatus.Success"/>, | ||||
| 		/// the referenced settings may be <c>null</c> or in an undefinable state! | ||||
| 		/// </summary> | ||||
| 		LoadStatus TryParse(Stream data, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); | ||||
| 		LoadStatus TryParse(Stream data, out Settings.Settings settings, string password = null); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -13,15 +13,10 @@ namespace SafeExamBrowser.Contracts.Configuration | |||
| 	/// </summary> | ||||
| 	public enum LoadStatus | ||||
| 	{ | ||||
| 		/// <summary> | ||||
| 		/// Indicates that an admin password is needed in order to load the settings. | ||||
| 		/// </summary> | ||||
| 		AdminPasswordNeeded = 1, | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Indicates that a resource does not comply with the declared data format. | ||||
| 		/// </summary> | ||||
| 		InvalidData, | ||||
| 		InvalidData = 1, | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Indicates that a resource needs to be loaded with the browser. | ||||
|  | @ -34,9 +29,9 @@ namespace SafeExamBrowser.Contracts.Configuration | |||
| 		NotSupported, | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// Indicates that a settings password is needed in order to load the settings. | ||||
| 		/// Indicates that a password is needed in order to load the settings. | ||||
| 		/// </summary> | ||||
| 		SettingsPasswordNeeded, | ||||
| 		PasswordNeeded, | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// The settings were loaded successfully. | ||||
|  |  | |||
|  | @ -60,14 +60,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			appConfig.ProgramDataFolder = location; | ||||
| 			appConfig.AppDataFolder = location; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			var resource = new Uri(url); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null), Times.Once); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -78,14 +78,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			appConfig.ProgramDataFolder = location; | ||||
| 			appConfig.AppDataFolder = $@"{location}\WRONG"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			var resource = new Uri(Path.Combine(location, "SettingsDummy.txt")); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null), Times.Once); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -95,14 +95,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 
 | ||||
| 			appConfig.AppDataFolder = location; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			var resource = new Uri(Path.Combine(location, "SettingsDummy.txt")); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null), Times.Once); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -127,7 +127,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		public void MustAbortIfWishedByUser() | ||||
| 		{ | ||||
| 			appConfig.ProgramDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations)); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -146,7 +146,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		[TestMethod] | ||||
| 		public void MustNotAbortIfNotWishedByUser() | ||||
| 		{ | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -166,7 +166,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		public void MustNotAllowToAbortIfNotInConfigureClientMode() | ||||
| 		{ | ||||
| 			settings.ConfigurationMode = ConfigurationMode.Exam; | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -218,7 +218,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		{ | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -231,7 +231,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 
 | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null), Times.Exactly(5)); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null), Times.Exactly(5)); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -239,7 +239,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		{ | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -252,7 +252,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 
 | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null), Times.Exactly(5)); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null), Times.Exactly(5)); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -261,8 +261,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			var password = "test"; | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, password, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, password)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -276,8 +276,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 
 | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, password, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, password), Times.Once); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -286,8 +286,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			var password = "test"; | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, password)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, password)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -301,8 +301,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 
 | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, password), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, password), Times.Once); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -310,7 +310,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		{ | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -331,7 +331,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 		{ | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -354,9 +354,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			var settingsPassword = "abc"; | ||||
| 			var url = @"http://www.safeexambrowser.org/whatever.seb"; | ||||
| 
 | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, settingsPassword)).Returns(LoadStatus.AdminPasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, adminPassword, settingsPassword)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.PasswordNeeded); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, settingsPassword)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); | ||||
| 			sut.ActionRequired += args => | ||||
|  | @ -370,9 +369,9 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 
 | ||||
| 			sut.Perform(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, settingsPassword), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, adminPassword, settingsPassword), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, settingsPassword), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, settingsPassword), Times.Once); | ||||
| 		} | ||||
| 
 | ||||
| 		[TestMethod] | ||||
|  | @ -382,13 +381,13 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			var resource = new Uri(Path.Combine(location, nameof(Operations), "SettingsDummy.txt")); | ||||
| 
 | ||||
| 			sessionContext.ReconfigurationFilePath = resource.LocalPath; | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 
 | ||||
| 			var result = sut.Repeat(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Once); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null), Times.Once); | ||||
| 
 | ||||
| 			Assert.AreEqual(OperationResult.Success, result); | ||||
| 		} | ||||
|  | @ -399,19 +398,19 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations | |||
| 			var resource = new Uri("file:///C:/does/not/exist.txt"); | ||||
| 
 | ||||
| 			sessionContext.ReconfigurationFilePath = null; | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null, null)).Returns(LoadStatus.Success); | ||||
| 			repository.Setup(r => r.TryLoadSettings(It.IsAny<Uri>(), out settings, null)).Returns(LoadStatus.Success); | ||||
| 
 | ||||
| 			sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); | ||||
| 
 | ||||
| 			var result = sut.Repeat(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Never); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null), Times.Never); | ||||
| 			Assert.AreEqual(OperationResult.Failed, result); | ||||
| 
 | ||||
| 			sessionContext.ReconfigurationFilePath = resource.LocalPath; | ||||
| 			result = sut.Repeat(); | ||||
| 
 | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Never); | ||||
| 			repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null), Times.Never); | ||||
| 			Assert.AreEqual(OperationResult.Failed, result); | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -73,9 +73,8 @@ | |||
|     <Reference Include="System.Threading.Tasks.Extensions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll</HintPath> | ||||
|     </Reference> | ||||
|     <Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll</HintPath> | ||||
|       <Private>True</Private> | ||||
|     <Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> | ||||
|       <HintPath>..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll</HintPath> | ||||
|     </Reference> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|       </dependentAssembly> | ||||
|       <dependentAssembly> | ||||
|         <assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" /> | ||||
|         <bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0" /> | ||||
|         <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" /> | ||||
|       </dependentAssembly> | ||||
|     </assemblyBinding> | ||||
|   </runtime> | ||||
|  |  | |||
|  | @ -5,5 +5,5 @@ | |||
|   <package id="MSTest.TestAdapter" version="1.2.0" targetFramework="net461" /> | ||||
|   <package id="MSTest.TestFramework" version="1.2.0" targetFramework="net461" /> | ||||
|   <package id="System.Threading.Tasks.Extensions" version="4.4.0" targetFramework="net461" /> | ||||
|   <package id="System.ValueTuple" version="4.4.0" targetFramework="net461" /> | ||||
|   <package id="System.ValueTuple" version="4.5.0" targetFramework="net461" /> | ||||
| </packages> | ||||
|  | @ -8,6 +8,7 @@ | |||
| 
 | ||||
| using System; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows; | ||||
| 
 | ||||
| namespace SafeExamBrowser.Runtime | ||||
|  | @ -60,6 +61,11 @@ namespace SafeExamBrowser.Runtime | |||
| 			instances.BuildObjectGraph(Shutdown); | ||||
| 			instances.LogStartupInformation(); | ||||
| 
 | ||||
| 			Task.Run(new Action(TryStart)); | ||||
| 		} | ||||
| 
 | ||||
| 		private void TryStart() | ||||
| 		{ | ||||
| 			var success = instances.RuntimeController.TryStart(); | ||||
| 
 | ||||
| 			if (!success) | ||||
|  | @ -70,18 +76,15 @@ namespace SafeExamBrowser.Runtime | |||
| 
 | ||||
| 		public new void Shutdown() | ||||
| 		{ | ||||
| 			void shutdown() | ||||
| 			{ | ||||
| 				instances.RuntimeController.Terminate(); | ||||
| 				instances.LogShutdownInformation(); | ||||
| 			Task.Run(new Action(ShutdownInternal)); | ||||
| 		} | ||||
| 
 | ||||
| 				// TODO: Which UI operation is being cancelled without the timeout? Is this only a debugger issue? Same problem with client? -> Debug! | ||||
| 				Thread.Sleep(20); | ||||
| 		private void ShutdownInternal() | ||||
| 		{ | ||||
| 			instances.RuntimeController.Terminate(); | ||||
| 			instances.LogShutdownInformation(); | ||||
| 
 | ||||
| 				base.Shutdown(); | ||||
| 			} | ||||
| 
 | ||||
| 			Dispatcher.BeginInvoke(new Action(shutdown)); | ||||
| 			Dispatcher.Invoke(base.Shutdown); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -159,34 +159,37 @@ namespace SafeExamBrowser.Runtime.Operations | |||
| 			var terminatedEvent = new AutoResetEvent(false); | ||||
| 			var terminatedEventHandler = new ProcessTerminatedEventHandler((_) => terminatedEvent.Set()); | ||||
| 
 | ||||
| 			runtimeHost.ClientDisconnected += disconnectedEventHandler; | ||||
| 			ClientProcess.Terminated += terminatedEventHandler; | ||||
| 
 | ||||
| 			logger.Info("Instructing client to initiate shutdown procedure."); | ||||
| 			ClientProxy.InitiateShutdown(); | ||||
| 
 | ||||
| 			logger.Info("Disconnecting from client communication host."); | ||||
| 			ClientProxy.Disconnect(); | ||||
| 
 | ||||
| 			logger.Info("Waiting for client to disconnect from runtime communication host..."); | ||||
| 			disconnected = disconnectedEvent.WaitOne(timeout_ms); | ||||
| 
 | ||||
| 			if (!disconnected) | ||||
| 			if (ClientProxy != null) | ||||
| 			{ | ||||
| 				logger.Error($"Client failed to disconnect within {timeout_ms / 1000} seconds!"); | ||||
| 				runtimeHost.ClientDisconnected += disconnectedEventHandler; | ||||
| 				ClientProcess.Terminated += terminatedEventHandler; | ||||
| 
 | ||||
| 				logger.Info("Instructing client to initiate shutdown procedure."); | ||||
| 				ClientProxy.InitiateShutdown(); | ||||
| 
 | ||||
| 				logger.Info("Disconnecting from client communication host."); | ||||
| 				ClientProxy.Disconnect(); | ||||
| 
 | ||||
| 				logger.Info("Waiting for client to disconnect from runtime communication host..."); | ||||
| 				disconnected = disconnectedEvent.WaitOne(timeout_ms); | ||||
| 
 | ||||
| 				if (!disconnected) | ||||
| 				{ | ||||
| 					logger.Error($"Client failed to disconnect within {timeout_ms / 1000} seconds!"); | ||||
| 				} | ||||
| 
 | ||||
| 				logger.Info("Waiting for client process to terminate..."); | ||||
| 				terminated = terminatedEvent.WaitOne(timeout_ms); | ||||
| 
 | ||||
| 				if (!terminated) | ||||
| 				{ | ||||
| 					logger.Error($"Client failed to terminate within {timeout_ms / 1000} seconds!"); | ||||
| 				} | ||||
| 
 | ||||
| 				runtimeHost.ClientDisconnected -= disconnectedEventHandler; | ||||
| 				ClientProcess.Terminated -= terminatedEventHandler; | ||||
| 			} | ||||
| 
 | ||||
| 			logger.Info("Waiting for client process to terminate..."); | ||||
| 			terminated = terminatedEvent.WaitOne(timeout_ms); | ||||
| 
 | ||||
| 			if (!terminated) | ||||
| 			{ | ||||
| 				logger.Error($"Client failed to terminate within {timeout_ms / 1000} seconds!"); | ||||
| 			} | ||||
| 
 | ||||
| 			runtimeHost.ClientDisconnected -= disconnectedEventHandler; | ||||
| 			ClientProcess.Terminated -= terminatedEventHandler; | ||||
| 
 | ||||
| 			if (disconnected && terminated) | ||||
| 			{ | ||||
| 				logger.Info("Client has been successfully terminated."); | ||||
|  |  | |||
|  | @ -99,33 +99,18 @@ namespace SafeExamBrowser.Runtime.Operations | |||
| 
 | ||||
| 		private OperationResult LoadSettings(Uri uri) | ||||
| 		{ | ||||
| 			var adminPassword = default(string); | ||||
| 			var settingsPassword = default(string); | ||||
| 			var settings = default(Settings); | ||||
| 			var status = default(LoadStatus); | ||||
| 			var status = configuration.TryLoadSettings(uri, out Settings settings); | ||||
| 
 | ||||
| 			for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;) | ||||
| 			for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++) | ||||
| 			{ | ||||
| 				status = configuration.TryLoadSettings(uri, out settings, adminPassword, settingsPassword); | ||||
| 				var result = TryGetPassword(status); | ||||
| 
 | ||||
| 				if (status == LoadStatus.AdminPasswordNeeded || status == LoadStatus.SettingsPasswordNeeded) | ||||
| 				if (!result.Success) | ||||
| 				{ | ||||
| 					var result = TryGetPassword(status); | ||||
| 
 | ||||
| 					if (!result.Success) | ||||
| 					{ | ||||
| 						return OperationResult.Aborted; | ||||
| 					} | ||||
| 
 | ||||
| 					adminAttempts += status == LoadStatus.AdminPasswordNeeded ? 1 : 0; | ||||
| 					adminPassword = status == LoadStatus.AdminPasswordNeeded ? result.Password : adminPassword; | ||||
| 					settingsAttempts += status == LoadStatus.SettingsPasswordNeeded ? 1 : 0; | ||||
| 					settingsPassword = status == LoadStatus.SettingsPasswordNeeded ? result.Password : settingsPassword; | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					break; | ||||
| 					return OperationResult.Aborted; | ||||
| 				} | ||||
| 
 | ||||
| 				status = configuration.TryLoadSettings(uri, out settings, result.Password); | ||||
| 			} | ||||
| 
 | ||||
| 			if (status == LoadStatus.Success) | ||||
|  | @ -158,7 +143,7 @@ namespace SafeExamBrowser.Runtime.Operations | |||
| 
 | ||||
| 		private PasswordRequiredEventArgs TryGetPassword(LoadStatus status) | ||||
| 		{ | ||||
| 			var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings; | ||||
| 			var purpose = PasswordRequestPurpose.Settings; | ||||
| 			var args = new PasswordRequiredEventArgs { Purpose = purpose }; | ||||
| 
 | ||||
| 			ActionRequired?.Invoke(args); | ||||
|  |  | |||
|  | @ -13,10 +13,10 @@ | |||
|             </ResourceDictionary.MergedDictionaries> | ||||
|         </ResourceDictionary> | ||||
|     </Window.Resources> | ||||
|     <Grid> | ||||
|     <Grid FocusManager.FocusedElement="{Binding ElementName=Password}"> | ||||
|         <Grid.RowDefinitions> | ||||
|             <RowDefinition Height="4*" /> | ||||
|             <RowDefinition Height="2*" /> | ||||
|             <RowDefinition Height="1*" /> | ||||
|         </Grid.RowDefinitions> | ||||
|         <Grid Grid.Row="0"> | ||||
|             <Grid> | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
|  */ | ||||
| 
 | ||||
| using System.Windows; | ||||
| using System.Windows.Input; | ||||
| using SafeExamBrowser.Contracts.I18n; | ||||
| using SafeExamBrowser.Contracts.UserInterface.Taskbar.Events; | ||||
| using SafeExamBrowser.Contracts.UserInterface.Windows; | ||||
|  | @ -72,6 +73,7 @@ namespace SafeExamBrowser.UserInterface.Classic | |||
| 			ConfirmButton.Click += ConfirmButton_Click; | ||||
| 
 | ||||
| 			Closing += (o, args) => closing?.Invoke(); | ||||
| 			Password.KeyUp += Password_KeyUp; | ||||
| 		} | ||||
| 
 | ||||
| 		private void CancelButton_Click(object sender, RoutedEventArgs e) | ||||
|  | @ -86,6 +88,15 @@ namespace SafeExamBrowser.UserInterface.Classic | |||
| 			Close(); | ||||
| 		} | ||||
| 
 | ||||
| 		private void Password_KeyUp(object sender, KeyEventArgs e) | ||||
| 		{ | ||||
| 			if (e.Key == Key.Enter) | ||||
| 			{ | ||||
| 				DialogResult = true; | ||||
| 				Close(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private class PasswordDialogResult : IPasswordDialogResult | ||||
| 		{ | ||||
| 			public string Password { get; set; } | ||||
|  |  | |||
|  | @ -141,6 +141,10 @@ namespace SafeExamBrowser.UserInterface.Classic | |||
| 			StatusTextBlock.DataContext = model; | ||||
| 
 | ||||
| 			Closing += (o, args) => args.Cancel = !allowClose; | ||||
| 
 | ||||
| #if DEBUG | ||||
| 			Topmost = false; | ||||
| #endif | ||||
| 		} | ||||
| 
 | ||||
| 		private void RuntimeWindow_Loaded(object sender, RoutedEventArgs e) | ||||
|  |  | |||
|  | @ -90,26 +90,7 @@ namespace SafeExamBrowser.UserInterface.Classic | |||
| 
 | ||||
| 		public IRuntimeWindow CreateRuntimeWindow(AppConfig appConfig) | ||||
| 		{ | ||||
| 			RuntimeWindow runtimeWindow = null; | ||||
| 			var windowReadyEvent = new AutoResetEvent(false); | ||||
| 			var runtimeWindowThread = new Thread(() => | ||||
| 			{ | ||||
| 				runtimeWindow = new RuntimeWindow(appConfig, text); | ||||
| 				runtimeWindow.Closed += (o, args) => runtimeWindow.Dispatcher.InvokeShutdown(); | ||||
| 
 | ||||
| 				windowReadyEvent.Set(); | ||||
| 
 | ||||
| 				System.Windows.Threading.Dispatcher.Run(); | ||||
| 			}); | ||||
| 
 | ||||
| 			runtimeWindowThread.SetApartmentState(ApartmentState.STA); | ||||
| 			runtimeWindowThread.Name = nameof(RuntimeWindow); | ||||
| 			runtimeWindowThread.IsBackground = true; | ||||
| 			runtimeWindowThread.Start(); | ||||
| 
 | ||||
| 			windowReadyEvent.WaitOne(); | ||||
| 
 | ||||
| 			return runtimeWindow; | ||||
| 			return Application.Current.Dispatcher.Invoke(() => new RuntimeWindow(appConfig, text)); | ||||
| 		} | ||||
| 
 | ||||
| 		public ISplashScreen CreateSplashScreen(AppConfig appConfig = null) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 dbuechel
						dbuechel