From 22927ec289880e9c259f237ff69b18ccac04af22 Mon Sep 17 00:00:00 2001 From: Levi B Date: Tue, 17 Mar 2015 15:04:59 -0700 Subject: [PATCH] Add simple file-based provider instantiation APIs --- .../DataProtectionProvider.cs | 60 ++++++++++++ ...er.cs => DataProtectionProviderFactory.cs} | 31 +------ .../DataProtectionServiceDescriptors.cs | 2 +- .../DataProtectionProviderTests.cs | 92 +++++++++++++++++++ 4 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionProvider.cs rename src/Microsoft.AspNet.DataProtection/{DataProtectionProvider.cs => DataProtectionProviderFactory.cs} (64%) create mode 100644 test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionProviderTests.cs diff --git a/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionProvider.cs new file mode 100644 index 0000000000..badb814072 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection.Extensions/DataProtectionProvider.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.DataProtection +{ + /// + /// A simple implementation of an where keys are stored + /// at a particular location on the file system. + /// + public sealed class DataProtectionProvider : IDataProtectionProvider + { + private readonly IDataProtectionProvider _innerProvider; + + /// + /// Creates an given a location at which to store keys. + /// + /// The in which keys should be stored. This may + /// represent a directory on a local disk or a UNC share. + public DataProtectionProvider([NotNull] DirectoryInfo keyDirectory) + : this(keyDirectory, configure: null) + { + } + + /// + /// Creates an given a location at which to store keys and an + /// optional configuration callback. + /// + /// 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. + public DataProtectionProvider([NotNull] DirectoryInfo keyDirectory, Action configure) + { + // build the service collection + ServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection.AddDataProtection(); + serviceCollection.ConfigureDataProtection(configurationObject => + { + configurationObject.PersistKeysToFileSystem(keyDirectory); + configure?.Invoke(configurationObject); + }); + + // extract the provider instance from the service collection + _innerProvider = serviceCollection.BuildServiceProvider().GetRequiredService(); + } + + /// + /// Implements . + /// + public IDataProtector CreateProtector([NotNull] string purpose) + { + return _innerProvider.CreateProtector(purpose); + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionProviderFactory.cs similarity index 64% rename from src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs rename to src/Microsoft.AspNet.DataProtection/DataProtectionProviderFactory.cs index de61cdd9f7..d08d326539 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionProviderFactory.cs @@ -12,37 +12,8 @@ namespace Microsoft.AspNet.DataProtection /// /// Contains static factory methods for creating instances. /// - public static class DataProtectionProvider + internal static class DataProtectionProviderFactory { - /// - /// Creates an ephemeral . - /// - /// An ephemeral . - /// - /// Payloads generated by any given instance of an - /// can only be unprotected by that same provider instance. Once an instance of an ephemeral - /// provider is lost, all payloads generated by that provider are permanently undecipherable. - /// - public static EphemeralDataProtectionProvider CreateNewEphemeralProvider() - { - return CreateNewEphemeralProvider(services: null); - } - - /// - /// Creates an ephemeral . - /// - /// Optional services (such as logging) for use by the provider. - /// An ephemeral . - /// - /// Payloads generated by any given instance of an - /// can only be unprotected by that same provider instance. Once an instance of an ephemeral - /// provider is lost, all payloads generated by that provider are permanently undecipherable. - /// - public static EphemeralDataProtectionProvider CreateNewEphemeralProvider(IServiceProvider services) - { - return new EphemeralDataProtectionProvider(services); - } - /// /// Creates an given an . /// diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs index f7a8c3da0b..280d0d63d8 100644 --- a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs +++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs @@ -85,7 +85,7 @@ namespace Microsoft.Framework.DependencyInjection public static ServiceDescriptor IDataProtectionProvider_Default() { return ServiceDescriptor.Singleton( - services => DataProtectionProvider.GetProviderFromServices( + services => DataProtectionProviderFactory.GetProviderFromServices( options: services.GetRequiredService>().Options, services: services, mustCreateImmediately: true /* this is the ultimate fallback */)); diff --git a/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionProviderTests.cs new file mode 100644 index 0000000000..3420dd030a --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Extensions.Test/DataProtectionProviderTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNet.DataProtection +{ + public class DataProtectionProviderTests + { + [ConditionalFact] + [ConditionalRunTestOnlyIfLocalAppDataAvailable] + 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 = new DataProtectionProvider(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); + }); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfLocalAppDataAvailable] + 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 = new DataProtectionProvider(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); + }); + } + + /// + /// 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 class ConditionalRunTestOnlyIfLocalAppDataAvailable : Attribute, ITestCondition + { + public bool IsMet => (Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) != null); + + public string SkipReason { get; } = "%LOCALAPPDATA% couldn't be located."; + } + } +}