Просмотр исходного кода

[Fixes #130] Added few DataProtectionProvider.Create overloads

Ajay Bhargav Baaskaran 10 лет назад
Родитель
Сommit
5654310a68

+ 123 - 4
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
     /// <remarks>Use these methods when not using dependency injection to provide the service to the application.</remarks>
     public static class DataProtectionProvider
     {
+        /// <summary>
+        /// Creates a <see cref="DataProtectionProvider"/> that store keys in a location based on
+        /// the platform and operating system.
+        /// </summary>
+        /// <param name="applicationName">An identifier that uniquely discriminates this application from all other
+        /// applications on the machine.</param>
+        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);
+        }
+
         /// <summary>
         /// Creates an <see cref="DataProtectionProvider"/> given a location at which to store keys.
         /// </summary>
@@ -21,7 +41,12 @@ namespace Microsoft.AspNetCore.DataProtection
         /// represent a directory on a local disk or a UNC share.</param>
         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);
         }
 
         /// <summary>
@@ -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
+        /// <summary>
+        /// Creates a <see cref="DataProtectionProvider"/> that store keys in a location based on
+        /// the platform and operating system and uses the given <see cref="X509Certificate2"/> to encrypt the keys.
+        /// </summary>
+        /// <param name="applicationName">An identifier that uniquely discriminates this application from all other
+        /// applications on the machine.</param>
+        /// <param name="certificate">The <see cref="X509Certificate2"/> to be used for encryption.</param>
+        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);
+        }
+
+        /// <summary>
+        /// Creates an <see cref="DataProtectionProvider"/> given a location at which to store keys
+        /// and a <see cref="X509Certificate2"/> used to encrypt the keys.
+        /// </summary>
+        /// <param name="keyDirectory">The <see cref="DirectoryInfo"/> in which keys should be stored. This may
+        /// represent a directory on a local disk or a UNC share.</param>
+        /// <param name="certificate">The <see cref="X509Certificate2"/> to be used for encryption.</param>
+        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);
+        }
+
+        /// <summary>
+        /// Creates an <see cref="DataProtectionProvider"/> given a location at which to store keys, an
+        /// optional configuration callback and a <see cref="X509Certificate2"/> used to encrypt the keys.
+        /// </summary>
+        /// <param name="keyDirectory">The <see cref="DirectoryInfo"/> in which keys should be stored. This may
+        /// represent a directory on a local disk or a UNC share.</param>
+        /// <param name="setupAction">An optional callback which provides further configuration of the data protection
+        /// system. See <see cref="IDataProtectionBuilder"/> for more information.</param>
+        /// <param name="certificate">The <see cref="X509Certificate2"/> to be used for encryption.</param>
+        public static IDataProtectionProvider Create(
+            DirectoryInfo keyDirectory,
+            Action<IDataProtectionBuilder> 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<IDataProtectionBuilder> 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<IDataProtectionProvider>();
         }

+ 109 - 0
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
+
         /// <summary>
         /// Runs a test and cleans up the temp directory afterward.
         /// </summary>
@@ -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);
+            }
+        }
     }
 }

BIN
test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx