SEBWIN-221: Implemented basic certificate decryption.

This commit is contained in:
dbuechel 2018-11-29 15:00:10 +01:00
parent 243d32879e
commit 483ba7ad4a
5 changed files with 214 additions and 34 deletions

View file

@ -81,8 +81,6 @@ namespace SafeExamBrowser.Configuration.Compression
var cm = data.ReadByte(); var cm = data.ReadByte();
var compressed = id1 == ID1 && id2 == ID2 && cm == CM; var compressed = id1 == ID1 && id2 == ID2 && cm == CM;
logger.Debug($"'{data}' is {(compressed ? string.Empty : "not ")}a gzip-compressed stream.");
return compressed; return compressed;
} }
@ -100,7 +98,6 @@ namespace SafeExamBrowser.Configuration.Compression
{ {
var stream = new GZipStream(data, CompressionMode.Decompress); var stream = new GZipStream(data, CompressionMode.Decompress);
logger.Debug($"Peeking {count} bytes from '{data}'...");
data.Seek(0, SeekOrigin.Begin); data.Seek(0, SeekOrigin.Begin);
using (var decompressed = new MemoryStream()) using (var decompressed = new MemoryStream())

View file

@ -95,16 +95,15 @@ namespace SafeExamBrowser.Configuration.DataFormats
private LoadStatus ParsePasswordBlock(Stream data, FormatType format, out Settings settings, string password, bool passwordIsHash) 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); settings = default(Settings);
// TODO: Check whether the hashing (bool passwordIsHash) can be extracted and moved to ConfigurationOperation!
if (format == FormatType.PasswordConfigureClient && !passwordIsHash) if (format == FormatType.PasswordConfigureClient && !passwordIsHash)
{ {
password = hashAlgorithm.GenerateHashFor(password); password = hashAlgorithm.GenerateHashFor(password);
} }
var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption)));
var status = encryption.Decrypt(data, out Stream decrypted, password); var status = encryption.Decrypt(data, out Stream decrypted, password);
if (status == LoadStatus.Success) if (status == LoadStatus.Success)
@ -129,12 +128,34 @@ namespace SafeExamBrowser.Configuration.DataFormats
private LoadStatus ParsePublicKeyHashBlock(Stream data, out Settings settings, string password, bool passwordIsHash) 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) 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) private string ParsePrefix(Stream data)

View file

@ -6,7 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -32,11 +31,6 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
this.logger = logger; this.logger = logger;
} }
internal Stream Encrypt(Stream data, string password)
{
throw new NotImplementedException();
}
internal LoadStatus Decrypt(Stream data, out Stream decrypted, string password) internal LoadStatus Decrypt(Stream data, out Stream decrypted, string password)
{ {
decrypted = default(Stream); decrypted = default(Stream);
@ -50,9 +44,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
if (version != VERSION || options != OPTIONS) if (version != VERSION || options != OPTIONS)
{ {
logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); return FailForInvalidHeader(version, options);
return LoadStatus.InvalidData;
} }
var (authenticationKey, encryptionKey) = GenerateKeys(data, password); var (authenticationKey, encryptionKey) = GenerateKeys(data, password);
@ -60,9 +52,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
if (!computedHmac.SequenceEqual(originalHmac)) if (!computedHmac.SequenceEqual(originalHmac))
{ {
logger.Warn($"The authentication failed due to an invalid password or corrupted data!"); return FailForInvalidHmac();
return LoadStatus.PasswordNeeded;
} }
decrypted = Decrypt(data, encryptionKey, originalHmac.Length); decrypted = Decrypt(data, encryptionKey, originalHmac.Length);
@ -73,6 +63,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
private (int version, int options) ParseHeader(Stream data) private (int version, int options) ParseHeader(Stream data)
{ {
data.Seek(0, SeekOrigin.Begin); data.Seek(0, SeekOrigin.Begin);
logger.Debug("Parsing encryption header...");
var version = data.ReadByte(); var version = data.ReadByte();
var options = data.ReadByte(); var options = data.ReadByte();
@ -80,11 +71,20 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
return (version, options); return (version, options);
} }
private LoadStatus FailForInvalidHeader(int version, int options)
{
logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]");
return LoadStatus.InvalidData;
}
private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password) private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password)
{ {
var authenticationSalt = new byte[SALT_SIZE]; var authenticationSalt = new byte[SALT_SIZE];
var encryptionSalt = new byte[SALT_SIZE]; var encryptionSalt = new byte[SALT_SIZE];
logger.Debug("Generating keys for authentication and decryption...");
data.Seek(HEADER_SIZE, SeekOrigin.Begin); data.Seek(HEADER_SIZE, SeekOrigin.Begin);
data.Read(encryptionSalt, 0, SALT_SIZE); data.Read(encryptionSalt, 0, SALT_SIZE);
data.Read(authenticationSalt, 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) 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 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.Seek(originalHmac.Length, SeekOrigin.End);
data.Read(originalHmac, 0, originalHmac.Length); 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) private Stream Decrypt(Stream data, byte[] encryptionKey, int hmacLength)
{ {
var initializationVector = new byte[BLOCK_SIZE]; var initializationVector = new byte[BLOCK_SIZE];
@ -124,8 +133,10 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
var decryptedData = new MemoryStream(); var decryptedData = new MemoryStream();
var encryptedData = new SubStream(data, data.Position, data.Length - data.Position - hmacLength); 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 }) logger.Debug("Decrypting data...");
using (var decryptor = aes.CreateDecryptor(encryptionKey, initializationVector))
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)) using (var cryptoStream = new CryptoStream(encryptedData, decryptor, CryptoStreamMode.Read))
{ {
cryptoStream.CopyTo(decryptedData); cryptoStream.CopyTo(decryptedData);

View file

@ -6,21 +6,130 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
using System;
using System.IO; using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.DataFormats.Cryptography namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
{ {
internal class PublicKeyHashEncryption 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;
} }
} }
} }

View file

@ -8,19 +8,61 @@
using System; using System;
using System.IO; using System.IO;
using System.Security.Cryptography.X509Certificates;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.DataFormats.Cryptography 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;
} }
} }
} }