// 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.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.DataProtection.Test.Shared; using Microsoft.AspNetCore.Testing.xunit; 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.Equal(1, allFiles.Length); 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() { Assert.NotNull(FileSystemXmlRepository.DefaultKeyStorageDirectory); var keysPath = FileSystemXmlRepository.DefaultKeyStorageDirectory.FullName; var tempPath = FileSystemXmlRepository.DefaultKeyStorageDirectory.FullName + "Temp"; try { // Step 1: Move the current contents, if any, to a temporary directory. if (Directory.Exists(keysPath)) { Directory.Move(keysPath, tempPath); } // Step 2: Instantiate the system and round-trip a payload var protector = DataProtectionProvider.Create("TestApplication").CreateProtector("purpose"); Assert.Equal("payload", protector.Unprotect(protector.Protect("payload"))); // Step 3: 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); } } finally { if (Directory.Exists(keysPath)) { Directory.Delete(keysPath, recursive: true); } if (Directory.Exists(tempPath)) { Directory.Move(tempPath, keysPath); } } } [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.Equal(1, allFiles.Length); 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); }); } [Fact] public void System_UsesProvidedDirectoryAndCertificate() { var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx"); 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"); 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.Equal(1, allFiles.Length); 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(); } }); } /// /// Runs a test and cleans up the temp directory afterward. /// private static void WithUniqueTempDirectory(Action testCode) { string uniqueTempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 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() { var projectName = typeof(DataProtectionProviderTests).GetTypeInfo().Assembly.GetName().Name; var projectPath = RecursiveFind(projectName, Path.GetFullPath(".")); return Path.Combine(projectPath, projectName, "TestFiles"); } private static string RecursiveFind(string path, string start) { var test = Path.Combine(start, path); if (Directory.Exists(test)) { return start; } else { return RecursiveFind(path, new DirectoryInfo(start).Parent.FullName); } } } }