Restore SEBPatch
This commit is contained in:
122
SafeExamBrowser.Configuration/Cryptography/CertificateStore.cs
Normal file
122
SafeExamBrowser.Configuration/Cryptography/CertificateStore.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using SafeExamBrowser.Configuration.ConfigurationData;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.Cryptography
|
||||
{
|
||||
public class CertificateStore : ICertificateStore
|
||||
{
|
||||
private ILogger logger;
|
||||
|
||||
private readonly X509Store[] stores = new[]
|
||||
{
|
||||
new X509Store(StoreLocation.CurrentUser),
|
||||
new X509Store(StoreLocation.LocalMachine),
|
||||
new X509Store(StoreName.TrustedPeople)
|
||||
};
|
||||
|
||||
public CertificateStore(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public bool TryGetCertificateWith(byte[] keyHash, out X509Certificate2 certificate)
|
||||
{
|
||||
certificate = default(X509Certificate2);
|
||||
|
||||
using (var algorithm = new SHA1CryptoServiceProvider())
|
||||
{
|
||||
foreach (var store in stores)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
store.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ExtractAndImportIdentities(IDictionary<string, object> data)
|
||||
{
|
||||
const int IDENTITY_CERTIFICATE = 1;
|
||||
var hasCertificates = data.TryGetValue(Keys.Network.Certificates.EmbeddedCertificates, out var value);
|
||||
|
||||
if (hasCertificates && value is IList<IDictionary<string, object>> certificates)
|
||||
{
|
||||
var toRemove = new List<IDictionary<string, object>>();
|
||||
|
||||
foreach (var certificate in certificates)
|
||||
{
|
||||
var hasData = certificate.TryGetValue(Keys.Network.Certificates.CertificateData, out var dataValue);
|
||||
var hasType = certificate.TryGetValue(Keys.Network.Certificates.CertificateType, out var typeValue);
|
||||
var isIdentity = typeValue is int type && type == IDENTITY_CERTIFICATE;
|
||||
|
||||
if (hasData && hasType && isIdentity && dataValue is byte[] certificateData)
|
||||
{
|
||||
ImportIdentityCertificate(certificateData, new X509Store(StoreLocation.CurrentUser));
|
||||
ImportIdentityCertificate(certificateData, new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine));
|
||||
|
||||
toRemove.Add(certificate);
|
||||
}
|
||||
}
|
||||
|
||||
toRemove.ForEach(c => certificates.Remove(c));
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportIdentityCertificate(byte[] certificateData, X509Store store)
|
||||
{
|
||||
try
|
||||
{
|
||||
var certificate = new X509Certificate2();
|
||||
|
||||
certificate.Import(certificateData, "Di𝈭l𝈖Ch𝈒aht𝈁aHai1972", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet);
|
||||
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
store.Add(certificate);
|
||||
|
||||
logger.Info($"Successfully imported identity certificate into {store.Location}.{store.Name}.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to import identity certificate into {store.Location}.{store.Name}!", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
store.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
SafeExamBrowser.Configuration/Cryptography/HashAlgorithm.cs
Normal file
31
SafeExamBrowser.Configuration/Cryptography/HashAlgorithm.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.Cryptography
|
||||
{
|
||||
public class HashAlgorithm : IHashAlgorithm
|
||||
{
|
||||
public string GenerateHashFor(string password)
|
||||
{
|
||||
using (var algorithm = new SHA256Managed())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(password);
|
||||
var hash = algorithm.ComputeHash(bytes);
|
||||
var hashString = String.Join(String.Empty, hash.Select(b => b.ToString("x2")));
|
||||
|
||||
return hashString;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
SafeExamBrowser.Configuration/Cryptography/KeyGenerator.cs
Normal file
129
SafeExamBrowser.Configuration/Cryptography/KeyGenerator.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Configuration.Contracts.Integrity;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.Cryptography
|
||||
{
|
||||
public class KeyGenerator : IKeyGenerator
|
||||
{
|
||||
private readonly object @lock = new object();
|
||||
|
||||
private readonly ThreadLocal<SHA256Managed> algorithm;
|
||||
private readonly AppConfig appConfig;
|
||||
private readonly IIntegrityModule integrityModule;
|
||||
private readonly ILogger logger;
|
||||
|
||||
private string browserExamKey;
|
||||
|
||||
public KeyGenerator(AppConfig appConfig, IIntegrityModule integrityModule, ILogger logger)
|
||||
{
|
||||
this.algorithm = new ThreadLocal<SHA256Managed>(() => new SHA256Managed());
|
||||
this.appConfig = appConfig;
|
||||
this.integrityModule = integrityModule;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public string CalculateAppSignatureKey(string connectionToken, string salt)
|
||||
{
|
||||
if (integrityModule.TryCalculateAppSignatureKey(connectionToken, salt, out var appSignatureKey))
|
||||
{
|
||||
logger.Debug("Successfully calculated app signature key using integrity module.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to calculate app signature key using integrity module!");
|
||||
}
|
||||
|
||||
return appSignatureKey;
|
||||
}
|
||||
|
||||
public string CalculateBrowserExamKeyHash(string configurationKey, byte[] salt, string url)
|
||||
{
|
||||
var urlWithoutFragment = url.Split('#')[0];
|
||||
var hash = algorithm.Value.ComputeHash(Encoding.UTF8.GetBytes(urlWithoutFragment + (browserExamKey ?? ComputeBrowserExamKey(configurationKey, salt))));
|
||||
var key = ToString(hash);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public string CalculateConfigurationKeyHash(string configurationKey, string url)
|
||||
{
|
||||
var urlWithoutFragment = url.Split('#')[0];
|
||||
var hash = algorithm.Value.ComputeHash(Encoding.UTF8.GetBytes(urlWithoutFragment + configurationKey));
|
||||
var key = ToString(hash);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public void UseCustomBrowserExamKey(string browserExamKey)
|
||||
{
|
||||
if (browserExamKey != default)
|
||||
{
|
||||
this.browserExamKey = browserExamKey;
|
||||
logger.Debug("Initialized custom browser exam key.");
|
||||
}
|
||||
}
|
||||
|
||||
private string ComputeBrowserExamKey(string configurationKey, byte[] salt)
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
if (browserExamKey == default)
|
||||
{
|
||||
logger.Debug("Initializing browser exam key...");
|
||||
|
||||
if (configurationKey == default)
|
||||
{
|
||||
configurationKey = "";
|
||||
logger.Warn("The current configuration does not contain a value for the configuration key!");
|
||||
}
|
||||
|
||||
if (salt == default || salt.Length == 0)
|
||||
{
|
||||
salt = new byte[0];
|
||||
logger.Warn("The current configuration does not contain a salt value for the browser exam key!");
|
||||
}
|
||||
|
||||
if (integrityModule.TryCalculateBrowserExamKey(configurationKey, ToString(salt), out browserExamKey))
|
||||
{
|
||||
logger.Debug("Successfully calculated browser exam key using integrity module.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("Failed to calculate browser exam key using integrity module! Falling back to simplified calculation...");
|
||||
|
||||
using (var algorithm = new HMACSHA256(salt))
|
||||
{
|
||||
var hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(appConfig.CodeSignatureHash + appConfig.ProgramBuildVersion + configurationKey));
|
||||
var key = ToString(hash);
|
||||
|
||||
browserExamKey = key;
|
||||
}
|
||||
|
||||
logger.Debug("Successfully calculated browser exam key using simplified calculation.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return browserExamKey;
|
||||
}
|
||||
|
||||
private string ToString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes).ToLower().Replace("-", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
223
SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs
Normal file
223
SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.Cryptography
|
||||
{
|
||||
public class PasswordEncryption : IPasswordEncryption
|
||||
{
|
||||
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 ILogger logger;
|
||||
|
||||
public PasswordEncryption(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public LoadStatus Decrypt(Stream data, string password, out Stream decrypted)
|
||||
{
|
||||
decrypted = default(Stream);
|
||||
|
||||
if (password == null)
|
||||
{
|
||||
return LoadStatus.PasswordNeeded;
|
||||
}
|
||||
|
||||
var (version, options) = ParseHeader(data);
|
||||
var (authenticationKey, encryptionKey) = GenerateKeysForDecryption(data, password);
|
||||
var (originalHmac, computedHmac) = GenerateHmacForDecryption(authenticationKey, data);
|
||||
|
||||
if (!computedHmac.SequenceEqual(originalHmac))
|
||||
{
|
||||
return FailForInvalidHmac();
|
||||
}
|
||||
|
||||
decrypted = Decrypt(data, encryptionKey, originalHmac.Length);
|
||||
|
||||
return LoadStatus.Success;
|
||||
}
|
||||
|
||||
public SaveStatus Encrypt(Stream data, string password, out Stream encrypted)
|
||||
{
|
||||
var (authKey, authSalt, encrKey, encrSalt) = GenerateKeysForEncryption(password);
|
||||
|
||||
encrypted = Encrypt(data, encrKey, out var initVector);
|
||||
encrypted = WriteEncryptionParameters(authKey, authSalt, encrSalt, initVector, encrypted);
|
||||
|
||||
return SaveStatus.Success;
|
||||
}
|
||||
|
||||
private (int version, int options) ParseHeader(Stream data)
|
||||
{
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
logger.Debug("Parsing encryption header...");
|
||||
|
||||
var version = data.ReadByte();
|
||||
var options = data.ReadByte();
|
||||
|
||||
if (version != VERSION || options != OPTIONS)
|
||||
{
|
||||
logger.Debug($"Unknown encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]");
|
||||
}
|
||||
|
||||
return (version, options);
|
||||
}
|
||||
|
||||
private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeysForDecryption(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);
|
||||
|
||||
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[] authKey, byte[] authSalt, byte[] encrKey, byte[] encrSalt) GenerateKeysForEncryption(string password)
|
||||
{
|
||||
logger.Debug("Generating keys for authentication and encryption...");
|
||||
|
||||
using (var authenticationGenerator = new Rfc2898DeriveBytes(password, SALT_SIZE, ITERATIONS))
|
||||
using (var encryptionGenerator = new Rfc2898DeriveBytes(password, SALT_SIZE, ITERATIONS))
|
||||
{
|
||||
var authenticationSalt = authenticationGenerator.Salt;
|
||||
var authenticationKey = authenticationGenerator.GetBytes(KEY_SIZE);
|
||||
var encryptionSalt = encryptionGenerator.Salt;
|
||||
var encryptionKey = encryptionGenerator.GetBytes(KEY_SIZE);
|
||||
|
||||
return (authenticationKey, authenticationSalt, encryptionKey, encryptionSalt);
|
||||
}
|
||||
}
|
||||
|
||||
private (byte[] originalHmac, byte[] computedHmac) GenerateHmacForDecryption(byte[] authenticationKey, Stream data)
|
||||
{
|
||||
logger.Debug("Generating HMACs for authentication...");
|
||||
|
||||
using (var algorithm = new HMACSHA256(authenticationKey))
|
||||
{
|
||||
var originalHmac = new byte[algorithm.HashSize / 8];
|
||||
var hashStream = new SubStream(data, 0, data.Length - originalHmac.Length);
|
||||
var computedHmac = algorithm.ComputeHash(hashStream);
|
||||
|
||||
data.Seek(-originalHmac.Length, SeekOrigin.End);
|
||||
data.Read(originalHmac, 0, originalHmac.Length);
|
||||
|
||||
return (originalHmac, computedHmac);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GenerateHmacForEncryption(byte[] authenticationKey, Stream data)
|
||||
{
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
logger.Debug("Generating HMAC for authentication...");
|
||||
|
||||
using (var algorithm = new HMACSHA256(authenticationKey))
|
||||
{
|
||||
return algorithm.ComputeHash(data);
|
||||
}
|
||||
}
|
||||
|
||||
private LoadStatus FailForInvalidHmac()
|
||||
{
|
||||
logger.Debug($"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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return decryptedData;
|
||||
}
|
||||
|
||||
private Stream Encrypt(Stream data, byte[] encryptionKey, out byte[] initializationVector)
|
||||
{
|
||||
var encryptedData = new MemoryStream();
|
||||
|
||||
logger.Debug("Encrypting data...");
|
||||
|
||||
using (var algorithm = new AesManaged { KeySize = KEY_SIZE * 8, BlockSize = BLOCK_SIZE * 8, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 })
|
||||
{
|
||||
algorithm.GenerateIV();
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
initializationVector = algorithm.IV;
|
||||
|
||||
using (var encryptor = algorithm.CreateEncryptor(encryptionKey, initializationVector))
|
||||
using (var cryptoStream = new CryptoStream(data, encryptor, CryptoStreamMode.Read))
|
||||
{
|
||||
cryptoStream.CopyTo(encryptedData);
|
||||
}
|
||||
|
||||
return encryptedData;
|
||||
}
|
||||
}
|
||||
|
||||
private Stream WriteEncryptionParameters(byte[] authKey, byte[] authSalt, byte[] encrSalt, byte[] initVector, Stream encryptedData)
|
||||
{
|
||||
var data = new MemoryStream();
|
||||
var header = new byte[] { VERSION, OPTIONS };
|
||||
|
||||
logger.Debug("Writing encryption parameters...");
|
||||
|
||||
data.Write(header, 0, header.Length);
|
||||
data.Write(encrSalt, 0, encrSalt.Length);
|
||||
data.Write(authSalt, 0, authSalt.Length);
|
||||
data.Write(initVector, 0, initVector.Length);
|
||||
|
||||
encryptedData.Seek(0, SeekOrigin.Begin);
|
||||
encryptedData.CopyTo(data);
|
||||
|
||||
var hmac = GenerateHmacForEncryption(authKey, data);
|
||||
|
||||
data.Seek(0, SeekOrigin.End);
|
||||
data.Write(hmac, 0, hmac.Length);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.Cryptography
|
||||
{
|
||||
public class PublicKeyEncryption : IPublicKeyEncryption
|
||||
{
|
||||
protected const int PUBLIC_KEY_HASH_SIZE = 20;
|
||||
|
||||
protected ICertificateStore store;
|
||||
protected ILogger logger;
|
||||
|
||||
public PublicKeyEncryption(ICertificateStore store, ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public virtual LoadStatus Decrypt(Stream data, out Stream decryptedData, out X509Certificate2 certificate)
|
||||
{
|
||||
var publicKeyHash = ParsePublicKeyHash(data);
|
||||
var found = store.TryGetCertificateWith(publicKeyHash, out certificate);
|
||||
|
||||
decryptedData = default(Stream);
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return FailForMissingCertificate();
|
||||
}
|
||||
|
||||
decryptedData = Decrypt(data, PUBLIC_KEY_HASH_SIZE, certificate);
|
||||
|
||||
return LoadStatus.Success;
|
||||
}
|
||||
|
||||
public virtual SaveStatus Encrypt(Stream data, X509Certificate2 certificate, out Stream encryptedData)
|
||||
{
|
||||
var publicKeyHash = GeneratePublicKeyHash(certificate);
|
||||
|
||||
encryptedData = Encrypt(data, certificate);
|
||||
encryptedData = WriteEncryptionParameters(encryptedData, publicKeyHash);
|
||||
|
||||
return SaveStatus.Success;
|
||||
}
|
||||
|
||||
protected LoadStatus FailForMissingCertificate()
|
||||
{
|
||||
logger.Error($"Could not find certificate which matches the given public key hash!");
|
||||
|
||||
return LoadStatus.InvalidData;
|
||||
}
|
||||
|
||||
protected byte[] GeneratePublicKeyHash(X509Certificate2 certificate)
|
||||
{
|
||||
var publicKey = certificate.PublicKey.EncodedKeyValue.RawData;
|
||||
|
||||
using (var sha = new SHA1CryptoServiceProvider())
|
||||
{
|
||||
return sha.ComputeHash(publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 decrypted = new MemoryStream();
|
||||
var decryptedBuffer = new byte[blockSize];
|
||||
var encryptedBuffer = new byte[blockSize];
|
||||
var remainingBytes = data.Length - offset - (blockSize * blockCount);
|
||||
|
||||
data.Seek(offset, SeekOrigin.Begin);
|
||||
logger.Debug("Decrypting data...");
|
||||
|
||||
using (algorithm)
|
||||
{
|
||||
for (var block = 0; block < blockCount; block++)
|
||||
{
|
||||
data.Read(encryptedBuffer, 0, encryptedBuffer.Length);
|
||||
decryptedBuffer = algorithm.Decrypt(encryptedBuffer, false);
|
||||
decrypted.Write(decryptedBuffer, 0, decryptedBuffer.Length);
|
||||
}
|
||||
|
||||
if (remainingBytes > 0)
|
||||
{
|
||||
encryptedBuffer = new byte[remainingBytes];
|
||||
data.Read(encryptedBuffer, 0, encryptedBuffer.Length);
|
||||
decryptedBuffer = algorithm.Decrypt(encryptedBuffer, false);
|
||||
decrypted.Write(decryptedBuffer, 0, decryptedBuffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
protected Stream Encrypt(Stream data, X509Certificate2 certificate)
|
||||
{
|
||||
var algorithm = certificate.PublicKey.Key as RSACryptoServiceProvider;
|
||||
var blockSize = (algorithm.KeySize / 8) - 32;
|
||||
var blockCount = data.Length / blockSize;
|
||||
var decryptedBuffer = new byte[blockSize];
|
||||
var encrypted = new MemoryStream();
|
||||
var encryptedBuffer = new byte[blockSize];
|
||||
var remainingBytes = data.Length - (blockCount * blockSize);
|
||||
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
logger.Debug("Encrypting data...");
|
||||
|
||||
using (algorithm)
|
||||
{
|
||||
for (var block = 0; block < blockCount; block++)
|
||||
{
|
||||
data.Read(decryptedBuffer, 0, decryptedBuffer.Length);
|
||||
encryptedBuffer = algorithm.Encrypt(decryptedBuffer, false);
|
||||
encrypted.Write(encryptedBuffer, 0, encryptedBuffer.Length);
|
||||
}
|
||||
|
||||
if (remainingBytes > 0)
|
||||
{
|
||||
decryptedBuffer = new byte[remainingBytes];
|
||||
data.Read(decryptedBuffer, 0, decryptedBuffer.Length);
|
||||
encryptedBuffer = algorithm.Encrypt(decryptedBuffer, false);
|
||||
encrypted.Write(encryptedBuffer, 0, encryptedBuffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
private Stream WriteEncryptionParameters(Stream encryptedData, byte[] keyHash)
|
||||
{
|
||||
var data = new MemoryStream();
|
||||
|
||||
logger.Debug("Writing encryption parameters...");
|
||||
data.Write(keyHash, 0, keyHash.Length);
|
||||
encryptedData.Seek(0, SeekOrigin.Begin);
|
||||
encryptedData.CopyTo(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Configuration.Cryptography
|
||||
{
|
||||
public class PublicKeySymmetricEncryption : PublicKeyEncryption
|
||||
{
|
||||
private const int ENCRYPTION_KEY_LENGTH = 32;
|
||||
private const int KEY_LENGTH_SIZE = 4;
|
||||
|
||||
private PasswordEncryption passwordEncryption;
|
||||
|
||||
public PublicKeySymmetricEncryption(ICertificateStore store, ILogger logger, PasswordEncryption passwordEncryption) : base(store, logger)
|
||||
{
|
||||
this.passwordEncryption = passwordEncryption;
|
||||
}
|
||||
|
||||
public override LoadStatus Decrypt(Stream data, out Stream decryptedData, out X509Certificate2 certificate)
|
||||
{
|
||||
var publicKeyHash = ParsePublicKeyHash(data);
|
||||
var found = store.TryGetCertificateWith(publicKeyHash, out certificate);
|
||||
|
||||
decryptedData = 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, symmetricKey, out decryptedData);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public override SaveStatus Encrypt(Stream data, X509Certificate2 certificate, out Stream encryptedData)
|
||||
{
|
||||
var publicKeyHash = GeneratePublicKeyHash(certificate);
|
||||
var symmetricKey = GenerateSymmetricKey();
|
||||
var symmetricKeyString = Convert.ToBase64String(symmetricKey);
|
||||
var status = passwordEncryption.Encrypt(data, symmetricKeyString, out encryptedData);
|
||||
|
||||
if (status != SaveStatus.Success)
|
||||
{
|
||||
return FailForUnsuccessfulPasswordEncryption(status);
|
||||
}
|
||||
|
||||
encryptedData = WriteEncryptionParameters(encryptedData, certificate, publicKeyHash, symmetricKey);
|
||||
|
||||
return SaveStatus.Success;
|
||||
}
|
||||
|
||||
private SaveStatus FailForUnsuccessfulPasswordEncryption(SaveStatus status)
|
||||
{
|
||||
logger.Error($"Password encryption has failed with status '{status}'!");
|
||||
|
||||
return SaveStatus.UnexpectedError;
|
||||
}
|
||||
|
||||
private byte[] GenerateSymmetricKey()
|
||||
{
|
||||
var key = new byte[ENCRYPTION_KEY_LENGTH];
|
||||
|
||||
using (var generator = RandomNumberGenerator.Create())
|
||||
{
|
||||
generator.GetBytes(key);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
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 encryptedKeyLength = BitConverter.ToInt32(keyLengthData, 0);
|
||||
var encryptedKey = new byte[encryptedKeyLength];
|
||||
|
||||
data.Read(encryptedKey, 0, encryptedKey.Length);
|
||||
|
||||
var stream = new SubStream(data, PUBLIC_KEY_HASH_SIZE + KEY_LENGTH_SIZE, encryptedKeyLength);
|
||||
var decryptedKey = Decrypt(stream, 0, certificate);
|
||||
var symmetricKey = Convert.ToBase64String(decryptedKey.ToArray());
|
||||
|
||||
return symmetricKey;
|
||||
}
|
||||
|
||||
private Stream WriteEncryptionParameters(Stream encryptedData, X509Certificate2 certificate, byte[] publicKeyHash, byte[] symmetricKey)
|
||||
{
|
||||
var data = new MemoryStream();
|
||||
var symmetricKeyData = new MemoryStream(symmetricKey);
|
||||
var encryptedKey = Encrypt(symmetricKeyData, certificate);
|
||||
// IMPORTANT: The key length must be exactly 4 Bytes, thus the cast to integer!
|
||||
var encryptedKeyLength = BitConverter.GetBytes((int) encryptedKey.Length);
|
||||
|
||||
logger.Debug("Writing encryption parameters...");
|
||||
|
||||
data.Write(publicKeyHash, 0, publicKeyHash.Length);
|
||||
data.Write(encryptedKeyLength, 0, encryptedKeyLength.Length);
|
||||
|
||||
encryptedKey.Seek(0, SeekOrigin.Begin);
|
||||
encryptedKey.CopyTo(data);
|
||||
|
||||
encryptedData.Seek(0, SeekOrigin.Begin);
|
||||
encryptedData.CopyTo(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user