Factor out internal interface for default directory testing

Create an internal abstraction for finding the default directories for key storage. This allows us to run tests without squashing on keys on the developer machine. It also allows us to isolate test runs from reach other.
This commit is contained in:
Nate McMaster 2018-05-01 14:13:26 -07:00
parent 16958c67bb
commit fb2f89ed51
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
8 changed files with 187 additions and 134 deletions

View File

@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.DataProtection
return CreateProvider(keyDirectory, setupAction, certificate);
}
private static IDataProtectionProvider CreateProvider(
internal static IDataProtectionProvider CreateProvider(
DirectoryInfo keyDirectory,
Action<IDataProtectionBuilder> setupAction,
X509Certificate2 certificate)

View File

@ -53,6 +53,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private readonly IEnumerable<IAuthenticatedEncryptorFactory> _encryptorFactories;
private readonly IDefaultKeyStorageDirectories _keyStorageDirectories;
private CancellationTokenSource _cacheExpirationTokenSource;
@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement
/// <param name="keyManagementOptions">The <see cref="IOptions{KeyManagementOptions}"/> instance that provides the configuration.</param>
/// <param name="activator">The <see cref="IActivator"/>.</param>
public XmlKeyManager(IOptions<KeyManagementOptions> keyManagementOptions, IActivator activator)
: this (keyManagementOptions, activator, NullLoggerFactory.Instance)
: this(keyManagementOptions, activator, NullLoggerFactory.Instance)
{ }
/// <summary>
@ -72,9 +73,18 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement
/// <param name="activator">The <see cref="IActivator"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public XmlKeyManager(IOptions<KeyManagementOptions> keyManagementOptions, IActivator activator, ILoggerFactory loggerFactory)
: this(keyManagementOptions, activator, loggerFactory, DefaultKeyStorageDirectories.Instance)
{ }
internal XmlKeyManager(
IOptions<KeyManagementOptions> keyManagementOptions,
IActivator activator,
ILoggerFactory loggerFactory,
IDefaultKeyStorageDirectories keyStorageDirectories)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_logger = _loggerFactory.CreateLogger<XmlKeyManager>();
_keyStorageDirectories = keyStorageDirectories ?? throw new ArgumentNullException(nameof(keyStorageDirectories));
KeyRepository = keyManagementOptions.Value.XmlRepository;
KeyEncryptor = keyManagementOptions.Value.XmlEncryptor;
@ -469,7 +479,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement
IXmlEncryptor encryptor = null;
// If we're running in Azure Web Sites, the key repository goes in the %HOME% directory.
var azureWebSitesKeysFolder = FileSystemXmlRepository.GetKeyStorageDirectoryForAzureWebSites();
var azureWebSitesKeysFolder = _keyStorageDirectories.GetKeyStorageDirectoryForAzureWebSites();
if (azureWebSitesKeysFolder != null)
{
_logger.UsingAzureAsKeyRepository(azureWebSitesKeysFolder.FullName);
@ -481,7 +491,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement
else
{
// If the user profile is available, store keys in the user profile directory.
var localAppDataKeysFolder = FileSystemXmlRepository.DefaultKeyStorageDirectory;
var localAppDataKeysFolder = _keyStorageDirectories.GetKeyStorageDirectory();
if (localAppDataKeysFolder != null)
{
if (OSVersionUtil.IsWindows())

View File

@ -4,5 +4,6 @@
using System.Runtime.CompilerServices;
// for unit testing
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -0,0 +1,112 @@
// Copyright (c) .NET Foundation. 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.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.AspNetCore.DataProtection.Repositories
{
internal sealed class DefaultKeyStorageDirectories : IDefaultKeyStorageDirectories
{
private static readonly Lazy<DirectoryInfo> _defaultDirectoryLazy = new Lazy<DirectoryInfo>(GetKeyStorageDirectoryImpl);
private DefaultKeyStorageDirectories()
{
}
public static IDefaultKeyStorageDirectories Instance { get; } = new DefaultKeyStorageDirectories();
/// <summary>
/// The default key storage directory.
/// On Windows, this currently corresponds to "Environment.SpecialFolder.LocalApplication/ASP.NET/DataProtection-Keys".
/// On Linux and macOS, this currently corresponds to "$HOME/.aspnet/DataProtection-Keys".
/// </summary>
/// <remarks>
/// This property can return null if no suitable default key storage directory can
/// be found, such as the case when the user profile is unavailable.
/// </remarks>
public DirectoryInfo GetKeyStorageDirectory() => _defaultDirectoryLazy.Value;
private static DirectoryInfo GetKeyStorageDirectoryImpl()
{
DirectoryInfo retVal;
// Environment.GetFolderPath returns null if the user profile isn't loaded.
var localAppDataFromSystemPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var localAppDataFromEnvPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
var userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE");
var homePath = Environment.GetEnvironmentVariable("HOME");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(localAppDataFromSystemPath))
{
// To preserve backwards-compatibility with 1.x, Environment.SpecialFolder.LocalApplicationData
// cannot take precedence over $LOCALAPPDATA and $HOME/.aspnet on non-Windows platforms
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromSystemPath);
}
else if (localAppDataFromEnvPath != null)
{
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromEnvPath);
}
else if (userProfilePath != null)
{
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(Path.Combine(userProfilePath, "AppData", "Local"));
}
else if (homePath != null)
{
// If LOCALAPPDATA and USERPROFILE are not present but HOME is,
// it's a good guess that this is a *NIX machine. Use *NIX conventions for a folder name.
retVal = new DirectoryInfo(Path.Combine(homePath, ".aspnet", DataProtectionKeysFolderName));
}
else if (!string.IsNullOrEmpty(localAppDataFromSystemPath))
{
// Starting in 2.x, non-Windows platforms may use Environment.SpecialFolder.LocalApplicationData
// but only after checking for $LOCALAPPDATA, $USERPROFILE, and $HOME.
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromSystemPath);
}
else
{
return null;
}
Debug.Assert(retVal != null);
try
{
retVal.Create(); // throws if we don't have access, e.g., user profile not loaded
return retVal;
}
catch
{
return null;
}
}
public DirectoryInfo GetKeyStorageDirectoryForAzureWebSites()
{
// Azure Web Sites needs to be treated specially, as we need to store the keys in a
// correct persisted location. We use the existence of the %WEBSITE_INSTANCE_ID% env
// variable to determine if we're running in this environment, and if so we then use
// the %HOME% variable to build up our base key storage path.
if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")))
{
var homeEnvVar = Environment.GetEnvironmentVariable("HOME");
if (!String.IsNullOrEmpty(homeEnvVar))
{
return GetKeyStorageDirectoryFromBaseAppDataPath(homeEnvVar);
}
}
// nope
return null;
}
private const string DataProtectionKeysFolderName = "DataProtection-Keys";
private static DirectoryInfo GetKeyStorageDirectoryFromBaseAppDataPath(string basePath)
{
return new DirectoryInfo(Path.Combine(basePath, "ASP.NET", DataProtectionKeysFolderName));
}
}
}

View File

@ -3,10 +3,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.Extensions.Logging;
@ -18,8 +16,6 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories
/// </summary>
public class FileSystemXmlRepository : IXmlRepository
{
private static readonly Lazy<DirectoryInfo> _defaultDirectoryLazy = new Lazy<DirectoryInfo>(GetDefaultKeyStorageDirectory);
private readonly ILogger _logger;
/// <summary>
@ -29,12 +25,8 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public FileSystemXmlRepository(DirectoryInfo directory, ILoggerFactory loggerFactory)
{
if (directory == null)
{
throw new ArgumentNullException(nameof(directory));
}
Directory = directory ?? throw new ArgumentNullException(nameof(directory));
Directory = directory;
_logger = loggerFactory.CreateLogger<FileSystemXmlRepository>();
try
@ -63,20 +55,13 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories
/// This property can return null if no suitable default key storage directory can
/// be found, such as the case when the user profile is unavailable.
/// </remarks>
public static DirectoryInfo DefaultKeyStorageDirectory => _defaultDirectoryLazy.Value;
public static DirectoryInfo DefaultKeyStorageDirectory => DefaultKeyStorageDirectories.Instance.GetKeyStorageDirectory();
/// <summary>
/// The directory into which key material will be written.
/// </summary>
public DirectoryInfo Directory { get; }
private const string DataProtectionKeysFolderName = "DataProtection-Keys";
private static DirectoryInfo GetKeyStorageDirectoryFromBaseAppDataPath(string basePath)
{
return new DirectoryInfo(Path.Combine(basePath, "ASP.NET", DataProtectionKeysFolderName));
}
public virtual IReadOnlyCollection<XElement> GetAllElements()
{
// forces complete enumeration
@ -99,79 +84,6 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories
}
}
private static DirectoryInfo GetDefaultKeyStorageDirectory()
{
DirectoryInfo retVal;
// Environment.GetFolderPath returns null if the user profile isn't loaded.
var localAppDataFromSystemPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var localAppDataFromEnvPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
var userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE");
var homePath = Environment.GetEnvironmentVariable("HOME");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(localAppDataFromSystemPath))
{
// To preserve backwards-compatibility with 1.x, Environment.SpecialFolder.LocalApplicationData
// cannot take precedence over $LOCALAPPDATA and $HOME/.aspnet on non-Windows platforms
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromSystemPath);
}
else if (localAppDataFromEnvPath != null)
{
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromEnvPath);
}
else if (userProfilePath != null)
{
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(Path.Combine(userProfilePath, "AppData", "Local"));
}
else if (homePath != null)
{
// If LOCALAPPDATA and USERPROFILE are not present but HOME is,
// it's a good guess that this is a *NIX machine. Use *NIX conventions for a folder name.
retVal = new DirectoryInfo(Path.Combine(homePath, ".aspnet", DataProtectionKeysFolderName));
}
else if (!string.IsNullOrEmpty(localAppDataFromSystemPath))
{
// Starting in 2.x, non-Windows platforms may use Environment.SpecialFolder.LocalApplicationData
// but only after checking for $LOCALAPPDATA, $USERPROFILE, and $HOME.
retVal = GetKeyStorageDirectoryFromBaseAppDataPath(localAppDataFromSystemPath);
}
else
{
return null;
}
Debug.Assert(retVal != null);
try
{
retVal.Create(); // throws if we don't have access, e.g., user profile not loaded
return retVal;
}
catch
{
return null;
}
}
internal static DirectoryInfo GetKeyStorageDirectoryForAzureWebSites()
{
// Azure Web Sites needs to be treated specially, as we need to store the keys in a
// correct persisted location. We use the existence of the %WEBSITE_INSTANCE_ID% env
// variable to determine if we're running in this environment, and if so we then use
// the %HOME% variable to build up our base key storage path.
if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")))
{
var homeEnvVar = Environment.GetEnvironmentVariable("HOME");
if (!String.IsNullOrEmpty(homeEnvVar))
{
return GetKeyStorageDirectoryFromBaseAppDataPath(homeEnvVar);
}
}
// nope
return null;
}
private static bool IsSafeFilename(string filename)
{
// Must be non-empty and contain only a-zA-Z0-9, hyphen, and underscore.

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
namespace Microsoft.AspNetCore.DataProtection.Repositories
{
/// <summary>
/// This interface enables overridding the default storage location of keys on disk
/// </summary>
internal interface IDefaultKeyStorageDirectories
{
DirectoryInfo GetKeyStorageDirectory();
DirectoryInfo GetKeyStorageDirectoryForAzureWebSites();
}
}

View File

@ -7,9 +7,16 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.DataProtection.Test.Shared;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.DataProtection
@ -42,49 +49,42 @@ namespace Microsoft.AspNetCore.DataProtection
[Fact]
public void System_NoKeysDirectoryProvided_UsesDefaultKeysDirectory()
{
Assert.NotNull(FileSystemXmlRepository.DefaultKeyStorageDirectory);
var mock = new Mock<IDefaultKeyStorageDirectories>();
var keysPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName());
mock.Setup(m => m.GetKeyStorageDirectory()).Returns(new DirectoryInfo(keysPath));
var keysPath = FileSystemXmlRepository.DefaultKeyStorageDirectory.FullName;
var tempPath = FileSystemXmlRepository.DefaultKeyStorageDirectory.FullName + Path.GetRandomFileName();
try
// Step 1: Instantiate the system and round-trip a payload
var provider = DataProtectionProvider.CreateProvider(
keyDirectory: null,
certificate: null,
setupAction: builder =>
{
// Step 1: Move the current contents, if any, to a temporary directory.
if (Directory.Exists(keysPath))
{
Directory.Move(keysPath, tempPath);
}
builder.SetApplicationName("TestApplication");
builder.Services.AddSingleton<IKeyManager>(s =>
new XmlKeyManager(
s.GetRequiredService<IOptions<KeyManagementOptions>>(),
s.GetRequiredService<IActivator>(),
NullLoggerFactory.Instance,
mock.Object));
});
// 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")));
var protector = provider.CreateProtector("Protector");
Assert.Equal("payload", protector.Unprotect(protector.Protect("payload")));
// Step 3: Validate that there's now a single key in the directory
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);
// On Windows, validate that it's protected using Windows DPAPI.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
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);
}
else
{
Assert.Contains("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal);
}
// Step 2: Validate that there's now a single key in the directory
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);
// On Windows, validate that it's protected using Windows DPAPI.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
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
else
{
if (Directory.Exists(keysPath))
{
Directory.Delete(keysPath, recursive: true);
}
if (Directory.Exists(tempPath))
{
Directory.Move(tempPath, keysPath);
}
Assert.Contains("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal);
}
}

View File

@ -4,13 +4,14 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using Microsoft.AspNetCore.DataProtection.Extensions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using ExtResources = Microsoft.AspNetCore.DataProtection.Extensions.Resources;
namespace Microsoft.AspNetCore.DataProtection
{
public class TimeLimitedDataProtectorTests
{
private const string TimeLimitedPurposeString = "Microsoft.AspNetCore.DataProtection.TimeLimitedDataProtector.v1";
@ -106,7 +107,7 @@ namespace Microsoft.AspNetCore.DataProtection
=> timeLimitedProtector.UnprotectCore(new byte[] { 0x10, 0x11 }, now, out var _));
// Assert
Assert.Equal(Resources.FormatTimeLimitedDataProtector_PayloadExpired(expectedExpiration), ex.Message);
Assert.Equal(ExtResources.FormatTimeLimitedDataProtector_PayloadExpired(expectedExpiration), ex.Message);
}
[Fact]
@ -127,7 +128,7 @@ namespace Microsoft.AspNetCore.DataProtection
=> timeLimitedProtector.Unprotect(new byte[] { 0x10, 0x11 }, out var _));
// Assert
Assert.Equal(Resources.TimeLimitedDataProtector_PayloadInvalid, ex.Message);
Assert.Equal(ExtResources.TimeLimitedDataProtector_PayloadInvalid, ex.Message);
}
[Fact]