From 1fa389d9cd186465bfb6a0a1f9169079a25a6db6 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Thu, 31 Mar 2016 15:41:24 -0700 Subject: [PATCH] [Fixes #130] Added few DataProtectionProvider.Create overloads --- .../DataProtectionProvider.cs | 127 +++++++++++++++++- .../DataProtectionProviderTests.cs | 109 +++++++++++++++ .../TestFiles/TestCert.pfx | Bin 0 -> 2486 bytes 3 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx diff --git a/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs b/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs index cedcc2bd..58972aa4 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 @@ public static class DataProtectionProvider /// 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 @@ public static IDataProtectionProvider Create( { 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 5e882c70..baf03a74 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 @@ public void System_UsesProvidedDirectory() }); } + [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 @@ public void System_UsesProvidedDirectory_WithConfigurationCallback() }); } +#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 @@ private class ConditionalRunTestOnlyIfLocalAppDataAvailable : Attribute, ITestCo 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 0000000000000000000000000000000000000000..266754e8eeb5830fa41d542dc12c9d1b46766212 GIT binary patch literal 2486 zcmaJ@2{=^m8b7l#mWhyEZgq!<&J06n5h~deN|u_8t+MZ1e{(RBLW%rQ_GFFlFXEEK zWXqNYk+D^>jCCx@J$3Kx{_perpZlEip6`8s@B91S?|IMneD4QBxTi=23WRX(Seyb~ zpU&Y&a3L}w+(k5mvw`7rAcO_}$AwuyLzr0*!hC^c63hE<79S4+nF*m+K?uDFN@02a z1H+T!(cGd+V~lq`1~ep+s|yXGyDU}jafb?be$G%__vQK{X;R%_&zfXsrDW`OgbJ_1 zN9eaRv$RCh%EORQYlmy!bVlxT?#^l17+pP9!d!`YcCM=vL=VW4OPdSQcgB#7E(_Fb zNot8yX~)ESEh8EC*6fCBWg8h2egVZN@FYnCyTx{gD_N5-z2xb7rqgPufs&0vLie;T zJ$F5h6ja>&l-pNza>`qiHEVbFr%@H0z>?`>Ngv7v5OB?b2T?cUB;7caH7Z zJi=7VIQ0It27`ahVcA-yGo2O3Wv%0Wy)xQ+?5?-J;8Fs~Atpr47XOHfDP)>hzx7#c zQ$I!xPkCafPn159ctlRbzA!2fJ zz{x%CS_yZ8uktP_>}buD6>ep=t}wUfw61=>Ywz(T^;D4cbweZmv6U~I&7w8F-`(2F zgByd`CrN&OuRnxtP6{V0=~Bjyk2$Yg z=D{H&@05)c@fUI-Bg0!06!t5CXX4yLRO%GB&srunX>{}DI0y7IX7rvFpJ=sl z{@DHZjwp@w12>J`YHgbMB2)5w7h3XOi+qYNsu+~u`uM=@gzSD-4knQ%=I5e!GV5zbHb9xCUs5r}1)nmwKjl^}25m6{o z^jUE?hARH~XlQ88M)QF{o#9@owArNQ3R z)}eUwH&hY%8Tb2FGhgPNNH#kh(M93J+-jc+A9 zHub)D_Az_LpUy??Eg49#s9q?k_B6;F-@o(mtbB*hX_qnmGxsGjI1yvUI-kOdYM7ZlQHxT1Faf!ShLFo3gk*yd z;zK41i9jL+^lQU5LK^lyF}-Ns{N~L~I*7w!6!9peGzI{Lh=Sk_EXop%65_US^zqeo z^!5d1MEQwopejfvseo##Y8OQLRWyhs75F7rO0r`Ke9)Z9BC)awry6WtOo7a!+1gT$&W~0pV|W znF-pi_4bhbLFqW@UMuvu7ge&hMYibeYV9dkZBwf-Z=F1gDB1a!Q3F^W1mPVkDfLh@KsyuqWBs=Zy7v|1G%k+5TERnygF7*F?B3x$#AhA7q zJ*_`1kbL(`Qb5OIZn!9W_WahRi;7S!cPam-X?e9$Vaupaz02t$qY}Y5o~+P(HJz>d zQ-SS+d?p7f&ctq4B!_h#$1K<_TxqgaSUgly&G(4d8@pAuGCeid1C25-(LFhl_~6SM zbqXhYWXUzMEn7_vcp8)ZcB|Rt>dJdB*FE7DtlU`Apy9gHxj?>l8?i^a@As0Yr8dYO zj36BQB|`v54jYL+EuM