diff --git a/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs b/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs
index cedcc2bded..58972aa4d9 100644
--- a/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs
+++ b/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.DataProtection
@@ -14,6 +15,25 @@ namespace Microsoft.AspNetCore.DataProtection
/// Use these methods when not using dependency injection to provide the service to the application.
public static class DataProtectionProvider
{
+ ///
+ /// Creates a that store keys in a location based on
+ /// the platform and operating system.
+ ///
+ /// An identifier that uniquely discriminates this application from all other
+ /// applications on the machine.
+ public static IDataProtectionProvider Create(string applicationName)
+ {
+ if (string.IsNullOrEmpty(applicationName))
+ {
+ throw new ArgumentNullException(nameof(applicationName));
+ }
+
+ return CreateProvider(
+ keyDirectory: null,
+ setupAction: builder => { builder.SetApplicationName(applicationName); },
+ certificate: null);
+ }
+
///
/// Creates an given a location at which to store keys.
///
@@ -21,7 +41,12 @@ namespace Microsoft.AspNetCore.DataProtection
/// represent a directory on a local disk or a UNC share.
public static IDataProtectionProvider Create(DirectoryInfo keyDirectory)
{
- return Create(keyDirectory, setupAction: builder => { });
+ if (keyDirectory == null)
+ {
+ throw new ArgumentNullException(nameof(keyDirectory));
+ }
+
+ return CreateProvider(keyDirectory, setupAction: builder => { }, certificate: null);
}
///
@@ -40,22 +65,116 @@ namespace Microsoft.AspNetCore.DataProtection
{
throw new ArgumentNullException(nameof(keyDirectory));
}
-
if (setupAction == null)
{
throw new ArgumentNullException(nameof(setupAction));
}
+ return CreateProvider(keyDirectory, setupAction, certificate: null);
+ }
+
+#if !NETSTANDARD1_3 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+ ///
+ /// Creates a that store keys in a location based on
+ /// the platform and operating system and uses the given to encrypt the keys.
+ ///
+ /// An identifier that uniquely discriminates this application from all other
+ /// applications on the machine.
+ /// The to be used for encryption.
+ public static IDataProtectionProvider Create(string applicationName, X509Certificate2 certificate)
+ {
+ if (string.IsNullOrEmpty(applicationName))
+ {
+ throw new ArgumentNullException(nameof(applicationName));
+ }
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ return CreateProvider(
+ keyDirectory: null,
+ setupAction: builder => { builder.SetApplicationName(applicationName); },
+ certificate: certificate);
+ }
+
+ ///
+ /// Creates an given a location at which to store keys
+ /// and a used to encrypt the keys.
+ ///
+ /// The in which keys should be stored. This may
+ /// represent a directory on a local disk or a UNC share.
+ /// The to be used for encryption.
+ public static IDataProtectionProvider Create(
+ DirectoryInfo keyDirectory,
+ X509Certificate2 certificate)
+ {
+ if (keyDirectory == null)
+ {
+ throw new ArgumentNullException(nameof(keyDirectory));
+ }
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ return CreateProvider(keyDirectory, setupAction: builder => { }, certificate: certificate);
+ }
+
+ ///
+ /// Creates an given a location at which to store keys, an
+ /// optional configuration callback and a used to encrypt the keys.
+ ///
+ /// The in which keys should be stored. This may
+ /// represent a directory on a local disk or a UNC share.
+ /// An optional callback which provides further configuration of the data protection
+ /// system. See for more information.
+ /// The to be used for encryption.
+ public static IDataProtectionProvider Create(
+ DirectoryInfo keyDirectory,
+ Action setupAction,
+ X509Certificate2 certificate)
+ {
+ if (keyDirectory == null)
+ {
+ throw new ArgumentNullException(nameof(keyDirectory));
+ }
+ if (setupAction == null)
+ {
+ throw new ArgumentNullException(nameof(setupAction));
+ }
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ return CreateProvider(keyDirectory, setupAction, certificate);
+ }
+#endif
+
+ private static IDataProtectionProvider CreateProvider(
+ DirectoryInfo keyDirectory,
+ Action setupAction,
+ X509Certificate2 certificate)
+ {
// build the service collection
var serviceCollection = new ServiceCollection();
var builder = serviceCollection.AddDataProtection();
- builder.PersistKeysToFileSystem(keyDirectory);
- if (setupAction != null)
+ if (keyDirectory != null)
{
- setupAction(builder);
+ builder.PersistKeysToFileSystem(keyDirectory);
}
+#if !NETSTANDARD1_3 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+ if (certificate != null)
+ {
+ builder.ProtectKeysWithCertificate(certificate);
+ }
+#endif
+
+ setupAction(builder);
+
// extract the provider instance from the service collection
return serviceCollection.BuildServiceProvider().GetRequiredService();
}
diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
index 5e882c70c8..baf03a742f 100644
--- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
+++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
@@ -3,6 +3,8 @@
using System;
using System.IO;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.DataProtection.Test.Shared;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
@@ -35,6 +37,47 @@ namespace Microsoft.AspNetCore.DataProtection
});
}
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfLocalAppDataAvailable]
+ [ConditionalRunTestOnlyOnWindows]
+ public void System_NoKeysDirectoryProvided_UsesDefaultKeysDirectory()
+ {
+ var keysPath = Path.Combine(Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%"), "ASP.NET", "DataProtection-Keys");
+ var tempPath = Path.Combine(Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%"), "ASP.NET", "DataProtection-KeysTemp");
+
+ 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 and that it's protected using Windows DPAPI.
+ 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);
+ 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);
+ }
+ finally
+ {
+ if (Directory.Exists(keysPath))
+ {
+ Directory.Delete(keysPath, recursive: true);
+ }
+ if (Directory.Exists(tempPath))
+ {
+ Directory.Move(tempPath, keysPath);
+ }
+ }
+ }
+
[ConditionalFact]
[ConditionalRunTestOnlyIfLocalAppDataAvailable]
[ConditionalRunTestOnlyOnWindows]
@@ -63,6 +106,51 @@ namespace Microsoft.AspNetCore.DataProtection
});
}
+#if !NETSTANDARDAPP1_5 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfLocalAppDataAvailable]
+ [ConditionalRunTestOnlyOnWindows]
+ 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"));
+ 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();
+ }
+ });
+ }
+#endif
+
///
/// Runs a test and cleans up the temp directory afterward.
///
@@ -90,5 +178,26 @@ namespace Microsoft.AspNetCore.DataProtection
public string SkipReason { get; } = "%LOCALAPPDATA% couldn't be located.";
}
+
+ 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);
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx
new file mode 100644
index 0000000000..266754e8ee
Binary files /dev/null and b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx differ