Add simple file-based provider instantiation APIs

This commit is contained in:
Levi B 2015-03-17 15:04:59 -07:00
parent 84490846b6
commit 22927ec289
4 changed files with 154 additions and 31 deletions

View File

@ -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
{
/// <summary>
/// A simple implementation of an <see cref="IDataProtectionProvider"/> where keys are stored
/// at a particular location on the file system.
/// </summary>
public sealed class DataProtectionProvider : IDataProtectionProvider
{
private readonly IDataProtectionProvider _innerProvider;
/// <summary>
/// Creates an <see cref="DataProtectionProvider"/> given a location at which to store 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>
public DataProtectionProvider([NotNull] DirectoryInfo keyDirectory)
: this(keyDirectory, configure: null)
{
}
/// <summary>
/// Creates an <see cref="DataProtectionProvider"/> given a location at which to store keys and an
/// optional configuration callback.
/// </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="configure">An optional callback which provides further configuration of the data protection
/// system. See <see cref="DataProtectionConfiguration"/> for more information.</param>
public DataProtectionProvider([NotNull] DirectoryInfo keyDirectory, Action<DataProtectionConfiguration> 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<IDataProtectionProvider>();
}
/// <summary>
/// Implements <see cref="IDataProtectionProvider.CreateProtector(string)"/>.
/// </summary>
public IDataProtector CreateProtector([NotNull] string purpose)
{
return _innerProvider.CreateProtector(purpose);
}
}
}

View File

@ -12,37 +12,8 @@ namespace Microsoft.AspNet.DataProtection
/// <summary>
/// Contains static factory methods for creating <see cref="IDataProtectionProvider"/> instances.
/// </summary>
public static class DataProtectionProvider
internal static class DataProtectionProviderFactory
{
/// <summary>
/// Creates an ephemeral <see cref="IDataProtectionProvider"/>.
/// </summary>
/// <returns>An ephemeral <see cref="IDataProtectionProvider"/>.</returns>
/// <remarks>
/// Payloads generated by any given instance of an <see cref="EphemeralDataProtectionProvider"/>
/// 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.
/// </remarks>
public static EphemeralDataProtectionProvider CreateNewEphemeralProvider()
{
return CreateNewEphemeralProvider(services: null);
}
/// <summary>
/// Creates an ephemeral <see cref="IDataProtectionProvider"/>.
/// </summary>
/// <param name="services">Optional services (such as logging) for use by the provider.</param>
/// <returns>An ephemeral <see cref="IDataProtectionProvider"/>.</returns>
/// <remarks>
/// Payloads generated by any given instance of an <see cref="EphemeralDataProtectionProvider"/>
/// 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.
/// </remarks>
public static EphemeralDataProtectionProvider CreateNewEphemeralProvider(IServiceProvider services)
{
return new EphemeralDataProtectionProvider(services);
}
/// <summary>
/// Creates an <see cref="IDataProtectionProvider"/> given an <see cref="IServiceProvider"/>.
/// </summary>

View File

@ -85,7 +85,7 @@ namespace Microsoft.Framework.DependencyInjection
public static ServiceDescriptor IDataProtectionProvider_Default()
{
return ServiceDescriptor.Singleton<IDataProtectionProvider>(
services => DataProtectionProvider.GetProviderFromServices(
services => DataProtectionProviderFactory.GetProviderFromServices(
options: services.GetRequiredService<IOptions<DataProtectionOptions>>().Options,
services: services,
mustCreateImmediately: true /* this is the ultimate fallback */));

View File

@ -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);
});
}
/// <summary>
/// Runs a test and cleans up the temp directory afterward.
/// </summary>
private static void WithUniqueTempDirectory(Action<DirectoryInfo> 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.";
}
}
}