diff --git a/SafeExamBrowser.Configuration.UnitTests/Cryptography/PublicKeyHashEncryptionTests.cs b/SafeExamBrowser.Configuration.UnitTests/Cryptography/PublicKeyHashEncryptionTests.cs new file mode 100644 index 00000000..783f69d4 --- /dev/null +++ b/SafeExamBrowser.Configuration.UnitTests/Cryptography/PublicKeyHashEncryptionTests.cs @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019 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.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using SafeExamBrowser.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.UnitTests.Cryptography +{ + [TestClass] + public class PublicKeyHashEncryptionTests + { + private Mock logger; + private Mock store; + private X509Certificate2 certificate; + + private PublicKeyHashEncryption sut; + + [TestInitialize] + public void Initialize() + { + logger = new Mock(); + store = new Mock(); + + LoadCertificate(); + store.Setup(s => s.TryGetCertificateWith(It.IsAny(), out certificate)).Returns(true); + + sut = new PublicKeyHashEncryption(store.Object, logger.Object); + } + + [TestMethod] + public void MustPerformCorrectly() + { + var message = Encoding.UTF8.GetBytes("A super secret message!"); + var saveStatus = sut.Encrypt(new MemoryStream(message), certificate, out var encrypted); + var loadStatus = sut.Decrypt(encrypted, out var decrypted, out _); + var original = new MemoryStream(message); + + decrypted.Seek(0, SeekOrigin.Begin); + original.Seek(0, SeekOrigin.Begin); + + while (original.Position < original.Length) + { + Assert.AreEqual(original.ReadByte(), decrypted.ReadByte()); + } + + Assert.AreEqual(SaveStatus.Success, saveStatus); + Assert.AreEqual(LoadStatus.Success, loadStatus); + } + + [TestMethod] + public void MustFailIfCertificateNotFound() + { + store.Setup(s => s.TryGetCertificateWith(It.IsAny(), out certificate)).Returns(false); + + var buffer = new byte[20]; + new Random().NextBytes(buffer); + var data = new MemoryStream(buffer); + var status = sut.Decrypt(data, out _, out _); + + Assert.AreEqual(LoadStatus.InvalidData, status); + } + + /// + /// makecert -sv UnitTestCert.pvk -n "CN=Unit Test Certificate" UnitTestCert.cer -r -pe -sky eXchange + /// pvk2pfx -pvk UnitTestCert.pvk -spc UnitTestCert.cer -pfx UnitTestCert.pfx -f + /// + private void LoadCertificate() + { + var path = $"{nameof(SafeExamBrowser)}.{nameof(Configuration)}.{nameof(UnitTests)}.UnitTestCert.pfx"; + + using (var stream = Assembly.GetAssembly(GetType()).GetManifestResourceStream(path)) + { + var data = new byte[stream.Length]; + + stream.Read(data, 0, (int)stream.Length); + certificate = new X509Certificate2(data); + } + } + } +} diff --git a/SafeExamBrowser.Configuration.UnitTests/Cryptography/PublicKeyHashWithSymmetricKeyEncryptionTests.cs b/SafeExamBrowser.Configuration.UnitTests/Cryptography/PublicKeyHashWithSymmetricKeyEncryptionTests.cs new file mode 100644 index 00000000..61f28412 --- /dev/null +++ b/SafeExamBrowser.Configuration.UnitTests/Cryptography/PublicKeyHashWithSymmetricKeyEncryptionTests.cs @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019 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.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using SafeExamBrowser.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.UnitTests.Cryptography +{ + [TestClass] + public class PublicKeyHashWithSymmetricKeyEncryptionTests + { + private Mock logger; + private PasswordEncryption passwordEncryption; + private Mock store; + + private PublicKeyHashWithSymmetricKeyEncryption sut; + private X509Certificate2 certificate; + + [TestInitialize] + public void Initialize() + { + logger = new Mock(); + passwordEncryption = new PasswordEncryption(logger.Object); + store = new Mock(); + + LoadCertificate(); + store.Setup(s => s.TryGetCertificateWith(It.IsAny(), out certificate)).Returns(true); + + sut = new PublicKeyHashWithSymmetricKeyEncryption(store.Object, logger.Object, passwordEncryption); + } + + [TestMethod] + public void MustPerformCorrectly() + { + var message = Encoding.UTF8.GetBytes("A super secret message!"); + var saveStatus = sut.Encrypt(new MemoryStream(message), certificate, out var encrypted); + var loadStatus = sut.Decrypt(encrypted, out var decrypted, out _); + var original = new MemoryStream(message); + + decrypted.Seek(0, SeekOrigin.Begin); + original.Seek(0, SeekOrigin.Begin); + + while (original.Position < original.Length) + { + Assert.AreEqual(original.ReadByte(), decrypted.ReadByte()); + } + + Assert.AreEqual(SaveStatus.Success, saveStatus); + Assert.AreEqual(LoadStatus.Success, loadStatus); + } + + [TestMethod] + public void MustFailIfCertificateNotFound() + { + store.Setup(s => s.TryGetCertificateWith(It.IsAny(), out certificate)).Returns(false); + + var buffer = new byte[20]; + new Random().NextBytes(buffer); + var data = new MemoryStream(buffer); + var status = sut.Decrypt(data, out _, out _); + + Assert.AreEqual(LoadStatus.InvalidData, status); + } + + /// + /// makecert -sv UnitTestCert.pvk -n "CN=Unit Test Certificate" UnitTestCert.cer -r -pe -sky eXchange + /// pvk2pfx -pvk UnitTestCert.pvk -spc UnitTestCert.cer -pfx UnitTestCert.pfx -f + /// + private void LoadCertificate() + { + var path = $"{nameof(SafeExamBrowser)}.{nameof(Configuration)}.{nameof(UnitTests)}.UnitTestCert.pfx"; + + using (var stream = Assembly.GetAssembly(GetType()).GetManifestResourceStream(path)) + { + var data = new byte[stream.Length]; + + stream.Read(data, 0, (int)stream.Length); + certificate = new X509Certificate2(data); + } + } + } +} diff --git a/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj b/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj index 6d7f4ce7..dd7df7c7 100644 --- a/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj +++ b/SafeExamBrowser.Configuration.UnitTests/SafeExamBrowser.Configuration.UnitTests.csproj @@ -86,11 +86,14 @@ + + + diff --git a/SafeExamBrowser.Configuration/Cryptography/CertificateStore.cs b/SafeExamBrowser.Configuration/Cryptography/CertificateStore.cs new file mode 100644 index 00000000..f3860a4a --- /dev/null +++ b/SafeExamBrowser.Configuration/Cryptography/CertificateStore.cs @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 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.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using SafeExamBrowser.Contracts.Configuration.Cryptography; + +namespace SafeExamBrowser.Configuration.Cryptography +{ + internal class CertificateStore : ICertificateStore + { + private readonly X509Store[] stores = new[] + { + new X509Store(StoreLocation.CurrentUser), + new X509Store(StoreLocation.LocalMachine), + new X509Store(StoreName.TrustedPeople) + }; + + 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; + } + } +} diff --git a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs index 913b629c..a3407c62 100644 --- a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashEncryption.cs @@ -7,10 +7,10 @@ */ using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.Cryptography @@ -19,17 +19,19 @@ namespace SafeExamBrowser.Configuration.Cryptography { protected const int PUBLIC_KEY_HASH_SIZE = 20; + protected ICertificateStore store; protected ILogger logger; - internal PublicKeyHashEncryption(ILogger logger) + internal PublicKeyHashEncryption(ICertificateStore store, ILogger logger) { this.logger = logger; + this.store = store; } internal virtual LoadStatus Decrypt(Stream data, out Stream decryptedData, out X509Certificate2 certificate) { var publicKeyHash = ParsePublicKeyHash(data); - var found = TryGetCertificateWith(publicKeyHash, out certificate); + var found = store.TryGetCertificateWith(publicKeyHash, out certificate); decryptedData = default(Stream); @@ -82,45 +84,6 @@ namespace SafeExamBrowser.Configuration.Cryptography 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 MemoryStream Decrypt(Stream data, long offset, X509Certificate2 certificate) { var algorithm = certificate.PrivateKey as RSACryptoServiceProvider; diff --git a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs index 41428028..71faa9f7 100644 --- a/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs +++ b/SafeExamBrowser.Configuration/Cryptography/PublicKeyHashWithSymmetricKeyEncryption.cs @@ -11,6 +11,7 @@ using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Cryptography; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.Cryptography @@ -22,7 +23,7 @@ namespace SafeExamBrowser.Configuration.Cryptography private PasswordEncryption passwordEncryption; - internal PublicKeyHashWithSymmetricKeyEncryption(ILogger logger, PasswordEncryption passwordEncryption) : base(logger) + internal PublicKeyHashWithSymmetricKeyEncryption(ICertificateStore store, ILogger logger, PasswordEncryption passwordEncryption) : base(store, logger) { this.passwordEncryption = passwordEncryption; } @@ -30,7 +31,7 @@ namespace SafeExamBrowser.Configuration.Cryptography internal override LoadStatus Decrypt(Stream data, out Stream decryptedData, out X509Certificate2 certificate) { var publicKeyHash = ParsePublicKeyHash(data); - var found = TryGetCertificateWith(publicKeyHash, out certificate); + var found = store.TryGetCertificateWith(publicKeyHash, out certificate); decryptedData = default(Stream); diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs index 01af4bc4..b7af518e 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryParser.cs @@ -156,10 +156,10 @@ namespace SafeExamBrowser.Configuration.DataFormats if (prefix == BinaryBlock.PublicKeyHash) { - return new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); + return new PublicKeyHashEncryption(new CertificateStore(), logger.CloneFor(nameof(PublicKeyHashEncryption))); } - return new PublicKeyHashWithSymmetricKeyEncryption(logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); + return new PublicKeyHashWithSymmetricKeyEncryption(new CertificateStore(), logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); } private PasswordParameters DetermineEncryptionParametersFor(string prefix, PasswordParameters password) diff --git a/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs b/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs index 8f920883..ecf43047 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinarySerializer.cs @@ -141,10 +141,10 @@ namespace SafeExamBrowser.Configuration.DataFormats if (parameters.SymmetricEncryption) { - return new PublicKeyHashWithSymmetricKeyEncryption(logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); + return new PublicKeyHashWithSymmetricKeyEncryption(new CertificateStore(), logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption)), passwordEncryption); } - return new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption))); + return new PublicKeyHashEncryption(new CertificateStore(), logger.CloneFor(nameof(PublicKeyHashEncryption))); } private Stream WritePrefix(string prefix, Stream data) diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index 9a522188..bb0ba43b 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -61,6 +61,7 @@ + diff --git a/SafeExamBrowser.Contracts/Configuration/Cryptography/ICertificateStore.cs b/SafeExamBrowser.Contracts/Configuration/Cryptography/ICertificateStore.cs new file mode 100644 index 00000000..220978fa --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/Cryptography/ICertificateStore.cs @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019 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.Security.Cryptography.X509Certificates; + +namespace SafeExamBrowser.Contracts.Configuration.Cryptography +{ + /// + /// Provides functionality to load certificates installed on the computer. + /// + public interface ICertificateStore + { + /// + /// Attempts to retrieve the certificate which matches the specified public key hash value. + /// Returns true if the certificate was found, otherwise false. + /// + bool TryGetCertificateWith(byte[] keyHash, out X509Certificate2 certificate); + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index bcdef120..2892e1cc 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -58,6 +58,7 @@ +