// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.DataProtection.Internal; using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.DataProtection.Test.Shared; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.DataProtection { public class DataProtectionProviderTests { [Fact] public void System_UsesProvidedDirectory() { WithUniqueTempDirectory(directory => { // Step 1: directory should be completely empty directory.Create(); Assert.Empty(directory.GetFiles()); // Step 2: instantiate the system and round-trip a payload var protector = DataProtectionProvider.Create(directory).CreateProtector("purpose"); Assert.Equal("payload", protector.Unprotect(protector.Protect("payload"))); // Step 3: validate that there's now a single key in the directory and that it's not protected var allFiles = directory.GetFiles(); Assert.Single(allFiles); Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); string fileText = File.ReadAllText(allFiles[0].FullName); Assert.Contains("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); Assert.DoesNotContain("Windows DPAPI", fileText, StringComparison.Ordinal); }); } [Fact] public void System_NoKeysDirectoryProvided_UsesDefaultKeysDirectory() { var mock = new Mock(); var keysPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName()); mock.Setup(m => m.GetKeyStorageDirectory()).Returns(new DirectoryInfo(keysPath)); // Step 1: Instantiate the system and round-trip a payload var provider = DataProtectionProvider.CreateProvider( keyDirectory: null, certificate: null, setupAction: builder => { builder.SetApplicationName("TestApplication"); builder.Services.AddSingleton(s => new XmlKeyManager( s.GetRequiredService>(), s.GetRequiredService(), NullLoggerFactory.Instance, mock.Object)); }); var protector = provider.CreateProtector("Protector"); Assert.Equal("payload", protector.Unprotect(protector.Protect("payload"))); // Step 2: Validate that there's now a single key in the directory var newFileName = Assert.Single(Directory.GetFiles(keysPath)); var file = new FileInfo(newFileName); Assert.StartsWith("key-", file.Name, StringComparison.OrdinalIgnoreCase); var fileText = File.ReadAllText(file.FullName); // On Windows, validate that it's protected using Windows DPAPI. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Assert.DoesNotContain("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); Assert.Contains("This key is encrypted with Windows DPAPI.", fileText, StringComparison.Ordinal); } else { Assert.Contains("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); } } [ConditionalFact] [ConditionalRunTestOnlyOnWindows] public void System_UsesProvidedDirectory_WithConfigurationCallback() { WithUniqueTempDirectory(directory => { // Step 1: directory should be completely empty directory.Create(); Assert.Empty(directory.GetFiles()); // Step 2: instantiate the system and round-trip a payload var protector = DataProtectionProvider.Create(directory, configure => { configure.ProtectKeysWithDpapi(); }).CreateProtector("purpose"); Assert.Equal("payload", protector.Unprotect(protector.Protect("payload"))); // Step 3: validate that there's now a single key in the directory and that it's protected with DPAPI var allFiles = directory.GetFiles(); Assert.Single(allFiles); Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); string fileText = File.ReadAllText(allFiles[0].FullName); Assert.DoesNotContain("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); Assert.Contains("Windows DPAPI", fileText, StringComparison.Ordinal); }); } [ConditionalFact] [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)] public void System_UsesProvidedDirectoryAndCertificate() { var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx"); using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); store.Add(new X509Certificate2(filePath, "password", X509KeyStorageFlags.Exportable)); store.Close(); } WithUniqueTempDirectory(directory => { var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); certificateStore.Open(OpenFlags.ReadWrite); var certificate = certificateStore.Certificates.Find(X509FindType.FindBySubjectName, "TestCert", false)[0]; try { // Step 1: directory should be completely empty directory.Create(); Assert.Empty(directory.GetFiles()); // Step 2: instantiate the system and round-trip a payload var protector = DataProtectionProvider.Create(directory, certificate).CreateProtector("purpose"); var data = protector.Protect("payload"); // add a cert without the private key to ensure the decryption will still fallback to the cert store var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCertWithoutPrivateKey.pfx"), "password"); var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certWithoutKey)).CreateProtector("purpose"); Assert.Equal("payload", unprotector.Unprotect(data)); // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate var allFiles = directory.GetFiles(); Assert.Single(allFiles); Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); string fileText = File.ReadAllText(allFiles[0].FullName); Assert.DoesNotContain("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); Assert.Contains("X509Certificate", fileText, StringComparison.Ordinal); } finally { certificateStore.Remove(certificate); certificateStore.Close(); } }); } [ConditionalFact] [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)] public void System_UsesProvidedCertificateNotFromStore() { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3WithoutPrivateKey.pfx"), "password3", X509KeyStorageFlags.Exportable); Assert.False(certWithoutKey.HasPrivateKey, "Cert should not have private key"); store.Add(certWithoutKey); store.Close(); } WithUniqueTempDirectory(directory => { using (var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { certificateStore.Open(OpenFlags.ReadWrite); var certInStore = certificateStore.Certificates.Find(X509FindType.FindBySubjectName, "TestCert", false)[0]; Assert.NotNull(certInStore); Assert.False(certInStore.HasPrivateKey); try { var certWithKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3.pfx"), "password3"); var protector = DataProtectionProvider.Create(directory, certWithKey).CreateProtector("purpose"); var data = protector.Protect("payload"); var keylessUnprotector = DataProtectionProvider.Create(directory).CreateProtector("purpose"); Assert.Throws(() => keylessUnprotector.Unprotect(data)); var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certInStore, certWithKey)).CreateProtector("purpose"); Assert.Equal("payload", unprotector.Unprotect(data)); } finally { certificateStore.Remove(certInStore); certificateStore.Close(); } } }); } [Fact] public void System_UsesInMemoryCertificate() { var filePath = Path.Combine(GetTestFilesPath(), "TestCert2.pfx"); var certificate = new X509Certificate2(filePath, "password"); AssetStoreDoesNotContain(certificate); WithUniqueTempDirectory(directory => { // Step 1: directory should be completely empty directory.Create(); Assert.Empty(directory.GetFiles()); // Step 2: instantiate the system and round-trip a payload var protector = DataProtectionProvider.Create(directory, certificate).CreateProtector("purpose"); Assert.Equal("payload", protector.Unprotect(protector.Protect("payload"))); // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate var allFiles = directory.GetFiles(); Assert.Single(allFiles); Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); string fileText = File.ReadAllText(allFiles[0].FullName); Assert.DoesNotContain("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); Assert.Contains("X509Certificate", fileText, StringComparison.Ordinal); }); } private static void AssetStoreDoesNotContain(X509Certificate2 certificate) { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { try { store.Open(OpenFlags.ReadOnly); } catch { return; } // ensure this cert is not in the x509 store Assert.Empty(store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false)); } } [Fact] public void System_CanUnprotectWithCert() { var filePath = Path.Combine(GetTestFilesPath(), "TestCert2.pfx"); var certificate = new X509Certificate2(filePath, "password"); WithUniqueTempDirectory(directory => { // Step 1: directory should be completely empty directory.Create(); Assert.Empty(directory.GetFiles()); // Step 2: instantiate the system and create some data var protector = DataProtectionProvider .Create(directory, certificate) .CreateProtector("purpose"); var data = protector.Protect("payload"); // Step 3: validate that there's now a single key in the directory and that it's is protected using the certificate var allFiles = directory.GetFiles(); Assert.Single(allFiles); Assert.StartsWith("key-", allFiles[0].Name, StringComparison.OrdinalIgnoreCase); string fileText = File.ReadAllText(allFiles[0].FullName); Assert.DoesNotContain("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal); Assert.Contains("X509Certificate", fileText, StringComparison.Ordinal); // Step 4: setup a second system and validate it can decrypt keys and unprotect data var unprotector = DataProtectionProvider.Create(directory, b => b.UnprotectKeysWithAnyCertificate(certificate)); Assert.Equal("payload", unprotector.CreateProtector("purpose").Unprotect(data)); }); } /// /// Runs a test and cleans up the temp directory afterward. /// private static void WithUniqueTempDirectory(Action testCode) { string uniqueTempPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName()); var dirInfo = new DirectoryInfo(uniqueTempPath); try { testCode(dirInfo); } finally { // clean up when test is done if (dirInfo.Exists) { dirInfo.Delete(recursive: true); } } } private static string GetTestFilesPath() => Path.Combine(AppContext.BaseDirectory, "TestFiles"); } }