From fb2f89ed5194fac3ed2bfa1e816d310347cb462c Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 1 May 2018 14:13:26 -0700 Subject: [PATCH] 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. --- .../DataProtectionProvider.cs | 2 +- .../KeyManagement/XmlKeyManager.cs | 16 ++- .../Properties/AssemblyInfo.cs | 1 + .../DefaultKeyStorageDirectories.cs | 112 ++++++++++++++++++ .../Repositories/FileSystemXmlRepository.cs | 92 +------------- .../IDefaultKeyStorageDirectory.cs | 17 +++ .../DataProtectionProviderTests.cs | 74 ++++++------ .../TimeLimitedDataProtectorTests.cs | 7 +- 8 files changed, 187 insertions(+), 134 deletions(-) create mode 100644 src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs diff --git a/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs b/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs index 7b080a9a87..cc82fe9ef8 100644 --- a/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs +++ b/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs @@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.DataProtection return CreateProvider(keyDirectory, setupAction, certificate); } - private static IDataProtectionProvider CreateProvider( + internal static IDataProtectionProvider CreateProvider( DirectoryInfo keyDirectory, Action setupAction, X509Certificate2 certificate) diff --git a/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs b/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs index 66e7a96dcb..06baad13ed 100644 --- a/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs +++ b/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs @@ -53,6 +53,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly IEnumerable _encryptorFactories; + private readonly IDefaultKeyStorageDirectories _keyStorageDirectories; private CancellationTokenSource _cacheExpirationTokenSource; @@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement /// The instance that provides the configuration. /// The . public XmlKeyManager(IOptions keyManagementOptions, IActivator activator) - : this (keyManagementOptions, activator, NullLoggerFactory.Instance) + : this(keyManagementOptions, activator, NullLoggerFactory.Instance) { } /// @@ -72,9 +73,18 @@ namespace Microsoft.AspNetCore.DataProtection.KeyManagement /// The . /// The . public XmlKeyManager(IOptions keyManagementOptions, IActivator activator, ILoggerFactory loggerFactory) + : this(keyManagementOptions, activator, loggerFactory, DefaultKeyStorageDirectories.Instance) + { } + + internal XmlKeyManager( + IOptions keyManagementOptions, + IActivator activator, + ILoggerFactory loggerFactory, + IDefaultKeyStorageDirectories keyStorageDirectories) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _logger = _loggerFactory.CreateLogger(); + _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()) diff --git a/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs index 7816360b8b..614112bd73 100644 --- a/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs b/src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs new file mode 100644 index 0000000000..a0717263fb --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs @@ -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 _defaultDirectoryLazy = new Lazy(GetKeyStorageDirectoryImpl); + + private DefaultKeyStorageDirectories() + { + } + + public static IDefaultKeyStorageDirectories Instance { get; } = new DefaultKeyStorageDirectories(); + + /// + /// 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". + /// + /// + /// 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. + /// + 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)); + } + } +} diff --git a/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs b/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs index 914cc3f9ba..7ceede33d1 100644 --- a/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs +++ b/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs @@ -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 /// public class FileSystemXmlRepository : IXmlRepository { - private static readonly Lazy _defaultDirectoryLazy = new Lazy(GetDefaultKeyStorageDirectory); - private readonly ILogger _logger; /// @@ -29,12 +25,8 @@ namespace Microsoft.AspNetCore.DataProtection.Repositories /// The . 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(); 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. /// - public static DirectoryInfo DefaultKeyStorageDirectory => _defaultDirectoryLazy.Value; + public static DirectoryInfo DefaultKeyStorageDirectory => DefaultKeyStorageDirectories.Instance.GetKeyStorageDirectory(); /// /// The directory into which key material will be written. /// 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 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. diff --git a/src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs b/src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs new file mode 100644 index 0000000000..e7e1410e79 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs @@ -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 +{ + /// + /// This interface enables overridding the default storage location of keys on disk + /// + internal interface IDefaultKeyStorageDirectories + { + DirectoryInfo GetKeyStorageDirectory(); + + DirectoryInfo GetKeyStorageDirectoryForAzureWebSites(); + } +} diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs index 7c7bcf9c36..40e470ea58 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs +++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs @@ -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(); + 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(s => + new XmlKeyManager( + s.GetRequiredService>(), + s.GetRequiredService(), + 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); } } diff --git a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs index 6f71977154..47dfc26fd7 100644 --- a/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs +++ b/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs @@ -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]