using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Windows.Forms;
//
// SEBProtectionController.cs
// SafeExamBrowser
//
// SafeExamBrowser
//
// Copyright (c) 2010-2020 Daniel R. Schneider,
// ETH Zurich, IT Services,
// based on the original idea of Safe Exam Browser
// by Stefan Schneider, University of Giessen
// Project concept: Thomas Piendl, Daniel R. Schneider,
// Dirk Bauer, Kai Reuter, Tobias Halbherr, Karsten Burger, Marco Lehre,
// Brigitte Schmucki, Oliver Rahs. French localization: Nicolas Dunand
//
// ``The contents of this file are subject to the Mozilla Public License
// Version 1.1 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at
// http://www.mozilla.org/MPL/
//
// Software distributed under the License is distributed on an "AS IS"
// basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
// License for the specific language governing rights and limitations
// under the License.
//
// The Original Code is Safe Exam Browser for Windows.
//
// The Initial Developer of the Original Code is Daniel R. Schneider.
// Portions created by Daniel R. Schneider
// are Copyright (c) 2010-2020 Daniel R. Schneider,
// ETH Zurich, IT Services,
// based on the original idea of Safe Exam Browser
// by Stefan Schneider, University of Giessen. All Rights Reserved.
//
// Contributor(s): ______________________________________.
//
namespace SebWindowsConfig.Utilities
{
public class SEBProtectionController
{
const string DLL_NAME =
#if X86
"seb_x86.dll";
#else
"seb_x64.dll";
#endif
// Prefix
private const int PREFIX_LENGTH = 4;
private const string PUBLIC_KEY_HASH_MODE = "pkhs";
private const string PASSWORD_MODE = "pswd";
private const string PLAIN_DATA_MODE = "plnd";
private const string PASSWORD_CONFIGURING_CLIENT_MODE = "pwcc";
// Public key
private const int PUBLIC_KEY_HASH_LENGTH = 20;
// RNCryptor non-secret payload (header)
// First byte: Data format version. Currently 2.
// Second byte: Options, bit 0 - uses password (so currently 1).
private static readonly byte[] RNCRYPTOR_HEADER = new byte[] { 0x02, 0x01 };
enum EncryptionT
{
pkhs,
pswd,
plnd,
pwcc,
unknown
};
/// ------------------------------------------------------------------------------------------
///
/// Get array of certificate references and the according names from both certificate stores.
///
/// ------------------------------------------------------------------------------------------
public static ArrayList GetCertificatesAndNames(ref ArrayList certificateNames)
{
ArrayList certificates = new ArrayList();
// First search the Personal (standard) certificate store for the current user
X509Store store = new X509Store(StoreLocation.CurrentUser);
certificates = GetCertificatesAndNamesFromStore(ref certificateNames, store);
// Also search the store for Trusted Users
ArrayList certificateNamesTrustedUsers = new ArrayList();
store = new X509Store(StoreName.TrustedPeople);
certificates.AddRange(GetCertificatesAndNamesFromStore(ref certificateNamesTrustedUsers, store));
certificateNames.AddRange(certificateNamesTrustedUsers);
// Also search the Personal store for the local machine
ArrayList certificateNamesLocalMachine = new ArrayList();
store = new X509Store(StoreLocation.LocalMachine);
certificates.AddRange(GetCertificatesAndNamesFromStore(ref certificateNamesLocalMachine, store));
certificateNames.AddRange(certificateNamesLocalMachine);
return certificates;
}
/// ----------------------------------------------------------------------------------------
///
/// Helper method: Get array of certificate references and the according names from the passed certificate store.
///
/// ----------------------------------------------------------------------------------------
public static ArrayList GetCertificatesAndNamesFromStore(ref ArrayList certificateNames, X509Store store)
{
ArrayList certificates = new ArrayList();
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 x509Certificate in store.Certificates)
{
if (x509Certificate.HasPrivateKey)
{
certificates.Add(x509Certificate);
if (!String.IsNullOrWhiteSpace(x509Certificate.FriendlyName))
certificateNames.Add(x509Certificate.FriendlyName);
else if (!String.IsNullOrWhiteSpace(x509Certificate.SerialNumber))
certificateNames.Add(x509Certificate.SerialNumber);
}
}
//Close the store.
store.Close();
return certificates;
}
/// ----------------------------------------------------------------------------------------
///
/// Get array of CA certificate references and the according names from the certificate store.
///
/// ----------------------------------------------------------------------------------------
public static ArrayList GetSSLCertificatesAndNames(ref ArrayList certificateNames)
{
ArrayList certificates = new ArrayList();
X509Store store = new X509Store(StoreName.CertificateAuthority);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certsCollection = store.Certificates.Find(X509FindType.FindByKeyUsage, (X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment), false);
store = new X509Store(StoreName.AddressBook);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certsCollection2 = store.Certificates.Find(X509FindType.FindByKeyUsage, (X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment), false);
certsCollection.AddRange(certsCollection2);
foreach (X509Certificate2 x509Certificate in certsCollection)
{
certificates.Add(x509Certificate);
certificateNames.Add(Parse(x509Certificate.Subject, "CN")?.FirstOrDefault() ?? x509Certificate.FriendlyName);
}
//Close the store.
store.Close();
return certificates;
}
///
/// Recursively searches the supplied AD string for all groups.
///
/// The string returned from AD to parse for a group.
/// The string to use as the seperator for the data. ex. ","
/// null if no groups were found -OR- data is null or empty.
public static List Parse(string data, string delimiter)
{
if (data == null) return null;
if (!delimiter.EndsWith("=")) delimiter = delimiter + "=";
if (!data.Contains(delimiter)) return null;
//base case
var result = new List();
int start = data.IndexOf(delimiter) + delimiter.Length;
int length = data.IndexOf(',', start) - start;
if (length == 0) return null; //the group is empty
if (length > 0)
{
result.Add(data.Substring(start, length));
//only need to recurse when the comma was found, because there could be more groups
var rec = Parse(data.Substring(start + length), delimiter);
if (rec != null) result.AddRange(rec); //can't pass null into AddRange() :(
}
else //no comma found after current group so just use the whole remaining string
{
result.Add(data.Substring(start));
}
return result;
}
/// ----------------------------------------------------------------------------------------
///
/// Get certificate from both stores.
///
/// ----------------------------------------------------------------------------------------
public static X509Certificate2 GetCertificateFromStore(byte[] publicKeyHash)
{
X509Certificate2 sebCertificate = null;
// First search the Personal certificate store for the current user
X509Store store = new X509Store(StoreLocation.CurrentUser);
sebCertificate = GetCertificateFromPassedStore(publicKeyHash, store);
// If the certificate wasn't found there, search the store for Trusted People
if (sebCertificate == null)
{
store = new X509Store(StoreName.TrustedPeople);
sebCertificate = GetCertificateFromPassedStore(publicKeyHash, store);
}
// If the certificate wasn't found there, search the Personal store for the local machine
if (sebCertificate == null)
{
store = new X509Store(StoreLocation.LocalMachine);
sebCertificate = GetCertificateFromPassedStore(publicKeyHash, store);
}
return sebCertificate;
}
/// ----------------------------------------------------------------------------------------
///
/// Helper method: Search passed store for certificate with passed public key hash.
///
/// ----------------------------------------------------------------------------------------
public static X509Certificate2 GetCertificateFromPassedStore(byte[] publicKeyHash, X509Store store)
{
X509Certificate2 sebCertificate = null;
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 x509Certificate in store.Certificates)
{
byte[] publicKeyRawData = x509Certificate.PublicKey.EncodedKeyValue.RawData;
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] certificateHash = sha.ComputeHash(publicKeyRawData);
//certificateName = x509Certificate.Subject;
if (certificateHash.SequenceEqual(publicKeyHash))
{
sebCertificate = x509Certificate;
break;
}
}
//Close the store.
store.Close();
return sebCertificate;
}
/// ----------------------------------------------------------------------------------------
///
/// Store certificate into the Windows Certificate Store.
///
/// ----------------------------------------------------------------------------------------
public static void StoreCertificateIntoStore(byte[] certificateData)
{
X509Store store = null;
// Save the certificate into the Personal store
try
{
store = new X509Store(StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
}
catch (Exception storeOpenException)
{
Logger.AddError("The X509 store in Windows Certificate Store could not be opened: ", null, storeOpenException, storeOpenException.Message);
}
if (store != null)
{
StoreCertificateIntoPassedStore(certificateData, store);
}
// In addition try to save the certificate into the Trusted People store for the Local Machine
// This will only work if SEB is run in an administrator account
try
{
store = new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
}
catch (Exception storeOpenException)
{
Logger.AddError("The X509 Trusted People store in Windows Certificate Store for Local Machine could not be opened: ", null, storeOpenException, storeOpenException.Message);
return;
}
StoreCertificateIntoPassedStore(certificateData, store);
}
/// ----------------------------------------------------------------------------------------
///
/// Store certificate into the passed, already opened store.
///
/// ----------------------------------------------------------------------------------------
public static void StoreCertificateIntoPassedStore(byte[] certificateData, X509Store store)
{
X509Certificate2 x509 = new X509Certificate2();
try
{
x509.Import(certificateData, SEBClientInfo.DEFAULT_KEY, X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet);
}
catch (Exception certImportException)
{
Logger.AddError("The identity data could not be imported into the X509 certificate store.", null, certImportException, certImportException.Message);
store.Close();
return;
}
try
{
store.Add(x509);
}
catch (Exception certAddingException)
{
Logger.AddError("The identity could not be added to the Windows Certificate Store", null, certAddingException, certAddingException.Message);
store.Close();
return;
}
Logger.AddInformation("The identity was successfully added to the Windows Certificate Store");
store.Close();
}
/// ----------------------------------------------------------------------------------------
///
/// Get the public key hash for the certificate from the store.
///
/// ----------------------------------------------------------------------------------------
public static byte[] GetPublicKeyHashFromCertificate(X509Certificate2 certificateRef)
{
//string certificateHash;
//Create new X509 store from the local certificate store.
//X509Store store = new X509Store(StoreLocation.CurrentUser);
//store.Open(OpenFlags.ReadOnly);
byte[] publicKeyRawData = certificateRef.PublicKey.EncodedKeyValue.RawData;
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] certificateHash = sha.ComputeHash(publicKeyRawData);
//Close the store.
//store.Close();
return certificateHash;
}
/// ----------------------------------------------------------------------------------------
///
/// Encrypt with certificate/public key and RSA algorithm
///
/// ----------------------------------------------------------------------------------------
public static byte[] EncryptDataWithCertificate(byte[] plainInputData, X509Certificate2 sebCertificate)
{
try
{
// Encrypt config data
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
RSACryptoServiceProvider publicKey = sebCertificate.PublicKey.Key as RSACryptoServiceProvider;
// Blocksize is for example 2048/8 = 256
int blockSize = (publicKey.KeySize / 8) - 32;
// buffer to hold byte sequence of the encrypted information
byte[] encryptedBuffer = new byte[blockSize];
// buffer for the plain source data
byte[] plainBuffer = new byte[blockSize];
// initialize array so it holds at least the amount needed to decrypt.
//byte[] decryptedData = new byte[encryptedData.Length];
MemoryStream encryptedStream = new MemoryStream();
// Calculate number of full data blocks that will have to be decrypted
int blockCount = plainInputData.Length / blockSize;
for (int i = 0; i < blockCount; i++)
{
// copy byte sequence from encrypted source data to the buffer
Buffer.BlockCopy(plainInputData, i * blockSize, plainBuffer, 0, blockSize);
// decrypt the block in the buffer
encryptedBuffer = publicKey.Encrypt(plainBuffer, false);
// write decrypted result back to the destination array
encryptedStream.Write(encryptedBuffer, 0, encryptedBuffer.Length);
}
int remainingBytes = plainInputData.Length - (blockCount * blockSize);
if (remainingBytes > 0)
{
plainBuffer = new byte[remainingBytes];
// copy remaining bytes from encrypted source data to the buffer
Buffer.BlockCopy(plainInputData, blockCount * blockSize, plainBuffer, 0, remainingBytes);
// decrypt the block in the buffer
encryptedBuffer = publicKey.Encrypt(plainBuffer, false);
// write decrypted result back to the destination array
//decryptedBuffer.CopyTo(decryptedData, blockCount * blockSize);
encryptedStream.Write(encryptedBuffer, 0, encryptedBuffer.Length);
}
byte[] encryptedData = encryptedStream.ToArray();
return encryptedData;
}
catch (CryptographicException)
{
//return cex.Message;
return null;
}
catch (Exception)
{
//return ex.Message;
return null;
}
}
/// ----------------------------------------------------------------------------------------
///
/// Decrypt with X509 certificate/private key and RSA algorithm
///
/// ----------------------------------------------------------------------------------------
public static byte[] DecryptDataWithCertificate(byte[] encryptedData, X509Certificate2 sebCertificate)
{
try
{
// Decrypt config data
RSACryptoServiceProvider privateKey = sebCertificate.PrivateKey as RSACryptoServiceProvider;
//byte[] decryptedData = privateKey.Decrypt(encryptedDataBytes, false);
// Blocksize is for example 2048/8 = 256
int blockSize = privateKey.KeySize / 8;
// buffer to hold byte sequence of the encrypted source data
byte[] encryptedBuffer = new byte[blockSize];
// buffer for the decrypted information
byte[] decryptedBuffer = new byte[blockSize];
// initialize array so it holds at least the amount needed to decrypt.
//byte[] decryptedData = new byte[encryptedData.Length];
MemoryStream decryptedStream = new MemoryStream();
// Calculate number of full data blocks that will have to be decrypted
int blockCount = encryptedData.Length / blockSize;
for (int i = 0; i < blockCount; i++)
{
// copy byte sequence from encrypted source data to the buffer
Buffer.BlockCopy(encryptedData, i * blockSize, encryptedBuffer, 0, blockSize);
// decrypt the block in the buffer
decryptedBuffer = privateKey.Decrypt(encryptedBuffer, false);
// write decrypted result back to the destination array
//decryptedBuffer.CopyTo(decryptedData, i*blockSize);
decryptedStream.Write(decryptedBuffer, 0, decryptedBuffer.Length);
}
int remainingBytes = encryptedData.Length - (blockCount * blockSize);
if (remainingBytes > 0)
{
encryptedBuffer = new byte[remainingBytes];
// copy remaining bytes from encrypted source data to the buffer
Buffer.BlockCopy(encryptedData, blockCount * blockSize, encryptedBuffer, 0, remainingBytes);
// decrypt the block in the buffer
decryptedBuffer = privateKey.Decrypt(encryptedBuffer, false);
// write decrypted result back to the destination array
//decryptedBuffer.CopyTo(decryptedData, blockCount * blockSize);
decryptedStream.Write(decryptedBuffer, 0, decryptedBuffer.Length);
}
byte[] decryptedData = decryptedStream.ToArray();
return decryptedData;
}
catch (CryptographicException cex)
{
Logger.AddError("Decrypting SEB config data encrypted with an identity failed with cryptographic exception:", null, cex, cex.Message);
MessageBox.Show(SEBUIStrings.errorDecryptingSettings, SEBUIStrings.certificateDecryptingError + cex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
catch (Exception ex)
{
Logger.AddError("Decrypting SEB config data encrypted with an identity failed with exception:", null, ex, ex.Message);
MessageBox.Show(SEBUIStrings.errorDecryptingSettings, SEBUIStrings.certificateDecryptingError + ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
/// ----------------------------------------------------------------------------------------
///
/// Encrypt with password, key, salt using AES (Open SSL Encrypt).
///
/// ----------------------------------------------------------------------------------------
public static byte[] EncryptDataWithPassword(byte[] plainData, string password)
{
try
{
// encrypt bytes
byte[] encryptedData = AESThenHMAC.SimpleEncryptWithPassword(plainData, password, RNCRYPTOR_HEADER);
return encryptedData;
}
catch (CryptographicException)
{
//return cex.Message;
return null;
}
catch (Exception)
{
//return ex.Message;
return null;
}
}
/// ----------------------------------------------------------------------------------------
///
/// Decrypt with password, key, salt using AES (Open SSL Decrypt)..
///
/// ----------------------------------------------------------------------------------------
public static byte[] DecryptDataWithPassword(byte[] encryptedBytesWithSalt, string passphrase)
{
try
{
byte[] decryptedData = AESThenHMAC.SimpleDecryptWithPassword(encryptedBytesWithSalt, passphrase, nonSecretPayloadLength: 2);
return decryptedData;
}
catch (CryptographicException)
{
//return cex.Message;
return null;
}
catch (Exception)
{
//return ex.Message;
return null;
}
}
/// ----------------------------------------------------------------------------------------
///
/// Compute a SHA256 hash base16 string.
///
/// ----------------------------------------------------------------------------------------
public static string ComputePasswordHash(string input)
{
HashAlgorithm algorithm = new SHA256Managed();
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashedBytes = algorithm.ComputeHash(inputBytes);
string pswdHash = BitConverter.ToString(hashedBytes).ToLower();
return pswdHash.Replace("-", "");
}
/// ----------------------------------------------------------------------------------------
///
/// Compute a Browser Exam Key SHA256 hash base16 string.
///
/// ----------------------------------------------------------------------------------------
public static string ComputeBrowserExamKey()
{
var browserExamKey = default(string);
var configurationKey = ComputeConfigurationKey();
var executable = Assembly.GetExecutingAssembly();
var certificate = executable.Modules.First().GetSignerCertificate();
var salt = (byte[]) SEBSettings.settingsCurrent[SEBSettings.KeyExamKeySalt];
var signature = certificate?.GetCertHashString();
var version = FileVersionInfo.GetVersionInfo(executable.Location).FileVersion;
Logger.AddInformation("Initializing browser exam key...");
if (configurationKey == default)
{
configurationKey = "";
Logger.AddWarning("The current configuration does not contain a value for the configuration key!");
}
if (salt == default || salt.Length == 0)
{
salt = new byte[0];
Logger.AddWarning("The current configuration does not contain a salt value for the browser exam key!");
}
if (TryCalculateBrowserExamKey(configurationKey, BitConverter.ToString(salt).ToLower().Replace("-", string.Empty), out browserExamKey))
{
Logger.AddInformation("Successfully calculated BEK using integrity module.");
}
else
{
Logger.AddWarning("Failed to calculate BEK using integrity module! Falling back to simplified calculation...");
using (var algorithm = new HMACSHA256(salt))
{
var hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(signature + version + configurationKey));
var key = BitConverter.ToString(hash).ToLower().Replace("-", string.Empty);
browserExamKey = key;
}
}
return browserExamKey;
}
private static bool TryCalculateBrowserExamKey(string configurationKey, string salt, out string browserExamKey)
{
browserExamKey = default;
try
{
browserExamKey = CalculateBrowserExamKey(configurationKey, salt);
}
catch (DllNotFoundException)
{
Logger.AddWarning("Integrity module is not available!");
}
catch (Exception e)
{
Logger.AddError("Unexpected error while attempting to calculate browser exam key!", default, e);
}
return browserExamKey != default;
}
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.BStr)]
private static extern string CalculateBrowserExamKey(string configurationKey, string salt);
/// ----------------------------------------------------------------------------------------
///
/// Compute a Configuration Key SHA256 hash base16 string.
///
/// ----------------------------------------------------------------------------------------
public static string ComputeConfigurationKey()
{
using (var algorithm = new SHA256Managed())
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
{
Serialize(SEBSettings.settingsCurrentOriginal, writer);
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
var hash = algorithm.ComputeHash(stream);
var key = BitConverter.ToString(hash).ToLower().Replace("-", string.Empty);
return key;
}
}
private static void Serialize(IDictionary dictionary, StreamWriter stream)
{
var orderedByKey = dictionary.OrderBy(d => d.Key, StringComparer.InvariantCulture).ToList();
stream.Write('{');
foreach (var kvp in orderedByKey)
{
var process = true;
process &= !kvp.Key.Equals(SEBSettings.KeyOriginatorVersion, StringComparison.OrdinalIgnoreCase);
process &= !(kvp.Value is IDictionary d) || d.Any();
if (process)
{
stream.Write('"');
stream.Write(kvp.Key);
stream.Write('"');
stream.Write(':');
Serialize(kvp.Value, stream);
if (kvp.Key != orderedByKey.Last().Key)
{
stream.Write(',');
}
}
}
stream.Write('}');
}
private static void Serialize(IList