diff --git a/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs b/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs index e10d542d..cd762fc3 100644 --- a/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs +++ b/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs @@ -81,8 +81,6 @@ namespace SafeExamBrowser.Configuration.Compression var cm = data.ReadByte(); var compressed = id1 == ID1 && id2 == ID2 && cm == CM; - logger.Debug($"'{data}' is {(compressed ? string.Empty : "not ")}a gzip-compressed stream."); - return compressed; } @@ -100,7 +98,6 @@ namespace SafeExamBrowser.Configuration.Compression { var stream = new GZipStream(data, CompressionMode.Decompress); - logger.Debug($"Peeking {count} bytes from '{data}'..."); data.Seek(0, SeekOrigin.Begin); using (var decompressed = new MemoryStream()) diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs index ee01e132..006b12d9 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs @@ -95,16 +95,15 @@ namespace SafeExamBrowser.Configuration.DataFormats private LoadStatus ParsePasswordBlock(Stream data, FormatType format, out Settings settings, string password, bool passwordIsHash) { - var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); settings = default(Settings); - // TODO: Check whether the hashing (bool passwordIsHash) can be extracted and moved to ConfigurationOperation! if (format == FormatType.PasswordConfigureClient && !passwordIsHash) { password = hashAlgorithm.GenerateHashFor(password); } + var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); var status = encryption.Decrypt(data, out Stream decrypted, password); if (status == LoadStatus.Success) @@ -129,12 +128,34 @@ namespace SafeExamBrowser.Configuration.DataFormats private LoadStatus ParsePublicKeyHashBlock(Stream data, out Settings settings, string password, bool passwordIsHash) { - throw new NotImplementedException(); + var encryption = new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); + var status = encryption.Decrypt(data, out Stream decrypted); + + settings = default(Settings); + + if (status == LoadStatus.Success) + { + return TryParse(decrypted, out settings, password, passwordIsHash); + } + + return status; } private LoadStatus ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, out Settings settings, string password, bool passwordIsHash) { - throw new NotImplementedException(); + var logger = this.logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)); + var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption))); + var encryption = new PublicKeyHashWithSymmetricKeyEncryption(logger, passwordEncryption); + var status = encryption.Decrypt(data, out Stream decrypted); + + settings = default(Settings); + + if (status == LoadStatus.Success) + { + return TryParse(decrypted, out settings, password, passwordIsHash); + } + + return status; } private string ParsePrefix(Stream data) diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs index b8dedd08..12876d38 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs +++ b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PasswordEncryption.cs @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -32,11 +31,6 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography this.logger = logger; } - internal Stream Encrypt(Stream data, string password) - { - throw new NotImplementedException(); - } - internal LoadStatus Decrypt(Stream data, out Stream decrypted, string password) { decrypted = default(Stream); @@ -50,9 +44,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography if (version != VERSION || options != OPTIONS) { - logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); - - return LoadStatus.InvalidData; + return FailForInvalidHeader(version, options); } var (authenticationKey, encryptionKey) = GenerateKeys(data, password); @@ -60,9 +52,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography if (!computedHmac.SequenceEqual(originalHmac)) { - logger.Warn($"The authentication failed due to an invalid password or corrupted data!"); - - return LoadStatus.PasswordNeeded; + return FailForInvalidHmac(); } decrypted = Decrypt(data, encryptionKey, originalHmac.Length); @@ -73,6 +63,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography private (int version, int options) ParseHeader(Stream data) { data.Seek(0, SeekOrigin.Begin); + logger.Debug("Parsing encryption header..."); var version = data.ReadByte(); var options = data.ReadByte(); @@ -80,11 +71,20 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography return (version, options); } + private LoadStatus FailForInvalidHeader(int version, int options) + { + logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); + + return LoadStatus.InvalidData; + } + private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password) { var authenticationSalt = new byte[SALT_SIZE]; var encryptionSalt = new byte[SALT_SIZE]; + logger.Debug("Generating keys for authentication and decryption..."); + data.Seek(HEADER_SIZE, SeekOrigin.Begin); data.Read(encryptionSalt, 0, SALT_SIZE); data.Read(authenticationSalt, 0, SALT_SIZE); @@ -101,11 +101,13 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography private (byte[] originalHmac, byte[] computedHmac) GenerateHmac(Stream data, byte[] authenticationKey) { - using (var hmac = new HMACSHA256(authenticationKey)) + logger.Debug("Generating HMACs for authentication..."); + + using (var algorithm = new HMACSHA256(authenticationKey)) { - var originalHmac = new byte[hmac.HashSize / 8]; + var originalHmac = new byte[algorithm.HashSize / 8]; var hashStream = new SubStream(data, 0, data.Length - originalHmac.Length); - var computedHmac = hmac.ComputeHash(hashStream); + var computedHmac = algorithm.ComputeHash(hashStream); data.Seek(originalHmac.Length, SeekOrigin.End); data.Read(originalHmac, 0, originalHmac.Length); @@ -114,6 +116,13 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography } } + private LoadStatus FailForInvalidHmac() + { + 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]; @@ -124,8 +133,10 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography 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)) + logger.Debug("Decrypting data..."); + + using (var algorithm = new AesManaged { KeySize = KEY_SIZE * 8, BlockSize = BLOCK_SIZE * 8, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }) + using (var decryptor = algorithm.CreateDecryptor(encryptionKey, initializationVector)) using (var cryptoStream = new CryptoStream(encryptedData, decryptor, CryptoStreamMode.Read)) { cryptoStream.CopyTo(decryptedData); diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs index 7ea90761..7a04cd5b 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs +++ b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashEncryption.cs @@ -6,21 +6,130 @@ * 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.Security.Cryptography.X509Certificates; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats.Cryptography { internal class PublicKeyHashEncryption { - internal Stream Encrypt(Stream data, string password) + protected const int PUBLIC_KEY_HASH_SIZE = 20; + + protected ILogger logger; + + internal PublicKeyHashEncryption(ILogger logger) { - throw new NotImplementedException(); + this.logger = logger; } - internal Stream Decrypt(Stream data, string password) + internal virtual LoadStatus Decrypt(Stream data, out Stream decrypted) { - throw new NotImplementedException(); + var keyHash = ParsePublicKeyHash(data); + var found = TryGetCertificateWith(keyHash, out X509Certificate2 certificate); + + decrypted = default(Stream); + + if (!found) + { + return FailForMissingCertificate(); + } + + decrypted = Decrypt(data, PUBLIC_KEY_HASH_SIZE, certificate); + + return LoadStatus.Success; + } + + protected byte[] ParsePublicKeyHash(Stream data) + { + var keyHash = new byte[PUBLIC_KEY_HASH_SIZE]; + + logger.Debug("Parsing public key hash..."); + + data.Seek(0, SeekOrigin.Begin); + data.Read(keyHash, 0, keyHash.Length); + + return keyHash; + } + + protected bool TryGetCertificateWith(byte[] keyHash, out X509Certificate2 certificate) + { + var storesToSearch = new[] + { + new X509Store(StoreLocation.CurrentUser), + new X509Store(StoreLocation.LocalMachine), + new X509Store(StoreName.TrustedPeople) + }; + + certificate = default(X509Certificate2); + logger.Debug("Searching certificate for decryption..."); + + using (var algorithm = new SHA1CryptoServiceProvider()) + { + foreach (var store in storesToSearch) + { + store.Open(OpenFlags.ReadOnly); + + foreach (var current in store.Certificates) + { + var publicKey = current.PublicKey.EncodedKeyValue.RawData; + var publicKeyHash = algorithm.ComputeHash(publicKey); + + if (publicKeyHash.SequenceEqual(keyHash)) + { + certificate = current; + store.Close(); + + return true; + } + } + + store.Close(); + } + } + + return false; + } + + protected LoadStatus FailForMissingCertificate() + { + logger.Error($"Could not find certificate which matches the given public key hash!"); + + return LoadStatus.InvalidData; + } + + protected MemoryStream Decrypt(Stream data, long offset, X509Certificate2 certificate) + { + var algorithm = certificate.PrivateKey as RSACryptoServiceProvider; + var blockSize = algorithm.KeySize / 8; + var blockCount = (data.Length - offset) / blockSize; + var remainingBytes = data.Length - offset - (blockSize * blockCount); + var encrypted = new byte[blockSize]; + var decrypted = default(byte[]); + var decryptedData = new MemoryStream(); + + data.Seek(offset, SeekOrigin.Begin); + logger.Debug("Decrypting data..."); + + for (int i = 0; i < blockCount; i++) + { + data.Read(encrypted, 0, encrypted.Length); + decrypted = algorithm.Decrypt(encrypted, false); + decryptedData.Write(decrypted, 0, decrypted.Length); + } + + if (remainingBytes > 0) + { + encrypted = new byte[remainingBytes]; + data.Read(encrypted, 0, encrypted.Length); + decrypted = algorithm.Decrypt(encrypted, false); + decryptedData.Write(decrypted, 0, decrypted.Length); + } + + return decryptedData; } } } diff --git a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs index d7be15ae..678aa5fd 100644 --- a/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs +++ b/SafeExamBrowser.Configuration/DataFormats/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs @@ -8,19 +8,61 @@ using System; using System.IO; +using System.Security.Cryptography.X509Certificates; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats.Cryptography { - internal class PublicKeyHashWithSymmetricKeyEncryption + internal class PublicKeyHashWithSymmetricKeyEncryption : PublicKeyHashEncryption { - internal Stream Encrypt(Stream data, string password) + private const int KEY_LENGTH_SIZE = 4; + + private PasswordEncryption passwordEncryption; + + internal PublicKeyHashWithSymmetricKeyEncryption(ILogger logger, PasswordEncryption passwordEncryption) : base(logger) { - throw new NotImplementedException(); + this.passwordEncryption = passwordEncryption; } - internal Stream Decrypt(Stream data, string password) + internal override LoadStatus Decrypt(Stream data, out Stream decrypted) { - throw new NotImplementedException(); + var keyHash = ParsePublicKeyHash(data); + var found = TryGetCertificateWith(keyHash, out X509Certificate2 certificate); + + decrypted = default(Stream); + + if (!found) + { + return FailForMissingCertificate(); + } + + var symmetricKey = ParseSymmetricKey(data, certificate); + var stream = new SubStream(data, data.Position, data.Length - data.Position); + var status = passwordEncryption.Decrypt(stream, out decrypted, symmetricKey); + + return status; + } + + private string ParseSymmetricKey(Stream data, X509Certificate2 certificate) + { + var keyLengthData = new byte[KEY_LENGTH_SIZE]; + + logger.Debug("Parsing symmetric key..."); + + data.Seek(PUBLIC_KEY_HASH_SIZE, SeekOrigin.Begin); + data.Read(keyLengthData, 0, keyLengthData.Length); + + var keyLength = BitConverter.ToInt32(keyLengthData, 0); + var encryptedKey = new byte[keyLength]; + + data.Read(encryptedKey, 0, encryptedKey.Length); + + var stream = new SubStream(data, PUBLIC_KEY_HASH_SIZE + KEY_LENGTH_SIZE, keyLength); + var decryptedKey = Decrypt(stream, 0, certificate); + var symmetricKey = Convert.ToBase64String(decryptedKey.ToArray()); + + return symmetricKey; } } }