Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.azure/pipelines/fast-pr-validation.yml11
-rw-r--r--.gitmodules4
-rw-r--r--build/RepositoryBuild.targets3
-rw-r--r--build/buildorder.props11
-rw-r--r--build/dependencies.props2
-rw-r--r--build/submodules.props2
m---------modules/DataProtection7
-rw-r--r--run.ps115
-rwxr-xr-xrun.sh17
-rw-r--r--src/DataProtection/DataProtection.sln333
-rw-r--r--src/DataProtection/Directory.Build.props8
-rw-r--r--src/DataProtection/NuGetPackageVerifier.json7
-rw-r--r--src/DataProtection/Provision-AutoGenKeys.ps1117
-rw-r--r--src/DataProtection/README.md8
-rw-r--r--src/DataProtection/build.cmd3
-rwxr-xr-xsrc/DataProtection/build.sh7
-rw-r--r--src/DataProtection/build/repo.props10
-rw-r--r--src/DataProtection/dependencies.props33
-rw-r--r--src/DataProtection/samples/AzureBlob/AzureBlob.csproj19
-rw-r--r--src/DataProtection/samples/AzureBlob/Program.cs43
-rw-r--r--src/DataProtection/samples/AzureKeyVault/AzureKeyVault.csproj20
-rw-r--r--src/DataProtection/samples/AzureKeyVault/Program.cs44
-rw-r--r--src/DataProtection/samples/AzureKeyVault/settings.json5
-rw-r--r--src/DataProtection/samples/CustomEncryptorSample/CustomBuilderExtensions.cs31
-rw-r--r--src/DataProtection/samples/CustomEncryptorSample/CustomEncryptorSample.csproj18
-rw-r--r--src/DataProtection/samples/CustomEncryptorSample/CustomXmlDecryptor.cs32
-rw-r--r--src/DataProtection/samples/CustomEncryptorSample/CustomXmlEncryptor.cs38
-rw-r--r--src/DataProtection/samples/CustomEncryptorSample/Program.cs36
-rw-r--r--src/DataProtection/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj18
-rw-r--r--src/DataProtection/samples/EntityFrameworkCoreSample/Program.cs48
-rw-r--r--src/DataProtection/samples/KeyManagementSample/KeyManagementSample.csproj13
-rw-r--r--src/DataProtection/samples/KeyManagementSample/Program.cs66
-rw-r--r--src/DataProtection/samples/NonDISample/NonDISample.csproj12
-rw-r--r--src/DataProtection/samples/NonDISample/Program.cs41
-rw-r--r--src/DataProtection/samples/Redis/Program.cs35
-rw-r--r--src/DataProtection/samples/Redis/Redis.csproj18
-rw-r--r--src/DataProtection/shared/EncodingUtil.cs14
-rw-r--r--src/DataProtection/shared/ExceptionExtensions.cs20
-rw-r--r--src/DataProtection/src/Directory.Build.props7
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs38
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs46
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBuffer.cs17
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBufferDesc.cs26
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptEncryptFlags.cs13
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptGenRandomFlags.cs15
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptKeyDerivationBufferType.cs29
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptUtil.cs29
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs93
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/NCryptEncryptFlags.cs17
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/OSVersionUtil.cs66
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Constants.cs88
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/CryptoUtil.cs99
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/DATA_BLOB.cs16
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Microsoft.AspNetCore.Cryptography.Internal.csproj12
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/AssemblyInfo.cs16
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/Resources.Designer.cs86
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Resources.resx132
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs170
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHandle.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHashHandle.cs71
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptKeyHandle.cs33
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/LocalAllocHandle.cs26
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs42
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SafeLibraryHandle.cs176
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs61
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeBufferUtil.cs179
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeNativeMethods.cs346
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/WeakReferenceHelpers.cs59
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/baseline.netcore.json4
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivation.cs56
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs26
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj15
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/IPbkdf2Provider.cs15
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/ManagedPbkdf2Provider.cs103
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs71
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs46
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win7Pbkdf2Provider.cs100
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win8Pbkdf2Provider.cs211
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Properties/AssemblyInfo.cs6
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/baseline.netcore.json78
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/CryptoUtil.cs33
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/DataProtectionCommonExtensions.cs244
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Error.cs23
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtectionProvider.cs26
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtector.cs28
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Infrastructure/IApplicationDiscriminator.cs25
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Microsoft.AspNetCore.DataProtection.Abstractions.csproj21
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/AssemblyInfo.cs7
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/Resources.Designer.cs86
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Resources.resx132
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/baseline.netcore.json231
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs118
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs52
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs77
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs14
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs29
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj20
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs9
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureBlobXmlRepository.cs297
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureDataProtectionBuilderExtensions.cs175
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj19
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/baseline.netcore.json156
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs26
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs39
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs81
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs18
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs31
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj23
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json203
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/BitHelpers.cs42
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionAdvancedExtensions.cs169
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs178
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/ITimeLimitedDataProtector.cs55
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Microsoft.AspNetCore.DataProtection.Extensions.csproj22
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/AssemblyInfo.cs6
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/Resources.Designer.cs72
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Resources.resx129
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/TimeLimitedDataProtector.cs149
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/baseline.netcore.json298
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj19
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisDataProtectionBuilderExtensions.cs79
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisXmlRepository.cs59
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/CompatibilityDataProtector.cs133
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/DataProtectionStartup.cs102
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Microsoft.AspNetCore.DataProtection.SystemWeb.csproj25
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Properties/Resources.Designer.cs58
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Resources.resx126
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/baseline.netframework.json157
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/web.config.transform14
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ActivatorExtensions.cs43
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ApplyPolicyAttribute.cs13
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ArraySegmentExtensions.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs55
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs55
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs182
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactory.cs127
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactory.cs92
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AlgorithmConfiguration.cs20
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs59
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs59
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs48
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs100
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs68
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs48
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs76
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs60
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs44
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs20
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAlgorithmConfiguration.cs27
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs108
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs91
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs77
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs63
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs57
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs52
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.cs36
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorFactory.cs22
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IOptimizedAuthenticatedEncryptor.cs42
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactory.cs132
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs24
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/BitHelpers.cs59
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/BCryptGenRandomImpl.cs22
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/CbcAuthenticatedEncryptor.cs422
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/DpapiSecretSerializerHelper.cs356
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/GcmAuthenticatedEncryptor.cs289
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/IBCryptGenRandom.cs12
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/Internal/CngAuthenticatedEncryptorBase.cs87
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs628
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionOptions.cs25
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs106
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionUtilityExtensions.cs46
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/EphemeralDataProtectionProvider.cs124
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Error.cs95
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IDataProtectionBuilder.cs43
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IPersistedDataProtector.cs36
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IRegistryPolicyResolver.cs13
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ISecret.cs27
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionBuilder.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionOptionsSetup.cs23
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionStartupFilter.cs48
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DockerUtils.cs99
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/HostingApplicationDiscriminator.cs25
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/IActivator.cs20
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/KeyManagementOptionsSetup.cs78
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DefaultKeyResolver.cs146
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DeferredKey.cs55
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKey.cs60
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyEscrowSink.cs27
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyManager.cs77
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/CacheableKeyRing.cs52
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/DefaultKeyResolution.cs36
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/ICacheableKeyRingProvider.cs12
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IDefaultKeyResolver.cs19
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IInternalXmlKeyManager.cs18
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRing.cs36
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRingProvider.cs10
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Key.cs33
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyBase.cs78
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs42
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyExtensions.cs15
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyManagementOptions.cs168
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRing.cs92
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs35
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs396
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingProvider.cs257
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs564
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingExtensions.cs804
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingServiceProviderExtensions.cs43
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/HashAlgorithmExtensions.cs19
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/IManagedGenRandom.cs12
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs375
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedGenRandomImpl.cs25
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/SymmetricAlgorithmExtensions.cs19
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/MemoryProtection.cs42
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Microsoft.AspNetCore.DataProtection.csproj31
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs9
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/Resources.Designer.cs394
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicy.cs28
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicyResolver.cs140
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs112
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/EphemeralXmlRepository.cs60
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs155
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs17
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IXmlRepository.cs37
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/RegistryXmlRepository.cs160
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Resources.resx198
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs12
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs66
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs37
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs64
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs80
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs108
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Secret.cs284
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SimpleActivator.cs56
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeExtensions.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs69
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlConstants.cs39
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateResolver.cs63
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs147
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs35
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs91
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs114
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs74
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs90
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs162
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlInfo.cs56
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/ICertificateResolver.cs20
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.cs17
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs16
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlDecryptor.cs24
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlEncryptor.cs28
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlDecryptor.cs36
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlEncryptor.cs65
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs185
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs39
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlExtensions.cs30
-rw-r--r--src/DataProtection/src/Microsoft.AspNetCore.DataProtection/baseline.netcore.json3071
-rw-r--r--src/DataProtection/test/CreateTestCert.ps114
-rw-r--r--src/DataProtection/test/Directory.Build.props19
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs33
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs58
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs61
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs189
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/CryptoUtilTests.cs54
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Microsoft.AspNetCore.Cryptography.Internal.Test.csproj16
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Properties/AssemblyInfo.cs7
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs31
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs162
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/WeakReferenceHelpersTests.cs84
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test.csproj17
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs196
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Properties/AssemblyInfo.cs7
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/DataProtectionCommonExtensionsTests.cs313
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/Microsoft.AspNetCore.DataProtection.Abstractions.Test.csproj16
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs78
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj17
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureBlobXmlRepositoryTests.cs111
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureDataProtectionBuilderExtensionsTest.cs32
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test.csproj17
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs69
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs14
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs26
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj15
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionAdvancedExtensionsTests.cs101
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs313
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj17
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Properties/AssemblyInfo.cs8
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfxbin0 -> 2486 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfxbin0 -> 2670 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfxbin0 -> 2429 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfxbin0 -> 1040 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfxbin0 -> 968 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs180
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/X509StoreIsAvailableAttribute.cs43
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/DataProtectionRedisTests.cs110
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test.csproj28
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/RedisDataProtectionBuilderExtensionsTest.cs32
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServer.cs28
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServerIsAvailableAttribute.cs15
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/testconfig.json10
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ActivatorTests.cs117
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AnonymousImpersonation.cs92
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactoryTest.cs52
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactoryTest.cs52
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs59
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs189
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs40
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs66
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs69
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs40
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs63
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs64
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs40
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs101
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs115
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactoryTest.cs50
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs120
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs110
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs109
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DataProtectionUtilityExtensionsTests.cs87
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DockerUtilsTests.cs56
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/EphemeralDataProtectionProviderTests.cs65
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/HostingTests.cs104
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Internal/KeyManagementOptionsSetupTest.cs154
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs61
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs277
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DeferredKeyTests.cs84
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs90
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs500
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs652
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingTests.cs126
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyTests.cs50
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs770
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs112
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj22
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/MockExtensions.cs62
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Properties/AssemblyInfo.cs7
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/RegistryPolicyResolverTests.cs314
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.cs40
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs182
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs168
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SP800_108/SP800_108Tests.cs173
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretAssert.cs45
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretTests.cs269
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SequentialGenRandom.cs32
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ServiceCollectionTests.cs67
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/StringLoggerFactory.cs82
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cerbin0 -> 796 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfxbin0 -> 2670 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfxbin0 -> 2662 bytes
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs172
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlAssert.cs151
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.cs61
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.cs34
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.cs61
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/EncryptedXmlDecryptorTests.cs91
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.cs39
-rw-r--r--src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs235
-rw-r--r--src/DataProtection/test/shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs16
-rw-r--r--src/DataProtection/test/shared/ConditionalRunTestOnlyWindowsAttribute.cs16
-rw-r--r--src/DataProtection/test/shared/ExceptionAssert2.cs23
-rw-r--r--src/DataProtection/version.props12
-rw-r--r--test/Cli.FunctionalTests/Templates/RazorBootstrapJQueryTemplate.cs2
-rw-r--r--version.props2
366 files changed, 32517 insertions, 30 deletions
diff --git a/.azure/pipelines/fast-pr-validation.yml b/.azure/pipelines/fast-pr-validation.yml
index 364aad5a68..2126e161d9 100644
--- a/.azure/pipelines/fast-pr-validation.yml
+++ b/.azure/pipelines/fast-pr-validation.yml
@@ -15,3 +15,14 @@ phases:
- template: .vsts-pipelines/templates/project-ci.yml@buildtools
parameters:
buildArgs: "/t:CheckUniverse"
+- phase: DataProtection
+ queue: Hosted VS2017
+ steps:
+ - script: src/DataProtection/build.cmd -ci
+ displayName: Run src/DataProtection/build.cmd
+ - task: PublishTestResults@2
+ displayName: Publish test results
+ condition: always()
+ inputs:
+ testRunner: vstest
+ testResultsFiles: 'src/DataProtection/artifacts/logs/**/*.trx'
diff --git a/.gitmodules b/.gitmodules
index e210bf2b38..6436140b01 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -38,10 +38,6 @@
path = modules/CORS
url = https://github.com/aspnet/CORS.git
branch = master
-[submodule "modules/DataProtection"]
- path = modules/DataProtection
- url = https://github.com/aspnet/DataProtection.git
- branch = master
[submodule "modules/DependencyInjection"]
path = modules/DependencyInjection
url = https://github.com/aspnet/DependencyInjection.git
diff --git a/build/RepositoryBuild.targets b/build/RepositoryBuild.targets
index 45d9b63cdf..95e08cb4cb 100644
--- a/build/RepositoryBuild.targets
+++ b/build/RepositoryBuild.targets
@@ -11,6 +11,9 @@
<Target Name="GetRepoBatches" DependsOnTargets="GeneratePropsFiles;ComputeGraph">
<ItemGroup>
+ <RepositoryBuildOrder Condition="'%(RootPath)' == ''">
+ <RootPath>$(SubmoduleRoot)%(Identity)\</RootPath>
+ </RepositoryBuildOrder>
<BatchedRepository Include="$(MSBuildProjectFullPath)">
<BuildGroup>%(RepositoryBuildOrder.Order)</BuildGroup>
<Repository>%(RepositoryBuildOrder.Identity)</Repository>
diff --git a/build/buildorder.props b/build/buildorder.props
index f20188b8d0..073ba5c494 100644
--- a/build/buildorder.props
+++ b/build/buildorder.props
@@ -1,4 +1,11 @@
<Project>
+ <ItemDefinitionGroup>
+ <RepositoryBuildOrder>
+ <Order></Order>
+ <RootPath></RootPath>
+ </RepositoryBuildOrder>
+ </ItemDefinitionGroup>
+
<ItemGroup>
<RepositoryBuildOrder Include="Common" Order="1" />
<RepositoryBuildOrder Include="Microsoft.Data.Sqlite" Order="1" />
@@ -20,7 +27,7 @@
<RepositoryBuildOrder Include="EntityFrameworkCore" Order="8" />
<RepositoryBuildOrder Include="HttpSysServer" Order="8" />
<RepositoryBuildOrder Include="BrowserLink" Order="8" />
- <RepositoryBuildOrder Include="DataProtection" Order="9" />
+ <RepositoryBuildOrder Include="DataProtection" Order="9" RootPath="$(RepositoryRoot)src\DataProtection\" />
<RepositoryBuildOrder Include="BasicMiddleware" Order="9" />
<RepositoryBuildOrder Include="Antiforgery" Order="10" />
<RepositoryBuildOrder Include="IISIntegration" Order="10" />
@@ -45,7 +52,5 @@
<RepositoryBuildOrder Include="SignalR" Order="16" />
<RepositoryBuildOrder Include="AuthSamples" Order="16" />
<RepositoryBuildOrder Include="Templating" Order="17" />
-
- <RepositoryBuildOrder Update="@(RepositoryBuildOrder)" RootPath="$(SubmoduleRoot)%(Identity)" />
</ItemGroup>
</Project>
diff --git a/build/dependencies.props b/build/dependencies.props
index d3f828619f..3467502827 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -1,4 +1,4 @@
-<Project>
+<Project>
<!-- These package versions may be overridden or updated by automation. -->
<PropertyGroup Label="Package Versions: Auto" Condition=" '$(DotNetPackageVersionPropsPath)' == '' ">
<MicrosoftCSharpPackageVersion>4.6.0-preview1-26907-04</MicrosoftCSharpPackageVersion>
diff --git a/build/submodules.props b/build/submodules.props
index 588663e9b5..8f60821673 100644
--- a/build/submodules.props
+++ b/build/submodules.props
@@ -45,7 +45,7 @@
<Repository Include="Common" />
<Repository Include="Configuration" />
<Repository Include="CORS" />
- <Repository Include="DataProtection" />
+ <Repository Include="DataProtection" RootPath="$(RepositoryRoot)src\DataProtection\" />
<Repository Include="DependencyInjection" />
<Repository Include="Diagnostics" />
<Repository Include="DotNetTools" />
diff --git a/modules/DataProtection b/modules/DataProtection
deleted file mode 160000
-Subproject 9c7731f1fab12009d6060c748e93f542f3b1f7b
diff --git a/run.ps1 b/run.ps1
index 60e533097c..6b7e36c6cf 100644
--- a/run.ps1
+++ b/run.ps1
@@ -14,6 +14,9 @@ The KoreBuild command to run.
.PARAMETER Path
The folder to build. Defaults to the folder containing this script.
+.PARAMETER LockFile
+The path to the korebuild-lock.txt file. Defaults to $Path/korebuild-lock.txt
+
.PARAMETER Channel
The channel of KoreBuild to download. Overrides the value from the config file.
@@ -75,6 +78,7 @@ param(
[Parameter(Mandatory=$true, Position = 0)]
[string]$Command,
[string]$Path = $PSScriptRoot,
+ [string]$LockFile,
[Alias('c')]
[string]$Channel,
[Alias('d')]
@@ -104,15 +108,13 @@ $ErrorActionPreference = 'Stop'
function Get-KoreBuild {
- $lockFile = Join-Path $Path 'korebuild-lock.txt'
-
- if (!(Test-Path $lockFile) -or $Update) {
- Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile
+ if (!(Test-Path $LockFile) -or $Update) {
+ Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $LockFile
}
- $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
+ $version = Get-Content $LockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
if (!$version) {
- Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'"
+ Write-Error "Failed to parse version from $LockFile. Expected a line that begins with 'version:'"
}
$version = $version.TrimStart('version:').Trim()
$korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version)
@@ -207,6 +209,7 @@ if (!$DotNetHome) {
else { Join-Path $PSScriptRoot '.dotnet'}
}
+if (!$LockFile) { $LockFile = Join-Path $Path 'korebuild-lock.txt' }
if (!$Channel) { $Channel = 'master' }
if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' }
diff --git a/run.sh b/run.sh
index beaca3094f..cfc0a36904 100755
--- a/run.sh
+++ b/run.sh
@@ -15,6 +15,7 @@ verbose=false
update=false
reinstall=false
repo_path="$DIR"
+lockfile_path=''
channel=''
tools_source=''
ci=false
@@ -41,6 +42,7 @@ __usage() {
echo " --config-file <FILE> The path to the configuration file that stores values. Defaults to korebuild.json."
echo " -d|--dotnet-home <DIR> The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet."
echo " --path <PATH> The directory to build. Defaults to the directory containing the script."
+ echo " --lockfile <PATH> The path to the korebuild-lock.txt file. Defaults to \$repo_path/korebuild-lock.txt"
echo " -s|--tools-source|-ToolsSource <URL> The base url where build tools can be downloaded. Overrides the value from the config file."
echo " --package-version-props-url <URL> The url of the package versions props path containing dependency versions."
echo " --access-token <Token> The query string to append to any blob store access for PackageVersionPropsUrl, if any."
@@ -61,13 +63,12 @@ __usage() {
get_korebuild() {
local version
- local lock_file="$repo_path/korebuild-lock.txt"
- if [ ! -f "$lock_file" ] || [ "$update" = true ]; then
- __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file"
+ if [ ! -f "$lockfile_path" ] || [ "$update" = true ]; then
+ __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lockfile_path"
fi
- version="$(grep 'version:*' -m 1 "$lock_file")"
+ version="$(grep 'version:*' -m 1 "$lockfile_path")"
if [[ "$version" == '' ]]; then
- __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'"
+ __error "Failed to parse version from $lockfile_path. Expected a line that begins with 'version:'"
return 1
fi
version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
@@ -176,6 +177,11 @@ while [[ $# -gt 0 ]]; do
repo_path="${1:-}"
[ -z "$repo_path" ] && __error "Missing value for parameter --path" && __usage
;;
+ --[Ll]ock[Ff]ile)
+ shift
+ lockfile_path="${1:-}"
+ [ -z "$lockfile_path" ] && __error "Missing value for parameter --lockfile" && __usage
+ ;;
-s|--tools-source|-ToolsSource)
shift
tools_source="${1:-}"
@@ -296,6 +302,7 @@ if [ ! -z "$product_build_id" ]; then
msbuild_args[${#msbuild_args[*]}]="-p:DotNetProductBuildId=$product_build_id"
fi
+[ -z "$lockfile_path" ] && lockfile_path="$repo_path/korebuild-lock.txt"
[ -z "$channel" ] && channel='master'
[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
diff --git a/src/DataProtection/DataProtection.sln b/src/DataProtection/DataProtection.sln
new file mode 100644
index 0000000000..3e9512f1d9
--- /dev/null
+++ b/src/DataProtection/DataProtection.sln
@@ -0,0 +1,333 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26814.1
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{60336AB3-948D-4D15-A5FB-F32A2B91E814}"
+ ProjectSection(SolutionItems) = preProject
+ test\CreateTestCert.ps1 = test\CreateTestCert.ps1
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5A3A5DE3-49AD-431C-971D-B01B62D94AE2}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E1D86B1B-41D8-43C9-97FD-C2BF65C414E2}"
+ ProjectSection(SolutionItems) = preProject
+ .appveyor.yml = .appveyor.yml
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ .travis.yml = .travis.yml
+ CONTRIBUTING.md = CONTRIBUTING.md
+ build\dependencies.props = build\dependencies.props
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ korebuild.json = korebuild.json
+ LICENSE.txt = LICENSE.txt
+ NuGet.config = NuGet.config
+ NuGetPackageVerifier.json = NuGetPackageVerifier.json
+ Provision-AutoGenKeys.ps1 = Provision-AutoGenKeys.ps1
+ README.md = README.md
+ version.props = version.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection", "src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj", "{1E570CD4-6F12-44F4-961E-005EE2002BC2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.Test", "test\Microsoft.AspNetCore.DataProtection.Test\Microsoft.AspNetCore.DataProtection.Test.csproj", "{7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cryptography.Internal", "src\Microsoft.AspNetCore.Cryptography.Internal\Microsoft.AspNetCore.Cryptography.Internal.csproj", "{E2779976-A28C-4365-A4BB-4AD854FAF23E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cryptography.KeyDerivation", "src\Microsoft.AspNetCore.Cryptography.KeyDerivation\Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj", "{421F0383-34B1-402D-807B-A94542513ABA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cryptography.KeyDerivation.Test", "test\Microsoft.AspNetCore.Cryptography.KeyDerivation.Test\Microsoft.AspNetCore.Cryptography.KeyDerivation.Test.csproj", "{42C97F52-8D56-46BD-A712-4F22BED157A7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cryptography.Internal.Test", "test\Microsoft.AspNetCore.Cryptography.Internal.Test\Microsoft.AspNetCore.Cryptography.Internal.Test.csproj", "{37053D5F-5B61-47CE-8B72-298CE007FFB0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.Abstractions", "src\Microsoft.AspNetCore.DataProtection.Abstractions\Microsoft.AspNetCore.DataProtection.Abstractions.csproj", "{4B115BDE-B253-46A6-97BF-A8B37B344FF2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.Abstractions.Test", "test\Microsoft.AspNetCore.DataProtection.Abstractions.Test\Microsoft.AspNetCore.DataProtection.Abstractions.Test.csproj", "{FF650A69-DEE4-4B36-9E30-264EE7CFB478}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.SystemWeb", "src\Microsoft.AspNetCore.DataProtection.SystemWeb\Microsoft.AspNetCore.DataProtection.SystemWeb.csproj", "{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.Extensions.Test", "test\Microsoft.AspNetCore.DataProtection.Extensions.Test\Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj", "{04AA8E60-A053-4D50-89FE-E76C3DF45200}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.Extensions", "src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.csproj", "{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureStorage", "src\Microsoft.AspNetCore.DataProtection.AzureStorage\Microsoft.AspNetCore.DataProtection.AzureStorage.csproj", "{CC799B57-81E2-4F45-8A32-0D5F49753C3F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureBlob", "samples\AzureBlob\AzureBlob.csproj", "{B07435B3-CD81-4E3B-88A5-6384821E1C01}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureStorage.Test", "test\Microsoft.AspNetCore.DataProtection.AzureStorage.Test\Microsoft.AspNetCore.DataProtection.AzureStorage.Test.csproj", "{8C41240E-48F8-402F-9388-74CFE27F4D76}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Redis", "samples\Redis\Redis.csproj", "{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonDISample", "samples\NonDISample\NonDISample.csproj", "{32CF970B-E2F1-4CD9-8DB3-F5715475373A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyManagementSample", "samples\KeyManagementSample\KeyManagementSample.csproj", "{6E066F8D-2910-404F-8949-F58125E28495}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomEncryptorSample", "samples\CustomEncryptorSample\CustomEncryptorSample.csproj", "{F4D59BBD-6145-4EE0-BA6E-AD03605BF151}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureKeyVault", "src\Microsoft.AspNetCore.DataProtection.AzureKeyVault\Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj", "{4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureKeyVault", "samples\AzureKeyVault\AzureKeyVault.csproj", "{295E8539-5450-4764-B3F5-51F968628022}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test", "test\Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test\Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj", "{C85ED942-8121-453F-8308-9DB730843B63}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test", "test\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj", "{06728BF2-C5EB-44C7-9F30-14FAA5649E14}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore", "src\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj", "{3E4CA7FE-741B-4C78-A775-220E0E3C1B03}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCoreSample", "samples\EntityFrameworkCoreSample\EntityFrameworkCoreSample.csproj", "{22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.StackExchangeRedis", "src\Microsoft.AspNetCore.DataProtection.StackExchangeRedis\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj", "{57713B23-CCAB-44DB-A08D-55F9D236D05B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test", "test\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test.csproj", "{33BB1B86-64BF-45BB-A334-3E1A4802253C}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Release|x86.ActiveCfg = Release|Any CPU
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Release|x86.ActiveCfg = Release|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Debug|x86.Build.0 = Debug|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Release|x86.ActiveCfg = Release|Any CPU
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E}.Release|x86.Build.0 = Release|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Debug|x86.Build.0 = Debug|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Release|x86.ActiveCfg = Release|Any CPU
+ {421F0383-34B1-402D-807B-A94542513ABA}.Release|x86.Build.0 = Release|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Debug|x86.Build.0 = Debug|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Release|x86.ActiveCfg = Release|Any CPU
+ {42C97F52-8D56-46BD-A712-4F22BED157A7}.Release|x86.Build.0 = Release|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Debug|x86.Build.0 = Debug|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Release|x86.ActiveCfg = Release|Any CPU
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0}.Release|x86.Build.0 = Release|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Debug|x86.Build.0 = Debug|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Release|x86.ActiveCfg = Release|Any CPU
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2}.Release|x86.Build.0 = Release|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Debug|x86.Build.0 = Debug|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Release|x86.ActiveCfg = Release|Any CPU
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478}.Release|x86.Build.0 = Release|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Debug|x86.Build.0 = Debug|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|x86.ActiveCfg = Release|Any CPU
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}.Release|x86.Build.0 = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Debug|x86.Build.0 = Debug|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|Any CPU.Build.0 = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|x86.ActiveCfg = Release|Any CPU
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200}.Release|x86.Build.0 = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Debug|x86.Build.0 = Debug|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.ActiveCfg = Release|Any CPU
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.Build.0 = Release|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|x86.Build.0 = Debug|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|x86.ActiveCfg = Release|Any CPU
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|x86.Build.0 = Release|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|x86.Build.0 = Debug|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|x86.ActiveCfg = Release|Any CPU
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|x86.Build.0 = Release|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|x86.Build.0 = Debug|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|x86.ActiveCfg = Release|Any CPU
+ {8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|x86.Build.0 = Release|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.Build.0 = Debug|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.ActiveCfg = Release|Any CPU
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.Build.0 = Release|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Debug|x86.Build.0 = Debug|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Release|x86.ActiveCfg = Release|Any CPU
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A}.Release|x86.Build.0 = Release|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Debug|x86.Build.0 = Debug|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Release|x86.ActiveCfg = Release|Any CPU
+ {6E066F8D-2910-404F-8949-F58125E28495}.Release|x86.Build.0 = Release|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Debug|x86.Build.0 = Debug|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|x86.ActiveCfg = Release|Any CPU
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|x86.Build.0 = Release|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|x86.Build.0 = Debug|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|x86.ActiveCfg = Release|Any CPU
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|x86.Build.0 = Release|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Debug|x86.Build.0 = Debug|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Release|Any CPU.Build.0 = Release|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Release|x86.ActiveCfg = Release|Any CPU
+ {295E8539-5450-4764-B3F5-51F968628022}.Release|x86.Build.0 = Release|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Debug|x86.Build.0 = Debug|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Release|x86.ActiveCfg = Release|Any CPU
+ {C85ED942-8121-453F-8308-9DB730843B63}.Release|x86.Build.0 = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|x86.Build.0 = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|x86.ActiveCfg = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|x86.Build.0 = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|x86.Build.0 = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|x86.ActiveCfg = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|x86.Build.0 = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|x86.Build.0 = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|x86.ActiveCfg = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|x86.Build.0 = Release|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Debug|x86.Build.0 = Debug|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Release|x86.ActiveCfg = Release|Any CPU
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B}.Release|x86.Build.0 = Release|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Debug|x86.Build.0 = Debug|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Release|x86.ActiveCfg = Release|Any CPU
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {1E570CD4-6F12-44F4-961E-005EE2002BC2} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {E2779976-A28C-4365-A4BB-4AD854FAF23E} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {421F0383-34B1-402D-807B-A94542513ABA} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {42C97F52-8D56-46BD-A712-4F22BED157A7} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {37053D5F-5B61-47CE-8B72-298CE007FFB0} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {4B115BDE-B253-46A6-97BF-A8B37B344FF2} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {FF650A69-DEE4-4B36-9E30-264EE7CFB478} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {04AA8E60-A053-4D50-89FE-E76C3DF45200} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {BF8681DB-C28B-441F-BD92-0DCFE9537A9F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {CC799B57-81E2-4F45-8A32-0D5F49753C3F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {B07435B3-CD81-4E3B-88A5-6384821E1C01} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {8C41240E-48F8-402F-9388-74CFE27F4D76} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {24AAEC96-DF46-4F61-B2FF-3D5E056685D9} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {32CF970B-E2F1-4CD9-8DB3-F5715475373A} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {6E066F8D-2910-404F-8949-F58125E28495} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {F4D59BBD-6145-4EE0-BA6E-AD03605BF151} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {295E8539-5450-4764-B3F5-51F968628022} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {C85ED942-8121-453F-8308-9DB730843B63} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
+ {57713B23-CCAB-44DB-A08D-55F9D236D05B} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {33BB1B86-64BF-45BB-A334-3E1A4802253C} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {DD305D75-BD1B-43AE-BF04-869DA6A0858F}
+ EndGlobalSection
+EndGlobal
diff --git a/src/DataProtection/Directory.Build.props b/src/DataProtection/Directory.Build.props
new file mode 100644
index 0000000000..deb7bb4ee6
--- /dev/null
+++ b/src/DataProtection/Directory.Build.props
@@ -0,0 +1,8 @@
+<Project>
+
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
+
+ <Import Project="version.props" />
+ <Import Project="dependencies.props" />
+
+</Project>
diff --git a/src/DataProtection/NuGetPackageVerifier.json b/src/DataProtection/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..22ef3c09c0
--- /dev/null
+++ b/src/DataProtection/NuGetPackageVerifier.json
@@ -0,0 +1,7 @@
+{
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+}
diff --git a/src/DataProtection/Provision-AutoGenKeys.ps1 b/src/DataProtection/Provision-AutoGenKeys.ps1
new file mode 100644
index 0000000000..9be7e1601d
--- /dev/null
+++ b/src/DataProtection/Provision-AutoGenKeys.ps1
@@ -0,0 +1,117 @@
+param (
+ [Parameter(Mandatory = $True)]
+ [string] $appPoolName
+ )
+
+# Provisions the HKLM registry so that the specified user account can persist auto-generated machine keys.
+function Provision-AutoGenKeys {
+ [CmdletBinding()]
+ param (
+ [ValidateSet("2.0", "4.0")]
+ [Parameter(Mandatory = $True)]
+ [string] $frameworkVersion,
+ [ValidateSet("32", "64")]
+ [Parameter(Mandatory = $True)]
+ [string] $architecture,
+ [Parameter(Mandatory = $True)]
+ [string] $sid
+ )
+ process {
+ # We require administrative permissions to continue.
+ if (-Not (new-object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
+ Write-Error "This cmdlet requires Administrator permissions."
+ return
+ }
+ # Open HKLM with an appropriate view into the registry
+ if ($architecture -eq "32") {
+ $regView = [Microsoft.Win32.RegistryView]::Registry32;
+ } else {
+ $regView = [Microsoft.Win32.RegistryView]::Registry64;
+ }
+ $baseRegKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
+ # Open ASP.NET base key
+ if ($frameworkVersion -eq "2.0") {
+ $expandedVersion = "2.0.50727.0"
+ } else {
+ $expandedVersion = "4.0.30319.0"
+ }
+ $softwareMicrosoftKey = $baseRegKey.OpenSubKey("SOFTWARE\Microsoft\", $True);
+
+ $aspNetKey = $softwareMicrosoftKey.OpenSubKey("ASP.NET", $True);
+ if ($aspNetKey -eq $null)
+ {
+ $aspNetKey = $softwareMicrosoftKey.CreateSubKey("ASP.NET")
+ }
+
+ $aspNetBaseKey = $aspNetKey.OpenSubKey("$expandedVersion", $True);
+ if ($aspNetBaseKey -eq $null)
+ {
+ $aspNetBaseKey = $aspNetKey.CreateSubKey("$expandedVersion")
+ }
+
+ # Create AutoGenKeys subkey if it doesn't already exist
+ $autoGenBaseKey = $aspNetBaseKey.OpenSubKey("AutoGenKeys", $True)
+ if ($autoGenBaseKey -eq $null) {
+ $autoGenBaseKey = $aspNetBaseKey.CreateSubKey("AutoGenKeys")
+ }
+ # SYSTEM, ADMINISTRATORS, and the target SID get full access
+ $regSec = New-Object System.Security.AccessControl.RegistrySecurity
+ $regSec.SetSecurityDescriptorSddlForm("D:P(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)(A;OICI;GA;;;$sid)")
+ $userAutoGenKey = $autoGenBaseKey.OpenSubKey($sid, $True)
+ if ($userAutoGenKey -eq $null) {
+ # Subkey didn't exist; create and ACL appropriately
+ $userAutoGenKey = $autoGenBaseKey.CreateSubKey($sid, [Microsoft.Win32.RegistryKeyPermissionCheck]::Default, $regSec)
+ } else {
+ # Subkey existed; make sure ACLs are correct
+ $userAutoGenKey.SetAccessControl($regSec)
+ }
+ }
+}
+
+$ErrorActionPreference = "Stop"
+if (Get-Command Get-IISAppPool -errorAction SilentlyContinue)
+{
+ $processModel = (Get-IISAppPool $appPoolName).processModel
+}
+else
+{
+ Import-Module WebAdministration
+ $processModel = Get-ItemProperty -Path "IIS:\AppPools\$appPoolName" -Name "processModel"
+}
+
+$identityType = $processModel.identityType
+Write-Output "Pool process model: '$identityType'"
+
+Switch ($identityType)
+{
+ "LocalService" {
+ $userName = "LocalService";
+ }
+ "LocalSystem" {
+ $userName = "System";
+ }
+ "NetworkService" {
+ $userName = "NetworkService";
+ }
+ "ApplicationPoolIdentity" {
+ $userName = "IIS APPPOOL\$appPoolName";
+ }
+ "SpecificUser" {
+ $userName = $processModel.userName;
+ }
+}
+Write-Output "Pool user name: '$userName'"
+
+Try
+{
+ $poolSid = (New-Object System.Security.Principal.NTAccount($userName)).Translate([System.Security.Principal.SecurityIdentifier]).Value
+}
+Catch [System.Security.Principal.IdentityNotMappedException]
+{
+ Write-Error "Application pool '$appPoolName' account cannot be resolved."
+}
+
+Write-Output "Pool SID: '$poolSid'"
+
+Provision-AutoGenKeys "4.0" "32" $poolSid
+Provision-AutoGenKeys "4.0" "64" $poolSid
diff --git a/src/DataProtection/README.md b/src/DataProtection/README.md
new file mode 100644
index 0000000000..cd58074d9e
--- /dev/null
+++ b/src/DataProtection/README.md
@@ -0,0 +1,8 @@
+DataProtection
+==============
+
+Data Protection APIs for protecting and unprotecting data. You can find documentation for Data Protection in the [ASP.NET Core Documentation](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/).
+
+## Community Maintained Data Protection Providers & Projects
+
+ - [ASP.NET Core DataProtection for Service Fabric](https://github.com/MedAnd/AspNetCore.DataProtection.ServiceFabric)
diff --git a/src/DataProtection/build.cmd b/src/DataProtection/build.cmd
new file mode 100644
index 0000000000..f4169ea5e4
--- /dev/null
+++ b/src/DataProtection/build.cmd
@@ -0,0 +1,3 @@
+@ECHO OFF
+SET RepoRoot="%~dp0..\.."
+%RepoRoot%\build.cmd -LockFile %RepoRoot%\korebuild-lock.txt -Path %~dp0 %*
diff --git a/src/DataProtection/build.sh b/src/DataProtection/build.sh
new file mode 100755
index 0000000000..d5bb0cf631
--- /dev/null
+++ b/src/DataProtection/build.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+repo_root="$DIR/../.."
+"$repo_root/build.sh" --path "$DIR" --lockfile "$repo_root/korebuild-lock.txt" "$@"
diff --git a/src/DataProtection/build/repo.props b/src/DataProtection/build/repo.props
new file mode 100644
index 0000000000..3fa98a9b36
--- /dev/null
+++ b/src/DataProtection/build/repo.props
@@ -0,0 +1,10 @@
+<Project>
+ <Import Project="..\..\..\build\dependencies.props" />
+ <PropertyGroup>
+ <!-- TODO: temporary while we reorganize source code and refactor dependency management -->
+ <DisablePackageReferenceRestrictions>true</DisablePackageReferenceRestrictions>
+ </PropertyGroup>
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/DataProtection/dependencies.props b/src/DataProtection/dependencies.props
new file mode 100644
index 0000000000..f339af7555
--- /dev/null
+++ b/src/DataProtection/dependencies.props
@@ -0,0 +1,33 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <!-- Fallback when not building from command line. -->
+ <InternalAspNetCoreSdkPackageVersion Condition="'$(InternalAspNetCoreSdkPackageVersion)' == ''">2.2.0-preview2-20181004.6</InternalAspNetCoreSdkPackageVersion>
+ <LastGoodAspBuildVersion>3.0.0-alpha1-10584</LastGoodAspBuildVersion>
+ <MicrosoftAspNetCoreHostingAbstractionsPackageVersion Condition=" '$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
+ <MicrosoftAspNetCoreHostingPackageVersion Condition=" '$(MicrosoftAspNetCoreHostingPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftAspNetCoreHostingPackageVersion>
+ <MicrosoftAspNetCoreTestingPackageVersion Condition=" '$(MicrosoftAspNetCoreTestingPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftAspNetCoreTestingPackageVersion>
+ <MicrosoftEntityFrameworkCoreInMemoryPackageVersion Condition=" '$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftEntityFrameworkCoreInMemoryPackageVersion>
+ <MicrosoftEntityFrameworkCorePackageVersion Condition=" '$(MicrosoftEntityFrameworkCorePackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftEntityFrameworkCorePackageVersion>
+ <MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion Condition=" '$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
+ <MicrosoftExtensionsConfigurationJsonPackageVersion Condition=" '$(MicrosoftExtensionsConfigurationJsonPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsConfigurationJsonPackageVersion>
+ <MicrosoftExtensionsConfigurationPackageVersion Condition=" '$(MicrosoftExtensionsConfigurationPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsConfigurationPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion Condition=" '$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionPackageVersion Condition=" '$(MicrosoftExtensionsDependencyInjectionPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsDependencyInjectionPackageVersion>
+ <MicrosoftExtensionsLoggingAbstractionsPackageVersion Condition=" '$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
+ <MicrosoftExtensionsLoggingConsolePackageVersion Condition=" '$(MicrosoftExtensionsLoggingConsolePackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsLoggingConsolePackageVersion>
+ <MicrosoftExtensionsLoggingPackageVersion Condition=" '$(MicrosoftExtensionsLoggingPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsLoggingPackageVersion>
+ <MicrosoftExtensionsOptionsPackageVersion Condition=" '$(MicrosoftExtensionsOptionsPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsOptionsPackageVersion>
+ <MicrosoftExtensionsWebEncodersSourcesPackageVersion Condition=" '$(MicrosoftExtensionsWebEncodersSourcesPackageVersion)' == '' ">$(LastGoodAspBuildVersion)</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
+ </PropertyGroup>
+
+ <!--
+ The versions are present to maintain compatibility with the 2.2.0 release of DataProtection,
+ even though source code and infrastructure has changed.
+ -->
+ <PropertyGroup Label="Package Versions: Overrides">
+ </PropertyGroup>
+</Project>
diff --git a/src/DataProtection/samples/AzureBlob/AzureBlob.csproj b/src/DataProtection/samples/AzureBlob/AzureBlob.csproj
new file mode 100644
index 0000000000..72bce10880
--- /dev/null
+++ b/src/DataProtection/samples/AzureBlob/AzureBlob.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
+ <OutputType>exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.AzureStorage\Microsoft.AspNetCore.DataProtection.AzureStorage.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/samples/AzureBlob/Program.cs b/src/DataProtection/samples/AzureBlob/Program.cs
new file mode 100644
index 0000000000..cce8604648
--- /dev/null
+++ b/src/DataProtection/samples/AzureBlob/Program.cs
@@ -0,0 +1,43 @@
+// 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 Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.WindowsAzure.Storage;
+using LogLevel = Microsoft.Extensions.Logging.LogLevel;
+
+namespace AzureBlob
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var storageAccount = CloudStorageAccount.DevelopmentStorageAccount;
+ var client = storageAccount.CreateCloudBlobClient();
+ var container = client.GetContainerReference("key-container");
+
+ // The container must exist before calling the DataProtection APIs.
+ // The specific file within the container does not have to exist,
+ // as it will be created on-demand.
+
+ container.CreateIfNotExistsAsync().GetAwaiter().GetResult();
+
+ // Configure
+ using (var services = new ServiceCollection()
+ .AddLogging(o => o.AddConsole().SetMinimumLevel(LogLevel.Debug))
+ .AddDataProtection()
+ .PersistKeysToAzureBlobStorage(container, "keys.xml")
+ .Services
+ .BuildServiceProvider())
+ {
+ // Run a sample payload
+
+ var protector = services.GetDataProtector("sample-purpose");
+ var protectedData = protector.Protect("Hello world!");
+ Console.WriteLine(protectedData);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/samples/AzureKeyVault/AzureKeyVault.csproj b/src/DataProtection/samples/AzureKeyVault/AzureKeyVault.csproj
new file mode 100644
index 0000000000..faa7d75be0
--- /dev/null
+++ b/src/DataProtection/samples/AzureKeyVault/AzureKeyVault.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
+ <OutputType>exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.AzureKeyVault\Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftExtensionsConfigurationPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/samples/AzureKeyVault/Program.cs b/src/DataProtection/samples/AzureKeyVault/Program.cs
new file mode 100644
index 0000000000..7d6299f3e5
--- /dev/null
+++ b/src/DataProtection/samples/AzureKeyVault/Program.cs
@@ -0,0 +1,44 @@
+// 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.IO;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace ConsoleApplication
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var builder = new ConfigurationBuilder();
+ builder.SetBasePath(Directory.GetCurrentDirectory());
+ builder.AddJsonFile("settings.json");
+ var config = builder.Build();
+
+ var store = new X509Store(StoreLocation.CurrentUser);
+ store.Open(OpenFlags.ReadOnly);
+ var cert = store.Certificates.Find(X509FindType.FindByThumbprint, config["CertificateThumbprint"], false);
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging();
+ serviceCollection.AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo("."))
+ .ProtectKeysWithAzureKeyVault(config["KeyId"], config["ClientId"], cert.OfType<X509Certificate2>().Single());
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
+ loggerFactory.AddConsole();
+
+ var protector = serviceProvider.GetDataProtector("Test");
+
+ Console.WriteLine(protector.Protect("Hello world"));
+ }
+ }
+}
diff --git a/src/DataProtection/samples/AzureKeyVault/settings.json b/src/DataProtection/samples/AzureKeyVault/settings.json
new file mode 100644
index 0000000000..ef7d4d81b8
--- /dev/null
+++ b/src/DataProtection/samples/AzureKeyVault/settings.json
@@ -0,0 +1,5 @@
+{
+ "CertificateThumbprint": "",
+ "KeyId": "",
+ "ClientId": ""
+} \ No newline at end of file
diff --git a/src/DataProtection/samples/CustomEncryptorSample/CustomBuilderExtensions.cs b/src/DataProtection/samples/CustomEncryptorSample/CustomBuilderExtensions.cs
new file mode 100644
index 0000000000..faa99a4a5d
--- /dev/null
+++ b/src/DataProtection/samples/CustomEncryptorSample/CustomBuilderExtensions.cs
@@ -0,0 +1,31 @@
+// 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 Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace CustomEncryptorSample
+{
+ public static class CustomBuilderExtensions
+ {
+ public static IDataProtectionBuilder UseXmlEncryptor(
+ this IDataProtectionBuilder builder,
+ Func<IServiceProvider, IXmlEncryptor> factory)
+ {
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(serviceProvider =>
+ {
+ var instance = factory(serviceProvider);
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.XmlEncryptor = instance;
+ });
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/src/DataProtection/samples/CustomEncryptorSample/CustomEncryptorSample.csproj b/src/DataProtection/samples/CustomEncryptorSample/CustomEncryptorSample.csproj
new file mode 100644
index 0000000000..f55e580fed
--- /dev/null
+++ b/src/DataProtection/samples/CustomEncryptorSample/CustomEncryptorSample.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp3.0</TargetFrameworks>
+ <OutputType>exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/samples/CustomEncryptorSample/CustomXmlDecryptor.cs b/src/DataProtection/samples/CustomEncryptorSample/CustomXmlDecryptor.cs
new file mode 100644
index 0000000000..a8925f12f6
--- /dev/null
+++ b/src/DataProtection/samples/CustomEncryptorSample/CustomXmlDecryptor.cs
@@ -0,0 +1,32 @@
+// 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.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace CustomEncryptorSample
+{
+ public class CustomXmlDecryptor : IXmlDecryptor
+ {
+ private readonly ILogger _logger;
+
+ public CustomXmlDecryptor(IServiceProvider services)
+ {
+ _logger = services.GetRequiredService<ILoggerFactory>().CreateLogger<CustomXmlDecryptor>();
+ }
+
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ if (encryptedElement == null)
+ {
+ throw new ArgumentNullException(nameof(encryptedElement));
+ }
+
+ return new XElement(encryptedElement.Elements().Single());
+ }
+ }
+}
diff --git a/src/DataProtection/samples/CustomEncryptorSample/CustomXmlEncryptor.cs b/src/DataProtection/samples/CustomEncryptorSample/CustomXmlEncryptor.cs
new file mode 100644
index 0000000000..f6653f776a
--- /dev/null
+++ b/src/DataProtection/samples/CustomEncryptorSample/CustomXmlEncryptor.cs
@@ -0,0 +1,38 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace CustomEncryptorSample
+{
+ public class CustomXmlEncryptor : IXmlEncryptor
+ {
+ private readonly ILogger _logger;
+
+ public CustomXmlEncryptor(IServiceProvider services)
+ {
+ _logger = services.GetRequiredService<ILoggerFactory>().CreateLogger<CustomXmlEncryptor>();
+ }
+
+ public EncryptedXmlInfo Encrypt(XElement plaintextElement)
+ {
+ if (plaintextElement == null)
+ {
+ throw new ArgumentNullException(nameof(plaintextElement));
+ }
+
+ _logger.LogInformation("Not encrypting key");
+
+ var newElement = new XElement("unencryptedKey",
+ new XComment(" This key is not encrypted. "),
+ new XElement(plaintextElement));
+ var encryptedTextElement = new EncryptedXmlInfo(newElement, typeof(CustomXmlDecryptor));
+
+ return encryptedTextElement;
+ }
+ }
+}
diff --git a/src/DataProtection/samples/CustomEncryptorSample/Program.cs b/src/DataProtection/samples/CustomEncryptorSample/Program.cs
new file mode 100644
index 0000000000..9079aeee3f
--- /dev/null
+++ b/src/DataProtection/samples/CustomEncryptorSample/Program.cs
@@ -0,0 +1,36 @@
+// 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.IO;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace CustomEncryptorSample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var keysFolder = Path.Combine(Directory.GetCurrentDirectory(), "temp-keys");
+ using (var services = new ServiceCollection()
+ .AddLogging(o => o.AddConsole().SetMinimumLevel(LogLevel.Debug))
+ .AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
+ .UseXmlEncryptor(s => new CustomXmlEncryptor(s))
+ .Services.BuildServiceProvider())
+ {
+ var protector = services.GetDataProtector("SamplePurpose");
+
+ // protect the payload
+ var protectedPayload = protector.Protect("Hello World!");
+ Console.WriteLine($"Protect returned: {protectedPayload}");
+
+ // unprotect the payload
+ var unprotectedPayload = protector.Unprotect(protectedPayload);
+ Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj b/src/DataProtection/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj
new file mode 100644
index 0000000000..4b8bf7da45
--- /dev/null
+++ b/src/DataProtection/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>exe</OutputType>
+ <TargetFrameworks>net461;netcoreapp3.0</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/samples/EntityFrameworkCoreSample/Program.cs b/src/DataProtection/samples/EntityFrameworkCoreSample/Program.cs
new file mode 100644
index 0000000000..d4e978a7b8
--- /dev/null
+++ b/src/DataProtection/samples/EntityFrameworkCoreSample/Program.cs
@@ -0,0 +1,48 @@
+// 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 Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace EntityFrameworkCoreSample
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ // Configure
+ var services = new ServiceCollection()
+ .AddLogging(o => o.AddConsole().SetMinimumLevel(LogLevel.Debug))
+ .AddDbContext<DataProtectionKeyContext>(o =>
+ {
+ o.UseInMemoryDatabase("DataProtection_EntityFrameworkCore");
+ o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
+ o.EnableSensitiveDataLogging();
+ })
+ .AddDataProtection()
+ .PersistKeysToDbContext<DataProtectionKeyContext>()
+ .SetDefaultKeyLifetime(TimeSpan.FromDays(7))
+ .Services
+ .BuildServiceProvider(validateScopes: true);
+
+ using(services)
+ {
+ // Run a sample payload
+ var protector = services.GetDataProtector("sample-purpose");
+ var protectedData = protector.Protect("Hello world!");
+ Console.WriteLine(protectedData);
+ }
+ }
+ }
+
+ class DataProtectionKeyContext : DbContext, IDataProtectionKeyContext
+ {
+ public DataProtectionKeyContext(DbContextOptions<DataProtectionKeyContext> options) : base(options) { }
+
+ public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
+ }
+}
diff --git a/src/DataProtection/samples/KeyManagementSample/KeyManagementSample.csproj b/src/DataProtection/samples/KeyManagementSample/KeyManagementSample.csproj
new file mode 100644
index 0000000000..d97d58f988
--- /dev/null
+++ b/src/DataProtection/samples/KeyManagementSample/KeyManagementSample.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp3.0</TargetFrameworks>
+ <OutputType>exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/samples/KeyManagementSample/Program.cs b/src/DataProtection/samples/KeyManagementSample/Program.cs
new file mode 100644
index 0000000000..be128aa11c
--- /dev/null
+++ b/src/DataProtection/samples/KeyManagementSample/Program.cs
@@ -0,0 +1,66 @@
+// 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.IO;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace KeyManagementSample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var keysFolder = Path.Combine(Directory.GetCurrentDirectory(), "temp-keys");
+ var serviceCollection = new ServiceCollection();
+ var builder = serviceCollection
+ .AddDataProtection()
+ // point at a specific folder and use DPAPI to encrypt keys
+ .PersistKeysToFileSystem(new DirectoryInfo(keysFolder));
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ builder.ProtectKeysWithDpapi();
+ }
+
+ using (var services = serviceCollection.BuildServiceProvider())
+ {
+ // perform a protect operation to force the system to put at least
+ // one key in the key ring
+ services.GetDataProtector("Sample.KeyManager.v1").Protect("payload");
+ Console.WriteLine("Performed a protect operation.");
+
+ // get a reference to the key manager
+ var keyManager = services.GetService<IKeyManager>();
+
+ // list all keys in the key ring
+ var allKeys = keyManager.GetAllKeys();
+ Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
+ foreach (var key in allKeys)
+ {
+ Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked = {key.IsRevoked}");
+ }
+
+ // revoke all keys in the key ring
+ keyManager.RevokeAllKeys(DateTimeOffset.Now, reason: "Revocation reason here.");
+ Console.WriteLine("Revoked all existing keys.");
+
+ // add a new key to the key ring with immediate activation and a 1-month expiration
+ keyManager.CreateNewKey(
+ activationDate: DateTimeOffset.Now,
+ expirationDate: DateTimeOffset.Now.AddMonths(1));
+ Console.WriteLine("Added a new key.");
+
+ // list all keys in the key ring
+ allKeys = keyManager.GetAllKeys();
+ Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
+ foreach (var key in allKeys)
+ {
+ Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked = {key.IsRevoked}");
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/samples/NonDISample/NonDISample.csproj b/src/DataProtection/samples/NonDISample/NonDISample.csproj
new file mode 100644
index 0000000000..6371351b01
--- /dev/null
+++ b/src/DataProtection/samples/NonDISample/NonDISample.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp3.0</TargetFrameworks>
+ <OutputType>exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/samples/NonDISample/Program.cs b/src/DataProtection/samples/NonDISample/Program.cs
new file mode 100644
index 0000000000..f9ccd92603
--- /dev/null
+++ b/src/DataProtection/samples/NonDISample/Program.cs
@@ -0,0 +1,41 @@
+// 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.IO;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace NonDISample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var keysFolder = Path.Combine(Directory.GetCurrentDirectory(), "temp-keys");
+
+ // instantiate the data protection system at this folder
+ var dataProtectionProvider = DataProtectionProvider.Create(
+ new DirectoryInfo(keysFolder),
+ configuration =>
+ {
+ configuration.SetApplicationName("my app name");
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ configuration.ProtectKeysWithDpapi();
+ }
+ });
+
+ var protector = dataProtectionProvider.CreateProtector("Program.No-DI");
+
+ // protect the payload
+ var protectedPayload = protector.Protect("Hello World!");
+ Console.WriteLine($"Protect returned: {protectedPayload}");
+
+ // unprotect the payload
+ var unprotectedPayload = protector.Unprotect(protectedPayload);
+ Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
+ }
+ }
+}
diff --git a/src/DataProtection/samples/Redis/Program.cs b/src/DataProtection/samples/Redis/Program.cs
new file mode 100644
index 0000000000..57d910ae8f
--- /dev/null
+++ b/src/DataProtection/samples/Redis/Program.cs
@@ -0,0 +1,35 @@
+// 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 Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.StackExchangeRedis;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using StackExchange.Redis;
+
+namespace RedisSample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ // Connect
+ var redis = ConnectionMultiplexer.Connect("localhost:6379");
+
+ // Configure
+ using (var services = new ServiceCollection()
+ .AddLogging(o => o.AddConsole().SetMinimumLevel(LogLevel.Debug))
+ .AddDataProtection()
+ .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys")
+ .Services
+ .BuildServiceProvider())
+ {
+ // Run a sample payload
+ var protector = services.GetDataProtector("sample-purpose");
+ var protectedData = protector.Protect("Hello world!");
+ Console.WriteLine(protectedData);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/samples/Redis/Redis.csproj b/src/DataProtection/samples/Redis/Redis.csproj
new file mode 100644
index 0000000000..7b1433e9fe
--- /dev/null
+++ b/src/DataProtection/samples/Redis/Redis.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp3.0</TargetFrameworks>
+ <OutputType>exe</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.StackExchangeRedis\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/shared/EncodingUtil.cs b/src/DataProtection/shared/EncodingUtil.cs
new file mode 100644
index 0000000000..67b99eac3b
--- /dev/null
+++ b/src/DataProtection/shared/EncodingUtil.cs
@@ -0,0 +1,14 @@
+// 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.Text;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class EncodingUtil
+ {
+ // UTF8 encoding that fails on invalid chars
+ public static readonly UTF8Encoding SecureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+ }
+}
diff --git a/src/DataProtection/shared/ExceptionExtensions.cs b/src/DataProtection/shared/ExceptionExtensions.cs
new file mode 100644
index 0000000000..f441935d13
--- /dev/null
+++ b/src/DataProtection/shared/ExceptionExtensions.cs
@@ -0,0 +1,20 @@
+// 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.Security.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class ExceptionExtensions
+ {
+ /// <summary>
+ /// Determines whether an exception must be homogenized by being wrapped inside a
+ /// CryptographicException before being rethrown.
+ /// </summary>
+ public static bool RequiresHomogenization(this Exception ex)
+ {
+ return !(ex is CryptographicException);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Directory.Build.props b/src/DataProtection/src/Directory.Build.props
new file mode 100644
index 0000000000..4b89a431e7
--- /dev/null
+++ b/src/DataProtection/src/Directory.Build.props
@@ -0,0 +1,7 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs
new file mode 100644
index 0000000000..0c074b8280
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs
@@ -0,0 +1,38 @@
+// 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.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/cc562981(v=vs.85).aspx
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO
+ {
+ public uint cbSize;
+ public uint dwInfoVersion;
+ public byte* pbNonce;
+ public uint cbNonce;
+ public byte* pbAuthData;
+ public uint cbAuthData;
+ public byte* pbTag;
+ public uint cbTag;
+ public byte* pbMacContext;
+ public uint cbMacContext;
+ public uint cbAAD;
+ public ulong cbData;
+ public uint dwFlags;
+
+ // corresponds to the BCRYPT_INIT_AUTH_MODE_INFO macro in bcrypt.h
+ public static void Init(out BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info)
+ {
+ const uint BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION = 1;
+ info = new BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO
+ {
+ cbSize = (uint)sizeof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO),
+ dwInfoVersion = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION
+ };
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs
new file mode 100644
index 0000000000..0d4139018f
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs
@@ -0,0 +1,46 @@
+// 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.Runtime.InteropServices;
+using Microsoft.AspNetCore.Cryptography.Internal;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375525(v=vs.85).aspx
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct BCRYPT_KEY_LENGTHS_STRUCT
+ {
+ // MSDN says these fields represent the key length in bytes.
+ // It's wrong: these key lengths are all actually in bits.
+ internal uint dwMinLength;
+ internal uint dwMaxLength;
+ internal uint dwIncrement;
+
+ public void EnsureValidKeyLength(uint keyLengthInBits)
+ {
+ if (!IsValidKeyLength(keyLengthInBits))
+ {
+ string message = Resources.FormatBCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength(keyLengthInBits, dwMinLength, dwMaxLength, dwIncrement);
+ throw new ArgumentOutOfRangeException(nameof(keyLengthInBits), message);
+ }
+ CryptoUtil.Assert(keyLengthInBits % 8 == 0, "keyLengthInBits % 8 == 0");
+ }
+
+ private bool IsValidKeyLength(uint keyLengthInBits)
+ {
+ // If the step size is zero, then the key length must be exactly the min or the max. Otherwise,
+ // key length must be between min and max (inclusive) and a whole number of increments away from min.
+ if (dwIncrement == 0)
+ {
+ return (keyLengthInBits == dwMinLength || keyLengthInBits == dwMaxLength);
+ }
+ else
+ {
+ return (dwMinLength <= keyLengthInBits)
+ && (keyLengthInBits <= dwMaxLength)
+ && ((keyLengthInBits - dwMinLength) % dwIncrement == 0);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBuffer.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBuffer.cs
new file mode 100644
index 0000000000..c091859729
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBuffer.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;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375368(v=vs.85).aspx
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct BCryptBuffer
+ {
+ public uint cbBuffer; // Length of buffer, in bytes
+ public BCryptKeyDerivationBufferType BufferType; // Buffer type
+ public IntPtr pvBuffer; // Pointer to buffer
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBufferDesc.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBufferDesc.cs
new file mode 100644
index 0000000000..8fd699643e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptBufferDesc.cs
@@ -0,0 +1,26 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375370(v=vs.85).aspx
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct BCryptBufferDesc
+ {
+ private const int BCRYPTBUFFER_VERSION = 0;
+
+ public uint ulVersion; // Version number
+ public uint cBuffers; // Number of buffers
+ public BCryptBuffer* pBuffers; // Pointer to array of buffers
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Initialize(ref BCryptBufferDesc bufferDesc)
+ {
+ bufferDesc.ulVersion = BCRYPTBUFFER_VERSION;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptEncryptFlags.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptEncryptFlags.cs
new file mode 100644
index 0000000000..81ae0105cc
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptEncryptFlags.cs
@@ -0,0 +1,13 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ [Flags]
+ internal enum BCryptEncryptFlags
+ {
+ BCRYPT_BLOCK_PADDING = 0x00000001,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptGenRandomFlags.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptGenRandomFlags.cs
new file mode 100644
index 0000000000..ed20fec309
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptGenRandomFlags.cs
@@ -0,0 +1,15 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // from bcrypt.h
+ [Flags]
+ internal enum BCryptGenRandomFlags
+ {
+ BCRYPT_RNG_USE_ENTROPY_IN_BUFFER = 0x00000001,
+ BCRYPT_USE_SYSTEM_PREFERRED_RNG = 0x00000002,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptKeyDerivationBufferType.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptKeyDerivationBufferType.cs
new file mode 100644
index 0000000000..a68569e799
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptKeyDerivationBufferType.cs
@@ -0,0 +1,29 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // from bcrypt.h
+ internal enum BCryptKeyDerivationBufferType
+ {
+ KDF_HASH_ALGORITHM = 0x0,
+ KDF_SECRET_PREPEND = 0x1,
+ KDF_SECRET_APPEND = 0x2,
+ KDF_HMAC_KEY = 0x3,
+ KDF_TLS_PRF_LABEL = 0x4,
+ KDF_TLS_PRF_SEED = 0x5,
+ KDF_SECRET_HANDLE = 0x6,
+ KDF_TLS_PRF_PROTOCOL = 0x7,
+ KDF_ALGORITHMID = 0x8,
+ KDF_PARTYUINFO = 0x9,
+ KDF_PARTYVINFO = 0xA,
+ KDF_SUPPPUBINFO = 0xB,
+ KDF_SUPPPRIVINFO = 0xC,
+ KDF_LABEL = 0xD,
+ KDF_CONTEXT = 0xE,
+ KDF_SALT = 0xF,
+ KDF_ITERATION_COUNT = 0x10,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptUtil.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptUtil.cs
new file mode 100644
index 0000000000..86c86c64a8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/BCryptUtil.cs
@@ -0,0 +1,29 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ /// <summary>
+ /// Wraps utility BCRYPT APIs that don't work directly with handles.
+ /// </summary>
+ internal unsafe static class BCryptUtil
+ {
+ /// <summary>
+ /// Fills a buffer with cryptographically secure random data.
+ /// </summary>
+ public static void GenRandom(byte* pbBuffer, uint cbBuffer)
+ {
+ if (cbBuffer != 0)
+ {
+ int ntstatus = UnsafeNativeMethods.BCryptGenRandom(
+ hAlgorithm: IntPtr.Zero,
+ pbBuffer: pbBuffer,
+ cbBuffer: cbBuffer,
+ dwFlags: BCryptGenRandomFlags.BCRYPT_USE_SYSTEM_PREFERRED_RNG);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs
new file mode 100644
index 0000000000..48e63685fd
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs
@@ -0,0 +1,93 @@
+// 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 Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ /// <summary>
+ /// Provides cached CNG algorithm provider instances, as calling BCryptOpenAlgorithmProvider is expensive.
+ /// Callers should use caution never to dispose of the algorithm provider instances returned by this type.
+ /// </summary>
+ internal static class CachedAlgorithmHandles
+ {
+ private static CachedAlgorithmInfo _aesCbc = new CachedAlgorithmInfo(() => GetAesAlgorithm(chainingMode: Constants.BCRYPT_CHAIN_MODE_CBC));
+ private static CachedAlgorithmInfo _aesGcm = new CachedAlgorithmInfo(() => GetAesAlgorithm(chainingMode: Constants.BCRYPT_CHAIN_MODE_GCM));
+ private static CachedAlgorithmInfo _hmacSha1 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA1_ALGORITHM));
+ private static CachedAlgorithmInfo _hmacSha256 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA256_ALGORITHM));
+ private static CachedAlgorithmInfo _hmacSha512 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA512_ALGORITHM));
+ private static CachedAlgorithmInfo _pbkdf2 = new CachedAlgorithmInfo(GetPbkdf2Algorithm);
+ private static CachedAlgorithmInfo _sha1 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA1_ALGORITHM));
+ private static CachedAlgorithmInfo _sha256 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA256_ALGORITHM));
+ private static CachedAlgorithmInfo _sha512 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA512_ALGORITHM));
+ private static CachedAlgorithmInfo _sp800_108_ctr_hmac = new CachedAlgorithmInfo(GetSP800_108_CTR_HMACAlgorithm);
+
+ public static BCryptAlgorithmHandle AES_CBC => CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesCbc);
+
+ public static BCryptAlgorithmHandle AES_GCM => CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesGcm);
+
+ public static BCryptAlgorithmHandle HMAC_SHA1 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha1);
+
+ public static BCryptAlgorithmHandle HMAC_SHA256 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha256);
+
+ public static BCryptAlgorithmHandle HMAC_SHA512 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha512);
+
+ // Only available on Win8+.
+ public static BCryptAlgorithmHandle PBKDF2 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _pbkdf2);
+
+ public static BCryptAlgorithmHandle SHA1 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha1);
+
+ public static BCryptAlgorithmHandle SHA256 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha256);
+
+ public static BCryptAlgorithmHandle SHA512 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha512);
+
+ // Only available on Win8+.
+ public static BCryptAlgorithmHandle SP800_108_CTR_HMAC => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sp800_108_ctr_hmac);
+
+ private static BCryptAlgorithmHandle GetAesAlgorithm(string chainingMode)
+ {
+ var algHandle = BCryptAlgorithmHandle.OpenAlgorithmHandle(Constants.BCRYPT_AES_ALGORITHM);
+ algHandle.SetChainingMode(chainingMode);
+ return algHandle;
+ }
+
+ private static BCryptAlgorithmHandle GetHashAlgorithm(string algorithm)
+ {
+ return BCryptAlgorithmHandle.OpenAlgorithmHandle(algorithm, hmac: false);
+ }
+
+ private static BCryptAlgorithmHandle GetHmacAlgorithm(string algorithm)
+ {
+ return BCryptAlgorithmHandle.OpenAlgorithmHandle(algorithm, hmac: true);
+ }
+
+ private static BCryptAlgorithmHandle GetPbkdf2Algorithm()
+ {
+ return BCryptAlgorithmHandle.OpenAlgorithmHandle(Constants.BCRYPT_PBKDF2_ALGORITHM, implementation: Constants.MS_PRIMITIVE_PROVIDER);
+ }
+
+ private static BCryptAlgorithmHandle GetSP800_108_CTR_HMACAlgorithm()
+ {
+ return BCryptAlgorithmHandle.OpenAlgorithmHandle(Constants.BCRYPT_SP800108_CTR_HMAC_ALGORITHM, implementation: Constants.MS_PRIMITIVE_PROVIDER);
+ }
+
+ // Warning: mutable struct!
+ private struct CachedAlgorithmInfo
+ {
+ private WeakReference<BCryptAlgorithmHandle> _algorithmHandle;
+ private readonly Func<BCryptAlgorithmHandle> _factory;
+
+ public CachedAlgorithmInfo(Func<BCryptAlgorithmHandle> factory)
+ {
+ _algorithmHandle = null;
+ _factory = factory;
+ }
+
+ public static BCryptAlgorithmHandle GetAlgorithmHandle(ref CachedAlgorithmInfo cachedAlgorithmInfo)
+ {
+ return WeakReferenceHelpers.GetSharedInstance(ref cachedAlgorithmInfo._algorithmHandle, cachedAlgorithmInfo._factory);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/NCryptEncryptFlags.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/NCryptEncryptFlags.cs
new file mode 100644
index 0000000000..a0c1bc0fc4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/NCryptEncryptFlags.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;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ [Flags]
+ internal enum NCryptEncryptFlags
+ {
+ NCRYPT_NO_PADDING_FLAG = 0x00000001,
+ NCRYPT_PAD_PKCS1_FLAG = 0x00000002,
+ NCRYPT_PAD_OAEP_FLAG = 0x00000004,
+ NCRYPT_PAD_PSS_FLAG = 0x00000008,
+ NCRYPT_SILENT_FLAG = 0x00000040,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/OSVersionUtil.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/OSVersionUtil.cs
new file mode 100644
index 0000000000..0d09a0b6f8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Cng/OSVersionUtil.cs
@@ -0,0 +1,66 @@
+// 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 Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ internal static class OSVersionUtil
+ {
+ private static readonly OSVersion _osVersion = GetOSVersion();
+
+ private static OSVersion GetOSVersion()
+ {
+ const string BCRYPT_LIB = "bcrypt.dll";
+ SafeLibraryHandle bcryptLibHandle = null;
+ try
+ {
+ bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB);
+ }
+ catch
+ {
+ // we'll handle the exceptional case later
+ }
+
+ if (bcryptLibHandle != null)
+ {
+ using (bcryptLibHandle)
+ {
+ if (bcryptLibHandle.DoesProcExist("BCryptKeyDerivation"))
+ {
+ // We're running on Win8+.
+ return OSVersion.Win8OrLater;
+ }
+ else
+ {
+ // We're running on Win7+.
+ return OSVersion.Win7OrLater;
+ }
+ }
+ }
+ else
+ {
+ // Not running on Win7+.
+ return OSVersion.NotWindows;
+ }
+ }
+
+ public static bool IsWindows()
+ {
+ return (_osVersion >= OSVersion.Win7OrLater);
+ }
+
+ public static bool IsWindows8OrLater()
+ {
+ return (_osVersion >= OSVersion.Win8OrLater);
+ }
+
+ private enum OSVersion
+ {
+ NotWindows = 0,
+ Win7OrLater = 1,
+ Win8OrLater = 2
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Constants.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Constants.cs
new file mode 100644
index 0000000000..44b0568aa8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Constants.cs
@@ -0,0 +1,88 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ // The majority of these are from bcrypt.h
+ internal static class Constants
+ {
+ internal const int MAX_STACKALLOC_BYTES = 256; // greatest number of bytes that we'll ever allow to stackalloc in a single frame
+
+ // BCrypt(Import/Export)Key BLOB types
+ internal const string BCRYPT_OPAQUE_KEY_BLOB = "OpaqueKeyBlob";
+ internal const string BCRYPT_KEY_DATA_BLOB = "KeyDataBlob";
+ internal const string BCRYPT_AES_WRAP_KEY_BLOB = "Rfc3565KeyWrapBlob";
+
+ // Microsoft built-in providers
+ internal const string MS_PRIMITIVE_PROVIDER = "Microsoft Primitive Provider";
+ internal const string MS_PLATFORM_CRYPTO_PROVIDER = "Microsoft Platform Crypto Provider";
+
+ // Common algorithm identifiers
+ internal const string BCRYPT_RSA_ALGORITHM = "RSA";
+ internal const string BCRYPT_RSA_SIGN_ALGORITHM = "RSA_SIGN";
+ internal const string BCRYPT_DH_ALGORITHM = "DH";
+ internal const string BCRYPT_DSA_ALGORITHM = "DSA";
+ internal const string BCRYPT_RC2_ALGORITHM = "RC2";
+ internal const string BCRYPT_RC4_ALGORITHM = "RC4";
+ internal const string BCRYPT_AES_ALGORITHM = "AES";
+ internal const string BCRYPT_DES_ALGORITHM = "DES";
+ internal const string BCRYPT_DESX_ALGORITHM = "DESX";
+ internal const string BCRYPT_3DES_ALGORITHM = "3DES";
+ internal const string BCRYPT_3DES_112_ALGORITHM = "3DES_112";
+ internal const string BCRYPT_MD2_ALGORITHM = "MD2";
+ internal const string BCRYPT_MD4_ALGORITHM = "MD4";
+ internal const string BCRYPT_MD5_ALGORITHM = "MD5";
+ internal const string BCRYPT_SHA1_ALGORITHM = "SHA1";
+ internal const string BCRYPT_SHA256_ALGORITHM = "SHA256";
+ internal const string BCRYPT_SHA384_ALGORITHM = "SHA384";
+ internal const string BCRYPT_SHA512_ALGORITHM = "SHA512";
+ internal const string BCRYPT_AES_GMAC_ALGORITHM = "AES-GMAC";
+ internal const string BCRYPT_AES_CMAC_ALGORITHM = "AES-CMAC";
+ internal const string BCRYPT_ECDSA_P256_ALGORITHM = "ECDSA_P256";
+ internal const string BCRYPT_ECDSA_P384_ALGORITHM = "ECDSA_P384";
+ internal const string BCRYPT_ECDSA_P521_ALGORITHM = "ECDSA_P521";
+ internal const string BCRYPT_ECDH_P256_ALGORITHM = "ECDH_P256";
+ internal const string BCRYPT_ECDH_P384_ALGORITHM = "ECDH_P384";
+ internal const string BCRYPT_ECDH_P521_ALGORITHM = "ECDH_P521";
+ internal const string BCRYPT_RNG_ALGORITHM = "RNG";
+ internal const string BCRYPT_RNG_FIPS186_DSA_ALGORITHM = "FIPS186DSARNG";
+ internal const string BCRYPT_RNG_DUAL_EC_ALGORITHM = "DUALECRNG";
+ internal const string BCRYPT_SP800108_CTR_HMAC_ALGORITHM = "SP800_108_CTR_HMAC";
+ internal const string BCRYPT_SP80056A_CONCAT_ALGORITHM = "SP800_56A_CONCAT";
+ internal const string BCRYPT_PBKDF2_ALGORITHM = "PBKDF2";
+ internal const string BCRYPT_CAPI_KDF_ALGORITHM = "CAPI_KDF";
+
+ // BCryptGetProperty strings
+ internal const string BCRYPT_OBJECT_LENGTH = "ObjectLength";
+ internal const string BCRYPT_ALGORITHM_NAME = "AlgorithmName";
+ internal const string BCRYPT_PROVIDER_HANDLE = "ProviderHandle";
+ internal const string BCRYPT_CHAINING_MODE = "ChainingMode";
+ internal const string BCRYPT_BLOCK_LENGTH = "BlockLength";
+ internal const string BCRYPT_KEY_LENGTH = "KeyLength";
+ internal const string BCRYPT_KEY_OBJECT_LENGTH = "KeyObjectLength";
+ internal const string BCRYPT_KEY_STRENGTH = "KeyStrength";
+ internal const string BCRYPT_KEY_LENGTHS = "KeyLengths";
+ internal const string BCRYPT_BLOCK_SIZE_LIST = "BlockSizeList";
+ internal const string BCRYPT_EFFECTIVE_KEY_LENGTH = "EffectiveKeyLength";
+ internal const string BCRYPT_HASH_LENGTH = "HashDigestLength";
+ internal const string BCRYPT_HASH_OID_LIST = "HashOIDList";
+ internal const string BCRYPT_PADDING_SCHEMES = "PaddingSchemes";
+ internal const string BCRYPT_SIGNATURE_LENGTH = "SignatureLength";
+ internal const string BCRYPT_HASH_BLOCK_LENGTH = "HashBlockLength";
+ internal const string BCRYPT_AUTH_TAG_LENGTH = "AuthTagLength";
+ internal const string BCRYPT_PRIMITIVE_TYPE = "PrimitiveType";
+ internal const string BCRYPT_IS_KEYED_HASH = "IsKeyedHash";
+ internal const string BCRYPT_IS_REUSABLE_HASH = "IsReusableHash";
+ internal const string BCRYPT_MESSAGE_BLOCK_LENGTH = "MessageBlockLength";
+
+ // Property Strings
+ internal const string BCRYPT_CHAIN_MODE_NA = "ChainingModeN/A";
+ internal const string BCRYPT_CHAIN_MODE_CBC = "ChainingModeCBC";
+ internal const string BCRYPT_CHAIN_MODE_ECB = "ChainingModeECB";
+ internal const string BCRYPT_CHAIN_MODE_CFB = "ChainingModeCFB";
+ internal const string BCRYPT_CHAIN_MODE_CCM = "ChainingModeCCM";
+ internal const string BCRYPT_CHAIN_MODE_GCM = "ChainingModeGCM";
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/CryptoUtil.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/CryptoUtil.cs
new file mode 100644
index 0000000000..e60673634d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/CryptoUtil.cs
@@ -0,0 +1,99 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.Internal;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ internal unsafe static class CryptoUtil
+ {
+ // This isn't a typical Debug.Assert; the check is always performed, even in retail builds.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Assert(bool condition, string message)
+ {
+ if (!condition)
+ {
+ Fail(message);
+ }
+ }
+
+ // This isn't a typical Debug.Assert; the check is always performed, even in retail builds.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void AssertSafeHandleIsValid(SafeHandle safeHandle)
+ {
+ Assert(safeHandle != null && !safeHandle.IsInvalid, "Safe handle is invalid.");
+ }
+
+ // Asserts that the current platform is Windows; throws PlatformNotSupportedException otherwise.
+ public static void AssertPlatformIsWindows()
+ {
+ if (!OSVersionUtil.IsWindows())
+ {
+ throw new PlatformNotSupportedException(Resources.Platform_Windows7Required);
+ }
+ }
+
+ // Asserts that the current platform is Windows 8 or above; throws PlatformNotSupportedException otherwise.
+ public static void AssertPlatformIsWindows8OrLater()
+ {
+ if (!OSVersionUtil.IsWindows8OrLater())
+ {
+ throw new PlatformNotSupportedException(Resources.Platform_Windows8Required);
+ }
+ }
+
+ // This isn't a typical Debug.Fail; an error always occurs, even in retail builds.
+ // This method doesn't return, but since the CLR doesn't allow specifying a 'never'
+ // return type, we mimic it by specifying our return type as Exception. That way
+ // callers can write 'throw Fail(...);' to make the C# compiler happy, as the
+ // throw keyword is implicitly of type O.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static Exception Fail(string message)
+ {
+ Debug.Fail(message);
+ throw new CryptographicException("Assertion failed: " + message);
+ }
+
+ // Allows callers to write "var x = Method() ?? Fail<T>(message);" as a convenience to guard
+ // against a method returning null unexpectedly.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Fail<T>(string message) where T : class
+ {
+ throw Fail(message);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static bool TimeConstantBuffersAreEqual(byte* bufA, byte* bufB, uint count)
+ {
+ bool areEqual = true;
+ for (uint i = 0; i < count; i++)
+ {
+ areEqual &= (bufA[i] == bufB[i]);
+ }
+ return areEqual;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static bool TimeConstantBuffersAreEqual(byte[] bufA, int offsetA, int countA, byte[] bufB, int offsetB, int countB)
+ {
+ // Technically this is an early exit scenario, but it means that the caller did something bizarre.
+ // An error at the call site isn't usable for timing attacks.
+ Assert(countA == countB, "countA == countB");
+
+ bool areEqual = true;
+ for (int i = 0; i < countA; i++)
+ {
+ areEqual &= (bufA[offsetA + i] == bufB[offsetB + i]);
+ }
+ return areEqual;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/DATA_BLOB.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/DATA_BLOB.cs
new file mode 100644
index 0000000000..3c307bebce
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/DATA_BLOB.cs
@@ -0,0 +1,16 @@
+// 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.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa381414(v=vs.85).aspx
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct DATA_BLOB
+ {
+ public uint cbData;
+ public byte* pbData;
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Microsoft.AspNetCore.Cryptography.Internal.csproj b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Microsoft.AspNetCore.Cryptography.Internal.csproj
new file mode 100644
index 0000000000..ff4ef3babe
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Microsoft.AspNetCore.Cryptography.Internal.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Infrastructure for ASP.NET Core cryptographic packages. Applications and libraries should not reference this package directly.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection</PackageTags>
+ </PropertyGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/AssemblyInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..62865ae945
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/AssemblyInfo.cs
@@ -0,0 +1,16 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// we only ever p/invoke into DLLs known to be in the System32 folder
+[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Cryptography.Internal.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Cryptography.KeyDerivation, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Cryptography.KeyDerivation.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Abstractions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/Resources.Designer.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..df010bc683
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Properties/Resources.Designer.cs
@@ -0,0 +1,86 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Cryptography.Internal
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Cryptography.Internal.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// A provider could not be found for algorithm '{0}'.
+ /// </summary>
+ internal static string BCryptAlgorithmHandle_ProviderNotFound
+ {
+ get => GetString("BCryptAlgorithmHandle_ProviderNotFound");
+ }
+
+ /// <summary>
+ /// A provider could not be found for algorithm '{0}'.
+ /// </summary>
+ internal static string FormatBCryptAlgorithmHandle_ProviderNotFound(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("BCryptAlgorithmHandle_ProviderNotFound"), p0);
+
+ /// <summary>
+ /// The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}).
+ /// </summary>
+ internal static string BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength
+ {
+ get => GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength");
+ }
+
+ /// <summary>
+ /// The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}).
+ /// </summary>
+ internal static string FormatBCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength(object p0, object p1, object p2, object p3)
+ => string.Format(CultureInfo.CurrentCulture, GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"), p0, p1, p2, p3);
+
+ /// <summary>
+ /// This operation requires Windows 7 / Windows Server 2008 R2 or later.
+ /// </summary>
+ internal static string Platform_Windows7Required
+ {
+ get => GetString("Platform_Windows7Required");
+ }
+
+ /// <summary>
+ /// This operation requires Windows 7 / Windows Server 2008 R2 or later.
+ /// </summary>
+ internal static string FormatPlatform_Windows7Required()
+ => GetString("Platform_Windows7Required");
+
+ /// <summary>
+ /// This operation requires Windows 8 / Windows Server 2012 or later.
+ /// </summary>
+ internal static string Platform_Windows8Required
+ {
+ get => GetString("Platform_Windows8Required");
+ }
+
+ /// <summary>
+ /// This operation requires Windows 8 / Windows Server 2012 or later.
+ /// </summary>
+ internal static string FormatPlatform_Windows8Required()
+ => GetString("Platform_Windows8Required");
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Resources.resx b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Resources.resx
new file mode 100644
index 0000000000..125f619abb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/Resources.resx
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="BCryptAlgorithmHandle_ProviderNotFound" xml:space="preserve">
+ <value>A provider could not be found for algorithm '{0}'.</value>
+ </data>
+ <data name="BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength" xml:space="preserve">
+ <value>The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}).</value>
+ </data>
+ <data name="Platform_Windows7Required" xml:space="preserve">
+ <value>This operation requires Windows 7 / Windows Server 2008 R2 or later.</value>
+ </data>
+ <data name="Platform_Windows8Required" xml:space="preserve">
+ <value>This operation requires Windows 8 / Windows Server 2012 or later.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs
new file mode 100644
index 0000000000..45f4c4e041
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs
@@ -0,0 +1,170 @@
+// 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.Globalization;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.Internal;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ /// <summary>
+ /// Represents a handle to a BCrypt algorithm provider from which keys and hashes can be created.
+ /// </summary>
+ internal unsafe sealed class BCryptAlgorithmHandle : BCryptHandle
+ {
+ // Called by P/Invoke when returning SafeHandles
+ private BCryptAlgorithmHandle() { }
+
+ /// <summary>
+ /// Creates an unkeyed hash handle from this hash algorithm.
+ /// </summary>
+ public BCryptHashHandle CreateHash()
+ {
+ return CreateHashCore(null, 0);
+ }
+
+ private BCryptHashHandle CreateHashCore(byte* pbKey, uint cbKey)
+ {
+ BCryptHashHandle retVal;
+ int ntstatus = UnsafeNativeMethods.BCryptCreateHash(this, out retVal, IntPtr.Zero, 0, pbKey, cbKey, dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(retVal);
+
+ retVal.SetAlgorithmProviderHandle(this);
+ return retVal;
+ }
+
+ /// <summary>
+ /// Creates an HMAC hash handle from this hash algorithm.
+ /// </summary>
+ public BCryptHashHandle CreateHmac(byte* pbKey, uint cbKey)
+ {
+ Debug.Assert(pbKey != null);
+ return CreateHashCore(pbKey, cbKey);
+ }
+
+ /// <summary>
+ /// Imports a key into a symmetric encryption or KDF algorithm.
+ /// </summary>
+ public BCryptKeyHandle GenerateSymmetricKey(byte* pbSecret, uint cbSecret)
+ {
+ BCryptKeyHandle retVal;
+ int ntstatus = UnsafeNativeMethods.BCryptGenerateSymmetricKey(this, out retVal, IntPtr.Zero, 0, pbSecret, cbSecret, 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(retVal);
+
+ retVal.SetAlgorithmProviderHandle(this);
+ return retVal;
+ }
+
+ /// <summary>
+ /// Gets the name of this BCrypt algorithm.
+ /// </summary>
+ public string GetAlgorithmName()
+ {
+ // First, calculate how many characters are in the name.
+ uint byteLengthOfNameWithTerminatingNull = GetProperty(Constants.BCRYPT_ALGORITHM_NAME, null, 0);
+ CryptoUtil.Assert(byteLengthOfNameWithTerminatingNull % sizeof(char) == 0 && byteLengthOfNameWithTerminatingNull > sizeof(char), "byteLengthOfNameWithTerminatingNull % sizeof(char) == 0 && byteLengthOfNameWithTerminatingNull > sizeof(char)");
+ uint numCharsWithoutNull = (byteLengthOfNameWithTerminatingNull - 1) / sizeof(char);
+
+ if (numCharsWithoutNull == 0)
+ {
+ return String.Empty; // degenerate case
+ }
+
+ // Allocate a string object and write directly into it (CLR team approves of this mechanism).
+ string retVal = new String((char)0, checked((int)numCharsWithoutNull));
+ uint numBytesCopied;
+ fixed (char* pRetVal = retVal)
+ {
+ numBytesCopied = GetProperty(Constants.BCRYPT_ALGORITHM_NAME, pRetVal, byteLengthOfNameWithTerminatingNull);
+ }
+ CryptoUtil.Assert(numBytesCopied == byteLengthOfNameWithTerminatingNull, "numBytesCopied == byteLengthOfNameWithTerminatingNull");
+ return retVal;
+ }
+
+ /// <summary>
+ /// Gets the cipher block length (in bytes) of this block cipher algorithm.
+ /// </summary>
+ public uint GetCipherBlockLength()
+ {
+ uint cipherBlockLength;
+ uint numBytesCopied = GetProperty(Constants.BCRYPT_BLOCK_LENGTH, &cipherBlockLength, sizeof(uint));
+ CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)");
+ return cipherBlockLength;
+ }
+
+ /// <summary>
+ /// Gets the hash block length (in bytes) of this hash algorithm.
+ /// </summary>
+ public uint GetHashBlockLength()
+ {
+ uint hashBlockLength;
+ uint numBytesCopied = GetProperty(Constants.BCRYPT_HASH_BLOCK_LENGTH, &hashBlockLength, sizeof(uint));
+ CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)");
+ return hashBlockLength;
+ }
+
+ /// <summary>
+ /// Gets the key lengths (in bits) supported by this algorithm.
+ /// </summary>
+ public BCRYPT_KEY_LENGTHS_STRUCT GetSupportedKeyLengths()
+ {
+ BCRYPT_KEY_LENGTHS_STRUCT supportedKeyLengths;
+ uint numBytesCopied = GetProperty(Constants.BCRYPT_KEY_LENGTHS, &supportedKeyLengths, (uint)sizeof(BCRYPT_KEY_LENGTHS_STRUCT));
+ CryptoUtil.Assert(numBytesCopied == sizeof(BCRYPT_KEY_LENGTHS_STRUCT), "numBytesCopied == sizeof(BCRYPT_KEY_LENGTHS_STRUCT)");
+ return supportedKeyLengths;
+ }
+
+ /// <summary>
+ /// Gets the digest length (in bytes) of this hash algorithm provider.
+ /// </summary>
+ public uint GetHashDigestLength()
+ {
+ uint digestLength;
+ uint numBytesCopied = GetProperty(Constants.BCRYPT_HASH_LENGTH, &digestLength, sizeof(uint));
+ CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)");
+ return digestLength;
+ }
+
+ public static BCryptAlgorithmHandle OpenAlgorithmHandle(string algorithmId, string implementation = null, bool hmac = false)
+ {
+ // from bcrypt.h
+ const uint BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008;
+
+ // from ntstatus.h
+ const int STATUS_NOT_FOUND = unchecked((int)0xC0000225);
+
+ BCryptAlgorithmHandle algHandle;
+ int ntstatus = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, algorithmId, implementation, dwFlags: (hmac) ? BCRYPT_ALG_HANDLE_HMAC_FLAG : 0);
+
+ // error checking
+ if (ntstatus == STATUS_NOT_FOUND)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, Resources.BCryptAlgorithmHandle_ProviderNotFound, algorithmId);
+ throw new CryptographicException(message);
+ }
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(algHandle);
+
+ return algHandle;
+ }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ return (UnsafeNativeMethods.BCryptCloseAlgorithmProvider(handle, dwFlags: 0) == 0);
+ }
+
+ public void SetChainingMode(string chainingMode)
+ {
+ fixed (char* pszChainingMode = chainingMode ?? String.Empty)
+ {
+ SetProperty(Constants.BCRYPT_CHAINING_MODE, pszChainingMode, checked((uint)(chainingMode.Length + 1 /* null terminator */) * sizeof(char)));
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHandle.cs
new file mode 100644
index 0000000000..66b2c1dbd4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHandle.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ internal unsafe abstract class BCryptHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ protected BCryptHandle()
+ : base(ownsHandle: true)
+ {
+ }
+
+ protected uint GetProperty(string pszProperty, void* pbOutput, uint cbOutput)
+ {
+ uint retVal;
+ int ntstatus = UnsafeNativeMethods.BCryptGetProperty(this, pszProperty, pbOutput, cbOutput, out retVal, dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ return retVal;
+ }
+
+ protected void SetProperty(string pszProperty, void* pbInput, uint cbInput)
+ {
+ int ntstatus = UnsafeNativeMethods.BCryptSetProperty(this, pszProperty, pbInput, cbInput, dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHashHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHashHandle.cs
new file mode 100644
index 0000000000..dace0f23ae
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptHashHandle.cs
@@ -0,0 +1,71 @@
+// 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 Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ internal unsafe sealed class BCryptHashHandle : BCryptHandle
+ {
+ private BCryptAlgorithmHandle _algProviderHandle;
+
+ // Called by P/Invoke when returning SafeHandles
+ private BCryptHashHandle() { }
+
+ /// <summary>
+ /// Duplicates this hash handle, including any existing hashed state.
+ /// </summary>
+ public BCryptHashHandle DuplicateHash()
+ {
+ BCryptHashHandle duplicateHandle;
+ int ntstatus = UnsafeNativeMethods.BCryptDuplicateHash(this, out duplicateHandle, IntPtr.Zero, 0, 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(duplicateHandle);
+
+ duplicateHandle._algProviderHandle = this._algProviderHandle;
+ return duplicateHandle;
+ }
+
+ /// <summary>
+ /// Calculates the cryptographic hash over a set of input data.
+ /// </summary>
+ public void HashData(byte* pbInput, uint cbInput, byte* pbHashDigest, uint cbHashDigest)
+ {
+ int ntstatus;
+ if (cbInput > 0)
+ {
+ ntstatus = UnsafeNativeMethods.BCryptHashData(
+ hHash: this,
+ pbInput: pbInput,
+ cbInput: cbInput,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ }
+
+ ntstatus = UnsafeNativeMethods.BCryptFinishHash(
+ hHash: this,
+ pbOutput: pbHashDigest,
+ cbOutput: cbHashDigest,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ return (UnsafeNativeMethods.BCryptDestroyHash(handle) == 0);
+ }
+
+ // We don't actually need to hold a reference to the algorithm handle, as the native CNG library
+ // already holds the reference for us. But once we create a hash from an algorithm provider, odds
+ // are good that we'll create another hash from the same algorithm provider at some point in the
+ // future. And since algorithm providers are expensive to create, we'll hold a strong reference
+ // to all known in-use providers. This way the cached algorithm provider handles utility class
+ // doesn't keep creating providers over and over.
+ internal void SetAlgorithmProviderHandle(BCryptAlgorithmHandle algProviderHandle)
+ {
+ _algProviderHandle = algProviderHandle;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptKeyHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptKeyHandle.cs
new file mode 100644
index 0000000000..cd7d05f8e3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/BCryptKeyHandle.cs
@@ -0,0 +1,33 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ internal sealed class BCryptKeyHandle : BCryptHandle
+ {
+ private BCryptAlgorithmHandle _algProviderHandle;
+
+ // Called by P/Invoke when returning SafeHandles
+ private BCryptKeyHandle() { }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ _algProviderHandle = null;
+ return (UnsafeNativeMethods.BCryptDestroyKey(handle) == 0);
+ }
+
+ // We don't actually need to hold a reference to the algorithm handle, as the native CNG library
+ // already holds the reference for us. But once we create a key from an algorithm provider, odds
+ // are good that we'll create another key from the same algorithm provider at some point in the
+ // future. And since algorithm providers are expensive to create, we'll hold a strong reference
+ // to all known in-use providers. This way the cached algorithm provider handles utility class
+ // doesn't keep creating providers over and over.
+ internal void SetAlgorithmProviderHandle(BCryptAlgorithmHandle algProviderHandle)
+ {
+ _algProviderHandle = algProviderHandle;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/LocalAllocHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/LocalAllocHandle.cs
new file mode 100644
index 0000000000..852c5d1594
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/LocalAllocHandle.cs
@@ -0,0 +1,26 @@
+// 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.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ /// <summary>
+ /// Represents a handle returned by LocalAlloc.
+ /// </summary>
+ internal class LocalAllocHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ // Called by P/Invoke when returning SafeHandles
+ protected LocalAllocHandle()
+ : base(ownsHandle: true) { }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle); // actually calls LocalFree
+ return true;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs
new file mode 100644
index 0000000000..3a181cf06b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs
@@ -0,0 +1,42 @@
+// 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 Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ internal unsafe sealed class NCryptDescriptorHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ private NCryptDescriptorHandle()
+ : base(ownsHandle: true)
+ {
+ }
+
+ public string GetProtectionDescriptorRuleString()
+ {
+ // from ncryptprotect.h
+ const int NCRYPT_PROTECTION_INFO_TYPE_DESCRIPTOR_STRING = 0x00000001;
+
+ LocalAllocHandle ruleStringHandle;
+ int ntstatus = UnsafeNativeMethods.NCryptGetProtectionDescriptorInfo(
+ hDescriptor: this,
+ pMemPara: IntPtr.Zero,
+ dwInfoType: NCRYPT_PROTECTION_INFO_TYPE_DESCRIPTOR_STRING,
+ ppvInfo: out ruleStringHandle);
+ UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(ruleStringHandle);
+
+ using (ruleStringHandle)
+ {
+ return new String((char*)ruleStringHandle.DangerousGetHandle());
+ }
+ }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ return (UnsafeNativeMethods.NCryptCloseProtectionDescriptor(handle) == 0);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SafeLibraryHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SafeLibraryHandle.cs
new file mode 100644
index 0000000000..ccd0b99c79
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SafeLibraryHandle.cs
@@ -0,0 +1,176 @@
+// 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.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ /// <summary>
+ /// Represents a handle to a Windows module (DLL).
+ /// </summary>
+ internal unsafe sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ // Called by P/Invoke when returning SafeHandles
+ private SafeLibraryHandle()
+ : base(ownsHandle: true)
+ { }
+
+ /// <summary>
+ /// Returns a value stating whether the library exports a given proc.
+ /// </summary>
+ public bool DoesProcExist(string lpProcName)
+ {
+ IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName);
+ return (pfnProc != IntPtr.Zero);
+ }
+
+ /// <summary>
+ /// Forbids this library from being unloaded. The library will remain loaded until process termination,
+ /// regardless of how many times FreeLibrary is called.
+ /// </summary>
+ public void ForbidUnload()
+ {
+ // from winbase.h
+ const uint GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 0x00000004U;
+ const uint GET_MODULE_HANDLE_EX_FLAG_PIN = 0x00000001U;
+
+ IntPtr unused;
+ bool retVal = UnsafeNativeMethods.GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, this, out unused);
+ if (!retVal)
+ {
+ UnsafeNativeMethods.ThrowExceptionForLastWin32Error();
+ }
+ }
+
+ /// <summary>
+ /// Formats a message string using the resource table in the specified library.
+ /// </summary>
+ public string FormatMessage(int messageId)
+ {
+ // from winbase.h
+ const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
+ const uint FORMAT_MESSAGE_FROM_HMODULE = 0x00000800;
+ const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
+ const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
+
+ LocalAllocHandle messageHandle;
+ int numCharsOutput = UnsafeNativeMethods.FormatMessage(
+ dwFlags: FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ lpSource: this,
+ dwMessageId: (uint)messageId,
+ dwLanguageId: 0 /* ignore current culture */,
+ lpBuffer: out messageHandle,
+ nSize: 0 /* unused */,
+ Arguments: IntPtr.Zero /* unused */);
+
+ if (numCharsOutput != 0 && messageHandle != null && !messageHandle.IsInvalid)
+ {
+ // Successfully retrieved the message.
+ using (messageHandle)
+ {
+ return new String((char*)messageHandle.DangerousGetHandle(), 0, numCharsOutput).Trim();
+ }
+ }
+ else
+ {
+ // Message not found - that's fine.
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets a delegate pointing to a given export from this library.
+ /// </summary>
+ public TDelegate GetProcAddress<TDelegate>(string lpProcName, bool throwIfNotFound = true) where TDelegate : class
+ {
+ IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName);
+ if (pfnProc == IntPtr.Zero)
+ {
+ if (throwIfNotFound)
+ {
+ UnsafeNativeMethods.ThrowExceptionForLastWin32Error();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return Marshal.GetDelegateForFunctionPointer<TDelegate>(pfnProc);
+ }
+
+ /// <summary>
+ /// Opens a library. If 'filename' is not a fully-qualified path, the default search path is used.
+ /// </summary>
+ public static SafeLibraryHandle Open(string filename)
+ {
+ const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800U; // from libloaderapi.h
+
+ SafeLibraryHandle handle = UnsafeNativeMethods.LoadLibraryEx(filename, IntPtr.Zero, LOAD_LIBRARY_SEARCH_SYSTEM32);
+ if (handle == null || handle.IsInvalid)
+ {
+ UnsafeNativeMethods.ThrowExceptionForLastWin32Error();
+ }
+ return handle;
+ }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ return UnsafeNativeMethods.FreeLibrary(handle);
+ }
+
+ [SuppressUnmanagedCodeSecurity]
+ private static class UnsafeNativeMethods
+ {
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/ms679351(v=vs.85).aspx
+ [DllImport("kernel32.dll", EntryPoint = "FormatMessageW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern int FormatMessage(
+ [In] uint dwFlags,
+ [In] SafeLibraryHandle lpSource,
+ [In] uint dwMessageId,
+ [In] uint dwLanguageId,
+ [Out] out LocalAllocHandle lpBuffer,
+ [In] uint nSize,
+ [In] IntPtr Arguments
+ );
+
+ // http://msdn.microsoft.com/en-us/library/ms683152(v=vs.85).aspx
+ [return: MarshalAs(UnmanagedType.Bool)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ internal static extern bool FreeLibrary(IntPtr hModule);
+
+ // http://msdn.microsoft.com/en-us/library/ms683200(v=vs.85).aspx
+ [return: MarshalAs(UnmanagedType.Bool)]
+ [DllImport("kernel32.dll", EntryPoint = "GetModuleHandleExW", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ internal static extern bool GetModuleHandleEx(
+ [In] uint dwFlags,
+ [In] SafeLibraryHandle lpModuleName, // can point to a location within the module if GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS is set
+ [Out] out IntPtr phModule);
+
+ // http://msdn.microsoft.com/en-us/library/ms683212(v=vs.85).aspx
+ [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ internal static extern IntPtr GetProcAddress(
+ [In] SafeLibraryHandle hModule,
+ [In, MarshalAs(UnmanagedType.LPStr)] string lpProcName);
+
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/ms684179(v=vs.85).aspx
+ [DllImport("kernel32.dll", EntryPoint = "LoadLibraryExW", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ internal static extern SafeLibraryHandle LoadLibraryEx(
+ [In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
+ [In] IntPtr hFile,
+ [In] uint dwFlags);
+
+ internal static void ThrowExceptionForLastWin32Error()
+ {
+ int hr = Marshal.GetHRForLastWin32Error();
+ Marshal.ThrowExceptionForHR(hr);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs
new file mode 100644
index 0000000000..ac1f3c6172
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs
@@ -0,0 +1,61 @@
+// 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.Runtime.InteropServices;
+using System.Runtime.ConstrainedExecution;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ /// <summary>
+ /// Represents a handle returned by LocalAlloc.
+ /// The memory will be zeroed out before it's freed.
+ /// </summary>
+ internal unsafe sealed class SecureLocalAllocHandle : LocalAllocHandle
+ {
+ private readonly IntPtr _cb;
+
+ private SecureLocalAllocHandle(IntPtr cb)
+ {
+ _cb = cb;
+ }
+
+ public IntPtr Length
+ {
+ get
+ {
+ return _cb;
+ }
+ }
+
+ /// <summary>
+ /// Allocates some amount of memory using LocalAlloc.
+ /// </summary>
+ public static SecureLocalAllocHandle Allocate(IntPtr cb)
+ {
+ SecureLocalAllocHandle newHandle = new SecureLocalAllocHandle(cb);
+ newHandle.AllocateImpl(cb);
+ return newHandle;
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ private void AllocateImpl(IntPtr cb)
+ {
+ handle = Marshal.AllocHGlobal(cb); // actually calls LocalAlloc
+ }
+
+ public SecureLocalAllocHandle Duplicate()
+ {
+ SecureLocalAllocHandle duplicateHandle = Allocate(_cb);
+ UnsafeBufferUtil.BlockCopy(from: this, to: duplicateHandle, length: _cb);
+ return duplicateHandle;
+ }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ UnsafeBufferUtil.SecureZeroMemory((byte*)handle, _cb); // compiler won't optimize this away
+ return base.ReleaseHandle();
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeBufferUtil.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeBufferUtil.cs
new file mode 100644
index 0000000000..681adb8bc3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeBufferUtil.cs
@@ -0,0 +1,179 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.ConstrainedExecution;
+using System.Threading;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ internal unsafe static class UnsafeBufferUtil
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static void BlockCopy(void* from, void* to, int byteCount)
+ {
+ BlockCopy(from, to, checked((uint)byteCount)); // will be checked before invoking the delegate
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static void BlockCopy(void* from, void* to, uint byteCount)
+ {
+ if (byteCount != 0)
+ {
+ BlockCopyCore((byte*)from, (byte*)to, byteCount);
+ }
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ public static void BlockCopy(LocalAllocHandle from, void* to, uint byteCount)
+ {
+ bool refAdded = false;
+ try
+ {
+ from.DangerousAddRef(ref refAdded);
+ BlockCopy((void*)from.DangerousGetHandle(), to, byteCount);
+ }
+ finally
+ {
+ if (refAdded)
+ {
+ from.DangerousRelease();
+ }
+ }
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ public static void BlockCopy(void* from, LocalAllocHandle to, uint byteCount)
+ {
+ bool refAdded = false;
+ try
+ {
+ to.DangerousAddRef(ref refAdded);
+ BlockCopy(from, (void*)to.DangerousGetHandle(), byteCount);
+ }
+ finally
+ {
+ if (refAdded)
+ {
+ to.DangerousRelease();
+ }
+ }
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ public static void BlockCopy(LocalAllocHandle from, LocalAllocHandle to, IntPtr length)
+ {
+ if (length == IntPtr.Zero)
+ {
+ return;
+ }
+
+ bool fromRefAdded = false;
+ bool toRefAdded = false;
+ try
+ {
+ from.DangerousAddRef(ref fromRefAdded);
+ to.DangerousAddRef(ref toRefAdded);
+ if (sizeof(IntPtr) == 4)
+ {
+ BlockCopyCore(from: (byte*)from.DangerousGetHandle(), to: (byte*)to.DangerousGetHandle(), byteCount: (uint)length.ToInt32());
+ }
+ else
+ {
+ BlockCopyCore(from: (byte*)from.DangerousGetHandle(), to: (byte*)to.DangerousGetHandle(), byteCount: (ulong)length.ToInt64());
+ }
+ }
+ finally
+ {
+ if (fromRefAdded)
+ {
+ from.DangerousRelease();
+ }
+ if (toRefAdded)
+ {
+ to.DangerousRelease();
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void BlockCopyCore(byte* from, byte* to, uint byteCount)
+ {
+ Buffer.MemoryCopy(from, to, (ulong)byteCount, (ulong)byteCount);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void BlockCopyCore(byte* from, byte* to, ulong byteCount)
+ {
+ Buffer.MemoryCopy(from, to, byteCount, byteCount);
+ }
+
+ /// <summary>
+ /// Securely clears a memory buffer.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static void SecureZeroMemory(byte* buffer, int byteCount)
+ {
+ SecureZeroMemory(buffer, checked((uint)byteCount));
+ }
+
+ /// <summary>
+ /// Securely clears a memory buffer.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static void SecureZeroMemory(byte* buffer, uint byteCount)
+ {
+ if (byteCount != 0)
+ {
+ do
+ {
+ buffer[--byteCount] = 0;
+ } while (byteCount != 0);
+
+ // Volatile to make sure the zero-writes don't get optimized away
+ Volatile.Write(ref *buffer, 0);
+ }
+ }
+
+ /// <summary>
+ /// Securely clears a memory buffer.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static void SecureZeroMemory(byte* buffer, ulong byteCount)
+ {
+ if (byteCount != 0)
+ {
+ do
+ {
+ buffer[--byteCount] = 0;
+ } while (byteCount != 0);
+
+ // Volatile to make sure the zero-writes don't get optimized away
+ Volatile.Write(ref *buffer, 0);
+ }
+ }
+
+ /// <summary>
+ /// Securely clears a memory buffer.
+ /// </summary>
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ public static void SecureZeroMemory(byte* buffer, IntPtr length)
+ {
+ if (sizeof(IntPtr) == 4)
+ {
+ SecureZeroMemory(buffer, (uint)length.ToInt32());
+ }
+ else
+ {
+ SecureZeroMemory(buffer, (ulong)length.ToInt64());
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeNativeMethods.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeNativeMethods.cs
new file mode 100644
index 0000000000..3a5a4d8db3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/UnsafeNativeMethods.cs
@@ -0,0 +1,346 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Security.Cryptography;
+using System.Threading;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ [SuppressUnmanagedCodeSecurity]
+ internal unsafe static class UnsafeNativeMethods
+ {
+ private const string BCRYPT_LIB = "bcrypt.dll";
+ private static readonly Lazy<SafeLibraryHandle> _lazyBCryptLibHandle = GetLazyLibraryHandle(BCRYPT_LIB);
+
+ private const string CRYPT32_LIB = "crypt32.dll";
+ private static readonly Lazy<SafeLibraryHandle> _lazyCrypt32LibHandle = GetLazyLibraryHandle(CRYPT32_LIB);
+
+ private const string NCRYPT_LIB = "ncrypt.dll";
+ private static readonly Lazy<SafeLibraryHandle> _lazyNCryptLibHandle = GetLazyLibraryHandle(NCRYPT_LIB);
+
+ private static Lazy<SafeLibraryHandle> GetLazyLibraryHandle(string libraryName)
+ {
+ // We don't need to worry about race conditions: SafeLibraryHandle will clean up after itself
+ return new Lazy<SafeLibraryHandle>(() => SafeLibraryHandle.Open(libraryName), LazyThreadSafetyMode.PublicationOnly);
+ }
+
+ /*
+ * BCRYPT.DLL
+ */
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375377(v=vs.85).aspx
+ internal static extern int BCryptCloseAlgorithmProvider(
+ [In] IntPtr hAlgorithm,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375383(v=vs.85).aspx
+ internal static extern int BCryptCreateHash(
+ [In] BCryptAlgorithmHandle hAlgorithm,
+ [Out] out BCryptHashHandle phHash,
+ [In] IntPtr pbHashObject,
+ [In] uint cbHashObject,
+ [In] byte* pbSecret,
+ [In] uint cbSecret,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375391(v=vs.85).aspx
+ internal static extern int BCryptDecrypt(
+ [In] BCryptKeyHandle hKey,
+ [In] byte* pbInput,
+ [In] uint cbInput,
+ [In] void* pPaddingInfo,
+ [In] byte* pbIV,
+ [In] uint cbIV,
+ [In] byte* pbOutput,
+ [In] uint cbOutput,
+ [Out] out uint pcbResult,
+ [In] BCryptEncryptFlags dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/dd433795(v=vs.85).aspx
+ internal static extern int BCryptDeriveKeyPBKDF2(
+ [In] BCryptAlgorithmHandle hPrf,
+ [In] byte* pbPassword,
+ [In] uint cbPassword,
+ [In] byte* pbSalt,
+ [In] uint cbSalt,
+ [In] ulong cIterations,
+ [In] byte* pbDerivedKey,
+ [In] uint cbDerivedKey,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375399(v=vs.85).aspx
+ internal static extern int BCryptDestroyHash(
+ [In] IntPtr hHash);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375404(v=vs.85).aspx
+ internal static extern int BCryptDestroyKey(
+ [In] IntPtr hKey);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375413(v=vs.85).aspx
+ internal static extern int BCryptDuplicateHash(
+ [In] BCryptHashHandle hHash,
+ [Out] out BCryptHashHandle phNewHash,
+ [In] IntPtr pbHashObject,
+ [In] uint cbHashObject,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375421(v=vs.85).aspx
+ internal static extern int BCryptEncrypt(
+ [In] BCryptKeyHandle hKey,
+ [In] byte* pbInput,
+ [In] uint cbInput,
+ [In] void* pPaddingInfo,
+ [In] byte* pbIV,
+ [In] uint cbIV,
+ [In] byte* pbOutput,
+ [In] uint cbOutput,
+ [Out] out uint pcbResult,
+ [In] BCryptEncryptFlags dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375443(v=vs.85).aspx
+ internal static extern int BCryptFinishHash(
+ [In] BCryptHashHandle hHash,
+ [In] byte* pbOutput,
+ [In] uint cbOutput,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375453(v=vs.85).aspx
+ internal static extern int BCryptGenerateSymmetricKey(
+ [In] BCryptAlgorithmHandle hAlgorithm,
+ [Out] out BCryptKeyHandle phKey,
+ [In] IntPtr pbKeyObject,
+ [In] uint cbKeyObject,
+ [In] byte* pbSecret,
+ [In] uint cbSecret,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375458(v=vs.85).aspx
+ internal static extern int BCryptGenRandom(
+ [In] IntPtr hAlgorithm,
+ [In] byte* pbBuffer,
+ [In] uint cbBuffer,
+ [In] BCryptGenRandomFlags dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375464(v=vs.85).aspx
+ internal static extern int BCryptGetProperty(
+ [In] BCryptHandle hObject,
+ [In, MarshalAs(UnmanagedType.LPWStr)] string pszProperty,
+ [In] void* pbOutput,
+ [In] uint cbOutput,
+ [Out] out uint pcbResult,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375468(v=vs.85).aspx
+ internal static extern int BCryptHashData(
+ [In] BCryptHashHandle hHash,
+ [In] byte* pbInput,
+ [In] uint cbInput,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/hh448506(v=vs.85).aspx
+ internal static extern int BCryptKeyDerivation(
+ [In] BCryptKeyHandle hKey,
+ [In] BCryptBufferDesc* pParameterList,
+ [In] byte* pbDerivedKey,
+ [In] uint cbDerivedKey,
+ [Out] out uint pcbResult,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375479(v=vs.85).aspx
+ internal static extern int BCryptOpenAlgorithmProvider(
+ [Out] out BCryptAlgorithmHandle phAlgorithm,
+ [In, MarshalAs(UnmanagedType.LPWStr)] string pszAlgId,
+ [In, MarshalAs(UnmanagedType.LPWStr)] string pszImplementation,
+ [In] uint dwFlags);
+
+ [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375504(v=vs.85).aspx
+ internal static extern int BCryptSetProperty(
+ [In] BCryptHandle hObject,
+ [In, MarshalAs(UnmanagedType.LPWStr)] string pszProperty,
+ [In] void* pbInput,
+ [In] uint cbInput,
+ [In] uint dwFlags);
+
+ /*
+ * CRYPT32.DLL
+ */
+
+ [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380261(v=vs.85).aspx
+ internal static extern bool CryptProtectData(
+ [In] DATA_BLOB* pDataIn,
+ [In] IntPtr szDataDescr,
+ [In] DATA_BLOB* pOptionalEntropy,
+ [In] IntPtr pvReserved,
+ [In] IntPtr pPromptStruct,
+ [In] uint dwFlags,
+ [Out] out DATA_BLOB pDataOut);
+
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380262(v=vs.85).aspx
+ [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ public static extern bool CryptProtectMemory(
+ [In] SafeHandle pData,
+ [In] uint cbData,
+ [In] uint dwFlags);
+
+ [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380882(v=vs.85).aspx
+ internal static extern bool CryptUnprotectData(
+ [In] DATA_BLOB* pDataIn,
+ [In] IntPtr ppszDataDescr,
+ [In] DATA_BLOB* pOptionalEntropy,
+ [In] IntPtr pvReserved,
+ [In] IntPtr pPromptStruct,
+ [In] uint dwFlags,
+ [Out] out DATA_BLOB pDataOut);
+
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380890(v=vs.85).aspx
+ [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ public static extern bool CryptUnprotectMemory(
+ [In] byte* pData,
+ [In] uint cbData,
+ [In] uint dwFlags);
+
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380890(v=vs.85).aspx
+ [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ public static extern bool CryptUnprotectMemory(
+ [In] SafeHandle pData,
+ [In] uint cbData,
+ [In] uint dwFlags);
+
+ /*
+ * NCRYPT.DLL
+ */
+
+ [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706799(v=vs.85).aspx
+ internal static extern int NCryptCloseProtectionDescriptor(
+ [In] IntPtr hDescriptor);
+
+ [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706800(v=vs.85).aspx
+ internal static extern int NCryptCreateProtectionDescriptor(
+ [In, MarshalAs(UnmanagedType.LPWStr)] string pwszDescriptorString,
+ [In] uint dwFlags,
+ [Out] out NCryptDescriptorHandle phDescriptor);
+
+ [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/hh706801(v=vs.85).aspx
+ internal static extern int NCryptGetProtectionDescriptorInfo(
+ [In] NCryptDescriptorHandle hDescriptor,
+ [In] IntPtr pMemPara,
+ [In] uint dwInfoType,
+ [Out] out LocalAllocHandle ppvInfo);
+
+ [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706802(v=vs.85).aspx
+ internal static extern int NCryptProtectSecret(
+ [In] NCryptDescriptorHandle hDescriptor,
+ [In] uint dwFlags,
+ [In] byte* pbData,
+ [In] uint cbData,
+ [In] IntPtr pMemPara,
+ [In] IntPtr hWnd,
+ [Out] out LocalAllocHandle ppbProtectedBlob,
+ [Out] out uint pcbProtectedBlob);
+
+ [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706811(v=vs.85).aspx
+ internal static extern int NCryptUnprotectSecret(
+ [In] IntPtr phDescriptor,
+ [In] uint dwFlags,
+ [In] byte* pbProtectedBlob,
+ [In] uint cbProtectedBlob,
+ [In] IntPtr pMemPara,
+ [In] IntPtr hWnd,
+ [Out] out LocalAllocHandle ppbData,
+ [Out] out uint pcbData);
+
+ [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706811(v=vs.85).aspx
+ internal static extern int NCryptUnprotectSecret(
+ [Out] out NCryptDescriptorHandle phDescriptor,
+ [In] uint dwFlags,
+ [In] byte* pbProtectedBlob,
+ [In] uint cbProtectedBlob,
+ [In] IntPtr pMemPara,
+ [In] IntPtr hWnd,
+ [Out] out LocalAllocHandle ppbData,
+ [Out] out uint pcbData);
+
+ /*
+ * HELPER FUNCTIONS
+ */
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ThrowExceptionForBCryptStatus(int ntstatus)
+ {
+ // This wrapper method exists because 'throw' statements won't always be inlined.
+ if (ntstatus != 0)
+ {
+ ThrowExceptionForBCryptStatusImpl(ntstatus);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowExceptionForBCryptStatusImpl(int ntstatus)
+ {
+ string message = _lazyBCryptLibHandle.Value.FormatMessage(ntstatus);
+ throw new CryptographicException(message);
+ }
+
+ public static void ThrowExceptionForLastCrypt32Error()
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ Debug.Assert(lastError != 0, "This method should only be called if there was an error.");
+
+ string message = _lazyCrypt32LibHandle.Value.FormatMessage(lastError);
+ throw new CryptographicException(message);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ThrowExceptionForNCryptStatus(int ntstatus)
+ {
+ // This wrapper method exists because 'throw' statements won't always be inlined.
+ if (ntstatus != 0)
+ {
+ ThrowExceptionForNCryptStatusImpl(ntstatus);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowExceptionForNCryptStatusImpl(int ntstatus)
+ {
+ string message = _lazyNCryptLibHandle.Value.FormatMessage(ntstatus);
+ throw new CryptographicException(message);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/WeakReferenceHelpers.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/WeakReferenceHelpers.cs
new file mode 100644
index 0000000000..71b77a58e5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/WeakReferenceHelpers.cs
@@ -0,0 +1,59 @@
+// 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.Threading;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ internal static class WeakReferenceHelpers
+ {
+ public static T GetSharedInstance<T>(ref WeakReference<T> weakReference, Func<T> factory)
+ where T : class, IDisposable
+ {
+ // First, see if the WR already exists and points to a live object.
+ WeakReference<T> existingWeakRef = Volatile.Read(ref weakReference);
+ T newTarget = null;
+ WeakReference<T> newWeakRef = null;
+
+ while (true)
+ {
+ if (existingWeakRef != null)
+ {
+ T existingTarget;
+ if (weakReference.TryGetTarget(out existingTarget))
+ {
+ // If we created a new target on a previous iteration of the loop but we
+ // weren't able to store the target into the desired location, dispose of it now.
+ newTarget?.Dispose();
+ return existingTarget;
+ }
+ }
+
+ // If the existing WR didn't point anywhere useful and this is our
+ // first iteration through the loop, create the new target and WR now.
+ if (newTarget == null)
+ {
+ newTarget = factory();
+ Debug.Assert(newTarget != null);
+ newWeakRef = new WeakReference<T>(newTarget);
+ }
+ Debug.Assert(newWeakRef != null);
+
+ // Try replacing the existing WR with our newly-created one.
+ WeakReference<T> currentWeakRef = Interlocked.CompareExchange(ref weakReference, newWeakRef, existingWeakRef);
+ if (ReferenceEquals(currentWeakRef, existingWeakRef))
+ {
+ // success, 'weakReference' now points to our newly-created WR
+ return newTarget;
+ }
+
+ // If we got to this point, somebody beat us to creating a new WR.
+ // We'll loop around and check it for validity.
+ Debug.Assert(currentWeakRef != null);
+ existingWeakRef = currentWeakRef;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/baseline.netcore.json
new file mode 100644
index 0000000000..01daa339ee
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.Internal/baseline.netcore.json
@@ -0,0 +1,4 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Cryptography.Internal, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": []
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivation.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivation.cs
new file mode 100644
index 0000000000..67ff1ca420
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivation.cs
@@ -0,0 +1,56 @@
+// 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 Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation
+{
+ /// <summary>
+ /// Provides algorithms for performing key derivation.
+ /// </summary>
+ public static class KeyDerivation
+ {
+ /// <summary>
+ /// Performs key derivation using the PBKDF2 algorithm.
+ /// </summary>
+ /// <param name="password">The password from which to derive the key.</param>
+ /// <param name="salt">The salt to be used during the key derivation process.</param>
+ /// <param name="prf">The pseudo-random function to be used in the key derivation process.</param>
+ /// <param name="iterationCount">The number of iterations of the pseudo-random function to apply
+ /// during the key derivation process.</param>
+ /// <param name="numBytesRequested">The desired length (in bytes) of the derived key.</param>
+ /// <returns>The derived key.</returns>
+ /// <remarks>
+ /// The PBKDF2 algorithm is specified in RFC 2898.
+ /// </remarks>
+ public static byte[] Pbkdf2(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
+ {
+ if (password == null)
+ {
+ throw new ArgumentNullException(nameof(password));
+ }
+
+ if (salt == null)
+ {
+ throw new ArgumentNullException(nameof(salt));
+ }
+
+ // parameter checking
+ if (prf < KeyDerivationPrf.HMACSHA1 || prf > KeyDerivationPrf.HMACSHA512)
+ {
+ throw new ArgumentOutOfRangeException(nameof(prf));
+ }
+ if (iterationCount <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(iterationCount));
+ }
+ if (numBytesRequested <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(numBytesRequested));
+ }
+
+ return Pbkdf2Util.Pbkdf2Provider.DeriveKey(password, salt, prf, iterationCount, numBytesRequested);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs
new file mode 100644
index 0000000000..57e740f04b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs
@@ -0,0 +1,26 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation
+{
+ /// <summary>
+ /// Specifies the PRF which should be used for the key derivation algorithm.
+ /// </summary>
+ public enum KeyDerivationPrf
+ {
+ /// <summary>
+ /// The HMAC algorithm (RFC 2104) using the SHA-1 hash function (FIPS 180-4).
+ /// </summary>
+ HMACSHA1,
+
+ /// <summary>
+ /// The HMAC algorithm (RFC 2104) using the SHA-256 hash function (FIPS 180-4).
+ /// </summary>
+ HMACSHA256,
+
+ /// <summary>
+ /// The HMAC algorithm (RFC 2104) using the SHA-512 hash function (FIPS 180-4).
+ /// </summary>
+ HMACSHA512,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj
new file mode 100644
index 0000000000..04dec66482
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core utilities for key derivation.</Description>
+ <TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Cryptography.Internal\Microsoft.AspNetCore.Cryptography.Internal.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/IPbkdf2Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/IPbkdf2Provider.cs
new file mode 100644
index 0000000000..8be8a5e809
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/IPbkdf2Provider.cs
@@ -0,0 +1,15 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
+{
+ /// <summary>
+ /// Internal interface used for abstracting away the PBKDF2 implementation since the implementation is OS-specific.
+ /// </summary>
+ internal interface IPbkdf2Provider
+ {
+ byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/ManagedPbkdf2Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/ManagedPbkdf2Provider.cs
new file mode 100644
index 0000000000..bf81ae65c5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/ManagedPbkdf2Provider.cs
@@ -0,0 +1,103 @@
+// 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.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
+{
+ /// <summary>
+ /// A PBKDF2 provider which utilizes the managed hash algorithm classes as PRFs.
+ /// This isn't the preferred provider since the implementation is slow, but it is provided as a fallback.
+ /// </summary>
+ internal sealed class ManagedPbkdf2Provider : IPbkdf2Provider
+ {
+ public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
+ {
+ Debug.Assert(password != null);
+ Debug.Assert(salt != null);
+ Debug.Assert(iterationCount > 0);
+ Debug.Assert(numBytesRequested > 0);
+
+ // PBKDF2 is defined in NIST SP800-132, Sec. 5.3.
+ // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
+
+ byte[] retVal = new byte[numBytesRequested];
+ int numBytesWritten = 0;
+ int numBytesRemaining = numBytesRequested;
+
+ // For each block index, U_0 := Salt || block_index
+ byte[] saltWithBlockIndex = new byte[checked(salt.Length + sizeof(uint))];
+ Buffer.BlockCopy(salt, 0, saltWithBlockIndex, 0, salt.Length);
+
+ using (var hashAlgorithm = PrfToManagedHmacAlgorithm(prf, password))
+ {
+ for (uint blockIndex = 1; numBytesRemaining > 0; blockIndex++)
+ {
+ // write the block index out as big-endian
+ saltWithBlockIndex[saltWithBlockIndex.Length - 4] = (byte)(blockIndex >> 24);
+ saltWithBlockIndex[saltWithBlockIndex.Length - 3] = (byte)(blockIndex >> 16);
+ saltWithBlockIndex[saltWithBlockIndex.Length - 2] = (byte)(blockIndex >> 8);
+ saltWithBlockIndex[saltWithBlockIndex.Length - 1] = (byte)blockIndex;
+
+ // U_1 = PRF(U_0) = PRF(Salt || block_index)
+ // T_blockIndex = U_1
+ byte[] U_iter = hashAlgorithm.ComputeHash(saltWithBlockIndex); // this is U_1
+ byte[] T_blockIndex = U_iter;
+
+ for (int iter = 1; iter < iterationCount; iter++)
+ {
+ U_iter = hashAlgorithm.ComputeHash(U_iter);
+ XorBuffers(src: U_iter, dest: T_blockIndex);
+ // At this point, the 'U_iter' variable actually contains U_{iter+1} (due to indexing differences).
+ }
+
+ // At this point, we're done iterating on this block, so copy the transformed block into retVal.
+ int numBytesToCopy = Math.Min(numBytesRemaining, T_blockIndex.Length);
+ Buffer.BlockCopy(T_blockIndex, 0, retVal, numBytesWritten, numBytesToCopy);
+ numBytesWritten += numBytesToCopy;
+ numBytesRemaining -= numBytesToCopy;
+ }
+ }
+
+ // retVal := T_1 || T_2 || ... || T_n, where T_n may be truncated to meet the desired output length
+ return retVal;
+ }
+
+ private static KeyedHashAlgorithm PrfToManagedHmacAlgorithm(KeyDerivationPrf prf, string password)
+ {
+ byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
+ try
+ {
+ switch (prf)
+ {
+ case KeyDerivationPrf.HMACSHA1:
+ return new HMACSHA1(passwordBytes);
+ case KeyDerivationPrf.HMACSHA256:
+ return new HMACSHA256(passwordBytes);
+ case KeyDerivationPrf.HMACSHA512:
+ return new HMACSHA512(passwordBytes);
+ default:
+ throw CryptoUtil.Fail("Unrecognized PRF.");
+ }
+ }
+ finally
+ {
+ // The HMAC ctor makes a duplicate of this key; we clear original buffer to limit exposure to the GC.
+ Array.Clear(passwordBytes, 0, passwordBytes.Length);
+ }
+ }
+
+ private static void XorBuffers(byte[] src, byte[] dest)
+ {
+ // Note: dest buffer is mutated.
+ Debug.Assert(src.Length == dest.Length);
+ for (int i = 0; i < src.Length; i++)
+ {
+ dest[i] ^= src[i];
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs
new file mode 100644
index 0000000000..a8ce1772eb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs
@@ -0,0 +1,71 @@
+// 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.
+
+#if NETCOREAPP3_0
+// Rfc2898DeriveBytes in .NET Standard 2.0 only supports SHA1
+
+using System;
+using System.Diagnostics;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
+{
+ /// <summary>
+ /// Implements Pbkdf2 using <see cref="Rfc2898DeriveBytes"/>.
+ /// </summary>
+ internal sealed class NetCorePbkdf2Provider : IPbkdf2Provider
+ {
+ private static readonly ManagedPbkdf2Provider _fallbackProvider = new ManagedPbkdf2Provider();
+
+ public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
+ {
+ Debug.Assert(password != null);
+ Debug.Assert(salt != null);
+ Debug.Assert(iterationCount > 0);
+ Debug.Assert(numBytesRequested > 0);
+
+ if (salt.Length < 8)
+ {
+ // Rfc2898DeriveBytes enforces the 8 byte recommendation.
+ // To maintain compatibility, we call into ManagedPbkdf2Provider for salts shorter than 8 bytes
+ // because we can't use Rfc2898DeriveBytes with this salt.
+ return _fallbackProvider.DeriveKey(password, salt, prf, iterationCount, numBytesRequested);
+ }
+ else
+ {
+ return DeriveKeyImpl(password, salt, prf, iterationCount, numBytesRequested);
+ }
+ }
+
+ private static byte[] DeriveKeyImpl(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
+ {
+ HashAlgorithmName algorithmName;
+ switch (prf)
+ {
+ case KeyDerivationPrf.HMACSHA1:
+ algorithmName = HashAlgorithmName.SHA1;
+ break;
+ case KeyDerivationPrf.HMACSHA256:
+ algorithmName = HashAlgorithmName.SHA256;
+ break;
+ case KeyDerivationPrf.HMACSHA512:
+ algorithmName = HashAlgorithmName.SHA512;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ var passwordBytes = Encoding.UTF8.GetBytes(password);
+ using (var rfc = new Rfc2898DeriveBytes(passwordBytes, salt, iterationCount, algorithmName))
+ {
+ return rfc.GetBytes(numBytesRequested);
+ }
+ }
+ }
+}
+
+#elif NETSTANDARD2_0
+#else
+#error Update target frameworks
+#endif
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs
new file mode 100644
index 0000000000..d8139c92f7
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs
@@ -0,0 +1,46 @@
+// 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 Microsoft.AspNetCore.Cryptography.Cng;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
+{
+ /// <summary>
+ /// Internal base class used for abstracting away the PBKDF2 implementation since the implementation is OS-specific.
+ /// </summary>
+ internal static class Pbkdf2Util
+ {
+ public static readonly IPbkdf2Provider Pbkdf2Provider = GetPbkdf2Provider();
+
+ private static IPbkdf2Provider GetPbkdf2Provider()
+ {
+ // In priority order, our three implementations are Win8, Win7, and "other".
+ if (OSVersionUtil.IsWindows8OrLater())
+ {
+ // fastest implementation
+ return new Win8Pbkdf2Provider();
+ }
+ else if (OSVersionUtil.IsWindows())
+ {
+ // acceptable implementation
+ return new Win7Pbkdf2Provider();
+ }
+#if NETCOREAPP3_0
+ else
+ {
+ // fastest implementation on .NET Core for Linux/macOS.
+ // Not supported on .NET Framework
+ return new NetCorePbkdf2Provider();
+ }
+#elif NETSTANDARD2_0
+ else
+ {
+ // slowest implementation
+ return new ManagedPbkdf2Provider();
+ }
+#else
+#error Update target frameworks
+#endif
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win7Pbkdf2Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win7Pbkdf2Provider.cs
new file mode 100644
index 0000000000..4c359b80f4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win7Pbkdf2Provider.cs
@@ -0,0 +1,100 @@
+// 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.Text;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
+{
+ /// <summary>
+ /// A PBKDF2 provider which utilizes the Win7 API BCryptDeriveKeyPBKDF2.
+ /// </summary>
+ internal unsafe sealed class Win7Pbkdf2Provider : IPbkdf2Provider
+ {
+ public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
+ {
+ Debug.Assert(password != null);
+ Debug.Assert(salt != null);
+ Debug.Assert(iterationCount > 0);
+ Debug.Assert(numBytesRequested > 0);
+
+ byte dummy; // CLR doesn't like pinning zero-length buffers, so this provides a valid memory address when working with zero-length buffers
+
+ // Don't dispose of this algorithm instance; it is cached and reused!
+ var algHandle = PrfToCachedCngAlgorithmInstance(prf);
+
+ // Convert password string to bytes.
+ // Allocate on the stack whenever we can to save allocations.
+ int cbPasswordBuffer = Encoding.UTF8.GetMaxByteCount(password.Length);
+ fixed (byte* pbHeapAllocatedPasswordBuffer = (cbPasswordBuffer > Constants.MAX_STACKALLOC_BYTES) ? new byte[cbPasswordBuffer] : null)
+ {
+ byte* pbPasswordBuffer = pbHeapAllocatedPasswordBuffer;
+ if (pbPasswordBuffer == null)
+ {
+ if (cbPasswordBuffer == 0)
+ {
+ pbPasswordBuffer = &dummy;
+ }
+ else
+ {
+ byte* pbStackAllocPasswordBuffer = stackalloc byte[cbPasswordBuffer]; // will be released when the frame unwinds
+ pbPasswordBuffer = pbStackAllocPasswordBuffer;
+ }
+ }
+
+ try
+ {
+ int cbPasswordBufferUsed; // we're not filling the entire buffer, just a partial buffer
+ fixed (char* pszPassword = password)
+ {
+ cbPasswordBufferUsed = Encoding.UTF8.GetBytes(pszPassword, password.Length, pbPasswordBuffer, cbPasswordBuffer);
+ }
+
+ fixed (byte* pbHeapAllocatedSalt = salt)
+ {
+ byte* pbSalt = (pbHeapAllocatedSalt != null) ? pbHeapAllocatedSalt : &dummy;
+
+ byte[] retVal = new byte[numBytesRequested];
+ fixed (byte* pbRetVal = retVal)
+ {
+ int ntstatus = UnsafeNativeMethods.BCryptDeriveKeyPBKDF2(
+ hPrf: algHandle,
+ pbPassword: pbPasswordBuffer,
+ cbPassword: (uint)cbPasswordBufferUsed,
+ pbSalt: pbSalt,
+ cbSalt: (uint)salt.Length,
+ cIterations: (ulong)iterationCount,
+ pbDerivedKey: pbRetVal,
+ cbDerivedKey: (uint)retVal.Length,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ }
+ return retVal;
+ }
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbPasswordBuffer, cbPasswordBuffer);
+ }
+ }
+ }
+
+ private static BCryptAlgorithmHandle PrfToCachedCngAlgorithmInstance(KeyDerivationPrf prf)
+ {
+ switch (prf)
+ {
+ case KeyDerivationPrf.HMACSHA1:
+ return CachedAlgorithmHandles.HMAC_SHA1;
+ case KeyDerivationPrf.HMACSHA256:
+ return CachedAlgorithmHandles.HMAC_SHA256;
+ case KeyDerivationPrf.HMACSHA512:
+ return CachedAlgorithmHandles.HMAC_SHA512;
+ default:
+ throw CryptoUtil.Fail("Unrecognized PRF.");
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win8Pbkdf2Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win8Pbkdf2Provider.cs
new file mode 100644
index 0000000000..296e85b7dd
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Win8Pbkdf2Provider.cs
@@ -0,0 +1,211 @@
+// 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.Runtime.CompilerServices;
+using System.Text;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
+{
+ /// <summary>
+ /// A PBKDF2 provider which utilizes the Win8 API BCryptKeyDerivation.
+ /// </summary>
+ internal unsafe sealed class Win8Pbkdf2Provider : IPbkdf2Provider
+ {
+ public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
+ {
+ Debug.Assert(password != null);
+ Debug.Assert(salt != null);
+ Debug.Assert(iterationCount > 0);
+ Debug.Assert(numBytesRequested > 0);
+
+ string algorithmName = PrfToCngAlgorithmId(prf);
+ fixed (byte* pbHeapAllocatedSalt = salt)
+ {
+ byte dummy; // CLR doesn't like pinning zero-length buffers, so this provides a valid memory address when working with zero-length buffers
+ byte* pbSalt = (pbHeapAllocatedSalt != null) ? pbHeapAllocatedSalt : &dummy;
+
+ byte[] retVal = new byte[numBytesRequested];
+ using (BCryptKeyHandle keyHandle = PasswordToPbkdfKeyHandle(password, CachedAlgorithmHandles.PBKDF2, prf))
+ {
+ fixed (byte* pbRetVal = retVal)
+ {
+ DeriveKeyCore(keyHandle, algorithmName, pbSalt, (uint)salt.Length, (ulong)iterationCount, pbRetVal, (uint)retVal.Length);
+ }
+ return retVal;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint GetTotalByteLengthIncludingNullTerminator(string input)
+ {
+ if (input == null)
+ {
+ // degenerate case
+ return 0;
+ }
+ else
+ {
+ uint numChars = (uint)input.Length + 1U; // no overflow check necessary since Length is signed
+ return checked(numChars * sizeof(char));
+ }
+ }
+
+ private static BCryptKeyHandle PasswordToPbkdfKeyHandle(string password, BCryptAlgorithmHandle pbkdf2AlgHandle, KeyDerivationPrf prf)
+ {
+ byte dummy; // CLR doesn't like pinning zero-length buffers, so this provides a valid memory address when working with zero-length buffers
+
+ // Convert password string to bytes.
+ // Allocate on the stack whenever we can to save allocations.
+ int cbPasswordBuffer = Encoding.UTF8.GetMaxByteCount(password.Length);
+ fixed (byte* pbHeapAllocatedPasswordBuffer = (cbPasswordBuffer > Constants.MAX_STACKALLOC_BYTES) ? new byte[cbPasswordBuffer] : null)
+ {
+ byte* pbPasswordBuffer = pbHeapAllocatedPasswordBuffer;
+ if (pbPasswordBuffer == null)
+ {
+ if (cbPasswordBuffer == 0)
+ {
+ pbPasswordBuffer = &dummy;
+ }
+ else
+ {
+ byte* pbStackAllocPasswordBuffer = stackalloc byte[cbPasswordBuffer]; // will be released when the frame unwinds
+ pbPasswordBuffer = pbStackAllocPasswordBuffer;
+ }
+ }
+
+ try
+ {
+ int cbPasswordBufferUsed; // we're not filling the entire buffer, just a partial buffer
+ fixed (char* pszPassword = password)
+ {
+ cbPasswordBufferUsed = Encoding.UTF8.GetBytes(pszPassword, password.Length, pbPasswordBuffer, cbPasswordBuffer);
+ }
+
+ return PasswordToPbkdfKeyHandleStep2(pbkdf2AlgHandle, pbPasswordBuffer, (uint)cbPasswordBufferUsed, prf);
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbPasswordBuffer, cbPasswordBuffer);
+ }
+ }
+ }
+
+ private static BCryptKeyHandle PasswordToPbkdfKeyHandleStep2(BCryptAlgorithmHandle pbkdf2AlgHandle, byte* pbPassword, uint cbPassword, KeyDerivationPrf prf)
+ {
+ const uint PBKDF2_MAX_KEYLENGTH_IN_BYTES = 2048; // GetSupportedKeyLengths() on a Win8 box; value should never be lowered in any future version of Windows
+ if (cbPassword <= PBKDF2_MAX_KEYLENGTH_IN_BYTES)
+ {
+ // Common case: the password is small enough to be consumed directly by the PBKDF2 algorithm.
+ return pbkdf2AlgHandle.GenerateSymmetricKey(pbPassword, cbPassword);
+ }
+ else
+ {
+ // Rare case: password is very long; we must hash manually.
+ // PBKDF2 uses the PRFs in HMAC mode, and when the HMAC input key exceeds the hash function's
+ // block length the key is hashed and run back through the key initialization function.
+
+ BCryptAlgorithmHandle prfAlgorithmHandle; // cached; don't dispose
+ switch (prf)
+ {
+ case KeyDerivationPrf.HMACSHA1:
+ prfAlgorithmHandle = CachedAlgorithmHandles.SHA1;
+ break;
+ case KeyDerivationPrf.HMACSHA256:
+ prfAlgorithmHandle = CachedAlgorithmHandles.SHA256;
+ break;
+ case KeyDerivationPrf.HMACSHA512:
+ prfAlgorithmHandle = CachedAlgorithmHandles.SHA512;
+ break;
+ default:
+ throw CryptoUtil.Fail("Unrecognized PRF.");
+ }
+
+ // Final sanity check: don't hash the password if the HMAC key initialization function wouldn't have done it for us.
+ if (cbPassword <= prfAlgorithmHandle.GetHashBlockLength() /* in bytes */)
+ {
+ return pbkdf2AlgHandle.GenerateSymmetricKey(pbPassword, cbPassword);
+ }
+
+ // Hash the password and use the hash as input to PBKDF2.
+ uint cbPasswordDigest = prfAlgorithmHandle.GetHashDigestLength();
+ CryptoUtil.Assert(cbPasswordDigest > 0, "cbPasswordDigest > 0");
+ fixed (byte* pbPasswordDigest = new byte[cbPasswordDigest])
+ {
+ try
+ {
+ using (var hashHandle = prfAlgorithmHandle.CreateHash())
+ {
+ hashHandle.HashData(pbPassword, cbPassword, pbPasswordDigest, cbPasswordDigest);
+ }
+ return pbkdf2AlgHandle.GenerateSymmetricKey(pbPasswordDigest, cbPasswordDigest);
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbPasswordDigest, cbPasswordDigest);
+ }
+ }
+ }
+ }
+
+ private static void DeriveKeyCore(BCryptKeyHandle pbkdf2KeyHandle, string hashAlgorithm, byte* pbSalt, uint cbSalt, ulong iterCount, byte* pbDerivedBytes, uint cbDerivedBytes)
+ {
+ // First, build the buffers necessary to pass (hash alg, salt, iter count) into the KDF
+ BCryptBuffer* pBuffers = stackalloc BCryptBuffer[3];
+
+ pBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_ITERATION_COUNT;
+ pBuffers[0].pvBuffer = (IntPtr)(&iterCount);
+ pBuffers[0].cbBuffer = sizeof(ulong);
+
+ pBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_SALT;
+ pBuffers[1].pvBuffer = (IntPtr)pbSalt;
+ pBuffers[1].cbBuffer = cbSalt;
+
+ fixed (char* pszHashAlgorithm = hashAlgorithm)
+ {
+ pBuffers[2].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM;
+ pBuffers[2].pvBuffer = (IntPtr)pszHashAlgorithm;
+ pBuffers[2].cbBuffer = GetTotalByteLengthIncludingNullTerminator(hashAlgorithm);
+
+ // Add the header which points to the buffers
+ BCryptBufferDesc bufferDesc = default(BCryptBufferDesc);
+ BCryptBufferDesc.Initialize(ref bufferDesc);
+ bufferDesc.cBuffers = 3;
+ bufferDesc.pBuffers = pBuffers;
+
+ // Finally, import the KDK into the KDF algorithm, then invoke the KDF
+ uint numBytesDerived;
+ int ntstatus = UnsafeNativeMethods.BCryptKeyDerivation(
+ hKey: pbkdf2KeyHandle,
+ pParameterList: &bufferDesc,
+ pbDerivedKey: pbDerivedBytes,
+ cbDerivedKey: cbDerivedBytes,
+ pcbResult: out numBytesDerived,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+
+ // Final sanity checks before returning control to caller.
+ CryptoUtil.Assert(numBytesDerived == cbDerivedBytes, "numBytesDerived == cbDerivedBytes");
+ }
+ }
+
+ private static string PrfToCngAlgorithmId(KeyDerivationPrf prf)
+ {
+ switch (prf)
+ {
+ case KeyDerivationPrf.HMACSHA1:
+ return Constants.BCRYPT_SHA1_ALGORITHM;
+ case KeyDerivationPrf.HMACSHA256:
+ return Constants.BCRYPT_SHA256_ALGORITHM;
+ case KeyDerivationPrf.HMACSHA512:
+ return Constants.BCRYPT_SHA512_ALGORITHM;
+ default:
+ throw CryptoUtil.Fail("Unrecognized PRF.");
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Properties/AssemblyInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..2ca6553c5d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Cryptography.KeyDerivation.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/baseline.netcore.json
new file mode 100644
index 0000000000..ceddb40cc2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/baseline.netcore.json
@@ -0,0 +1,78 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Cryptography.KeyDerivation, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Pbkdf2",
+ "Parameters": [
+ {
+ "Name": "password",
+ "Type": "System.String"
+ },
+ {
+ "Name": "salt",
+ "Type": "System.Byte[]"
+ },
+ {
+ "Name": "prf",
+ "Type": "Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf"
+ },
+ {
+ "Name": "iterationCount",
+ "Type": "System.Int32"
+ },
+ {
+ "Name": "numBytesRequested",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "HMACSHA1",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "HMACSHA256",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ },
+ {
+ "Kind": "Field",
+ "Name": "HMACSHA512",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "2"
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/CryptoUtil.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/CryptoUtil.cs
new file mode 100644
index 0000000000..e3e361a3a8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/CryptoUtil.cs
@@ -0,0 +1,33 @@
+// 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.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class CryptoUtil
+ {
+ // This isn't a typical Debug.Fail; an error always occurs, even in retail builds.
+ // This method doesn't return, but since the CLR doesn't allow specifying a 'never'
+ // return type, we mimic it by specifying our return type as Exception. That way
+ // callers can write 'throw Fail(...);' to make the C# compiler happy, as the
+ // throw keyword is implicitly of type O.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static Exception Fail(string message)
+ {
+ Debug.Fail(message);
+ throw new CryptographicException("Assertion failed: " + message);
+ }
+
+ // Allows callers to write "var x = Method() ?? Fail<T>(message);" as a convenience to guard
+ // against a method returning null unexpectedly.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Fail<T>(string message) where T : class
+ {
+ throw Fail(message);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/DataProtectionCommonExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/DataProtectionCommonExtensions.cs
new file mode 100644
index 0000000000..f4fd8801ae
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/DataProtectionCommonExtensions.cs
@@ -0,0 +1,244 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.AspNetCore.DataProtection.Abstractions;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Helpful extension methods for data protection APIs.
+ /// </summary>
+ public static class DataProtectionCommonExtensions
+ {
+ /// <summary>
+ /// Creates an <see cref="IDataProtector"/> given a list of purposes.
+ /// </summary>
+ /// <param name="provider">The <see cref="IDataProtectionProvider"/> from which to generate the purpose chain.</param>
+ /// <param name="purposes">The list of purposes which contribute to the purpose chain. This list must
+ /// contain at least one element, and it may not contain null elements.</param>
+ /// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
+ /// <remarks>
+ /// This is a convenience method which chains together several calls to
+ /// <see cref="IDataProtectionProvider.CreateProtector(string)"/>. See that method's
+ /// documentation for more information.
+ /// </remarks>
+ public static IDataProtector CreateProtector(this IDataProtectionProvider provider, IEnumerable<string> purposes)
+ {
+ if (provider == null)
+ {
+ throw new ArgumentNullException(nameof(provider));
+ }
+
+ if (purposes == null)
+ {
+ throw new ArgumentNullException(nameof(purposes));
+ }
+
+ bool collectionIsEmpty = true;
+ IDataProtectionProvider retVal = provider;
+ foreach (string purpose in purposes)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesCollection, nameof(purposes));
+ }
+ retVal = retVal.CreateProtector(purpose) ?? CryptoUtil.Fail<IDataProtector>("CreateProtector returned null.");
+ collectionIsEmpty = false;
+ }
+
+ if (collectionIsEmpty)
+ {
+ throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesCollection, nameof(purposes));
+ }
+
+ Debug.Assert(retVal is IDataProtector); // CreateProtector is supposed to return an instance of this interface
+ return (IDataProtector)retVal;
+ }
+
+ /// <summary>
+ /// Creates an <see cref="IDataProtector"/> given a list of purposes.
+ /// </summary>
+ /// <param name="provider">The <see cref="IDataProtectionProvider"/> from which to generate the purpose chain.</param>
+ /// <param name="purpose">The primary purpose used to create the <see cref="IDataProtector"/>.</param>
+ /// <param name="subPurposes">An optional list of secondary purposes which contribute to the purpose chain.
+ /// If this list is provided it cannot contain null elements.</param>
+ /// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
+ /// <remarks>
+ /// This is a convenience method which chains together several calls to
+ /// <see cref="IDataProtectionProvider.CreateProtector(string)"/>. See that method's
+ /// documentation for more information.
+ /// </remarks>
+ public static IDataProtector CreateProtector(this IDataProtectionProvider provider, string purpose, params string[] subPurposes)
+ {
+ if (provider == null)
+ {
+ throw new ArgumentNullException(nameof(provider));
+ }
+
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ // The method signature isn't simply CreateProtector(this IDataProtectionProvider, params string[] purposes)
+ // because we don't want the code provider.CreateProtector() [parameterless] to inadvertently compile.
+ // The actual signature for this method forces at least one purpose to be provided at the call site.
+
+ IDataProtector protector = provider.CreateProtector(purpose);
+ if (subPurposes != null && subPurposes.Length > 0)
+ {
+ protector = protector?.CreateProtector((IEnumerable<string>)subPurposes);
+ }
+ return protector ?? CryptoUtil.Fail<IDataProtector>("CreateProtector returned null.");
+ }
+
+ /// <summary>
+ /// Retrieves an <see cref="IDataProtectionProvider"/> from an <see cref="IServiceProvider"/>.
+ /// </summary>
+ /// <param name="services">The service provider from which to retrieve the <see cref="IDataProtectionProvider"/>.</param>
+ /// <returns>An <see cref="IDataProtectionProvider"/>. This method is guaranteed never to return null.</returns>
+ /// <exception cref="InvalidOperationException">If no <see cref="IDataProtectionProvider"/> service exists in <paramref name="services"/>.</exception>
+ public static IDataProtectionProvider GetDataProtectionProvider(this IServiceProvider services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ // We have our own implementation of GetRequiredService<T> since we don't want to
+ // take a dependency on DependencyInjection.Interfaces.
+ IDataProtectionProvider provider = (IDataProtectionProvider)services.GetService(typeof(IDataProtectionProvider));
+ if (provider == null)
+ {
+ throw new InvalidOperationException(Resources.FormatDataProtectionExtensions_NoService(typeof(IDataProtectionProvider).FullName));
+ }
+ return provider;
+ }
+
+ /// <summary>
+ /// Retrieves an <see cref="IDataProtector"/> from an <see cref="IServiceProvider"/> given a list of purposes.
+ /// </summary>
+ /// <param name="services">An <see cref="IServiceProvider"/> which contains the <see cref="IDataProtectionProvider"/>
+ /// from which to generate the purpose chain.</param>
+ /// <param name="purposes">The list of purposes which contribute to the purpose chain. This list must
+ /// contain at least one element, and it may not contain null elements.</param>
+ /// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
+ /// <remarks>
+ /// This is a convenience method which calls <see cref="GetDataProtectionProvider(IServiceProvider)"/>
+ /// then <see cref="CreateProtector(IDataProtectionProvider, IEnumerable{string})"/>. See those methods'
+ /// documentation for more information.
+ /// </remarks>
+ public static IDataProtector GetDataProtector(this IServiceProvider services, IEnumerable<string> purposes)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (purposes == null)
+ {
+ throw new ArgumentNullException(nameof(purposes));
+ }
+
+ return services.GetDataProtectionProvider().CreateProtector(purposes);
+ }
+
+ /// <summary>
+ /// Retrieves an <see cref="IDataProtector"/> from an <see cref="IServiceProvider"/> given a list of purposes.
+ /// </summary>
+ /// <param name="services">An <see cref="IServiceProvider"/> which contains the <see cref="IDataProtectionProvider"/>
+ /// from which to generate the purpose chain.</param>
+ /// <param name="purpose">The primary purpose used to create the <see cref="IDataProtector"/>.</param>
+ /// <param name="subPurposes">An optional list of secondary purposes which contribute to the purpose chain.
+ /// If this list is provided it cannot contain null elements.</param>
+ /// <returns>An <see cref="IDataProtector"/> tied to the provided purpose chain.</returns>
+ /// <remarks>
+ /// This is a convenience method which calls <see cref="GetDataProtectionProvider(IServiceProvider)"/>
+ /// then <see cref="CreateProtector(IDataProtectionProvider, string, string[])"/>. See those methods'
+ /// documentation for more information.
+ /// </remarks>
+ public static IDataProtector GetDataProtector(this IServiceProvider services, string purpose, params string[] subPurposes)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ return services.GetDataProtectionProvider().CreateProtector(purpose, subPurposes);
+ }
+
+ /// <summary>
+ /// Cryptographically protects a piece of plaintext data.
+ /// </summary>
+ /// <param name="protector">The data protector to use for this operation.</param>
+ /// <param name="plaintext">The plaintext data to protect.</param>
+ /// <returns>The protected form of the plaintext data.</returns>
+ public static string Protect(this IDataProtector protector, string plaintext)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ try
+ {
+ byte[] plaintextAsBytes = EncodingUtil.SecureUtf8Encoding.GetBytes(plaintext);
+ byte[] protectedDataAsBytes = protector.Protect(plaintextAsBytes);
+ return WebEncoders.Base64UrlEncode(protectedDataAsBytes);
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize exceptions to CryptographicException
+ throw Error.CryptCommon_GenericError(ex);
+ }
+ }
+
+ /// <summary>
+ /// Cryptographically unprotects a piece of protected data.
+ /// </summary>
+ /// <param name="protector">The data protector to use for this operation.</param>
+ /// <param name="protectedData">The protected data to unprotect.</param>
+ /// <returns>The plaintext form of the protected data.</returns>
+ /// <exception cref="System.Security.Cryptography.CryptographicException">
+ /// Thrown if <paramref name="protectedData"/> is invalid or malformed.
+ /// </exception>
+ public static string Unprotect(this IDataProtector protector, string protectedData)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ try
+ {
+ byte[] protectedDataAsBytes = WebEncoders.Base64UrlDecode(protectedData);
+ byte[] plaintextAsBytes = protector.Unprotect(protectedDataAsBytes);
+ return EncodingUtil.SecureUtf8Encoding.GetString(plaintextAsBytes);
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize exceptions to CryptographicException
+ throw Error.CryptCommon_GenericError(ex);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Error.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Error.cs
new file mode 100644
index 0000000000..18b93c0ac7
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Error.cs
@@ -0,0 +1,23 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class Error
+ {
+ public static CryptographicException CryptCommon_GenericError(Exception inner = null)
+ {
+ return new CryptographicException(Resources.CryptCommon_GenericError, inner);
+ }
+
+ public static CryptographicException CryptCommon_PayloadInvalid()
+ {
+ string message = Resources.CryptCommon_PayloadInvalid;
+ return new CryptographicException(message);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtectionProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtectionProvider.cs
new file mode 100644
index 0000000000..02f772724b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtectionProvider.cs
@@ -0,0 +1,26 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// An interface that can be used to create <see cref="IDataProtector"/> instances.
+ /// </summary>
+ public interface IDataProtectionProvider
+ {
+ /// <summary>
+ /// Creates an <see cref="IDataProtector"/> given a purpose.
+ /// </summary>
+ /// <param name="purpose">
+ /// The purpose to be assigned to the newly-created <see cref="IDataProtector"/>.
+ /// </param>
+ /// <returns>An IDataProtector tied to the provided purpose.</returns>
+ /// <remarks>
+ /// The <paramref name="purpose"/> parameter must be unique for the intended use case; two
+ /// different <see cref="IDataProtector"/> instances created with two different <paramref name="purpose"/>
+ /// values will not be able to decipher each other's payloads. The <paramref name="purpose"/> parameter
+ /// value is not intended to be kept secret.
+ /// </remarks>
+ IDataProtector CreateProtector(string purpose);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtector.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtector.cs
new file mode 100644
index 0000000000..1d9c8c3946
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/IDataProtector.cs
@@ -0,0 +1,28 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// An interface that can provide data protection services.
+ /// </summary>
+ public interface IDataProtector : IDataProtectionProvider
+ {
+ /// <summary>
+ /// Cryptographically protects a piece of plaintext data.
+ /// </summary>
+ /// <param name="plaintext">The plaintext data to protect.</param>
+ /// <returns>The protected form of the plaintext data.</returns>
+ byte[] Protect(byte[] plaintext);
+
+ /// <summary>
+ /// Cryptographically unprotects a piece of protected data.
+ /// </summary>
+ /// <param name="protectedData">The protected data to unprotect.</param>
+ /// <returns>The plaintext form of the protected data.</returns>
+ /// <exception cref="System.Security.Cryptography.CryptographicException">
+ /// Thrown if the protected data is invalid or malformed.
+ /// </exception>
+ byte[] Unprotect(byte[] protectedData);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Infrastructure/IApplicationDiscriminator.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Infrastructure/IApplicationDiscriminator.cs
new file mode 100644
index 0000000000..d8c3af376f
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Infrastructure/IApplicationDiscriminator.cs
@@ -0,0 +1,25 @@
+// 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.ComponentModel;
+
+namespace Microsoft.AspNetCore.DataProtection.Infrastructure
+{
+ /// <summary>
+ /// Provides information used to discriminate applications.
+ /// </summary>
+ /// <remarks>
+ /// This type supports the data protection system and is not intended to be used
+ /// by consumers.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public interface IApplicationDiscriminator
+ {
+ /// <summary>
+ /// An identifier that uniquely discriminates this application from all other
+ /// applications on the machine.
+ /// </summary>
+ string Discriminator { get; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Microsoft.AspNetCore.DataProtection.Abstractions.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Microsoft.AspNetCore.DataProtection.Abstractions.csproj
new file mode 100644
index 0000000000..24bd9f5fb6
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Microsoft.AspNetCore.DataProtection.Abstractions.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core data protection abstractions.
+Commonly used types:
+Microsoft.AspNetCore.DataProtection.IDataProtectionProvider
+Microsoft.AspNetCore.DataProtection.IDataProtector</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.WebEncoders.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsWebEncodersSourcesPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/AssemblyInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..838462a81d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// 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.Runtime.CompilerServices;
+
+// for unit testing
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Abstractions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/Resources.Designer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..7f8422cf6b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Properties/Resources.Designer.cs
@@ -0,0 +1,86 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.DataProtection.Abstractions
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.DataProtection.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The payload was invalid.
+ /// </summary>
+ internal static string CryptCommon_PayloadInvalid
+ {
+ get => GetString("CryptCommon_PayloadInvalid");
+ }
+
+ /// <summary>
+ /// The payload was invalid.
+ /// </summary>
+ internal static string FormatCryptCommon_PayloadInvalid()
+ => GetString("CryptCommon_PayloadInvalid");
+
+ /// <summary>
+ /// The purposes collection cannot be null or empty and cannot contain null elements.
+ /// </summary>
+ internal static string DataProtectionExtensions_NullPurposesCollection
+ {
+ get => GetString("DataProtectionExtensions_NullPurposesCollection");
+ }
+
+ /// <summary>
+ /// The purposes collection cannot be null or empty and cannot contain null elements.
+ /// </summary>
+ internal static string FormatDataProtectionExtensions_NullPurposesCollection()
+ => GetString("DataProtectionExtensions_NullPurposesCollection");
+
+ /// <summary>
+ /// An error occurred during a cryptographic operation.
+ /// </summary>
+ internal static string CryptCommon_GenericError
+ {
+ get => GetString("CryptCommon_GenericError");
+ }
+
+ /// <summary>
+ /// An error occurred during a cryptographic operation.
+ /// </summary>
+ internal static string FormatCryptCommon_GenericError()
+ => GetString("CryptCommon_GenericError");
+
+ /// <summary>
+ /// No service for type '{0}' has been registered.
+ /// </summary>
+ internal static string DataProtectionExtensions_NoService
+ {
+ get => GetString("DataProtectionExtensions_NoService");
+ }
+
+ /// <summary>
+ /// No service for type '{0}' has been registered.
+ /// </summary>
+ internal static string FormatDataProtectionExtensions_NoService(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("DataProtectionExtensions_NoService"), p0);
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Resources.resx b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Resources.resx
new file mode 100644
index 0000000000..daa9e2cbd9
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/Resources.resx
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CryptCommon_PayloadInvalid" xml:space="preserve">
+ <value>The payload was invalid.</value>
+ </data>
+ <data name="DataProtectionExtensions_NullPurposesCollection" xml:space="preserve">
+ <value>The purposes collection cannot be null or empty and cannot contain null elements.</value>
+ </data>
+ <data name="CryptCommon_GenericError" xml:space="preserve">
+ <value>An error occurred during a cryptographic operation.</value>
+ </data>
+ <data name="DataProtectionExtensions_NoService" xml:space="preserve">
+ <value>No service for type '{0}' has been registered.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/baseline.netcore.json
new file mode 100644
index 0000000000..eb6e5030fe
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Abstractions/baseline.netcore.json
@@ -0,0 +1,231 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.DataProtectionCommonExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateProtector",
+ "Parameters": [
+ {
+ "Name": "provider",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ },
+ {
+ "Name": "purposes",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateProtector",
+ "Parameters": [
+ {
+ "Name": "provider",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ },
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ },
+ {
+ "Name": "subPurposes",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetDataProtectionProvider",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetDataProtector",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ },
+ {
+ "Name": "purposes",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetDataProtector",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ },
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ },
+ {
+ "Name": "subPurposes",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ },
+ {
+ "Name": "plaintext",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ },
+ {
+ "Name": "protectedData",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateProtector",
+ "Parameters": [
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "plaintext",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedData",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.Infrastructure.IApplicationDiscriminator",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Discriminator",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs
new file mode 100644
index 0000000000..0701220b4b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs
@@ -0,0 +1,118 @@
+// 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.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.DataProtection.AzureKeyVault;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Azure.KeyVault;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Clients.ActiveDirectory;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Contains Azure KeyVault-specific extension methods for modifying a <see cref="IDataProtectionBuilder"/>.
+ /// </summary>
+ public static class AzureDataProtectionBuilderExtensions
+ {
+ /// <summary>
+ /// Configures the data protection system to protect keys with specified key in Azure KeyVault.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="keyIdentifier">The Azure KeyVault key identifier used for key encryption.</param>
+ /// <param name="clientId">The application client id.</param>
+ /// <param name="certificate"></param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, string clientId, X509Certificate2 certificate)
+ {
+ if (string.IsNullOrEmpty(clientId))
+ {
+ throw new ArgumentException(nameof(clientId));
+ }
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ KeyVaultClient.AuthenticationCallback callback =
+ (authority, resource, scope) => GetTokenFromClientCertificate(authority, resource, clientId, certificate);
+
+ return ProtectKeysWithAzureKeyVault(builder, new KeyVaultClient(callback), keyIdentifier);
+ }
+
+ private static async Task<string> GetTokenFromClientCertificate(string authority, string resource, string clientId, X509Certificate2 certificate)
+ {
+ var authContext = new AuthenticationContext(authority);
+ var result = await authContext.AcquireTokenAsync(resource, new ClientAssertionCertificate(clientId, certificate));
+ return result.AccessToken;
+ }
+
+ /// <summary>
+ /// Configures the data protection system to protect keys with specified key in Azure KeyVault.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="keyIdentifier">The Azure KeyVault key identifier used for key encryption.</param>
+ /// <param name="clientId">The application client id.</param>
+ /// <param name="clientSecret">The client secret to use for authentication.</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, string clientId, string clientSecret)
+ {
+ if (string.IsNullOrEmpty(clientId))
+ {
+ throw new ArgumentNullException(nameof(clientId));
+ }
+ if (string.IsNullOrEmpty(clientSecret))
+ {
+ throw new ArgumentNullException(nameof(clientSecret));
+ }
+
+ KeyVaultClient.AuthenticationCallback callback =
+ (authority, resource, scope) => GetTokenFromClientSecret(authority, resource, clientId, clientSecret);
+
+ return ProtectKeysWithAzureKeyVault(builder, new KeyVaultClient(callback), keyIdentifier);
+ }
+
+ private static async Task<string> GetTokenFromClientSecret(string authority, string resource, string clientId, string clientSecret)
+ {
+ var authContext = new AuthenticationContext(authority);
+ var clientCred = new ClientCredential(clientId, clientSecret);
+ var result = await authContext.AcquireTokenAsync(resource, clientCred);
+ return result.AccessToken;
+ }
+
+ /// <summary>
+ /// Configures the data protection system to protect keys with specified key in Azure KeyVault.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="client">The <see cref="KeyVaultClient"/> to use for KeyVault access.</param>
+ /// <param name="keyIdentifier">The Azure KeyVault key identifier used for key encryption.</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, KeyVaultClient client, string keyIdentifier)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+ if (string.IsNullOrEmpty(keyIdentifier))
+ {
+ throw new ArgumentException(nameof(keyIdentifier));
+ }
+
+ var vaultClientWrapper = new KeyVaultClientWrapper(client);
+
+ builder.Services.AddSingleton<IKeyVaultWrappingClient>(vaultClientWrapper);
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.XmlEncryptor = new AzureKeyVaultXmlEncryptor(vaultClientWrapper, keyIdentifier);
+ });
+
+ return builder;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs
new file mode 100644
index 0000000000..b9942fa84f
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs
@@ -0,0 +1,52 @@
+// 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.IO;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault
+{
+ internal class AzureKeyVaultXmlDecryptor: IXmlDecryptor
+ {
+ private readonly IKeyVaultWrappingClient _client;
+
+ public AzureKeyVaultXmlDecryptor(IServiceProvider serviceProvider)
+ {
+ _client = serviceProvider.GetService<IKeyVaultWrappingClient>();
+ }
+
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ return DecryptAsync(encryptedElement).GetAwaiter().GetResult();
+ }
+
+ private async Task<XElement> DecryptAsync(XElement encryptedElement)
+ {
+ var kid = (string)encryptedElement.Element("kid");
+ var symmetricKey = Convert.FromBase64String((string)encryptedElement.Element("key"));
+ var symmetricIV = Convert.FromBase64String((string)encryptedElement.Element("iv"));
+
+ var encryptedValue = Convert.FromBase64String((string)encryptedElement.Element("value"));
+
+ var result = await _client.UnwrapKeyAsync(kid, AzureKeyVaultXmlEncryptor.DefaultKeyEncryption, symmetricKey);
+
+ byte[] decryptedValue;
+ using (var symmetricAlgorithm = AzureKeyVaultXmlEncryptor.DefaultSymmetricAlgorithmFactory())
+ {
+ using (var decryptor = symmetricAlgorithm.CreateDecryptor(result.Result, symmetricIV))
+ {
+ decryptedValue = decryptor.TransformFinalBlock(encryptedValue, 0, encryptedValue.Length);
+ }
+ }
+
+ using (var memoryStream = new MemoryStream(decryptedValue))
+ {
+ return XElement.Load(memoryStream);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs
new file mode 100644
index 0000000000..3451c3ded2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs
@@ -0,0 +1,77 @@
+// 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.IO;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Azure.KeyVault.WebKey;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault
+{
+ internal class AzureKeyVaultXmlEncryptor : IXmlEncryptor
+ {
+ internal static string DefaultKeyEncryption = JsonWebKeyEncryptionAlgorithm.RSAOAEP;
+ internal static Func<SymmetricAlgorithm> DefaultSymmetricAlgorithmFactory = Aes.Create;
+
+ private readonly RandomNumberGenerator _randomNumberGenerator;
+ private readonly IKeyVaultWrappingClient _client;
+ private readonly string _keyId;
+
+ public AzureKeyVaultXmlEncryptor(IKeyVaultWrappingClient client, string keyId)
+ : this(client, keyId, RandomNumberGenerator.Create())
+ {
+ }
+
+ internal AzureKeyVaultXmlEncryptor(IKeyVaultWrappingClient client, string keyId, RandomNumberGenerator randomNumberGenerator)
+ {
+ _client = client;
+ _keyId = keyId;
+ _randomNumberGenerator = randomNumberGenerator;
+ }
+
+ public EncryptedXmlInfo Encrypt(XElement plaintextElement)
+ {
+ return EncryptAsync(plaintextElement).GetAwaiter().GetResult();
+ }
+
+ private async Task<EncryptedXmlInfo> EncryptAsync(XElement plaintextElement)
+ {
+ byte[] value;
+ using (var memoryStream = new MemoryStream())
+ {
+ plaintextElement.Save(memoryStream, SaveOptions.DisableFormatting);
+ value = memoryStream.ToArray();
+ }
+
+ using (var symmetricAlgorithm = DefaultSymmetricAlgorithmFactory())
+ {
+ var symmetricBlockSize = symmetricAlgorithm.BlockSize / 8;
+ var symmetricKey = new byte[symmetricBlockSize];
+ var symmetricIV = new byte[symmetricBlockSize];
+ _randomNumberGenerator.GetBytes(symmetricKey);
+ _randomNumberGenerator.GetBytes(symmetricIV);
+
+ byte[] encryptedValue;
+ using (var encryptor = symmetricAlgorithm.CreateEncryptor(symmetricKey, symmetricIV))
+ {
+ encryptedValue = encryptor.TransformFinalBlock(value, 0, value.Length);
+ }
+
+ var wrappedKey = await _client.WrapKeyAsync(_keyId, DefaultKeyEncryption, symmetricKey);
+
+ var element = new XElement("encryptedKey",
+ new XComment(" This key is encrypted with Azure KeyVault. "),
+ new XElement("kid", wrappedKey.Kid),
+ new XElement("key", Convert.ToBase64String(wrappedKey.Result)),
+ new XElement("iv", Convert.ToBase64String(symmetricIV)),
+ new XElement("value", Convert.ToBase64String(encryptedValue)));
+
+ return new EncryptedXmlInfo(element, typeof(AzureKeyVaultXmlDecryptor));
+ }
+
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs
new file mode 100644
index 0000000000..2347460dc3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs
@@ -0,0 +1,14 @@
+// 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.Threading.Tasks;
+using Microsoft.Azure.KeyVault.Models;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault
+{
+ internal interface IKeyVaultWrappingClient
+ {
+ Task<KeyOperationResult> UnwrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText);
+ Task<KeyOperationResult> WrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText);
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs
new file mode 100644
index 0000000000..82fe0649e2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs
@@ -0,0 +1,29 @@
+// 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.Threading.Tasks;
+using Microsoft.Azure.KeyVault;
+using Microsoft.Azure.KeyVault.Models;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault
+{
+ internal class KeyVaultClientWrapper : IKeyVaultWrappingClient
+ {
+ private readonly KeyVaultClient _client;
+
+ public KeyVaultClientWrapper(KeyVaultClient client)
+ {
+ _client = client;
+ }
+
+ public Task<KeyOperationResult> UnwrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText)
+ {
+ return _client.UnwrapKeyAsync(keyIdentifier, algorithm, cipherText);
+ }
+
+ public Task<KeyOperationResult> WrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText)
+ {
+ return _client.WrapKeyAsync(keyIdentifier, algorithm, cipherText);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj
new file mode 100644
index 0000000000..0c7b084a2a
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Microsoft Azure KeyVault key encryption support.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection;azure;keyvault</PackageTags>
+ <EnableApiCheck>false</EnableApiCheck>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="$(MicrosoftIdentityModelClientsActiveDirectoryPackageVersion)" />
+ <PackageReference Include="Microsoft.Azure.KeyVault" Version="$(MicrosoftAzureKeyVaultPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..c23a3410b7
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+// 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.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureBlobXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureBlobXmlRepository.cs
new file mode 100644
index 0000000000..e39babaa31
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureBlobXmlRepository.cs
@@ -0,0 +1,297 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Blob;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureStorage
+{
+ /// <summary>
+ /// An <see cref="IXmlRepository"/> which is backed by Azure Blob Storage.
+ /// </summary>
+ /// <remarks>
+ /// Instances of this type are thread-safe.
+ /// </remarks>
+ public sealed class AzureBlobXmlRepository : IXmlRepository
+ {
+ private const int ConflictMaxRetries = 5;
+ private static readonly TimeSpan ConflictBackoffPeriod = TimeSpan.FromMilliseconds(200);
+
+ private static readonly XName RepositoryElementName = "repository";
+
+ private readonly Func<ICloudBlob> _blobRefFactory;
+ private readonly Random _random;
+ private BlobData _cachedBlobData;
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="AzureBlobXmlRepository"/>.
+ /// </summary>
+ /// <param name="blobRefFactory">A factory which can create <see cref="ICloudBlob"/>
+ /// instances. The factory must be thread-safe for invocation by multiple
+ /// concurrent threads, and each invocation must return a new object.</param>
+ public AzureBlobXmlRepository(Func<ICloudBlob> blobRefFactory)
+ {
+ if (blobRefFactory == null)
+ {
+ throw new ArgumentNullException(nameof(blobRefFactory));
+ }
+
+ _blobRefFactory = blobRefFactory;
+ _random = new Random();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyCollection<XElement> GetAllElements()
+ {
+ var blobRef = CreateFreshBlobRef();
+
+ // Shunt the work onto a ThreadPool thread so that it's independent of any
+ // existing sync context or other potentially deadlock-causing items.
+
+ var elements = Task.Run(() => GetAllElementsAsync(blobRef)).GetAwaiter().GetResult();
+ return new ReadOnlyCollection<XElement>(elements);
+ }
+
+ /// <inheritdoc />
+ public void StoreElement(XElement element, string friendlyName)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ var blobRef = CreateFreshBlobRef();
+
+ // Shunt the work onto a ThreadPool thread so that it's independent of any
+ // existing sync context or other potentially deadlock-causing items.
+
+ Task.Run(() => StoreElementAsync(blobRef, element)).GetAwaiter().GetResult();
+ }
+
+ private XDocument CreateDocumentFromBlob(byte[] blob)
+ {
+ using (var memoryStream = new MemoryStream(blob))
+ {
+ var xmlReaderSettings = new XmlReaderSettings()
+ {
+ DtdProcessing = DtdProcessing.Prohibit, IgnoreProcessingInstructions = true
+ };
+
+ using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings))
+ {
+ return XDocument.Load(xmlReader);
+ }
+ }
+ }
+
+ private ICloudBlob CreateFreshBlobRef()
+ {
+ // ICloudBlob instances aren't thread-safe, so we need to make sure we're working
+ // with a fresh instance that won't be mutated by another thread.
+
+ var blobRef = _blobRefFactory();
+ if (blobRef == null)
+ {
+ throw new InvalidOperationException("The ICloudBlob factory method returned null.");
+ }
+
+ return blobRef;
+ }
+
+ private async Task<IList<XElement>> GetAllElementsAsync(ICloudBlob blobRef)
+ {
+ var data = await GetLatestDataAsync(blobRef);
+
+ if (data == null)
+ {
+ // no data in blob storage
+ return new XElement[0];
+ }
+
+ // The document will look like this:
+ //
+ // <root>
+ // <child />
+ // <child />
+ // ...
+ // </root>
+ //
+ // We want to return the first-level child elements to our caller.
+
+ var doc = CreateDocumentFromBlob(data.BlobContents);
+ return doc.Root.Elements().ToList();
+ }
+
+ private async Task<BlobData> GetLatestDataAsync(ICloudBlob blobRef)
+ {
+ // Set the appropriate AccessCondition based on what we believe the latest
+ // file contents to be, then make the request.
+
+ var latestCachedData = Volatile.Read(ref _cachedBlobData); // local ref so field isn't mutated under our feet
+ var accessCondition = (latestCachedData != null)
+ ? AccessCondition.GenerateIfNoneMatchCondition(latestCachedData.ETag)
+ : null;
+
+ try
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ await blobRef.DownloadToStreamAsync(
+ target: memoryStream,
+ accessCondition: accessCondition,
+ options: null,
+ operationContext: null);
+
+ // At this point, our original cache either didn't exist or was outdated.
+ // We'll update it now and return the updated value;
+
+ latestCachedData = new BlobData()
+ {
+ BlobContents = memoryStream.ToArray(),
+ ETag = blobRef.Properties.ETag
+ };
+
+ }
+ Volatile.Write(ref _cachedBlobData, latestCachedData);
+ }
+ catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 304)
+ {
+ // 304 Not Modified
+ // Thrown when we already have the latest cached data.
+ // This isn't an error; we'll return our cached copy of the data.
+ }
+ catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
+ {
+ // 404 Not Found
+ // Thrown when no file exists in storage.
+ // This isn't an error; we'll delete our cached copy of data.
+
+ latestCachedData = null;
+ Volatile.Write(ref _cachedBlobData, latestCachedData);
+ }
+
+ return latestCachedData;
+ }
+
+ private int GetRandomizedBackoffPeriod()
+ {
+ // returns a TimeSpan in the range [0.8, 1.0) * ConflictBackoffPeriod
+ // not used for crypto purposes
+ var multiplier = 0.8 + (_random.NextDouble() * 0.2);
+ return (int) (multiplier * ConflictBackoffPeriod.Ticks);
+ }
+
+ private async Task StoreElementAsync(ICloudBlob blobRef, XElement element)
+ {
+ // holds the last error in case we need to rethrow it
+ ExceptionDispatchInfo lastError = null;
+
+ for (var i = 0; i < ConflictMaxRetries; i++)
+ {
+ if (i > 1)
+ {
+ // If multiple conflicts occurred, wait a small period of time before retrying
+ // the operation so that other writers can make forward progress.
+ await Task.Delay(GetRandomizedBackoffPeriod());
+ }
+
+ if (i > 0)
+ {
+ // If at least one conflict occurred, make sure we have an up-to-date
+ // view of the blob contents.
+ await GetLatestDataAsync(blobRef);
+ }
+
+ // Merge the new element into the document. If no document exists,
+ // create a new default document and inject this element into it.
+
+ var latestData = Volatile.Read(ref _cachedBlobData);
+ var doc = (latestData != null)
+ ? CreateDocumentFromBlob(latestData.BlobContents)
+ : new XDocument(new XElement(RepositoryElementName));
+ doc.Root.Add(element);
+
+ // Turn this document back into a byte[].
+
+ var serializedDoc = new MemoryStream();
+ doc.Save(serializedDoc, SaveOptions.DisableFormatting);
+
+ // Generate the appropriate precondition header based on whether or not
+ // we believe data already exists in storage.
+
+ AccessCondition accessCondition;
+ if (latestData != null)
+ {
+ accessCondition = AccessCondition.GenerateIfMatchCondition(blobRef.Properties.ETag);
+ }
+ else
+ {
+ accessCondition = AccessCondition.GenerateIfNotExistsCondition();
+ blobRef.Properties.ContentType = "application/xml; charset=utf-8"; // set content type on first write
+ }
+
+ try
+ {
+ // Send the request up to the server.
+
+ var serializedDocAsByteArray = serializedDoc.ToArray();
+
+ await blobRef.UploadFromByteArrayAsync(
+ buffer: serializedDocAsByteArray,
+ index: 0,
+ count: serializedDocAsByteArray.Length,
+ accessCondition: accessCondition,
+ options: null,
+ operationContext: null);
+
+ // If we got this far, success!
+ // We can update the cached view of the remote contents.
+
+ Volatile.Write(ref _cachedBlobData, new BlobData()
+ {
+ BlobContents = serializedDocAsByteArray,
+ ETag = blobRef.Properties.ETag // was updated by Upload routine
+ });
+
+ return;
+ }
+ catch (StorageException ex)
+ when (ex.RequestInformation.HttpStatusCode == 409 || ex.RequestInformation.HttpStatusCode == 412)
+ {
+ // 409 Conflict
+ // This error is rare but can be thrown in very special circumstances,
+ // such as if the blob in the process of being created. We treat it
+ // as equivalent to 412 for the purposes of retry logic.
+
+ // 412 Precondition Failed
+ // We'll get this error if another writer updated the repository and we
+ // have an outdated view of its contents. If this occurs, we'll just
+ // refresh our view of the remote contents and try again up to the max
+ // retry limit.
+
+ lastError = ExceptionDispatchInfo.Capture(ex);
+ }
+ }
+
+ // if we got this far, something went awry
+ lastError.Throw();
+ }
+
+ private sealed class BlobData
+ {
+ internal byte[] BlobContents;
+ internal string ETag;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureDataProtectionBuilderExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureDataProtectionBuilderExtensions.cs
new file mode 100644
index 0000000000..8ff62929e2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/AzureDataProtectionBuilderExtensions.cs
@@ -0,0 +1,175 @@
+// 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 Microsoft.AspNetCore.DataProtection.AzureStorage;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Auth;
+using Microsoft.WindowsAzure.Storage.Blob;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Contains Azure-specific extension methods for modifying a
+ /// <see cref="IDataProtectionBuilder"/>.
+ /// </summary>
+ public static class AzureDataProtectionBuilderExtensions
+ {
+ /// <summary>
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="storageAccount">The <see cref="CloudStorageAccount"/> which
+ /// should be utilized.</param>
+ /// <param name="relativePath">A relative path where the key file should be
+ /// stored, generally specified as "/containerName/[subDir/]keys.xml".</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ /// <remarks>
+ /// The container referenced by <paramref name="relativePath"/> must already exist.
+ /// </remarks>
+ public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudStorageAccount storageAccount, string relativePath)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (storageAccount == null)
+ {
+ throw new ArgumentNullException(nameof(storageAccount));
+ }
+ if (relativePath == null)
+ {
+ throw new ArgumentNullException(nameof(relativePath));
+ }
+
+ // Simply concatenate the root storage endpoint with the relative path,
+ // which includes the container name and blob name.
+
+ var uriBuilder = new UriBuilder(storageAccount.BlobEndpoint);
+ uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/" + relativePath.TrimStart('/');
+
+ // We can create a CloudBlockBlob from the storage URI and the creds.
+
+ var blobAbsoluteUri = uriBuilder.Uri;
+ var credentials = storageAccount.Credentials;
+
+ return PersistKeystoAzureBlobStorageInternal(builder, () => new CloudBlockBlob(blobAbsoluteUri, credentials));
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="blobUri">The full URI where the key file should be stored.
+ /// The URI must contain the SAS token as a query string parameter.</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ /// <remarks>
+ /// The container referenced by <paramref name="blobUri"/> must already exist.
+ /// </remarks>
+ public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, Uri blobUri)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (blobUri == null)
+ {
+ throw new ArgumentNullException(nameof(blobUri));
+ }
+
+ var uriBuilder = new UriBuilder(blobUri);
+
+ // The SAS token is present in the query string.
+
+ if (string.IsNullOrEmpty(uriBuilder.Query))
+ {
+ throw new ArgumentException(
+ message: "URI does not have a SAS token in the query string.",
+ paramName: nameof(blobUri));
+ }
+
+ var credentials = new StorageCredentials(uriBuilder.Query);
+ uriBuilder.Query = null; // no longer needed
+ var blobAbsoluteUri = uriBuilder.Uri;
+
+ return PersistKeystoAzureBlobStorageInternal(builder, () => new CloudBlockBlob(blobAbsoluteUri, credentials));
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="blobReference">The <see cref="CloudBlockBlob"/> where the
+ /// key file should be stored.</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ /// <remarks>
+ /// The container referenced by <paramref name="blobReference"/> must already exist.
+ /// </remarks>
+ public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudBlockBlob blobReference)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (blobReference == null)
+ {
+ throw new ArgumentNullException(nameof(blobReference));
+ }
+
+ // We're basically just going to make a copy of this blob.
+ // Use (container, blobName) instead of (storageuri, creds) since the container
+ // is tied to an existing service client, which contains user-settable defaults
+ // like retry policy and secondary connection URIs.
+
+ var container = blobReference.Container;
+ var blobName = blobReference.Name;
+
+ return PersistKeystoAzureBlobStorageInternal(builder, () => container.GetBlockBlobReference(blobName));
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="container">The <see cref="CloudBlobContainer"/> in which the
+ /// key file should be stored.</param>
+ /// <param name="blobName">The name of the key file, generally specified
+ /// as "[subdir/]keys.xml"</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ /// <remarks>
+ /// The container referenced by <paramref name="container"/> must already exist.
+ /// </remarks>
+ public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudBlobContainer container, string blobName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (container == null)
+ {
+ throw new ArgumentNullException(nameof(container));
+ }
+ if (blobName == null)
+ {
+ throw new ArgumentNullException(nameof(blobName));
+ }
+ return PersistKeystoAzureBlobStorageInternal(builder, () => container.GetBlockBlobReference(blobName));
+ }
+
+ // important: the Func passed into this method must return a new instance with each call
+ private static IDataProtectionBuilder PersistKeystoAzureBlobStorageInternal(IDataProtectionBuilder builder, Func<CloudBlockBlob> blobRefFactory)
+ {
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.XmlRepository = new AzureBlobXmlRepository(blobRefFactory);
+ });
+ return builder;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj
new file mode 100644
index 0000000000..ceb83f3925
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/Microsoft.AspNetCore.DataProtection.AzureStorage.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Microsoft Azure Blob storrage support as key store.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection;azure;blob</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="WindowsAzure.Storage" Version="$(WindowsAzureStoragePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/baseline.netcore.json
new file mode 100644
index 0000000000..09e208bfef
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.AzureStorage/baseline.netcore.json
@@ -0,0 +1,156 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection.AzureStorage, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AzureDataProtectionBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToAzureBlobStorage",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "storageAccount",
+ "Type": "Microsoft.WindowsAzure.Storage.CloudStorageAccount"
+ },
+ {
+ "Name": "relativePath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToAzureBlobStorage",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "blobUri",
+ "Type": "System.Uri"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToAzureBlobStorage",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "blobReference",
+ "Type": "Microsoft.WindowsAzure.Storage.Blob.CloudBlockBlob"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToAzureBlobStorage",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "container",
+ "Type": "Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer"
+ },
+ {
+ "Name": "blobName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AzureStorage.AzureBlobXmlRepository",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetAllElements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<System.Xml.Linq.XElement>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "StoreElement",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "friendlyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "blobRefFactory",
+ "Type": "System.Func<Microsoft.WindowsAzure.Storage.Blob.ICloudBlob>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs
new file mode 100644
index 0000000000..c236d5cb89
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs
@@ -0,0 +1,26 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
+{
+ /// <summary>
+ /// Code first model used by <see cref="EntityFrameworkCoreXmlRepository{TContext}"/>.
+ /// </summary>
+ public class DataProtectionKey
+ {
+ /// <summary>
+ /// The entity identifier of the <see cref="DataProtectionKey"/>.
+ /// </summary>
+ public int Id { get; set; }
+
+ /// <summary>
+ /// The friendly name of the <see cref="DataProtectionKey"/>.
+ /// </summary>
+ public string FriendlyName { get; set; }
+
+ /// <summary>
+ /// The XML representation of the <see cref="DataProtectionKey"/>.
+ /// </summary>
+ public string Xml { get; set; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs
new file mode 100644
index 0000000000..ff24b58eb9
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs
@@ -0,0 +1,39 @@
+// 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 Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Extension method class for configuring instances of <see cref="EntityFrameworkCoreXmlRepository{TContext}"/>
+ /// </summary>
+ public static class EntityFrameworkCoreDataProtectionExtensions
+ {
+ /// <summary>
+ /// Configures the data protection system to persist keys to an EntityFrameworkCore datastore
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/> instance to modify.</param>
+ /// <returns>The value <paramref name="builder"/>.</returns>
+ public static IDataProtectionBuilder PersistKeysToDbContext<TContext>(this IDataProtectionBuilder builder)
+ where TContext : DbContext, IDataProtectionKeyContext
+ {
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.XmlRepository = new EntityFrameworkCoreXmlRepository<TContext>(services, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs
new file mode 100644
index 0000000000..62250cf3ef
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs
@@ -0,0 +1,81 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
+{
+ /// <summary>
+ /// An <see cref="IXmlRepository"/> backed by an EntityFrameworkCore datastore.
+ /// </summary>
+ public class EntityFrameworkCoreXmlRepository<TContext> : IXmlRepository
+ where TContext : DbContext, IDataProtectionKeyContext
+ {
+ private readonly IServiceProvider _services;
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="EntityFrameworkCoreXmlRepository{TContext}"/>.
+ /// </summary>
+ /// <param name="services"></param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ public EntityFrameworkCoreXmlRepository(IServiceProvider services, ILoggerFactory loggerFactory)
+ {
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _logger = loggerFactory.CreateLogger<EntityFrameworkCoreXmlRepository<TContext>>();
+ _services = services ?? throw new ArgumentNullException(nameof(services));
+ }
+
+ /// <inheritdoc />
+ public virtual IReadOnlyCollection<XElement> GetAllElements()
+ {
+ using (var scope = _services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService<TContext>();
+ return context.DataProtectionKeys.AsNoTracking().Select(key => TryParseKeyXml(key.Xml)).ToList().AsReadOnly();
+ }
+ }
+
+ /// <inheritdoc />
+ public void StoreElement(XElement element, string friendlyName)
+ {
+ using (var scope = _services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService<TContext>();
+ var newKey = new DataProtectionKey()
+ {
+ FriendlyName = friendlyName,
+ Xml = element.ToString(SaveOptions.DisableFormatting)
+ };
+
+ context.DataProtectionKeys.Add(newKey);
+ _logger.LogSavingKeyToDbContext(friendlyName, typeof(TContext).Name);
+ context.SaveChanges();
+ }
+ }
+
+ private XElement TryParseKeyXml(string xml)
+ {
+ try
+ {
+ return XElement.Parse(xml);
+ }
+ catch (Exception e)
+ {
+ _logger?.LogExceptionWhileParsingKeyXml(xml, e);
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs
new file mode 100644
index 0000000000..39998d2a79
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs
@@ -0,0 +1,18 @@
+// 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 Microsoft.EntityFrameworkCore;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
+{
+ /// <summary>
+ /// Interface used to store instances of <see cref="DataProtectionKey"/> in a <see cref="DbContext"/>
+ /// </summary>
+ public interface IDataProtectionKeyContext
+ {
+ /// <summary>
+ /// A collection of <see cref="DataProtectionKey"/>
+ /// </summary>
+ DbSet<DataProtectionKey> DataProtectionKeys { get; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs
new file mode 100644
index 0000000000..d0aeb09271
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static readonly Action<ILogger, string, Exception> _anExceptionOccurredWhileParsingKeyXml;
+ private static readonly Action<ILogger, string, string, Exception> _savingKeyToDbContext;
+
+ static LoggingExtensions()
+ {
+ _anExceptionOccurredWhileParsingKeyXml = LoggerMessage.Define<string>(
+ eventId: 1,
+ logLevel: LogLevel.Warning,
+ formatString: "An exception occurred while parsing the key xml '{Xml}'.");
+ _savingKeyToDbContext = LoggerMessage.Define<string, string>(
+ eventId: 2,
+ logLevel: LogLevel.Debug,
+ formatString: "Saving key '{FriendlyName}' to '{DbContext}'.");
+ }
+
+ public static void LogExceptionWhileParsingKeyXml(this ILogger logger, string keyXml, Exception exception)
+ => _anExceptionOccurredWhileParsingKeyXml(logger, keyXml, exception);
+
+ public static void LogSavingKeyToDbContext(this ILogger logger, string friendlyName, string contextName)
+ => _savingKeyToDbContext(logger, friendlyName, contextName, null);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj
new file mode 100644
index 0000000000..e1715d94f2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Support for storing keys using Entity Framework Core.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection;entityframeworkcore</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="Properties\" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json
new file mode 100644
index 0000000000..9a9a7ebc1c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json
@@ -0,0 +1,203 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCoreDataProtectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToDbContext<T0>",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TContext",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.EntityFrameworkCore.DbContext",
+ "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.IDataProtectionKeyContext"
+ ]
+ }
+ ]
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Id",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Id",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_FriendlyName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_FriendlyName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Xml",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Xml",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.EntityFrameworkCoreXmlRepository<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetAllElements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<System.Xml.Linq.XElement>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "StoreElement",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "friendlyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TContext",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.EntityFrameworkCore.DbContext",
+ "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.IDataProtectionKeyContext"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.IDataProtectionKeyContext",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DataProtectionKeys",
+ "Parameters": [],
+ "ReturnType": "Microsoft.EntityFrameworkCore.DbSet<Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey>",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/BitHelpers.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/BitHelpers.cs
new file mode 100644
index 0000000000..eb2063fbd8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/BitHelpers.cs
@@ -0,0 +1,42 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class BitHelpers
+ {
+ /// <summary>
+ /// Reads an unsigned 64-bit integer from <paramref name="buffer"/>
+ /// starting at offset <paramref name="offset"/>. Data is read big-endian.
+ /// </summary>
+ public static ulong ReadUInt64(byte[] buffer, int offset)
+ {
+ return (((ulong)buffer[offset + 0]) << 56)
+ | (((ulong)buffer[offset + 1]) << 48)
+ | (((ulong)buffer[offset + 2]) << 40)
+ | (((ulong)buffer[offset + 3]) << 32)
+ | (((ulong)buffer[offset + 4]) << 24)
+ | (((ulong)buffer[offset + 5]) << 16)
+ | (((ulong)buffer[offset + 6]) << 8)
+ | (ulong)buffer[offset + 7];
+ }
+
+ /// <summary>
+ /// Writes an unsigned 64-bit integer to <paramref name="buffer"/> starting at
+ /// offset <paramref name="offset"/>. Data is written big-endian.
+ /// </summary>
+ public static void WriteUInt64(byte[] buffer, int offset, ulong value)
+ {
+ buffer[offset + 0] = (byte)(value >> 56);
+ buffer[offset + 1] = (byte)(value >> 48);
+ buffer[offset + 2] = (byte)(value >> 40);
+ buffer[offset + 3] = (byte)(value >> 32);
+ buffer[offset + 4] = (byte)(value >> 24);
+ buffer[offset + 5] = (byte)(value >> 16);
+ buffer[offset + 6] = (byte)(value >> 8);
+ buffer[offset + 7] = (byte)(value);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionAdvancedExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionAdvancedExtensions.cs
new file mode 100644
index 0000000000..6e4c2aabac
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionAdvancedExtensions.cs
@@ -0,0 +1,169 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Helpful extension methods for data protection APIs.
+ /// </summary>
+ public static class DataProtectionAdvancedExtensions
+ {
+ /// <summary>
+ /// Cryptographically protects a piece of plaintext data, expiring the data after
+ /// the specified amount of time has elapsed.
+ /// </summary>
+ /// <param name="protector">The protector to use.</param>
+ /// <param name="plaintext">The plaintext data to protect.</param>
+ /// <param name="lifetime">The amount of time after which the payload should no longer be unprotectable.</param>
+ /// <returns>The protected form of the plaintext data.</returns>
+ public static byte[] Protect(this ITimeLimitedDataProtector protector, byte[] plaintext, TimeSpan lifetime)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ return protector.Protect(plaintext, DateTimeOffset.UtcNow + lifetime);
+ }
+
+ /// <summary>
+ /// Cryptographically protects a piece of plaintext data, expiring the data at
+ /// the chosen time.
+ /// </summary>
+ /// <param name="protector">The protector to use.</param>
+ /// <param name="plaintext">The plaintext data to protect.</param>
+ /// <param name="expiration">The time when this payload should expire.</param>
+ /// <returns>The protected form of the plaintext data.</returns>
+ public static string Protect(this ITimeLimitedDataProtector protector, string plaintext, DateTimeOffset expiration)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ var wrappingProtector = new TimeLimitedWrappingProtector(protector) { Expiration = expiration };
+ return wrappingProtector.Protect(plaintext);
+ }
+
+ /// <summary>
+ /// Cryptographically protects a piece of plaintext data, expiring the data after
+ /// the specified amount of time has elapsed.
+ /// </summary>
+ /// <param name="protector">The protector to use.</param>
+ /// <param name="plaintext">The plaintext data to protect.</param>
+ /// <param name="lifetime">The amount of time after which the payload should no longer be unprotectable.</param>
+ /// <returns>The protected form of the plaintext data.</returns>
+ public static string Protect(this ITimeLimitedDataProtector protector, string plaintext, TimeSpan lifetime)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ return Protect(protector, plaintext, DateTimeOffset.Now + lifetime);
+ }
+
+ /// <summary>
+ /// Converts an <see cref="IDataProtector"/> into an <see cref="ITimeLimitedDataProtector"/>
+ /// so that payloads can be protected with a finite lifetime.
+ /// </summary>
+ /// <param name="protector">The <see cref="IDataProtector"/> to convert to a time-limited protector.</param>
+ /// <returns>An <see cref="ITimeLimitedDataProtector"/>.</returns>
+ public static ITimeLimitedDataProtector ToTimeLimitedDataProtector(this IDataProtector protector)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ return (protector as ITimeLimitedDataProtector) ?? new TimeLimitedDataProtector(protector);
+ }
+
+ /// <summary>
+ /// Cryptographically unprotects a piece of protected data.
+ /// </summary>
+ /// <param name="protector">The protector to use.</param>
+ /// <param name="protectedData">The protected data to unprotect.</param>
+ /// <param name="expiration">An 'out' parameter which upon a successful unprotect
+ /// operation receives the expiration date of the payload.</param>
+ /// <returns>The plaintext form of the protected data.</returns>
+ /// <exception cref="System.Security.Cryptography.CryptographicException">
+ /// Thrown if <paramref name="protectedData"/> is invalid, malformed, or expired.
+ /// </exception>
+ public static string Unprotect(this ITimeLimitedDataProtector protector, string protectedData, out DateTimeOffset expiration)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ var wrappingProtector = new TimeLimitedWrappingProtector(protector);
+ string retVal = wrappingProtector.Unprotect(protectedData);
+ expiration = wrappingProtector.Expiration;
+ return retVal;
+ }
+
+ private sealed class TimeLimitedWrappingProtector : IDataProtector
+ {
+ public DateTimeOffset Expiration;
+ private readonly ITimeLimitedDataProtector _innerProtector;
+
+ public TimeLimitedWrappingProtector(ITimeLimitedDataProtector innerProtector)
+ {
+ _innerProtector = innerProtector;
+ }
+
+ public IDataProtector CreateProtector(string purpose)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ throw new NotImplementedException();
+ }
+
+ public byte[] Protect(byte[] plaintext)
+ {
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ return _innerProtector.Protect(plaintext, Expiration);
+ }
+
+ public byte[] Unprotect(byte[] protectedData)
+ {
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ return _innerProtector.Unprotect(protectedData, out Expiration);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs
new file mode 100644
index 0000000000..cc82fe9ef8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/DataProtectionProvider.cs
@@ -0,0 +1,178 @@
+// 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.IO;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Contains factory methods for creating an <see cref="IDataProtectionProvider"/> where keys are stored
+ /// at a particular location on the file system.
+ /// </summary>
+ /// <remarks>Use these methods when not using dependency injection to provide the service to the application.</remarks>
+ public static class DataProtectionProvider
+ {
+ /// <summary>
+ /// Creates a <see cref="DataProtectionProvider"/> that store keys in a location based on
+ /// the platform and operating system.
+ /// </summary>
+ /// <param name="applicationName">An identifier that uniquely discriminates this application from all other
+ /// applications on the machine.</param>
+ 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);
+ }
+
+ /// <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 static IDataProtectionProvider Create(DirectoryInfo keyDirectory)
+ {
+ if (keyDirectory == null)
+ {
+ throw new ArgumentNullException(nameof(keyDirectory));
+ }
+
+ return CreateProvider(keyDirectory, setupAction: builder => { }, certificate: 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="setupAction">An optional callback which provides further configuration of the data protection
+ /// system. See <see cref="IDataProtectionBuilder"/> for more information.</param>
+ public static IDataProtectionProvider Create(
+ DirectoryInfo keyDirectory,
+ Action<IDataProtectionBuilder> setupAction)
+ {
+ if (keyDirectory == null)
+ {
+ throw new ArgumentNullException(nameof(keyDirectory));
+ }
+ if (setupAction == null)
+ {
+ throw new ArgumentNullException(nameof(setupAction));
+ }
+
+ return CreateProvider(keyDirectory, setupAction, certificate: null);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="DataProtectionProvider"/> that store keys in a location based on
+ /// the platform and operating system and uses the given <see cref="X509Certificate2"/> to encrypt the keys.
+ /// </summary>
+ /// <param name="applicationName">An identifier that uniquely discriminates this application from all other
+ /// applications on the machine.</param>
+ /// <param name="certificate">The <see cref="X509Certificate2"/> to be used for encryption.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="DataProtectionProvider"/> given a location at which to store keys
+ /// and a <see cref="X509Certificate2"/> used to encrypt the 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>
+ /// <param name="certificate">The <see cref="X509Certificate2"/> to be used for encryption.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="DataProtectionProvider"/> given a location at which to store keys, an
+ /// optional configuration callback and a <see cref="X509Certificate2"/> used to encrypt the 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>
+ /// <param name="setupAction">An optional callback which provides further configuration of the data protection
+ /// system. See <see cref="IDataProtectionBuilder"/> for more information.</param>
+ /// <param name="certificate">The <see cref="X509Certificate2"/> to be used for encryption.</param>
+ public static IDataProtectionProvider Create(
+ DirectoryInfo keyDirectory,
+ Action<IDataProtectionBuilder> 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);
+ }
+
+ internal static IDataProtectionProvider CreateProvider(
+ DirectoryInfo keyDirectory,
+ Action<IDataProtectionBuilder> setupAction,
+ X509Certificate2 certificate)
+ {
+ // build the service collection
+ var serviceCollection = new ServiceCollection();
+ var builder = serviceCollection.AddDataProtection();
+
+ if (keyDirectory != null)
+ {
+ builder.PersistKeysToFileSystem(keyDirectory);
+ }
+
+ if (certificate != null)
+ {
+ builder.ProtectKeysWithCertificate(certificate);
+ }
+
+ setupAction(builder);
+
+ // extract the provider instance from the service collection
+ return serviceCollection.BuildServiceProvider().GetRequiredService<IDataProtectionProvider>();
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/ITimeLimitedDataProtector.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/ITimeLimitedDataProtector.cs
new file mode 100644
index 0000000000..71fa609f21
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/ITimeLimitedDataProtector.cs
@@ -0,0 +1,55 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// An interface that can provide data protection services where payloads have
+ /// a finite lifetime.
+ /// </summary>
+ /// <remarks>
+ /// It is intended that payload lifetimes be somewhat short. Payloads protected
+ /// via this mechanism are not intended for long-term persistence (e.g., longer
+ /// than a few weeks).
+ /// </remarks>
+ public interface ITimeLimitedDataProtector : IDataProtector
+ {
+ /// <summary>
+ /// Creates an <see cref="ITimeLimitedDataProtector"/> given a purpose.
+ /// </summary>
+ /// <param name="purpose">
+ /// The purpose to be assigned to the newly-created <see cref="ITimeLimitedDataProtector"/>.
+ /// </param>
+ /// <returns>An <see cref="ITimeLimitedDataProtector"/> tied to the provided purpose.</returns>
+ /// <remarks>
+ /// The <paramref name="purpose"/> parameter must be unique for the intended use case; two
+ /// different <see cref="ITimeLimitedDataProtector"/> instances created with two different <paramref name="purpose"/>
+ /// values will not be able to decipher each other's payloads. The <paramref name="purpose"/> parameter
+ /// value is not intended to be kept secret.
+ /// </remarks>
+ new ITimeLimitedDataProtector CreateProtector(string purpose);
+
+ /// <summary>
+ /// Cryptographically protects a piece of plaintext data, expiring the data at
+ /// the chosen time.
+ /// </summary>
+ /// <param name="plaintext">The plaintext data to protect.</param>
+ /// <param name="expiration">The time when this payload should expire.</param>
+ /// <returns>The protected form of the plaintext data.</returns>
+ byte[] Protect(byte[] plaintext, DateTimeOffset expiration);
+
+ /// <summary>
+ /// Cryptographically unprotects a piece of protected data.
+ /// </summary>
+ /// <param name="protectedData">The protected data to unprotect.</param>
+ /// <param name="expiration">An 'out' parameter which upon a successful unprotect
+ /// operation receives the expiration date of the payload.</param>
+ /// <returns>The plaintext form of the protected data.</returns>
+ /// <exception cref="System.Security.Cryptography.CryptographicException">
+ /// Thrown if <paramref name="protectedData"/> is invalid, malformed, or expired.
+ /// </exception>
+ byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Microsoft.AspNetCore.DataProtection.Extensions.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Microsoft.AspNetCore.DataProtection.Extensions.csproj
new file mode 100644
index 0000000000..44885e5711
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Microsoft.AspNetCore.DataProtection.Extensions.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Additional APIs for ASP.NET Core data protection.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/AssemblyInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..022a5a3e6c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/Resources.Designer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..8fba5cd9f2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Properties/Resources.Designer.cs
@@ -0,0 +1,72 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.DataProtection.Extensions
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.DataProtection.Extensions.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// An error occurred during a cryptographic operation.
+ /// </summary>
+ internal static string CryptCommon_GenericError
+ {
+ get => GetString("CryptCommon_GenericError");
+ }
+
+ /// <summary>
+ /// An error occurred during a cryptographic operation.
+ /// </summary>
+ internal static string FormatCryptCommon_GenericError()
+ => GetString("CryptCommon_GenericError");
+
+ /// <summary>
+ /// The payload expired at {0}.
+ /// </summary>
+ internal static string TimeLimitedDataProtector_PayloadExpired
+ {
+ get => GetString("TimeLimitedDataProtector_PayloadExpired");
+ }
+
+ /// <summary>
+ /// The payload expired at {0}.
+ /// </summary>
+ internal static string FormatTimeLimitedDataProtector_PayloadExpired(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0);
+
+ /// <summary>
+ /// The payload is invalid.
+ /// </summary>
+ internal static string TimeLimitedDataProtector_PayloadInvalid
+ {
+ get => GetString("TimeLimitedDataProtector_PayloadInvalid");
+ }
+
+ /// <summary>
+ /// The payload is invalid.
+ /// </summary>
+ internal static string FormatTimeLimitedDataProtector_PayloadInvalid()
+ => GetString("TimeLimitedDataProtector_PayloadInvalid");
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Resources.resx b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Resources.resx
new file mode 100644
index 0000000000..b53d26e321
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/Resources.resx
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CryptCommon_GenericError" xml:space="preserve">
+ <value>An error occurred during a cryptographic operation.</value>
+ </data>
+ <data name="TimeLimitedDataProtector_PayloadExpired" xml:space="preserve">
+ <value>The payload expired at {0}.</value>
+ </data>
+ <data name="TimeLimitedDataProtector_PayloadInvalid" xml:space="preserve">
+ <value>The payload is invalid.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/TimeLimitedDataProtector.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/TimeLimitedDataProtector.cs
new file mode 100644
index 0000000000..71e9c3c553
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/TimeLimitedDataProtector.cs
@@ -0,0 +1,149 @@
+// 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.Security.Cryptography;
+using System.Threading;
+using Microsoft.AspNetCore.DataProtection.Extensions;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Wraps an existing <see cref="IDataProtector"/> and appends a purpose that allows
+ /// protecting data with a finite lifetime.
+ /// </summary>
+ internal sealed class TimeLimitedDataProtector : ITimeLimitedDataProtector
+ {
+ private const string MyPurposeString = "Microsoft.AspNetCore.DataProtection.TimeLimitedDataProtector.v1";
+
+ private readonly IDataProtector _innerProtector;
+ private IDataProtector _innerProtectorWithTimeLimitedPurpose; // created on-demand
+
+ public TimeLimitedDataProtector(IDataProtector innerProtector)
+ {
+ _innerProtector = innerProtector;
+ }
+
+ public ITimeLimitedDataProtector CreateProtector(string purpose)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ return new TimeLimitedDataProtector(_innerProtector.CreateProtector(purpose));
+ }
+
+ private IDataProtector GetInnerProtectorWithTimeLimitedPurpose()
+ {
+ // thread-safe lazy init pattern with multi-execution and single publication
+ var retVal = Volatile.Read(ref _innerProtectorWithTimeLimitedPurpose);
+ if (retVal == null)
+ {
+ var newValue = _innerProtector.CreateProtector(MyPurposeString); // we always append our purpose to the end of the chain
+ retVal = Interlocked.CompareExchange(ref _innerProtectorWithTimeLimitedPurpose, newValue, null) ?? newValue;
+ }
+ return retVal;
+ }
+
+ public byte[] Protect(byte[] plaintext, DateTimeOffset expiration)
+ {
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ // We prepend the expiration time (as a 64-bit UTC tick count) to the unprotected data.
+ byte[] plaintextWithHeader = new byte[checked(8 + plaintext.Length)];
+ BitHelpers.WriteUInt64(plaintextWithHeader, 0, (ulong)expiration.UtcTicks);
+ Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, 8, plaintext.Length);
+
+ return GetInnerProtectorWithTimeLimitedPurpose().Protect(plaintextWithHeader);
+ }
+
+ public byte[] Unprotect(byte[] protectedData, out DateTimeOffset expiration)
+ {
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ return UnprotectCore(protectedData, DateTimeOffset.UtcNow, out expiration);
+ }
+
+ internal byte[] UnprotectCore(byte[] protectedData, DateTimeOffset now, out DateTimeOffset expiration)
+ {
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ try
+ {
+ byte[] plaintextWithHeader = GetInnerProtectorWithTimeLimitedPurpose().Unprotect(protectedData);
+ if (plaintextWithHeader.Length < 8)
+ {
+ // header isn't present
+ throw new CryptographicException(Resources.TimeLimitedDataProtector_PayloadInvalid);
+ }
+
+ // Read expiration time back out of the payload
+ ulong utcTicksExpiration = BitHelpers.ReadUInt64(plaintextWithHeader, 0);
+ DateTimeOffset embeddedExpiration = new DateTimeOffset(checked((long)utcTicksExpiration), TimeSpan.Zero /* UTC */);
+
+ // Are we expired?
+ if (now > embeddedExpiration)
+ {
+ throw new CryptographicException(Resources.FormatTimeLimitedDataProtector_PayloadExpired(embeddedExpiration));
+ }
+
+ // Not expired - split and return payload
+ byte[] retVal = new byte[plaintextWithHeader.Length - 8];
+ Buffer.BlockCopy(plaintextWithHeader, 8, retVal, 0, retVal.Length);
+ expiration = embeddedExpiration;
+ return retVal;
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize all failures to CryptographicException
+ throw new CryptographicException(Resources.CryptCommon_GenericError, ex);
+ }
+ }
+
+ /*
+ * EXPLICIT INTERFACE IMPLEMENTATIONS
+ */
+
+ IDataProtector IDataProtectionProvider.CreateProtector(string purpose)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ return CreateProtector(purpose);
+ }
+
+ byte[] IDataProtector.Protect(byte[] plaintext)
+ {
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ // MaxValue essentially means 'no expiration'
+ return Protect(plaintext, DateTimeOffset.MaxValue);
+ }
+
+ byte[] IDataProtector.Unprotect(byte[] protectedData)
+ {
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ DateTimeOffset expiration; // unused
+ return Unprotect(protectedData, out expiration);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/baseline.netcore.json
new file mode 100644
index 0000000000..5bb3088d07
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.Extensions/baseline.netcore.json
@@ -0,0 +1,298 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection.Extensions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.DataProtectionAdvancedExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector"
+ },
+ {
+ "Name": "plaintext",
+ "Type": "System.Byte[]"
+ },
+ {
+ "Name": "lifetime",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector"
+ },
+ {
+ "Name": "plaintext",
+ "Type": "System.String"
+ },
+ {
+ "Name": "expiration",
+ "Type": "System.DateTimeOffset"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector"
+ },
+ {
+ "Name": "plaintext",
+ "Type": "System.String"
+ },
+ {
+ "Name": "lifetime",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ToTimeLimitedDataProtector",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector"
+ },
+ {
+ "Name": "protectedData",
+ "Type": "System.String"
+ },
+ {
+ "Name": "expiration",
+ "Type": "System.DateTimeOffset",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.DataProtectionProvider",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "applicationName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "keyDirectory",
+ "Type": "System.IO.DirectoryInfo"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "keyDirectory",
+ "Type": "System.IO.DirectoryInfo"
+ },
+ {
+ "Name": "setupAction",
+ "Type": "System.Action<Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "applicationName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "certificate",
+ "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "keyDirectory",
+ "Type": "System.IO.DirectoryInfo"
+ },
+ {
+ "Name": "certificate",
+ "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "keyDirectory",
+ "Type": "System.IO.DirectoryInfo"
+ },
+ {
+ "Name": "setupAction",
+ "Type": "System.Action<Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder>"
+ },
+ {
+ "Name": "certificate",
+ "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateProtector",
+ "Parameters": [
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.ITimeLimitedDataProtector",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "plaintext",
+ "Type": "System.Byte[]"
+ },
+ {
+ "Name": "expiration",
+ "Type": "System.DateTimeOffset"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedData",
+ "Type": "System.Byte[]"
+ },
+ {
+ "Name": "expiration",
+ "Type": "System.DateTimeOffset",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj
new file mode 100644
index 0000000000..1aa6874fff
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Support for storing data protection keys in Redis.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection;redis</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="StackExchange.Redis" Version="$(StackExchangeRedisPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisDataProtectionBuilderExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisDataProtectionBuilderExtensions.cs
new file mode 100644
index 0000000000..ead1b37db5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisDataProtectionBuilderExtensions.cs
@@ -0,0 +1,79 @@
+// 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 StackExchange.Redis;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.DataProtection.StackExchangeRedis;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Contains Redis-specific extension methods for modifying a <see cref="IDataProtectionBuilder"/>.
+ /// </summary>
+ public static class StackExchangeRedisDataProtectionBuilderExtensions
+ {
+ private const string DataProtectionKeysName = "DataProtection-Keys";
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to specified key in Redis database
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="databaseFactory">The delegate used to create <see cref="IDatabase"/> instances.</param>
+ /// <param name="key">The <see cref="RedisKey"/> used to store key list.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder PersistKeysToStackExchangeRedis(this IDataProtectionBuilder builder, Func<IDatabase> databaseFactory, RedisKey key)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (databaseFactory == null)
+ {
+ throw new ArgumentNullException(nameof(databaseFactory));
+ }
+ return PersistKeysToStackExchangeRedisInternal(builder, databaseFactory, key);
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the default key ('DataProtection-Keys') in Redis database
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="connectionMultiplexer">The <see cref="IConnectionMultiplexer"/> for database access.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder PersistKeysToStackExchangeRedis(this IDataProtectionBuilder builder, IConnectionMultiplexer connectionMultiplexer)
+ {
+ return PersistKeysToStackExchangeRedis(builder, connectionMultiplexer, DataProtectionKeysName);
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the specified key in Redis database
+ /// </summary>
+ /// <param name="builder">The builder instance to modify.</param>
+ /// <param name="connectionMultiplexer">The <see cref="IConnectionMultiplexer"/> for database access.</param>
+ /// <param name="key">The <see cref="RedisKey"/> used to store key list.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder PersistKeysToStackExchangeRedis(this IDataProtectionBuilder builder, IConnectionMultiplexer connectionMultiplexer, RedisKey key)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (connectionMultiplexer == null)
+ {
+ throw new ArgumentNullException(nameof(connectionMultiplexer));
+ }
+ return PersistKeysToStackExchangeRedisInternal(builder, () => connectionMultiplexer.GetDatabase(), key);
+ }
+
+ private static IDataProtectionBuilder PersistKeysToStackExchangeRedisInternal(IDataProtectionBuilder builder, Func<IDatabase> databaseFactory, RedisKey key)
+ {
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.XmlRepository = new RedisXmlRepository(databaseFactory, key);
+ });
+ return builder;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisXmlRepository.cs
new file mode 100644
index 0000000000..2665fd1408
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/RedisXmlRepository.cs
@@ -0,0 +1,59 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using StackExchange.Redis;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+
+namespace Microsoft.AspNetCore.DataProtection.StackExchangeRedis
+{
+ /// <summary>
+ /// An XML repository backed by a Redis list entry.
+ /// </summary>
+ public class RedisXmlRepository : IXmlRepository
+ {
+ private readonly Func<IDatabase> _databaseFactory;
+ private readonly RedisKey _key;
+
+ /// <summary>
+ /// Creates a <see cref="RedisXmlRepository"/> with keys stored at the given directory.
+ /// </summary>
+ /// <param name="databaseFactory">The delegate used to create <see cref="IDatabase"/> instances.</param>
+ /// <param name="key">The <see cref="RedisKey"/> used to store key list.</param>
+ public RedisXmlRepository(Func<IDatabase> databaseFactory, RedisKey key)
+ {
+ _databaseFactory = databaseFactory;
+ _key = key;
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyCollection<XElement> GetAllElements()
+ {
+ return GetAllElementsCore().ToList().AsReadOnly();
+ }
+
+ private IEnumerable<XElement> GetAllElementsCore()
+ {
+ // Note: Inability to read any value is considered a fatal error (since the file may contain
+ // revocation information), and we'll fail the entire operation rather than return a partial
+ // set of elements. If a value contains well-formed XML but its contents are meaningless, we
+ // won't fail that operation here. The caller is responsible for failing as appropriate given
+ // that scenario.
+ var database = _databaseFactory();
+ foreach (var value in database.ListRange(_key))
+ {
+ yield return XElement.Parse(value);
+ }
+ }
+
+ /// <inheritdoc />
+ public void StoreElement(XElement element, string friendlyName)
+ {
+ var database = _databaseFactory();
+ database.ListRightPush(_key, element.ToString(SaveOptions.DisableFormatting));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/CompatibilityDataProtector.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/CompatibilityDataProtector.cs
new file mode 100644
index 0000000000..739afe83bd
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/CompatibilityDataProtector.cs
@@ -0,0 +1,133 @@
+// 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.ComponentModel;
+using System.Configuration;
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.SystemWeb
+{
+ /// <summary>
+ /// A <see cref="DataProtector"/> that can be used by ASP.NET 4.x to interact with ASP.NET Core's
+ /// DataProtection stack. This type is for internal use only and shouldn't be directly used by
+ /// developers.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public sealed class CompatibilityDataProtector : DataProtector
+ {
+ private static readonly Lazy<IDataProtectionProvider> _lazyProtectionProvider = new Lazy<IDataProtectionProvider>(CreateProtectionProvider);
+
+ [ThreadStatic]
+ private static bool _suppressPrimaryPurpose;
+
+ private readonly Lazy<IDataProtector> _lazyProtector;
+ private readonly Lazy<IDataProtector> _lazyProtectorSuppressedPrimaryPurpose;
+
+ public CompatibilityDataProtector(string applicationName, string primaryPurpose, string[] specificPurposes)
+ : base("application-name", "primary-purpose", null) // we feed dummy values to the base ctor
+ {
+ // We don't want to evaluate the IDataProtectionProvider factory quite yet,
+ // as we'd rather defer failures to the call to Protect so that we can bubble
+ // up a good error message to the developer.
+
+ _lazyProtector = new Lazy<IDataProtector>(() => _lazyProtectionProvider.Value.CreateProtector(primaryPurpose, specificPurposes));
+
+ // System.Web always provides "User.MachineKey.Protect" as the primary purpose for calls
+ // to MachineKey.Protect. Only in this case should we allow suppressing the primary
+ // purpose, as then we can easily map calls to MachineKey.Protect(userData, purposes)
+ // into calls to provider.GetProtector(purposes).Protect(userData).
+ if (primaryPurpose == "User.MachineKey.Protect")
+ {
+ _lazyProtectorSuppressedPrimaryPurpose = new Lazy<IDataProtector>(() => _lazyProtectionProvider.Value.CreateProtector(specificPurposes));
+ }
+ else
+ {
+ _lazyProtectorSuppressedPrimaryPurpose = _lazyProtector;
+ }
+ }
+
+ // We take care of flowing purposes ourselves.
+ protected override bool PrependHashedPurposeToPlaintext { get; } = false;
+
+ // Retrieves the appropriate protector (potentially with a suppressed primary purpose) for this operation.
+ private IDataProtector Protector => ((_suppressPrimaryPurpose) ? _lazyProtectorSuppressedPrimaryPurpose : _lazyProtector).Value;
+
+ private static IDataProtectionProvider CreateProtectionProvider()
+ {
+ // Read from <appSettings> the startup type we need to use, then create it
+ const string APPSETTINGS_KEY = "aspnet:dataProtectionStartupType";
+ string startupTypeName = ConfigurationManager.AppSettings[APPSETTINGS_KEY];
+ if (String.IsNullOrEmpty(startupTypeName))
+ {
+ // fall back to default startup type if one hasn't been specified in config
+ startupTypeName = typeof(DataProtectionStartup).AssemblyQualifiedName;
+ }
+ Type startupType = Type.GetType(startupTypeName, throwOnError: true);
+ var startupInstance = (DataProtectionStartup)Activator.CreateInstance(startupType);
+
+ // Use it to initialize the system.
+ return startupInstance.InternalConfigureServicesAndCreateProtectionProvider();
+ }
+
+ public override bool IsReprotectRequired(byte[] encryptedData)
+ {
+ // Nobody ever calls this.
+ return false;
+ }
+
+ protected override byte[] ProviderProtect(byte[] userData)
+ {
+ try
+ {
+ return Protector.Protect(userData);
+ }
+ catch (Exception ex)
+ {
+ // System.Web special-cases ConfigurationException errors and allows them to bubble
+ // up to the developer without being homogenized. Since a call to Protect should
+ // never fail, any exceptions here really do imply a misconfiguration.
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ throw new ConfigurationException(Resources.DataProtector_ProtectFailed, ex);
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+ }
+
+ protected override byte[] ProviderUnprotect(byte[] encryptedData)
+ {
+ return Protector.Unprotect(encryptedData);
+ }
+
+ /// <summary>
+ /// Invokes a delegate where calls to <see cref="ProviderProtect(byte[])"/>
+ /// and <see cref="ProviderUnprotect(byte[])"/> will ignore the primary
+ /// purpose and instead use only the sub-purposes.
+ /// </summary>
+ public static byte[] RunWithSuppressedPrimaryPurpose(Func<object, byte[], byte[]> callback, object state, byte[] input)
+ {
+ if (_suppressPrimaryPurpose)
+ {
+ return callback(state, input); // already suppressed - just forward call
+ }
+
+ try
+ {
+ try
+ {
+ _suppressPrimaryPurpose = true;
+ return callback(state, input);
+ }
+ finally
+ {
+ _suppressPrimaryPurpose = false;
+ }
+ }
+ catch
+ {
+ // defeat exception filters
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/DataProtectionStartup.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/DataProtectionStartup.cs
new file mode 100644
index 0000000000..f3760df207
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/DataProtectionStartup.cs
@@ -0,0 +1,102 @@
+// 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.Configuration;
+using System.Web;
+using System.Web.Configuration;
+using Microsoft.AspNetCore.DataProtection.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection.SystemWeb
+{
+ /// <summary>
+ /// Allows controlling the configuration of the ASP.NET Core Data Protection system.
+ /// </summary>
+ /// <remarks>
+ /// Developers should not call these APIs directly. Instead, developers should subclass
+ /// this type and override the <see cref="ConfigureServices(IServiceCollection)"/>
+ /// method or <see cref="CreateDataProtectionProvider(IServiceProvider)"/> methods
+ /// as appropriate.
+ /// </remarks>
+ public class DataProtectionStartup
+ {
+ /// <summary>
+ /// Configures services used by the Data Protection system.
+ /// </summary>
+ /// <param name="services">A mutable collection of services.</param>
+ /// <remarks>
+ /// Developers may override this method to change the default behaviors of
+ /// the Data Protection system.
+ /// </remarks>
+ public virtual void ConfigureServices(IServiceCollection services)
+ {
+ // InternalConfigureServices already takes care of default configuration.
+ // The reason we don't configure default logic in this method is that we don't
+ // want to punish the developer for forgetting to call base.ConfigureServices
+ // from within his own override.
+ }
+
+ /// <summary>
+ /// Creates a new instance of an <see cref="IDataProtectionProvider"/>.
+ /// </summary>
+ /// <param name="services">A collection of services from which to create the <see cref="IDataProtectionProvider"/>.</param>
+ /// <returns>An <see cref="IDataProtectionProvider"/>.</returns>
+ /// <remarks>
+ /// Developers should generally override the <see cref="ConfigureServices(IServiceCollection)"/>
+ /// method instead of this method.
+ /// </remarks>
+ public virtual IDataProtectionProvider CreateDataProtectionProvider(IServiceProvider services)
+ {
+ return services.GetDataProtectionProvider();
+ }
+
+ /// <summary>
+ /// Provides a default implementation of required services, calls the developer's
+ /// configuration overrides, then creates an <see cref="IDataProtectionProvider"/>.
+ /// </summary>
+ internal IDataProtectionProvider InternalConfigureServicesAndCreateProtectionProvider()
+ {
+ // Configure the default implementation, passing in our custom discriminator
+ var services = new ServiceCollection();
+ services.AddDataProtection();
+ services.AddSingleton<IApplicationDiscriminator>(new SystemWebApplicationDiscriminator());
+
+ // Run user-specified configuration and get an instance of the provider
+ ConfigureServices(services);
+ var provider = CreateDataProtectionProvider(services.BuildServiceProvider());
+ if (provider == null)
+ {
+ throw new InvalidOperationException(Resources.Startup_CreateProviderReturnedNull);
+ }
+
+ // And we're done!
+ return provider;
+ }
+
+ private sealed class SystemWebApplicationDiscriminator : IApplicationDiscriminator
+ {
+ private readonly Lazy<string> _lazyDiscriminator = new Lazy<string>(GetAppDiscriminatorCore);
+
+ public string Discriminator => _lazyDiscriminator.Value;
+
+ private static string GetAppDiscriminatorCore()
+ {
+ // Try reading the discriminator from <machineKey applicationName="..." /> defined
+ // at the web app root. If the value was set explicitly (even if the value is empty),
+ // honor it as the discriminator.
+ var machineKeySection = (MachineKeySection)WebConfigurationManager.GetWebApplicationSection("system.web/machineKey");
+ if (machineKeySection.ElementInformation.Properties["applicationName"].ValueOrigin != PropertyValueOrigin.Default)
+ {
+ return machineKeySection.ApplicationName;
+ }
+ else
+ {
+ // Otherwise, fall back to the IIS metabase config path.
+ // This is unique per machine.
+ return HttpRuntime.AppDomainAppId;
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Microsoft.AspNetCore.DataProtection.SystemWeb.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Microsoft.AspNetCore.DataProtection.SystemWeb.csproj
new file mode 100644
index 0000000000..a40024990e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Microsoft.AspNetCore.DataProtection.SystemWeb.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A component to allow the ASP.NET Core data protection stack to work with the ASP.NET 4.x &lt;machineKey&gt; element.</Description>
+ <TargetFramework>net461</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnet;aspnetcore;dataprotection</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Content Include="web.config.transform" PackagePath="content/net46/" Pack="true" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Web" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Properties/Resources.Designer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..ddc7e53910
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.DataProtection.SystemWeb
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.DataProtection.SystemWeb.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.
+ /// </summary>
+ internal static string DataProtector_ProtectFailed
+ {
+ get => GetString("DataProtector_ProtectFailed");
+ }
+
+ /// <summary>
+ /// A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.
+ /// </summary>
+ internal static string FormatDataProtector_ProtectFailed()
+ => GetString("DataProtector_ProtectFailed");
+
+ /// <summary>
+ /// The CreateDataProtectionProvider method returned null.
+ /// </summary>
+ internal static string Startup_CreateProviderReturnedNull
+ {
+ get => GetString("Startup_CreateProviderReturnedNull");
+ }
+
+ /// <summary>
+ /// The CreateDataProtectionProvider method returned null.
+ /// </summary>
+ internal static string FormatStartup_CreateProviderReturnedNull()
+ => GetString("Startup_CreateProviderReturnedNull");
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Resources.resx b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Resources.resx
new file mode 100644
index 0000000000..0923e71d3c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/Resources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="DataProtector_ProtectFailed" xml:space="preserve">
+ <value>A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.</value>
+ </data>
+ <data name="Startup_CreateProviderReturnedNull" xml:space="preserve">
+ <value>The CreateDataProtectionProvider method returned null.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/baseline.netframework.json b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/baseline.netframework.json
new file mode 100644
index 0000000000..c068f832bb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/baseline.netframework.json
@@ -0,0 +1,157 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection.SystemWeb, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.SystemWeb.CompatibilityDataProtector",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "BaseType": "System.Security.Cryptography.DataProtector",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_PrependHashedPurposeToPlaintext",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "IsReprotectRequired",
+ "Parameters": [
+ {
+ "Name": "encryptedData",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProviderProtect",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProviderUnprotect",
+ "Parameters": [
+ {
+ "Name": "encryptedData",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RunWithSuppressedPrimaryPurpose",
+ "Parameters": [
+ {
+ "Name": "callback",
+ "Type": "System.Func<System.Object, System.Byte[], System.Byte[]>"
+ },
+ {
+ "Name": "state",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "input",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "applicationName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "primaryPurpose",
+ "Type": "System.String"
+ },
+ {
+ "Name": "specificPurposes",
+ "Type": "System.String[]"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.SystemWeb.DataProtectionStartup",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ConfigureServices",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateDataProtectionProvider",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/web.config.transform b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/web.config.transform
new file mode 100644
index 0000000000..8d5a699252
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection.SystemWeb/web.config.transform
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <!--
+ If you want to customize the behavior of the ASP.NET Core Data Protection stack, set the
+ "aspnet:dataProtectionStartupType" switch below to be the fully-qualified name of a
+ type which subclasses Microsoft.AspNetCore.DataProtection.SystemWeb.DataProtectionStartup.
+ -->
+ <add key="aspnet:dataProtectionStartupType" value="" />
+ </appSettings>
+ <system.web>
+ <machineKey compatibilityMode="Framework45" dataProtectorType="Microsoft.AspNetCore.DataProtection.SystemWeb.CompatibilityDataProtector, Microsoft.AspNetCore.DataProtection.SystemWeb" />
+ </system.web>
+</configuration>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ActivatorExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ActivatorExtensions.cs
new file mode 100644
index 0000000000..a485958fc9
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ActivatorExtensions.cs
@@ -0,0 +1,43 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Extension methods for working with <see cref="IActivator"/>.
+ /// </summary>
+ internal static class ActivatorExtensions
+ {
+ /// <summary>
+ /// Creates an instance of <paramref name="implementationTypeName"/> and ensures
+ /// that it is assignable to <typeparamref name="T"/>.
+ /// </summary>
+ public static T CreateInstance<T>(this IActivator activator, string implementationTypeName)
+ where T : class
+ {
+ if (implementationTypeName == null)
+ {
+ throw new ArgumentNullException(nameof(implementationTypeName));
+ }
+
+ return activator.CreateInstance(typeof(T), implementationTypeName) as T
+ ?? CryptoUtil.Fail<T>("CreateInstance returned null.");
+ }
+
+ /// <summary>
+ /// Returns a <see cref="IActivator"/> given an <see cref="IServiceProvider"/>.
+ /// Guaranteed to return non-null, even if <paramref name="serviceProvider"/> is null.
+ /// </summary>
+ public static IActivator GetActivator(this IServiceProvider serviceProvider)
+ {
+ return (serviceProvider != null)
+ ? (serviceProvider.GetService<IActivator>() ?? new SimpleActivator(serviceProvider))
+ : SimpleActivator.DefaultWithoutServices;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ApplyPolicyAttribute.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ApplyPolicyAttribute.cs
new file mode 100644
index 0000000000..f73a745b1e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ApplyPolicyAttribute.cs
@@ -0,0 +1,13 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Signifies that the <see cref="RegistryPolicyResolver"/> should bind this property from the registry.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+ internal sealed class ApplyPolicyAttribute : Attribute { }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ArraySegmentExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ArraySegmentExtensions.cs
new file mode 100644
index 0000000000..f468560f77
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ArraySegmentExtensions.cs
@@ -0,0 +1,30 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class ArraySegmentExtensions
+ {
+ public static byte[] AsStandaloneArray(this ArraySegment<byte> arraySegment)
+ {
+ // Fast-track: Don't need to duplicate the array.
+ if (arraySegment.Offset == 0 && arraySegment.Count == arraySegment.Array.Length)
+ {
+ return arraySegment.Array;
+ }
+
+ var retVal = new byte[arraySegment.Count];
+ Buffer.BlockCopy(arraySegment.Array, arraySegment.Offset, retVal, 0, retVal.Length);
+ return retVal;
+ }
+
+ public static void Validate<T>(this ArraySegment<T> arraySegment)
+ {
+ // Since ArraySegment<T> is a struct, it can be improperly initialized or torn.
+ // We call the ctor again to make sure the instance data is valid.
+ var unused = new ArraySegment<T>(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs
new file mode 100644
index 0000000000..cd3dd8432e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs
@@ -0,0 +1,55 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ internal static class AlgorithmAssert
+ {
+ // Our analysis re: IV collision resistance for CBC only holds if we're working with block ciphers
+ // with a block length of 64 bits or greater.
+ private const uint SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BITS = 64;
+
+ // Min security bar: encryption algorithm must have a min 128-bit key.
+ private const uint SYMMETRIC_ALG_MIN_KEY_LENGTH_IN_BITS = 128;
+
+ // Min security bar: authentication tag must have at least 128 bits of output.
+ private const uint HASH_ALG_MIN_DIGEST_LENGTH_IN_BITS = 128;
+
+ // Since we're performing some stack allocs based on these buffers, make sure we don't explode.
+ private const uint MAX_SIZE_IN_BITS = Constants.MAX_STACKALLOC_BYTES * 8;
+
+ public static void IsAllowableSymmetricAlgorithmBlockSize(uint blockSizeInBits)
+ {
+ if (!IsValidCore(blockSizeInBits, SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BITS))
+ {
+ throw new InvalidOperationException(Resources.FormatAlgorithmAssert_BadBlockSize(blockSizeInBits));
+ }
+ }
+
+ public static void IsAllowableSymmetricAlgorithmKeySize(uint keySizeInBits)
+ {
+ if (!IsValidCore(keySizeInBits, SYMMETRIC_ALG_MIN_KEY_LENGTH_IN_BITS))
+ {
+ throw new InvalidOperationException(Resources.FormatAlgorithmAssert_BadKeySize(keySizeInBits));
+ }
+ }
+
+ public static void IsAllowableValidationAlgorithmDigestSize(uint digestSizeInBits)
+ {
+ if (!IsValidCore(digestSizeInBits, HASH_ALG_MIN_DIGEST_LENGTH_IN_BITS))
+ {
+ throw new InvalidOperationException(Resources.FormatAlgorithmAssert_BadDigestSize(digestSizeInBits));
+ }
+ }
+
+ private static bool IsValidCore(uint value, uint minValue)
+ {
+ return (value % 8 == 0) // must be whole bytes
+ && (value >= minValue) // must meet our basic security requirements
+ && (value <= MAX_SIZE_IN_BITS); // mustn't overflow our stack
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs
new file mode 100644
index 0000000000..31f31a9a28
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs
@@ -0,0 +1,55 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ internal static class AuthenticatedEncryptorExtensions
+ {
+ public static byte[] Encrypt(this IAuthenticatedEncryptor encryptor, ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize)
+ {
+ // Can we call the optimized version?
+ var optimizedEncryptor = encryptor as IOptimizedAuthenticatedEncryptor;
+ if (optimizedEncryptor != null)
+ {
+ return optimizedEncryptor.Encrypt(plaintext, additionalAuthenticatedData, preBufferSize, postBufferSize);
+ }
+
+ // Fall back to the unoptimized version
+ if (preBufferSize == 0 && postBufferSize == 0)
+ {
+ // optimization: call through to inner encryptor with no modifications
+ return encryptor.Encrypt(plaintext, additionalAuthenticatedData);
+ }
+ else
+ {
+ var temp = encryptor.Encrypt(plaintext, additionalAuthenticatedData);
+ var retVal = new byte[checked(preBufferSize + temp.Length + postBufferSize)];
+ Buffer.BlockCopy(temp, 0, retVal, checked((int)preBufferSize), temp.Length);
+ return retVal;
+ }
+ }
+
+ /// <summary>
+ /// Performs a self-test of this encryptor by running a sample payload through an
+ /// encrypt-then-decrypt operation. Throws if the operation fails.
+ /// </summary>
+ public static void PerformSelfTest(this IAuthenticatedEncryptor encryptor)
+ {
+ // Arrange
+ var plaintextAsGuid = Guid.NewGuid();
+ var plaintextAsBytes = plaintextAsGuid.ToByteArray();
+ var aad = Guid.NewGuid().ToByteArray();
+
+ // Act
+ var protectedData = encryptor.Encrypt(new ArraySegment<byte>(plaintextAsBytes), new ArraySegment<byte>(aad));
+ var roundTrippedData = encryptor.Decrypt(new ArraySegment<byte>(protectedData), new ArraySegment<byte>(aad));
+
+ // Assert
+ CryptoUtil.Assert(roundTrippedData != null && roundTrippedData.Length == plaintextAsBytes.Length && plaintextAsGuid == new Guid(roundTrippedData),
+ "Plaintext did not round-trip properly through the authenticated encryptor.");
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs
new file mode 100644
index 0000000000..f9be1e1994
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs
@@ -0,0 +1,182 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// An <see cref="IAuthenticatedEncryptorFactory"/> to create an <see cref="IAuthenticatedEncryptor"/>
+ /// based on the <see cref="AuthenticatedEncryptorConfiguration"/>.
+ /// </summary>
+ public sealed class AuthenticatedEncryptorFactory : IAuthenticatedEncryptorFactory
+ {
+ private readonly ILoggerFactory _loggerFactory;
+
+ public AuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
+ {
+ _loggerFactory = loggerFactory;
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance(IKey key)
+ {
+ var descriptor = key.Descriptor as AuthenticatedEncryptorDescriptor;
+ if (descriptor == null)
+ {
+ return null;
+ }
+
+ return CreateAuthenticatedEncryptorInstance(descriptor.MasterKey, descriptor.Configuration);
+ }
+
+ internal IAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(
+ ISecret secret,
+ AuthenticatedEncryptorConfiguration authenticatedConfiguration)
+ {
+ if (authenticatedConfiguration == null)
+ {
+ return null;
+ }
+
+ if (IsGcmAlgorithm(authenticatedConfiguration.EncryptionAlgorithm))
+ {
+ // GCM requires CNG, and CNG is only supported on Windows.
+ if (!OSVersionUtil.IsWindows())
+ {
+ throw new PlatformNotSupportedException(Resources.Platform_WindowsRequiredForGcm);
+ }
+
+ var configuration = new CngGcmAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = GetBCryptAlgorithmNameFromEncryptionAlgorithm(authenticatedConfiguration.EncryptionAlgorithm),
+ EncryptionAlgorithmKeySize = GetAlgorithmKeySizeInBits(authenticatedConfiguration.EncryptionAlgorithm)
+ };
+
+ return new CngGcmAuthenticatedEncryptorFactory(_loggerFactory).CreateAuthenticatedEncryptorInstance(secret, configuration);
+ }
+ else
+ {
+ if (OSVersionUtil.IsWindows())
+ {
+ // CNG preferred over managed implementations if running on Windows
+ var configuration = new CngCbcAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = GetBCryptAlgorithmNameFromEncryptionAlgorithm(authenticatedConfiguration.EncryptionAlgorithm),
+ EncryptionAlgorithmKeySize = GetAlgorithmKeySizeInBits(authenticatedConfiguration.EncryptionAlgorithm),
+ HashAlgorithm = GetBCryptAlgorithmNameFromValidationAlgorithm(authenticatedConfiguration.ValidationAlgorithm)
+ };
+
+ return new CngCbcAuthenticatedEncryptorFactory(_loggerFactory).CreateAuthenticatedEncryptorInstance(secret, configuration);
+ }
+ else
+ {
+ // Use managed implementations as a fallback
+ var configuration = new ManagedAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithmType = GetManagedTypeFromEncryptionAlgorithm(authenticatedConfiguration.EncryptionAlgorithm),
+ EncryptionAlgorithmKeySize = GetAlgorithmKeySizeInBits(authenticatedConfiguration.EncryptionAlgorithm),
+ ValidationAlgorithmType = GetManagedTypeFromValidationAlgorithm(authenticatedConfiguration.ValidationAlgorithm)
+ };
+
+ return new ManagedAuthenticatedEncryptorFactory(_loggerFactory).CreateAuthenticatedEncryptorInstance(secret, configuration);
+ }
+ }
+ }
+
+ internal static bool IsGcmAlgorithm(EncryptionAlgorithm algorithm)
+ {
+ return (EncryptionAlgorithm.AES_128_GCM <= algorithm && algorithm <= EncryptionAlgorithm.AES_256_GCM);
+ }
+
+ private static int GetAlgorithmKeySizeInBits(EncryptionAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case EncryptionAlgorithm.AES_128_CBC:
+ case EncryptionAlgorithm.AES_128_GCM:
+ return 128;
+
+ case EncryptionAlgorithm.AES_192_CBC:
+ case EncryptionAlgorithm.AES_192_GCM:
+ return 192;
+
+ case EncryptionAlgorithm.AES_256_CBC:
+ case EncryptionAlgorithm.AES_256_GCM:
+ return 256;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(EncryptionAlgorithm));
+ }
+ }
+
+ private static string GetBCryptAlgorithmNameFromEncryptionAlgorithm(EncryptionAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case EncryptionAlgorithm.AES_128_CBC:
+ case EncryptionAlgorithm.AES_192_CBC:
+ case EncryptionAlgorithm.AES_256_CBC:
+ case EncryptionAlgorithm.AES_128_GCM:
+ case EncryptionAlgorithm.AES_192_GCM:
+ case EncryptionAlgorithm.AES_256_GCM:
+ return Constants.BCRYPT_AES_ALGORITHM;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(EncryptionAlgorithm));
+ }
+ }
+
+ private static string GetBCryptAlgorithmNameFromValidationAlgorithm(ValidationAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case ValidationAlgorithm.HMACSHA256:
+ return Constants.BCRYPT_SHA256_ALGORITHM;
+
+ case ValidationAlgorithm.HMACSHA512:
+ return Constants.BCRYPT_SHA512_ALGORITHM;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(ValidationAlgorithm));
+ }
+ }
+
+ private static Type GetManagedTypeFromEncryptionAlgorithm(EncryptionAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case EncryptionAlgorithm.AES_128_CBC:
+ case EncryptionAlgorithm.AES_192_CBC:
+ case EncryptionAlgorithm.AES_256_CBC:
+ case EncryptionAlgorithm.AES_128_GCM:
+ case EncryptionAlgorithm.AES_192_GCM:
+ case EncryptionAlgorithm.AES_256_GCM:
+ return typeof(Aes);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(EncryptionAlgorithm));
+ }
+ }
+
+ private static Type GetManagedTypeFromValidationAlgorithm(ValidationAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case ValidationAlgorithm.HMACSHA256:
+ return typeof(HMACSHA256);
+
+ case ValidationAlgorithm.HMACSHA512:
+ return typeof(HMACSHA512);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(ValidationAlgorithm));
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactory.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactory.cs
new file mode 100644
index 0000000000..1ccc76d501
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactory.cs
@@ -0,0 +1,127 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="CbcAuthenticatedEncryptor"/>.
+ /// </summary>
+ public sealed class CngCbcAuthenticatedEncryptorFactory : IAuthenticatedEncryptorFactory
+ {
+ private readonly ILogger _logger;
+
+ public CngCbcAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger<CngCbcAuthenticatedEncryptorFactory>();
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance(IKey key)
+ {
+ var descriptor = key.Descriptor as CngCbcAuthenticatedEncryptorDescriptor;
+ if (descriptor == null)
+ {
+ return null;
+ }
+
+ return CreateAuthenticatedEncryptorInstance(descriptor.MasterKey, descriptor.Configuration);
+ }
+
+ internal CbcAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(
+ ISecret secret,
+ CngCbcAuthenticatedEncryptorConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ return null;
+ }
+
+ return new CbcAuthenticatedEncryptor(
+ keyDerivationKey: new Secret(secret),
+ symmetricAlgorithmHandle: GetSymmetricBlockCipherAlgorithmHandle(configuration),
+ symmetricAlgorithmKeySizeInBytes: (uint)(configuration.EncryptionAlgorithmKeySize / 8),
+ hmacAlgorithmHandle: GetHmacAlgorithmHandle(configuration));
+ }
+
+ private BCryptAlgorithmHandle GetHmacAlgorithmHandle(CngCbcAuthenticatedEncryptorConfiguration configuration)
+ {
+ // basic argument checking
+ if (String.IsNullOrEmpty(configuration.HashAlgorithm))
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(configuration.HashAlgorithm));
+ }
+
+ _logger.OpeningCNGAlgorithmFromProviderWithHMAC(configuration.HashAlgorithm, configuration.HashAlgorithmProvider);
+ BCryptAlgorithmHandle algorithmHandle = null;
+
+ // Special-case cached providers
+ if (configuration.HashAlgorithmProvider == null)
+ {
+ if (configuration.HashAlgorithm == Constants.BCRYPT_SHA1_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA1; }
+ else if (configuration.HashAlgorithm == Constants.BCRYPT_SHA256_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA256; }
+ else if (configuration.HashAlgorithm == Constants.BCRYPT_SHA512_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA512; }
+ }
+
+ // Look up the provider dynamically if we couldn't fetch a cached instance
+ if (algorithmHandle == null)
+ {
+ algorithmHandle = BCryptAlgorithmHandle.OpenAlgorithmHandle(configuration.HashAlgorithm, configuration.HashAlgorithmProvider, hmac: true);
+ }
+
+ // Make sure we're using a hash algorithm. We require a minimum 128-bit digest.
+ uint digestSize = algorithmHandle.GetHashDigestLength();
+ AlgorithmAssert.IsAllowableValidationAlgorithmDigestSize(checked(digestSize * 8));
+
+ // all good!
+ return algorithmHandle;
+ }
+
+ private BCryptAlgorithmHandle GetSymmetricBlockCipherAlgorithmHandle(CngCbcAuthenticatedEncryptorConfiguration configuration)
+ {
+ // basic argument checking
+ if (String.IsNullOrEmpty(configuration.EncryptionAlgorithm))
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(EncryptionAlgorithm));
+ }
+ if (configuration.EncryptionAlgorithmKeySize < 0)
+ {
+ throw Error.Common_PropertyMustBeNonNegative(nameof(configuration.EncryptionAlgorithmKeySize));
+ }
+
+ _logger.OpeningCNGAlgorithmFromProviderWithChainingModeCBC(configuration.EncryptionAlgorithm, configuration.EncryptionAlgorithmProvider);
+
+ BCryptAlgorithmHandle algorithmHandle = null;
+
+ // Special-case cached providers
+ if (configuration.EncryptionAlgorithmProvider == null)
+ {
+ if (configuration.EncryptionAlgorithm == Constants.BCRYPT_AES_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.AES_CBC; }
+ }
+
+ // Look up the provider dynamically if we couldn't fetch a cached instance
+ if (algorithmHandle == null)
+ {
+ algorithmHandle = BCryptAlgorithmHandle.OpenAlgorithmHandle(configuration.EncryptionAlgorithm, configuration.EncryptionAlgorithmProvider);
+ algorithmHandle.SetChainingMode(Constants.BCRYPT_CHAIN_MODE_CBC);
+ }
+
+ // make sure we're using a block cipher with an appropriate key size & block size
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmBlockSize(checked(algorithmHandle.GetCipherBlockLength() * 8));
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked((uint)configuration.EncryptionAlgorithmKeySize));
+
+ // make sure the provided key length is valid
+ algorithmHandle.GetSupportedKeyLengths().EnsureValidKeyLength((uint)configuration.EncryptionAlgorithmKeySize);
+
+ // all good!
+ return algorithmHandle;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactory.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactory.cs
new file mode 100644
index 0000000000..39b6c0e55d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactory.cs
@@ -0,0 +1,92 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="GcmAuthenticatedEncryptor"/>.
+ /// </summary>
+ public sealed class CngGcmAuthenticatedEncryptorFactory : IAuthenticatedEncryptorFactory
+ {
+ private readonly ILogger _logger;
+
+ public CngGcmAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger<CngGcmAuthenticatedEncryptorFactory>();
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance(IKey key)
+ {
+ var descriptor = key.Descriptor as CngGcmAuthenticatedEncryptorDescriptor;
+ if (descriptor == null)
+ {
+ return null;
+ }
+
+ return CreateAuthenticatedEncryptorInstance(descriptor.MasterKey, descriptor.Configuration);
+ }
+
+ internal GcmAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(
+ ISecret secret,
+ CngGcmAuthenticatedEncryptorConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ return null;
+ }
+
+ return new GcmAuthenticatedEncryptor(
+ keyDerivationKey: new Secret(secret),
+ symmetricAlgorithmHandle: GetSymmetricBlockCipherAlgorithmHandle(configuration),
+ symmetricAlgorithmKeySizeInBytes: (uint)(configuration.EncryptionAlgorithmKeySize / 8));
+ }
+
+ private BCryptAlgorithmHandle GetSymmetricBlockCipherAlgorithmHandle(CngGcmAuthenticatedEncryptorConfiguration configuration)
+ {
+ // basic argument checking
+ if (String.IsNullOrEmpty(configuration.EncryptionAlgorithm))
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(EncryptionAlgorithm));
+ }
+ if (configuration.EncryptionAlgorithmKeySize < 0)
+ {
+ throw Error.Common_PropertyMustBeNonNegative(nameof(configuration.EncryptionAlgorithmKeySize));
+ }
+
+ BCryptAlgorithmHandle algorithmHandle = null;
+
+ _logger?.OpeningCNGAlgorithmFromProviderWithChainingModeGCM(configuration.EncryptionAlgorithm, configuration.EncryptionAlgorithmProvider);
+ // Special-case cached providers
+ if (configuration.EncryptionAlgorithmProvider == null)
+ {
+ if (configuration.EncryptionAlgorithm == Constants.BCRYPT_AES_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.AES_GCM; }
+ }
+
+ // Look up the provider dynamically if we couldn't fetch a cached instance
+ if (algorithmHandle == null)
+ {
+ algorithmHandle = BCryptAlgorithmHandle.OpenAlgorithmHandle(configuration.EncryptionAlgorithm, configuration.EncryptionAlgorithmProvider);
+ algorithmHandle.SetChainingMode(Constants.BCRYPT_CHAIN_MODE_GCM);
+ }
+
+ // make sure we're using a block cipher with an appropriate key size & block size
+ CryptoUtil.Assert(algorithmHandle.GetCipherBlockLength() == 128 / 8, "GCM requires a block cipher algorithm with a 128-bit block size.");
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked((uint)configuration.EncryptionAlgorithmKeySize));
+
+ // make sure the provided key length is valid
+ algorithmHandle.GetSupportedKeyLengths().EnsureValidKeyLength((uint)configuration.EncryptionAlgorithmKeySize);
+
+ // all good!
+ return algorithmHandle;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AlgorithmConfiguration.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AlgorithmConfiguration.cs
new file mode 100644
index 0000000000..4fddb0a706
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AlgorithmConfiguration.cs
@@ -0,0 +1,20 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public abstract class AlgorithmConfiguration
+ {
+ internal const int KDK_SIZE_IN_BYTES = 512 / 8;
+
+ /// <summary>
+ /// Creates a new <see cref="IAuthenticatedEncryptorDescriptor"/> instance based on this
+ /// configuration. The newly-created instance contains unique key material and is distinct
+ /// from all other descriptors created by the <see cref="CreateNewDescriptor"/> method.
+ /// </summary>
+ /// <returns>A unique <see cref="IAuthenticatedEncryptorDescriptor"/>.</returns>
+ public abstract IAuthenticatedEncryptorDescriptor CreateNewDescriptor();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..606d7484fb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,59 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// Represents a generalized authenticated encryption mechanism.
+ /// </summary>
+ public sealed class AuthenticatedEncryptorConfiguration : AlgorithmConfiguration, IInternalAlgorithmConfiguration
+ {
+ /// <summary>
+ /// The algorithm to use for symmetric encryption (confidentiality).
+ /// </summary>
+ /// <remarks>
+ /// The default value is <see cref="EncryptionAlgorithm.AES_256_CBC"/>.
+ /// </remarks>
+ public EncryptionAlgorithm EncryptionAlgorithm { get; set; } = EncryptionAlgorithm.AES_256_CBC;
+
+ /// <summary>
+ /// The algorithm to use for message authentication (tamper-proofing).
+ /// </summary>
+ /// <remarks>
+ /// The default value is <see cref="ValidationAlgorithm.HMACSHA256"/>.
+ /// This property is ignored if <see cref="EncryptionAlgorithm"/> specifies a 'GCM' algorithm.
+ /// </remarks>
+ public ValidationAlgorithm ValidationAlgorithm { get; set; } = ValidationAlgorithm.HMACSHA256;
+
+ public override IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ var internalConfiguration = (IInternalAlgorithmConfiguration)this;
+ return internalConfiguration.CreateDescriptorFromSecret(Secret.Random(KDK_SIZE_IN_BYTES));
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAlgorithmConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new AuthenticatedEncryptorDescriptor(this, secret);
+ }
+
+ void IInternalAlgorithmConfiguration.Validate()
+ {
+ var factory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ var encryptor = factory.CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8), this);
+ try
+ {
+ encryptor.PerformSelfTest();
+ }
+ finally
+ {
+ (encryptor as IDisposable)?.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..9539c9eb76
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,59 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an <see cref="AuthenticatedEncryptorConfiguration"/> object.
+ /// </summary>
+ public sealed class AuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public AuthenticatedEncryptorDescriptor(AuthenticatedEncryptorConfiguration configuration, ISecret masterKey)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ if (masterKey == null)
+ {
+ throw new ArgumentNullException(nameof(masterKey));
+ }
+
+ Configuration = configuration;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal AuthenticatedEncryptorConfiguration Configuration { get; }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ // <descriptor>
+ // <encryption algorithm="..." />
+ // <validation algorithm="..." /> <!-- only if not GCM -->
+ // <masterKey requiresEncryption="true">...</masterKey>
+ // </descriptor>
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", Configuration.EncryptionAlgorithm));
+
+ var validationElement = (AuthenticatedEncryptorFactory.IsGcmAlgorithm(Configuration.EncryptionAlgorithm))
+ ? (object)new XComment(" AES-GCM includes a 128-bit authentication tag, no extra validation algorithm required. ")
+ : (object)new XElement("validation",
+ new XAttribute("algorithm", Configuration.ValidationAlgorithm));
+
+ var outerElement = new XElement("descriptor",
+ encryptionElement,
+ validationElement,
+ MasterKey.ToMasterKeyElement());
+
+ return new XmlSerializedDescriptorInfo(outerElement, typeof(AuthenticatedEncryptorDescriptorDeserializer));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..96737b75c3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,48 @@
+// 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.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A class that can deserialize an <see cref="XElement"/> that represents the serialized version
+ /// of an <see cref="AuthenticatedEncryptorDescriptor"/>.
+ /// </summary>
+ public sealed class AuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ /// <summary>
+ /// Imports the <see cref="AuthenticatedEncryptorDescriptor"/> from serialized XML.
+ /// </summary>
+ public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ // <descriptor>
+ // <encryption algorithm="..." />
+ // <validation algorithm="..." /> <!-- only if not GCM -->
+ // <masterKey requiresEncryption="true">...</masterKey>
+ // </descriptor>
+
+ var configuration = new AuthenticatedEncryptorConfiguration();
+
+ var encryptionElement = element.Element("encryption");
+ configuration.EncryptionAlgorithm = (EncryptionAlgorithm)Enum.Parse(typeof(EncryptionAlgorithm), (string)encryptionElement.Attribute("algorithm"));
+
+ // only read <validation> if not GCM
+ if (!AuthenticatedEncryptorFactory.IsGcmAlgorithm(configuration.EncryptionAlgorithm))
+ {
+ var validationElement = element.Element("validation");
+ configuration.ValidationAlgorithm = (ValidationAlgorithm)Enum.Parse(typeof(ValidationAlgorithm), (string)validationElement.Attribute("algorithm"));
+ }
+
+ Secret masterKey = ((string)element.Elements("masterKey").Single()).ToSecret();
+ return new AuthenticatedEncryptorDescriptor(configuration, masterKey);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..1c23957db2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,100 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// Represents a configured authenticated encryption mechanism which uses
+ /// Windows CNG algorithms in CBC encryption + HMAC authentication modes.
+ /// </summary>
+ public sealed class CngCbcAuthenticatedEncryptorConfiguration : AlgorithmConfiguration, IInternalAlgorithmConfiguration
+ {
+ /// <summary>
+ /// The name of the algorithm to use for symmetric encryption.
+ /// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The algorithm must support CBC-style encryption and must have a block size of 64 bits
+ /// or greater.
+ /// The default value is 'AES'.
+ /// </remarks>
+ [ApplyPolicy]
+ public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
+
+ /// <summary>
+ /// The name of the provider which contains the implementation of the symmetric encryption algorithm.
+ /// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider.
+ /// This property is optional.
+ /// </summary>
+ /// <remarks>
+ /// The default value is null.
+ /// </remarks>
+ [ApplyPolicy]
+ public string EncryptionAlgorithmProvider { get; set; } = null;
+
+ /// <summary>
+ /// The length (in bits) of the key that will be used for symmetric encryption.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The key length must be 128 bits or greater.
+ /// The default value is 256.
+ /// </remarks>
+ [ApplyPolicy]
+ public int EncryptionAlgorithmKeySize { get; set; } = 256;
+
+ /// <summary>
+ /// The name of the algorithm to use for hashing data.
+ /// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The algorithm must support being opened in HMAC mode and must have a digest length
+ /// of 128 bits or greater.
+ /// The default value is 'SHA256'.
+ /// </remarks>
+ [ApplyPolicy]
+ public string HashAlgorithm { get; set; } = Constants.BCRYPT_SHA256_ALGORITHM;
+
+ /// <summary>
+ /// The name of the provider which contains the implementation of the hash algorithm.
+ /// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider.
+ /// This property is optional.
+ /// </summary>
+ /// <remarks>
+ /// The default value is null.
+ /// </remarks>
+ [ApplyPolicy]
+ public string HashAlgorithmProvider { get; set; } = null;
+
+ public override IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ var internalConfiguration = (IInternalAlgorithmConfiguration)this;
+ return internalConfiguration.CreateDescriptorFromSecret(Secret.Random(KDK_SIZE_IN_BYTES));
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAlgorithmConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new CngCbcAuthenticatedEncryptorDescriptor(this, secret);
+ }
+
+ /// <summary>
+ /// Validates that this <see cref="CngCbcAuthenticatedEncryptorConfiguration"/> is well-formed, i.e.,
+ /// that the specified algorithms actually exist and that they can be instantiated properly.
+ /// An exception will be thrown if validation fails.
+ /// </summary>
+ void IInternalAlgorithmConfiguration.Validate()
+ {
+ var factory = new CngCbcAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ using (var encryptor = factory.CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8), this))
+ {
+ encryptor.PerformSelfTest();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..0003f948ae
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,68 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an <see cref="CngCbcAuthenticatedEncryptorConfiguration"/> object.
+ /// </summary>
+ public sealed class CngCbcAuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public CngCbcAuthenticatedEncryptorDescriptor(CngCbcAuthenticatedEncryptorConfiguration configuration, ISecret masterKey)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ if (masterKey == null)
+ {
+ throw new ArgumentNullException(nameof(masterKey));
+ }
+
+ Configuration = configuration;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal CngCbcAuthenticatedEncryptorConfiguration Configuration { get; }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ // <descriptor>
+ // <!-- Windows CNG-CBC -->
+ // <encryption algorithm="..." keyLength="..." [provider="..."] />
+ // <hash algorithm="..." [provider="..."] />
+ // <masterKey>...</masterKey>
+ // </descriptor>
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", Configuration.EncryptionAlgorithm),
+ new XAttribute("keyLength", Configuration.EncryptionAlgorithmKeySize));
+ if (Configuration.EncryptionAlgorithmProvider != null)
+ {
+ encryptionElement.SetAttributeValue("provider", Configuration.EncryptionAlgorithmProvider);
+ }
+
+ var hashElement = new XElement("hash",
+ new XAttribute("algorithm", Configuration.HashAlgorithm));
+ if (Configuration.HashAlgorithmProvider != null)
+ {
+ hashElement.SetAttributeValue("provider", Configuration.HashAlgorithmProvider);
+ }
+
+ var rootElement = new XElement("descriptor",
+ new XComment(" Algorithms provided by Windows CNG, using CBC-mode encryption with HMAC validation "),
+ encryptionElement,
+ hashElement,
+ MasterKey.ToMasterKeyElement());
+
+ return new XmlSerializedDescriptorInfo(rootElement, typeof(CngCbcAuthenticatedEncryptorDescriptorDeserializer));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..534604839a
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,48 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A class that can deserialize an <see cref="XElement"/> that represents the serialized version
+ /// of an <see cref="CngCbcAuthenticatedEncryptorDescriptor"/>.
+ /// </summary>
+ public sealed class CngCbcAuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ /// <summary>
+ /// Imports the <see cref="CngCbcAuthenticatedEncryptorDescriptor"/> from serialized XML.
+ /// </summary>
+ public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ // <descriptor>
+ // <!-- Windows CNG-CBC -->
+ // <encryption algorithm="..." keyLength="..." [provider="..."] />
+ // <hash algorithm="..." [provider="..."] />
+ // <masterKey>...</masterKey>
+ // </descriptor>
+
+ var configuration = new CngCbcAuthenticatedEncryptorConfiguration();
+
+ var encryptionElement = element.Element("encryption");
+ configuration.EncryptionAlgorithm = (string)encryptionElement.Attribute("algorithm");
+ configuration.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
+ configuration.EncryptionAlgorithmProvider = (string)encryptionElement.Attribute("provider"); // could be null
+
+ var hashElement = element.Element("hash");
+ configuration.HashAlgorithm = (string)hashElement.Attribute("algorithm");
+ configuration.HashAlgorithmProvider = (string)hashElement.Attribute("provider"); // could be null
+
+ Secret masterKey = ((string)element.Element("masterKey")).ToSecret();
+
+ return new CngCbcAuthenticatedEncryptorDescriptor(configuration, masterKey);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..d9c1f84718
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,76 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// Represents a configured authenticated encryption mechanism which uses
+ /// Windows CNG algorithms in GCM encryption + authentication modes.
+ /// </summary>
+ public sealed class CngGcmAuthenticatedEncryptorConfiguration : AlgorithmConfiguration, IInternalAlgorithmConfiguration
+ {
+ /// <summary>
+ /// The name of the algorithm to use for symmetric encryption.
+ /// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The algorithm must support GCM-style encryption and must have a block size exactly
+ /// 128 bits.
+ /// The default value is 'AES'.
+ /// </remarks>
+ [ApplyPolicy]
+ public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
+
+ /// <summary>
+ /// The name of the provider which contains the implementation of the symmetric encryption algorithm.
+ /// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider.
+ /// This property is optional.
+ /// </summary>
+ /// <remarks>
+ /// The default value is null.
+ /// </remarks>
+ [ApplyPolicy]
+ public string EncryptionAlgorithmProvider { get; set; } = null;
+
+ /// <summary>
+ /// The length (in bits) of the key that will be used for symmetric encryption.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The key length must be 128 bits or greater.
+ /// The default value is 256.
+ /// </remarks>
+ [ApplyPolicy]
+ public int EncryptionAlgorithmKeySize { get; set; } = 256;
+
+ public override IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ var internalConfiguration = (IInternalAlgorithmConfiguration)this;
+ return internalConfiguration.CreateDescriptorFromSecret(Secret.Random(KDK_SIZE_IN_BYTES));
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAlgorithmConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new CngGcmAuthenticatedEncryptorDescriptor(this, secret);
+ }
+
+ /// <summary>
+ /// Validates that this <see cref="CngGcmAuthenticatedEncryptorConfiguration"/> is well-formed, i.e.,
+ /// that the specified algorithm actually exists and can be instantiated properly.
+ /// An exception will be thrown if validation fails.
+ /// </summary>
+ void IInternalAlgorithmConfiguration.Validate()
+ {
+ var factory = new CngGcmAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ using (var encryptor = factory.CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8), this))
+ {
+ encryptor.PerformSelfTest();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..28c0103a95
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,60 @@
+// 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.Xml.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an <see cref="CngGcmAuthenticatedEncryptorConfiguration"/> object.
+ /// </summary>
+ public sealed class CngGcmAuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public CngGcmAuthenticatedEncryptorDescriptor(CngGcmAuthenticatedEncryptorConfiguration configuration, ISecret masterKey)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ if (masterKey == null)
+ {
+ throw new ArgumentNullException(nameof(masterKey));
+ }
+
+ Configuration = configuration;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal CngGcmAuthenticatedEncryptorConfiguration Configuration { get; }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ // <descriptor>
+ // <!-- Windows CNG-GCM -->
+ // <encryption algorithm="..." keyLength="..." [provider="..."] />
+ // <masterKey>...</masterKey>
+ // </descriptor>
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", Configuration.EncryptionAlgorithm),
+ new XAttribute("keyLength", Configuration.EncryptionAlgorithmKeySize));
+ if (Configuration.EncryptionAlgorithmProvider != null)
+ {
+ encryptionElement.SetAttributeValue("provider", Configuration.EncryptionAlgorithmProvider);
+ }
+
+ var rootElement = new XElement("descriptor",
+ new XComment(" Algorithms provided by Windows CNG, using Galois/Counter Mode encryption and validation "),
+ encryptionElement,
+ MasterKey.ToMasterKeyElement());
+
+ return new XmlSerializedDescriptorInfo(rootElement, typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..0981fb55af
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,44 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A class that can deserialize an <see cref="XElement"/> that represents the serialized version
+ /// of an <see cref="CngGcmAuthenticatedEncryptorDescriptor"/>.
+ /// </summary>
+ public sealed class CngGcmAuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+
+ /// <summary>
+ /// Imports the <see cref="CngCbcAuthenticatedEncryptorDescriptor"/> from serialized XML.
+ /// </summary>
+ public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ // <descriptor>
+ // <!-- Windows CNG-GCM -->
+ // <encryption algorithm="..." keyLength="..." [provider="..."] />
+ // <masterKey>...</masterKey>
+ // </descriptor>
+
+ var configuration = new CngGcmAuthenticatedEncryptorConfiguration();
+
+ var encryptionElement = element.Element("encryption");
+ configuration.EncryptionAlgorithm = (string)encryptionElement.Attribute("algorithm");
+ configuration.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
+ configuration.EncryptionAlgorithmProvider = (string)encryptionElement.Attribute("provider"); // could be null
+
+ Secret masterKey = ((string)element.Element("masterKey")).ToSecret();
+
+ return new CngGcmAuthenticatedEncryptorDescriptor(configuration, masterKey);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..6176929583
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,30 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A self-contained descriptor that wraps all information (including secret key
+ /// material) necessary to create an instance of an <see cref="IAuthenticatedEncryptor"/>.
+ /// </summary>
+ public interface IAuthenticatedEncryptorDescriptor
+ {
+ /// <summary>
+ /// Exports the current descriptor to XML.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="XmlSerializedDescriptorInfo"/> wrapping the <see cref="XElement"/> which represents the serialized
+ /// current descriptor object. The deserializer type must be assignable to <see cref="IAuthenticatedEncryptorDescriptorDeserializer"/>.
+ /// </returns>
+ /// <remarks>
+ /// If an element contains sensitive information (such as key material), the
+ /// element should be marked via the <see cref="XmlExtensions.MarkAsRequiresEncryption(XElement)" />
+ /// extension method, and the caller should encrypt the element before persisting
+ /// the XML to storage.
+ /// </remarks>
+ XmlSerializedDescriptorInfo ExportToXml();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..c1db3bcc91
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,20 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// The basic interface for deserializing an XML element into an <see cref="IAuthenticatedEncryptorDescriptor"/>.
+ /// </summary>
+ public interface IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ /// <summary>
+ /// Deserializes the specified XML element.
+ /// </summary>
+ /// <param name="element">The element to deserialize.</param>
+ /// <returns>The <see cref="IAuthenticatedEncryptorDescriptor"/> represented by <paramref name="element"/>.</returns>
+ IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAlgorithmConfiguration.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAlgorithmConfiguration.cs
new file mode 100644
index 0000000000..ede736e99d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAlgorithmConfiguration.cs
@@ -0,0 +1,27 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A type that knows how to create instances of an <see cref="IAuthenticatedEncryptorDescriptor"/>
+ /// given specific secret key material.
+ /// </summary>
+ /// <remarks>
+ /// This type is not public because we don't want to lock ourselves into a contract stating
+ /// that a descriptor is simply a configuration plus a single serializable, reproducible secret.
+ /// </remarks>
+ internal interface IInternalAlgorithmConfiguration
+ {
+ /// <summary>
+ /// Creates a new <see cref="IAuthenticatedEncryptorDescriptor"/> instance from this configuration
+ /// given specific secret key material.
+ /// </summary>
+ IAuthenticatedEncryptorDescriptor CreateDescriptorFromSecret(ISecret secret);
+
+ /// <summary>
+ /// Performs a self-test of the algorithm specified by the configuration object.
+ /// </summary>
+ void Validate();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..dad6cd9dbc
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,108 @@
+// 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.Security.Cryptography;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// Represents a configured authenticated encryption mechanism which uses
+ /// managed <see cref="System.Security.Cryptography.SymmetricAlgorithm"/> and
+ /// <see cref="System.Security.Cryptography.KeyedHashAlgorithm"/> types.
+ /// </summary>
+ public sealed class ManagedAuthenticatedEncryptorConfiguration : AlgorithmConfiguration, IInternalAlgorithmConfiguration
+ {
+ /// <summary>
+ /// The type of the algorithm to use for symmetric encryption.
+ /// The type must subclass <see cref="SymmetricAlgorithm"/>.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The algorithm must support CBC-style encryption and PKCS#7 padding and must have a block size of 64 bits or greater.
+ /// The default algorithm is AES.
+ /// </remarks>
+ [ApplyPolicy]
+ public Type EncryptionAlgorithmType { get; set; } = typeof(Aes);
+
+ /// <summary>
+ /// The length (in bits) of the key that will be used for symmetric encryption.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The key length must be 128 bits or greater.
+ /// The default value is 256.
+ /// </remarks>
+ [ApplyPolicy]
+ public int EncryptionAlgorithmKeySize { get; set; } = 256;
+
+ /// <summary>
+ /// The type of the algorithm to use for validation.
+ /// Type type must subclass <see cref="KeyedHashAlgorithm"/>.
+ /// This property is required to have a value.
+ /// </summary>
+ /// <remarks>
+ /// The algorithm must have a digest length of 128 bits or greater.
+ /// The default algorithm is HMACSHA256.
+ /// </remarks>
+ [ApplyPolicy]
+ public Type ValidationAlgorithmType { get; set; } = typeof(HMACSHA256);
+
+ public override IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ var internalConfiguration = (IInternalAlgorithmConfiguration)this;
+ return internalConfiguration.CreateDescriptorFromSecret(Secret.Random(KDK_SIZE_IN_BYTES));
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAlgorithmConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new ManagedAuthenticatedEncryptorDescriptor(this, secret);
+ }
+
+ /// <summary>
+ /// Validates that this <see cref="ManagedAuthenticatedEncryptorConfiguration"/> is well-formed, i.e.,
+ /// that the specified algorithms actually exist and can be instantiated properly.
+ /// An exception will be thrown if validation fails.
+ /// </summary>
+ void IInternalAlgorithmConfiguration.Validate()
+ {
+ var factory = new ManagedAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ using (var encryptor = factory.CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8), this))
+ {
+ encryptor.PerformSelfTest();
+ }
+ }
+
+ // Any changes to this method should also be be reflected
+ // in ManagedAuthenticatedEncryptorDescriptorDeserializer.FriendlyNameToType.
+ private static string TypeToFriendlyName(Type type)
+ {
+ if (type == typeof(Aes))
+ {
+ return nameof(Aes);
+ }
+ else if (type == typeof(HMACSHA1))
+ {
+ return nameof(HMACSHA1);
+ }
+ else if (type == typeof(HMACSHA256))
+ {
+ return nameof(HMACSHA256);
+ }
+ else if (type == typeof(HMACSHA384))
+ {
+ return nameof(HMACSHA384);
+ }
+ else if (type == typeof(HMACSHA512))
+ {
+ return nameof(HMACSHA512);
+ }
+ else
+ {
+ return type.AssemblyQualifiedName;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..2061115b42
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,91 @@
+// 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.Security.Cryptography;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an <see cref="ManagedAuthenticatedEncryptorConfiguration"/> object.
+ /// </summary>
+ public sealed class ManagedAuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public ManagedAuthenticatedEncryptorDescriptor(ManagedAuthenticatedEncryptorConfiguration configuration, ISecret masterKey)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ if (masterKey == null)
+ {
+ throw new ArgumentNullException(nameof(masterKey));
+ }
+
+ Configuration = configuration;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal ManagedAuthenticatedEncryptorConfiguration Configuration { get; }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ // <descriptor>
+ // <!-- managed implementations -->
+ // <encryption algorithm="..." keyLength="..." />
+ // <validation algorithm="..." />
+ // <masterKey>...</masterKey>
+ // </descriptor>
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", TypeToFriendlyName(Configuration.EncryptionAlgorithmType)),
+ new XAttribute("keyLength", Configuration.EncryptionAlgorithmKeySize));
+
+ var validationElement = new XElement("validation",
+ new XAttribute("algorithm", TypeToFriendlyName(Configuration.ValidationAlgorithmType)));
+
+ var rootElement = new XElement("descriptor",
+ new XComment(" Algorithms provided by specified SymmetricAlgorithm and KeyedHashAlgorithm "),
+ encryptionElement,
+ validationElement,
+ MasterKey.ToMasterKeyElement());
+
+ return new XmlSerializedDescriptorInfo(rootElement, typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer));
+ }
+
+ // Any changes to this method should also be be reflected
+ // in ManagedAuthenticatedEncryptorDescriptorDeserializer.FriendlyNameToType.
+ private static string TypeToFriendlyName(Type type)
+ {
+ if (type == typeof(Aes))
+ {
+ return nameof(Aes);
+ }
+ else if (type == typeof(HMACSHA1))
+ {
+ return nameof(HMACSHA1);
+ }
+ else if (type == typeof(HMACSHA256))
+ {
+ return nameof(HMACSHA256);
+ }
+ else if (type == typeof(HMACSHA384))
+ {
+ return nameof(HMACSHA384);
+ }
+ else if (type == typeof(HMACSHA512))
+ {
+ return nameof(HMACSHA512);
+ }
+ else
+ {
+ return type.AssemblyQualifiedName;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..0f09c3a52e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,77 @@
+// 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.Security.Cryptography;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// A class that can deserialize an <see cref="XElement"/> that represents the serialized version
+ /// of an <see cref="ManagedAuthenticatedEncryptorDescriptor"/>.
+ /// </summary>
+ public sealed class ManagedAuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ /// <summary>
+ /// Imports the <see cref="ManagedAuthenticatedEncryptorDescriptor"/> from serialized XML.
+ /// </summary>
+ public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ // <descriptor>
+ // <!-- managed implementations -->
+ // <encryption algorithm="..." keyLength="..." />
+ // <validation algorithm="..." />
+ // <masterKey>...</masterKey>
+ // </descriptor>
+
+ var configuration = new ManagedAuthenticatedEncryptorConfiguration();
+
+ var encryptionElement = element.Element("encryption");
+ configuration.EncryptionAlgorithmType = FriendlyNameToType((string)encryptionElement.Attribute("algorithm"));
+ configuration.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
+
+ var validationElement = element.Element("validation");
+ configuration.ValidationAlgorithmType = FriendlyNameToType((string)validationElement.Attribute("algorithm"));
+
+ Secret masterKey = ((string)element.Element("masterKey")).ToSecret();
+
+ return new ManagedAuthenticatedEncryptorDescriptor(configuration, masterKey);
+ }
+
+ // Any changes to this method should also be be reflected
+ // in ManagedAuthenticatedEncryptorDescriptor.TypeToFriendlyName.
+ private static Type FriendlyNameToType(string typeName)
+ {
+ if (typeName == nameof(Aes))
+ {
+ return typeof(Aes);
+ }
+ else if (typeName == nameof(HMACSHA1))
+ {
+ return typeof(HMACSHA1);
+ }
+ else if (typeName == nameof(HMACSHA256))
+ {
+ return typeof(HMACSHA256);
+ }
+ else if (typeName == nameof(HMACSHA384))
+ {
+ return typeof(HMACSHA384);
+ }
+ else if (typeName == nameof(HMACSHA512))
+ {
+ return typeof(HMACSHA512);
+ }
+ else
+ {
+ return Type.GetType(typeName, throwOnError: true);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs
new file mode 100644
index 0000000000..75444140c8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs
@@ -0,0 +1,63 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ internal unsafe static class SecretExtensions
+ {
+ /// <summary>
+ /// Converts an <see cref="ISecret"/> to an &lt;masterKey&gt; element which is marked
+ /// as requiring encryption.
+ /// </summary>
+ /// <returns></returns>
+ public static XElement ToMasterKeyElement(this ISecret secret)
+ {
+ // Technically we'll be keeping the unprotected secret around in memory as
+ // a string, so it can get moved by the GC, but we should be good citizens
+ // and try to pin / clear our our temporary buffers regardless.
+ byte[] unprotectedSecretRawBytes = new byte[secret.Length];
+ string unprotectedSecretAsBase64String;
+ fixed (byte* __unused__ = unprotectedSecretRawBytes)
+ {
+ try
+ {
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(unprotectedSecretRawBytes));
+ unprotectedSecretAsBase64String = Convert.ToBase64String(unprotectedSecretRawBytes);
+ }
+ finally
+ {
+ Array.Clear(unprotectedSecretRawBytes, 0, unprotectedSecretRawBytes.Length);
+ }
+ }
+
+ var masterKeyElement = new XElement("masterKey",
+ new XComment(" Warning: the key below is in an unencrypted form. "),
+ new XElement("value", unprotectedSecretAsBase64String));
+ masterKeyElement.MarkAsRequiresEncryption();
+ return masterKeyElement;
+ }
+
+ /// <summary>
+ /// Converts a base64-encoded string into an <see cref="ISecret"/>.
+ /// </summary>
+ /// <returns></returns>
+ public static Secret ToSecret(this string base64String)
+ {
+ byte[] unprotectedSecret = Convert.FromBase64String(base64String);
+ fixed (byte* __unused__ = unprotectedSecret)
+ {
+ try
+ {
+ return new Secret(unprotectedSecret);
+ }
+ finally
+ {
+ Array.Clear(unprotectedSecret, 0, unprotectedSecret.Length);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs
new file mode 100644
index 0000000000..572a0cba59
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs
@@ -0,0 +1,30 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public static class XmlExtensions
+ {
+ internal static bool IsMarkedAsRequiringEncryption(this XElement element)
+ {
+ return ((bool?)element.Attribute(XmlConstants.RequiresEncryptionAttributeName)).GetValueOrDefault();
+ }
+
+ /// <summary>
+ /// Marks the provided <see cref="XElement"/> as requiring encryption before being persisted
+ /// to storage. Use when implementing <see cref="IAuthenticatedEncryptorDescriptor.ExportToXml"/>.
+ /// </summary>
+ public static void MarkAsRequiresEncryption(this XElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ element.SetAttributeValue(XmlConstants.RequiresEncryptionAttributeName, true);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs
new file mode 100644
index 0000000000..1b935d8e15
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs
@@ -0,0 +1,57 @@
+// 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.Reflection;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ /// <summary>
+ /// Wraps an <see cref="XElement"/> that contains the XML-serialized representation of an
+ /// <see cref="IAuthenticatedEncryptorDescriptor"/> along with the type that can be used
+ /// to deserialize it.
+ /// </summary>
+ public sealed class XmlSerializedDescriptorInfo
+ {
+ /// <summary>
+ /// Creates an instance of an <see cref="XmlSerializedDescriptorInfo"/>.
+ /// </summary>
+ /// <param name="serializedDescriptorElement">The XML-serialized form of the <see cref="IAuthenticatedEncryptorDescriptor"/>.</param>
+ /// <param name="deserializerType">The class whose <see cref="IAuthenticatedEncryptorDescriptorDeserializer.ImportFromXml(XElement)"/>
+ /// method can be used to deserialize <paramref name="serializedDescriptorElement"/>.</param>
+ public XmlSerializedDescriptorInfo(XElement serializedDescriptorElement, Type deserializerType)
+ {
+ if (serializedDescriptorElement == null)
+ {
+ throw new ArgumentNullException(nameof(serializedDescriptorElement));
+ }
+
+ if (deserializerType == null)
+ {
+ throw new ArgumentNullException(nameof(deserializerType));
+ }
+
+ if (!typeof(IAuthenticatedEncryptorDescriptorDeserializer).IsAssignableFrom(deserializerType))
+ {
+ throw new ArgumentException(
+ Resources.FormatTypeExtensions_BadCast(deserializerType.FullName, typeof(IAuthenticatedEncryptorDescriptorDeserializer).FullName),
+ nameof(deserializerType));
+ }
+
+ SerializedDescriptorElement = serializedDescriptorElement;
+ DeserializerType = deserializerType;
+ }
+
+ /// <summary>
+ /// The class whose <see cref="IAuthenticatedEncryptorDescriptorDeserializer.ImportFromXml(XElement)"/>
+ /// method can be used to deserialize the value stored in <see cref="SerializedDescriptorElement"/>.
+ /// </summary>
+ public Type DeserializerType { get; }
+
+ /// <summary>
+ /// An XML-serialized representation of an <see cref="IAuthenticatedEncryptorDescriptor"/>.
+ /// </summary>
+ public XElement SerializedDescriptorElement { get; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs
new file mode 100644
index 0000000000..d6fbf28020
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs
@@ -0,0 +1,52 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// Specifies a symmetric encryption algorithm to use for providing confidentiality
+ /// to protected payloads.
+ /// </summary>
+ public enum EncryptionAlgorithm
+ {
+ /// <summary>
+ /// The AES algorithm (FIPS 197) with a 128-bit key running in Cipher Block Chaining mode.
+ /// </summary>
+ AES_128_CBC,
+
+ /// <summary>
+ /// The AES algorithm (FIPS 197) with a 192-bit key running in Cipher Block Chaining mode.
+ /// </summary>
+ AES_192_CBC,
+
+ /// <summary>
+ /// The AES algorithm (FIPS 197) with a 256-bit key running in Cipher Block Chaining mode.
+ /// </summary>
+ AES_256_CBC,
+
+ /// <summary>
+ /// The AES algorithm (FIPS 197) with a 128-bit key running in Galois/Counter Mode (FIPS SP 800-38D).
+ /// </summary>
+ /// <remarks>
+ /// This cipher mode produces a 128-bit authentication tag. This algorithm is currently only
+ /// supported on Windows.
+ /// </remarks>
+ AES_128_GCM,
+
+ /// <summary>
+ /// The AES algorithm (FIPS 197) with a 192-bit key running in Galois/Counter Mode (FIPS SP 800-38D).
+ /// </summary>
+ /// <remarks>
+ /// This cipher mode produces a 128-bit authentication tag.
+ /// </remarks>
+ AES_192_GCM,
+
+ /// <summary>
+ /// The AES algorithm (FIPS 197) with a 256-bit key running in Galois/Counter Mode (FIPS SP 800-38D).
+ /// </summary>
+ /// <remarks>
+ /// This cipher mode produces a 128-bit authentication tag.
+ /// </remarks>
+ AES_256_GCM,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.cs
new file mode 100644
index 0000000000..5ec2fa8444
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.cs
@@ -0,0 +1,36 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// The basic interface for providing an authenticated encryption and decryption routine.
+ /// </summary>
+ public interface IAuthenticatedEncryptor
+ {
+ /// <summary>
+ /// Validates the authentication tag of and decrypts a blob of encrypted data.
+ /// </summary>
+ /// <param name="ciphertext">The ciphertext (including authentication tag) to decrypt.</param>
+ /// <param name="additionalAuthenticatedData">Any ancillary data which was used during computation
+ /// of the authentication tag. The same AAD must have been specified in the corresponding
+ /// call to 'Encrypt'.</param>
+ /// <returns>The original plaintext data (if the authentication tag was validated and decryption succeeded).</returns>
+ /// <remarks>All cryptography-related exceptions should be homogenized to CryptographicException.</remarks>
+ byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData);
+
+ /// <summary>
+ /// Encrypts and tamper-proofs a piece of data.
+ /// </summary>
+ /// <param name="plaintext">The plaintext to encrypt. This input may be zero bytes in length.</param>
+ /// <param name="additionalAuthenticatedData">A piece of data which will not be included in
+ /// the returned ciphertext but which will still be covered by the authentication tag.
+ /// This input may be zero bytes in length. The same AAD must be specified in the corresponding
+ /// call to Decrypt.</param>
+ /// <returns>The ciphertext blob, including authentication tag.</returns>
+ /// <remarks>All cryptography-related exceptions should be homogenized to CryptographicException.</remarks>
+ byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorFactory.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorFactory.cs
new file mode 100644
index 0000000000..b66f14422c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorFactory.cs
@@ -0,0 +1,22 @@
+// 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 Microsoft.AspNetCore.DataProtection.KeyManagement;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ public interface IAuthenticatedEncryptorFactory
+ {
+ /// <summary>
+ /// Creates an <see cref="IAuthenticatedEncryptor"/> instance based on the given <see cref="IKey.Descriptor"/>.
+ /// </summary>
+ /// <returns>An <see cref="IAuthenticatedEncryptor"/> instance.</returns>
+ /// <remarks>
+ /// For a given <see cref="IKey.Descriptor"/>, any two instances returned by this method should
+ /// be considered equivalent, e.g., the payload returned by one's <see cref="IAuthenticatedEncryptor.Encrypt(ArraySegment{byte}, ArraySegment{byte})"/>
+ /// method should be consumable by the other's <see cref="IAuthenticatedEncryptor.Decrypt(ArraySegment{byte}, ArraySegment{byte})"/> method.
+ /// </remarks>
+ IAuthenticatedEncryptor CreateEncryptorInstance(IKey key);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IOptimizedAuthenticatedEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IOptimizedAuthenticatedEncryptor.cs
new file mode 100644
index 0000000000..3cc0a7ca92
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/IOptimizedAuthenticatedEncryptor.cs
@@ -0,0 +1,42 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// An optimized encryptor that can avoid buffer allocations in common code paths.
+ /// </summary>
+ internal interface IOptimizedAuthenticatedEncryptor : IAuthenticatedEncryptor
+ {
+ /// <summary>
+ /// Encrypts and tamper-proofs a piece of data.
+ /// </summary>
+ /// <param name="plaintext">The plaintext to encrypt. This input may be zero bytes in length.</param>
+ /// <param name="additionalAuthenticatedData">A piece of data which will not be included in
+ /// the returned ciphertext but which will still be covered by the authentication tag.
+ /// This input may be zero bytes in length. The same AAD must be specified in the corresponding
+ /// call to Decrypt.</param>
+ /// <param name="preBufferSize">The number of bytes to pad before the ciphertext in the output.</param>
+ /// <param name="postBufferSize">The number of bytes to pad after the ciphertext in the output.</param>
+ /// <returns>
+ /// The ciphertext blob, including authentication tag. The ciphertext blob will be surrounded by
+ /// the number of padding bytes requested. For instance, if the given (plaintext, AAD) input results
+ /// in a (ciphertext, auth tag) output of 0x0102030405, and if 'preBufferSize' is 3 and
+ /// 'postBufferSize' is 5, then the return value will be 0xYYYYYY0102030405ZZZZZZZZZZ, where bytes
+ /// YY and ZZ are undefined.
+ /// </returns>
+ /// <remarks>
+ /// This method allows for a slight performance improvement over IAuthenticatedEncryptor.Encrypt
+ /// in the case where the caller needs to prepend or append some data to the resulting ciphertext.
+ /// For instance, if the caller needs to append a 32-bit header to the resulting ciphertext, then
+ /// he can specify 4 for 'preBufferSize' and overwrite the first 32 bits of the buffer returned
+ /// by this function. This saves the caller from having to allocate a new buffer to hold the final
+ /// transformed result.
+ ///
+ /// All cryptography-related exceptions should be homogenized to CryptographicException.
+ /// </remarks>
+ byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactory.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactory.cs
new file mode 100644
index 0000000000..32fb4f44f4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactory.cs
@@ -0,0 +1,132 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Managed;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// An <see cref="IAuthenticatedEncryptorFactory"/> for <see cref="ManagedAuthenticatedEncryptor"/>.
+ /// </summary>
+ public sealed class ManagedAuthenticatedEncryptorFactory : IAuthenticatedEncryptorFactory
+ {
+ private readonly ILogger _logger;
+
+ public ManagedAuthenticatedEncryptorFactory(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger<ManagedAuthenticatedEncryptorFactory>();
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance(IKey key)
+ {
+ var descriptor = key.Descriptor as ManagedAuthenticatedEncryptorDescriptor;
+ if (descriptor == null)
+ {
+ return null;
+ }
+
+ return CreateAuthenticatedEncryptorInstance(descriptor.MasterKey, descriptor.Configuration);
+ }
+
+ internal ManagedAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(
+ ISecret secret,
+ ManagedAuthenticatedEncryptorConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ return null;
+ }
+
+ return new ManagedAuthenticatedEncryptor(
+ keyDerivationKey: new Secret(secret),
+ symmetricAlgorithmFactory: GetSymmetricBlockCipherAlgorithmFactory(configuration),
+ symmetricAlgorithmKeySizeInBytes: configuration.EncryptionAlgorithmKeySize / 8,
+ validationAlgorithmFactory: GetKeyedHashAlgorithmFactory(configuration));
+ }
+
+ private Func<KeyedHashAlgorithm> GetKeyedHashAlgorithmFactory(ManagedAuthenticatedEncryptorConfiguration configuration)
+ {
+ // basic argument checking
+ if (configuration.ValidationAlgorithmType == null)
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(configuration.ValidationAlgorithmType));
+ }
+
+ _logger.UsingManagedKeyedHashAlgorithm(configuration.ValidationAlgorithmType.FullName);
+ if (configuration.ValidationAlgorithmType == typeof(HMACSHA256))
+ {
+ return () => new HMACSHA256();
+ }
+ else if (configuration.ValidationAlgorithmType == typeof(HMACSHA512))
+ {
+ return () => new HMACSHA512();
+ }
+ else
+ {
+ return AlgorithmActivator.CreateFactory<KeyedHashAlgorithm>(configuration.ValidationAlgorithmType);
+ }
+ }
+
+ private Func<SymmetricAlgorithm> GetSymmetricBlockCipherAlgorithmFactory(ManagedAuthenticatedEncryptorConfiguration configuration)
+ {
+ // basic argument checking
+ if (configuration.EncryptionAlgorithmType == null)
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(configuration.EncryptionAlgorithmType));
+ }
+ typeof(SymmetricAlgorithm).AssertIsAssignableFrom(configuration.EncryptionAlgorithmType);
+ if (configuration.EncryptionAlgorithmKeySize < 0)
+ {
+ throw Error.Common_PropertyMustBeNonNegative(nameof(configuration.EncryptionAlgorithmKeySize));
+ }
+
+ _logger.UsingManagedSymmetricAlgorithm(configuration.EncryptionAlgorithmType.FullName);
+
+ if (configuration.EncryptionAlgorithmType == typeof(Aes))
+ {
+ Func<Aes> factory = null;
+ if (OSVersionUtil.IsWindows())
+ {
+ // If we're on desktop CLR and running on Windows, use the FIPS-compliant implementation.
+ factory = () => new AesCryptoServiceProvider();
+ }
+
+ return factory ?? Aes.Create;
+ }
+ else
+ {
+ return AlgorithmActivator.CreateFactory<SymmetricAlgorithm>(configuration.EncryptionAlgorithmType);
+ }
+ }
+
+ /// <summary>
+ /// Contains helper methods for generating cryptographic algorithm factories.
+ /// </summary>
+ private static class AlgorithmActivator
+ {
+ /// <summary>
+ /// Creates a factory that wraps a call to <see cref="Activator.CreateInstance{T}"/>.
+ /// </summary>
+ public static Func<T> CreateFactory<T>(Type implementation)
+ {
+ return ((IActivator<T>)Activator.CreateInstance(typeof(AlgorithmActivatorCore<>).MakeGenericType(implementation))).Creator;
+ }
+
+ private interface IActivator<out T>
+ {
+ Func<T> Creator { get; }
+ }
+
+ private class AlgorithmActivatorCore<T> : IActivator<T> where T : new()
+ {
+ public Func<T> Creator { get; } = Activator.CreateInstance<T>;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs
new file mode 100644
index 0000000000..520cb707a4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs
@@ -0,0 +1,24 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ /// <summary>
+ /// Specifies a message authentication algorithm to use for providing tamper-proofing
+ /// to protected payloads.
+ /// </summary>
+ public enum ValidationAlgorithm
+ {
+ /// <summary>
+ /// The HMAC algorithm (RFC 2104) using the SHA-256 hash function (FIPS 180-4).
+ /// </summary>
+ HMACSHA256,
+
+ /// <summary>
+ /// The HMAC algorithm (RFC 2104) using the SHA-512 hash function (FIPS 180-4).
+ /// </summary>
+ HMACSHA512,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/BitHelpers.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/BitHelpers.cs
new file mode 100644
index 0000000000..65e7415008
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/BitHelpers.cs
@@ -0,0 +1,59 @@
+// 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.Runtime.CompilerServices;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal unsafe static class BitHelpers
+ {
+ /// <summary>
+ /// Writes an unsigned 32-bit value to a memory address, big-endian.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteTo(void* ptr, uint value)
+ {
+ byte* bytePtr = (byte*)ptr;
+ bytePtr[0] = (byte)(value >> 24);
+ bytePtr[1] = (byte)(value >> 16);
+ bytePtr[2] = (byte)(value >> 8);
+ bytePtr[3] = (byte)(value);
+ }
+
+ /// <summary>
+ /// Writes an unsigned 32-bit value to a memory address, big-endian.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteTo(ref byte* ptr, uint value)
+ {
+ byte* pTemp = ptr;
+ pTemp[0] = (byte)(value >> 24);
+ pTemp[1] = (byte)(value >> 16);
+ pTemp[2] = (byte)(value >> 8);
+ pTemp[3] = (byte)(value);
+ ptr = &pTemp[4];
+ }
+
+ /// <summary>
+ /// Writes a signed 32-bit value to a memory address, big-endian.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteTo(byte[] buffer, ref int idx, int value)
+ {
+ WriteTo(buffer, ref idx, (uint)value);
+ }
+
+ /// <summary>
+ /// Writes a signed 32-bit value to a memory address, big-endian.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteTo(byte[] buffer, ref int idx, uint value)
+ {
+ buffer[idx++] = (byte)(value >> 24);
+ buffer[idx++] = (byte)(value >> 16);
+ buffer[idx++] = (byte)(value >> 8);
+ buffer[idx++] = (byte)(value);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/BCryptGenRandomImpl.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/BCryptGenRandomImpl.cs
new file mode 100644
index 0000000000..5bdceabb6d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/BCryptGenRandomImpl.cs
@@ -0,0 +1,22 @@
+// 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 Microsoft.AspNetCore.Cryptography.Cng;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ internal unsafe sealed class BCryptGenRandomImpl : IBCryptGenRandom
+ {
+ public static readonly BCryptGenRandomImpl Instance = new BCryptGenRandomImpl();
+
+ private BCryptGenRandomImpl()
+ {
+ }
+
+ public void GenRandom(byte* pbBuffer, uint cbBuffer)
+ {
+ BCryptUtil.GenRandom(pbBuffer, cbBuffer);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/CbcAuthenticatedEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/CbcAuthenticatedEncryptor.cs
new file mode 100644
index 0000000000..c8840beff4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/CbcAuthenticatedEncryptor.cs
@@ -0,0 +1,422 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.Cng.Internal;
+using Microsoft.AspNetCore.DataProtection.SP800_108;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ // An encryptor which does Encrypt(CBC) + HMAC using the Windows CNG (BCrypt*) APIs.
+ // The payloads produced by this encryptor should be compatible with the payloads
+ // produced by the managed Encrypt(CBC) + HMAC encryptor.
+ internal unsafe sealed class CbcAuthenticatedEncryptor : CngAuthenticatedEncryptorBase
+ {
+ // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single
+ // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block
+ // encryption operations, which a high-traffic web server might perform in mere hours.
+ // AES and other 128-bit block ciphers are less susceptible to this due to the larger IV
+ // space, but unfortunately some organizations require older 64-bit block ciphers. To address
+ // the collision issue, we'll feed 128 bits of entropy to the KDF when performing subkey
+ // generation. This creates >= 192 bits total entropy for each operation, so we shouldn't
+ // expect a collision until >= 2^96 operations. Even 2^80 operations still maintains a <= 2^-32
+ // probability of collision, and this is acceptable for the expected KDK lifetime.
+ private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
+
+ private readonly byte[] _contextHeader;
+ private readonly IBCryptGenRandom _genRandom;
+ private readonly BCryptAlgorithmHandle _hmacAlgorithmHandle;
+ private readonly uint _hmacAlgorithmDigestLengthInBytes;
+ private readonly uint _hmacAlgorithmSubkeyLengthInBytes;
+ private readonly ISP800_108_CTR_HMACSHA512Provider _sp800_108_ctr_hmac_provider;
+ private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle;
+ private readonly uint _symmetricAlgorithmBlockSizeInBytes;
+ private readonly uint _symmetricAlgorithmSubkeyLengthInBytes;
+
+ public CbcAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, BCryptAlgorithmHandle hmacAlgorithmHandle, IBCryptGenRandom genRandom = null)
+ {
+ _genRandom = genRandom ?? BCryptGenRandomImpl.Instance;
+ _sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey);
+ _symmetricAlgorithmHandle = symmetricAlgorithmHandle;
+ _symmetricAlgorithmBlockSizeInBytes = symmetricAlgorithmHandle.GetCipherBlockLength();
+ _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes;
+ _hmacAlgorithmHandle = hmacAlgorithmHandle;
+ _hmacAlgorithmDigestLengthInBytes = hmacAlgorithmHandle.GetHashDigestLength();
+ _hmacAlgorithmSubkeyLengthInBytes = _hmacAlgorithmDigestLengthInBytes; // for simplicity we'll generate HMAC subkeys with a length equal to the digest length
+
+ // Argument checking on the algorithms and lengths passed in to us
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmBlockSize(checked(_symmetricAlgorithmBlockSizeInBytes * 8));
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked(_symmetricAlgorithmSubkeyLengthInBytes * 8));
+ AlgorithmAssert.IsAllowableValidationAlgorithmDigestSize(checked(_hmacAlgorithmDigestLengthInBytes * 8));
+
+ _contextHeader = CreateContextHeader();
+ }
+
+ private byte[] CreateContextHeader()
+ {
+ var retVal = new byte[checked(
+ 1 /* KDF alg */
+ + 1 /* chaining mode */
+ + sizeof(uint) /* sym alg key size */
+ + sizeof(uint) /* sym alg block size */
+ + sizeof(uint) /* hmac alg key size */
+ + sizeof(uint) /* hmac alg digest size */
+ + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */
+ + _hmacAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)];
+
+ fixed (byte* pbRetVal = retVal)
+ {
+ byte* ptr = pbRetVal;
+
+ // First is the two-byte header
+ *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF
+ *(ptr++) = 0; // 0x00 = CBC encryption + HMAC authentication
+
+ // Next is information about the symmetric algorithm (key size followed by block size)
+ BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes);
+ BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmBlockSizeInBytes);
+
+ // Next is information about the HMAC algorithm (key size followed by digest size)
+ BitHelpers.WriteTo(ref ptr, _hmacAlgorithmSubkeyLengthInBytes);
+ BitHelpers.WriteTo(ref ptr, _hmacAlgorithmDigestLengthInBytes);
+
+ // See the design document for an explanation of the following code.
+ var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes];
+ fixed (byte* pbTempKeys = tempKeys)
+ {
+ byte dummy;
+
+ // Derive temporary keys for encryption + HMAC.
+ using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider())
+ {
+ provider.DeriveKey(
+ pbLabel: &dummy,
+ cbLabel: 0,
+ pbContext: &dummy,
+ cbContext: 0,
+ pbDerivedKey: pbTempKeys,
+ cbDerivedKey: (uint)tempKeys.Length);
+ }
+
+ // At this point, tempKeys := { K_E || K_H }.
+ byte* pbSymmetricEncryptionSubkey = pbTempKeys;
+ byte* pbHmacSubkey = &pbTempKeys[_symmetricAlgorithmSubkeyLengthInBytes];
+
+ // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer.
+ using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
+ {
+ fixed (byte* pbIV = new byte[_symmetricAlgorithmBlockSizeInBytes] /* will be zero-initialized */)
+ {
+ DoCbcEncrypt(
+ symmetricKeyHandle: symmetricKeyHandle,
+ pbIV: pbIV,
+ pbInput: &dummy,
+ cbInput: 0,
+ pbOutput: ptr,
+ cbOutput: _symmetricAlgorithmBlockSizeInBytes);
+ }
+ }
+ ptr += _symmetricAlgorithmBlockSizeInBytes;
+
+ // MAC a zero-length input string and copy the digest to the return buffer.
+ using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
+ {
+ hashHandle.HashData(
+ pbInput: &dummy,
+ cbInput: 0,
+ pbHashDigest: ptr,
+ cbHashDigest: _hmacAlgorithmDigestLengthInBytes);
+ }
+
+ ptr += _hmacAlgorithmDigestLengthInBytes;
+ CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length");
+ }
+ }
+
+ // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || hmacAlgKeySize || hmacAlgDigestSize || E("") || MAC("") }.
+ return retVal;
+ }
+
+ protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
+ {
+ // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC
+ if (cbCiphertext < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes))
+ {
+ throw Error.CryptCommon_PayloadInvalid();
+ }
+
+ // Assumption: pbCipherText := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
+
+ var cbEncryptedData = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes));
+
+ // Calculate offsets
+ byte* pbKeyModifier = pbCiphertext;
+ byte* pbIV = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
+ byte* pbEncryptedData = &pbIV[_symmetricAlgorithmBlockSizeInBytes];
+ byte* pbActualHmac = &pbEncryptedData[cbEncryptedData];
+
+ // Use the KDF to recreate the symmetric encryption and HMAC subkeys
+ // We'll need a temporary buffer to hold them
+ var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes);
+ byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)];
+ try
+ {
+ _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
+ pbLabel: pbAdditionalAuthenticatedData,
+ cbLabel: cbAdditionalAuthenticatedData,
+ contextHeader: _contextHeader,
+ pbContext: pbKeyModifier,
+ cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
+ pbDerivedKey: pbTempSubkeys,
+ cbDerivedKey: cbTempSubkeys);
+
+ // Calculate offsets
+ byte* pbSymmetricEncryptionSubkey = pbTempSubkeys;
+ byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes];
+
+ // First, perform an explicit integrity check over (iv | encryptedPayload) to ensure the
+ // data hasn't been tampered with. The integrity check is also implicitly performed over
+ // keyModifier since that value was provided to the KDF earlier.
+ using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
+ {
+ if (!ValidateHash(hashHandle, pbIV, _symmetricAlgorithmBlockSizeInBytes + cbEncryptedData, pbActualHmac))
+ {
+ throw Error.CryptCommon_PayloadInvalid();
+ }
+ }
+
+ // If the integrity check succeeded, decrypt the payload.
+ using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
+ {
+ return DoCbcDecrypt(decryptionSubkeyHandle, pbIV, pbEncryptedData, cbEncryptedData);
+ }
+ }
+ finally
+ {
+ // Buffer contains sensitive key material; delete.
+ UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
+ }
+ }
+
+ public override void Dispose()
+ {
+ _sp800_108_ctr_hmac_provider.Dispose();
+
+ // We don't want to dispose of the underlying algorithm instances because they
+ // might be reused.
+ }
+
+ // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size.
+ private byte[] DoCbcDecrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput)
+ {
+ // BCryptDecrypt mutates the provided IV; we need to clone it to prevent mutation of the original value
+ byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
+ UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes);
+
+ // First, figure out how large an output buffer we require.
+ // Ideally we'd be able to transform the last block ourselves and strip
+ // off the padding before creating the return value array, but we don't
+ // know the actual padding scheme being used under the covers (we can't
+ // assume PKCS#7). So unfortunately we're stuck with the temporary buffer.
+ // (Querying the output size won't mutate the IV.)
+ uint dwEstimatedDecryptedByteCount;
+ var ntstatus = UnsafeNativeMethods.BCryptDecrypt(
+ hKey: symmetricKeyHandle,
+ pbInput: pbInput,
+ cbInput: cbInput,
+ pPaddingInfo: null,
+ pbIV: pbClonedIV,
+ cbIV: _symmetricAlgorithmBlockSizeInBytes,
+ pbOutput: null,
+ cbOutput: 0,
+ pcbResult: out dwEstimatedDecryptedByteCount,
+ dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+
+ var decryptedPayload = new byte[dwEstimatedDecryptedByteCount];
+ uint dwActualDecryptedByteCount;
+ fixed (byte* pbDecryptedPayload = decryptedPayload)
+ {
+ byte dummy;
+
+ // Perform the actual decryption.
+ ntstatus = UnsafeNativeMethods.BCryptDecrypt(
+ hKey: symmetricKeyHandle,
+ pbInput: pbInput,
+ cbInput: cbInput,
+ pPaddingInfo: null,
+ pbIV: pbClonedIV,
+ cbIV: _symmetricAlgorithmBlockSizeInBytes,
+ pbOutput: (pbDecryptedPayload != null) ? pbDecryptedPayload : &dummy, // CLR won't pin zero-length arrays
+ cbOutput: dwEstimatedDecryptedByteCount,
+ pcbResult: out dwActualDecryptedByteCount,
+ dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ }
+
+ // Decryption finished!
+ CryptoUtil.Assert(dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount, "dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount");
+ if (dwActualDecryptedByteCount == dwEstimatedDecryptedByteCount)
+ {
+ // payload takes up the entire buffer
+ return decryptedPayload;
+ }
+ else
+ {
+ // payload takes up only a partial buffer
+ var resizedDecryptedPayload = new byte[dwActualDecryptedByteCount];
+ Buffer.BlockCopy(decryptedPayload, 0, resizedDecryptedPayload, 0, resizedDecryptedPayload.Length);
+ return resizedDecryptedPayload;
+ }
+ }
+
+ // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size.
+ private void DoCbcEncrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput, byte* pbOutput, uint cbOutput)
+ {
+ // BCryptEncrypt mutates the provided IV; we need to clone it to prevent mutation of the original value
+ byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
+ UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes);
+
+ uint dwEncryptedBytes;
+ var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
+ hKey: symmetricKeyHandle,
+ pbInput: pbInput,
+ cbInput: cbInput,
+ pPaddingInfo: null,
+ pbIV: pbClonedIV,
+ cbIV: _symmetricAlgorithmBlockSizeInBytes,
+ pbOutput: pbOutput,
+ cbOutput: cbOutput,
+ pcbResult: out dwEncryptedBytes,
+ dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+
+ // Need to make sure we didn't underrun the buffer - means caller passed a bad value
+ CryptoUtil.Assert(dwEncryptedBytes == cbOutput, "dwEncryptedBytes == cbOutput");
+ }
+
+ protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer)
+ {
+ // This buffer will be used to hold the symmetric encryption and HMAC subkeys
+ // used in the generation of this payload.
+ var cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes);
+ byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)];
+
+ try
+ {
+ // Randomly generate the key modifier and IV.
+ var cbKeyModifierAndIV = checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes);
+ byte* pbKeyModifierAndIV = stackalloc byte[checked((int)cbKeyModifierAndIV)];
+ _genRandom.GenRandom(pbKeyModifierAndIV, cbKeyModifierAndIV);
+
+ // Calculate offsets
+ byte* pbKeyModifier = pbKeyModifierAndIV;
+ byte* pbIV = &pbKeyModifierAndIV[KEY_MODIFIER_SIZE_IN_BYTES];
+
+ // Use the KDF to generate a new symmetric encryption and HMAC subkey
+ _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
+ pbLabel: pbAdditionalAuthenticatedData,
+ cbLabel: cbAdditionalAuthenticatedData,
+ contextHeader: _contextHeader,
+ pbContext: pbKeyModifier,
+ cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
+ pbDerivedKey: pbTempSubkeys,
+ cbDerivedKey: cbTempSubkeys);
+
+ // Calculate offsets
+ byte* pbSymmetricEncryptionSubkey = pbTempSubkeys;
+ byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes];
+
+ using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
+ {
+ // We can't assume PKCS#7 padding (maybe the underlying provider is really using CTS),
+ // so we need to query the padded output size before we can allocate the return value array.
+ var cbOutputCiphertext = GetCbcEncryptedOutputSizeWithPadding(symmetricKeyHandle, pbPlaintext, cbPlaintext);
+
+ // Allocate return value array and start copying some data
+ var retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext + _hmacAlgorithmDigestLengthInBytes + cbPostBuffer)];
+ fixed (byte* pbRetVal = retVal)
+ {
+ // Calculate offsets
+ byte* pbOutputKeyModifier = &pbRetVal[cbPreBuffer];
+ byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
+ byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes];
+ byte* pbOutputHmac = &pbOutputCiphertext[cbOutputCiphertext];
+
+ UnsafeBufferUtil.BlockCopy(from: pbKeyModifierAndIV, to: pbOutputKeyModifier, byteCount: cbKeyModifierAndIV);
+
+ // retVal will eventually contain { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer }
+ // At this point, retVal := { preBuffer | keyModifier | iv | _____ | _____ | postBuffer }
+
+ DoCbcEncrypt(
+ symmetricKeyHandle: symmetricKeyHandle,
+ pbIV: pbIV,
+ pbInput: pbPlaintext,
+ cbInput: cbPlaintext,
+ pbOutput: pbOutputCiphertext,
+ cbOutput: cbOutputCiphertext);
+
+ // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | _____ | postBuffer }
+
+ // Compute the HMAC over the IV and the ciphertext (prevents IV tampering).
+ // The HMAC is already implicitly computed over the key modifier since the key
+ // modifier is used as input to the KDF.
+ using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes))
+ {
+ hashHandle.HashData(
+ pbInput: pbOutputIV,
+ cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext),
+ pbHashDigest: pbOutputHmac,
+ cbHashDigest: _hmacAlgorithmDigestLengthInBytes);
+ }
+
+ // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer }
+ // And we're done!
+ return retVal;
+ }
+ }
+ }
+ finally
+ {
+ // Buffer contains sensitive material; delete it.
+ UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys);
+ }
+ }
+
+ private uint GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle symmetricKeyHandle, byte* pbInput, uint cbInput)
+ {
+ // ok for this memory to remain uninitialized since nobody depends on it
+ byte* pbIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)];
+
+ // Calling BCryptEncrypt with a null output pointer will cause it to return the total number
+ // of bytes required for the output buffer.
+ uint dwResult;
+ var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
+ hKey: symmetricKeyHandle,
+ pbInput: pbInput,
+ cbInput: cbInput,
+ pPaddingInfo: null,
+ pbIV: pbIV,
+ cbIV: _symmetricAlgorithmBlockSizeInBytes,
+ pbOutput: null,
+ cbOutput: 0,
+ pcbResult: out dwResult,
+ dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+
+ return dwResult;
+ }
+
+ // 'pbExpectedDigest' must point to a '_hmacAlgorithmDigestLengthInBytes'-length buffer
+ private bool ValidateHash(BCryptHashHandle hashHandle, byte* pbInput, uint cbInput, byte* pbExpectedDigest)
+ {
+ byte* pbActualDigest = stackalloc byte[checked((int)_hmacAlgorithmDigestLengthInBytes)];
+ hashHandle.HashData(pbInput, cbInput, pbActualDigest, _hmacAlgorithmDigestLengthInBytes);
+ return CryptoUtil.TimeConstantBuffersAreEqual(pbExpectedDigest, pbActualDigest, _hmacAlgorithmDigestLengthInBytes);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/DpapiSecretSerializerHelper.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/DpapiSecretSerializerHelper.cs
new file mode 100644
index 0000000000..61bbb4f9ea
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/DpapiSecretSerializerHelper.cs
@@ -0,0 +1,356 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ internal unsafe static class DpapiSecretSerializerHelper
+ {
+ // from ncrypt.h
+ private const uint NCRYPT_SILENT_FLAG = 0x00000040;
+
+ // from dpapi.h
+ private const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1;
+ private const uint CRYPTPROTECT_LOCAL_MACHINE = 0x4;
+
+ private static readonly byte[] _purpose = Encoding.UTF8.GetBytes("DPAPI-Protected Secret");
+
+ // Probes to see if protecting to the current Windows user account is available.
+ // In theory this should never fail if the user profile is available, so it's more a defense-in-depth check.
+ public static bool CanProtectToCurrentUserAccount()
+ {
+ try
+ {
+ Guid dummy;
+ ProtectWithDpapi(new Secret((byte*)&dummy, sizeof(Guid)), protectToLocalMachine: false);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public static byte[] ProtectWithDpapi(ISecret secret, bool protectToLocalMachine = false)
+ {
+ Debug.Assert(secret != null);
+
+ var plaintextSecret = new byte[secret.Length];
+ fixed (byte* pbPlaintextSecret = plaintextSecret)
+ {
+ try
+ {
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(plaintextSecret));
+ fixed (byte* pbPurpose = _purpose)
+ {
+ return ProtectWithDpapiCore(pbPlaintextSecret, (uint)plaintextSecret.Length, pbPurpose, (uint)_purpose.Length, fLocalMachine: protectToLocalMachine);
+ }
+ }
+ finally
+ {
+ // To limit exposure to the GC.
+ Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
+ }
+ }
+ }
+
+ internal static byte[] ProtectWithDpapiCore(byte* pbSecret, uint cbSecret, byte* pbOptionalEntropy, uint cbOptionalEntropy, bool fLocalMachine = false)
+ {
+ byte dummy; // provides a valid memory address if the secret or entropy has zero length
+
+ var dataIn = new DATA_BLOB()
+ {
+ cbData = cbSecret,
+ pbData = (pbSecret != null) ? pbSecret : &dummy
+ };
+ var entropy = new DATA_BLOB()
+ {
+ cbData = cbOptionalEntropy,
+ pbData = (pbOptionalEntropy != null) ? pbOptionalEntropy : &dummy
+ };
+ var dataOut = default(DATA_BLOB);
+
+ RuntimeHelpers.PrepareConstrainedRegions();
+
+ try
+ {
+ var success = UnsafeNativeMethods.CryptProtectData(
+ pDataIn: &dataIn,
+ szDataDescr: IntPtr.Zero,
+ pOptionalEntropy: &entropy,
+ pvReserved: IntPtr.Zero,
+ pPromptStruct: IntPtr.Zero,
+ dwFlags: CRYPTPROTECT_UI_FORBIDDEN | ((fLocalMachine) ? CRYPTPROTECT_LOCAL_MACHINE : 0),
+ pDataOut: out dataOut);
+ if (!success)
+ {
+ var errorCode = Marshal.GetLastWin32Error();
+ throw new CryptographicException(errorCode);
+ }
+
+ var dataLength = checked((int)dataOut.cbData);
+ var retVal = new byte[dataLength];
+ Marshal.Copy((IntPtr)dataOut.pbData, retVal, 0, dataLength);
+ return retVal;
+ }
+ finally
+ {
+ // Free memory so that we don't leak.
+ // FreeHGlobal actually calls LocalFree.
+ if (dataOut.pbData != null)
+ {
+ Marshal.FreeHGlobal((IntPtr)dataOut.pbData);
+ }
+ }
+ }
+
+ public static byte[] ProtectWithDpapiNG(ISecret secret, NCryptDescriptorHandle protectionDescriptorHandle)
+ {
+ Debug.Assert(secret != null);
+ Debug.Assert(protectionDescriptorHandle != null);
+
+ var plaintextSecret = new byte[secret.Length];
+ fixed (byte* pbPlaintextSecret = plaintextSecret)
+ {
+ try
+ {
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(plaintextSecret));
+
+ byte dummy; // used to provide a valid memory address if secret is zero-length
+ return ProtectWithDpapiNGCore(
+ protectionDescriptorHandle: protectionDescriptorHandle,
+ pbData: (pbPlaintextSecret != null) ? pbPlaintextSecret : &dummy,
+ cbData: (uint)plaintextSecret.Length);
+ }
+ finally
+ {
+ // Limits secret exposure to garbage collector.
+ Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
+ }
+ }
+ }
+
+ private static byte[] ProtectWithDpapiNGCore(NCryptDescriptorHandle protectionDescriptorHandle, byte* pbData, uint cbData)
+ {
+ Debug.Assert(protectionDescriptorHandle != null);
+ Debug.Assert(pbData != null);
+
+ // Perform the encryption operation, putting the protected data into LocalAlloc-allocated memory.
+ LocalAllocHandle protectedData;
+ uint cbProtectedData;
+ var ntstatus = UnsafeNativeMethods.NCryptProtectSecret(
+ hDescriptor: protectionDescriptorHandle,
+ dwFlags: NCRYPT_SILENT_FLAG,
+ pbData: pbData,
+ cbData: cbData,
+ pMemPara: IntPtr.Zero,
+ hWnd: IntPtr.Zero,
+ ppbProtectedBlob: out protectedData,
+ pcbProtectedBlob: out cbProtectedData);
+ UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(protectedData);
+
+ // Copy the data from LocalAlloc-allocated memory into a managed memory buffer.
+ using (protectedData)
+ {
+ var retVal = new byte[cbProtectedData];
+ if (cbProtectedData > 0)
+ {
+ fixed (byte* pbRetVal = retVal)
+ {
+ var handleAcquired = false;
+
+ RuntimeHelpers.PrepareConstrainedRegions();
+
+ try
+ {
+ protectedData.DangerousAddRef(ref handleAcquired);
+ UnsafeBufferUtil.BlockCopy(from: (void*)protectedData.DangerousGetHandle(), to: pbRetVal, byteCount: cbProtectedData);
+ }
+ finally
+ {
+ if (handleAcquired)
+ {
+ protectedData.DangerousRelease();
+ }
+ }
+ }
+ }
+ return retVal;
+ }
+ }
+
+ public static Secret UnprotectWithDpapi(byte[] protectedSecret)
+ {
+ Debug.Assert(protectedSecret != null);
+
+ fixed (byte* pbProtectedSecret = protectedSecret)
+ {
+ fixed (byte* pbPurpose = _purpose)
+ {
+ return UnprotectWithDpapiCore(pbProtectedSecret, (uint)protectedSecret.Length, pbPurpose, (uint)_purpose.Length);
+ }
+ }
+ }
+
+ internal static Secret UnprotectWithDpapiCore(byte* pbProtectedData, uint cbProtectedData, byte* pbOptionalEntropy, uint cbOptionalEntropy)
+ {
+ byte dummy; // provides a valid memory address if the secret or entropy has zero length
+
+ var dataIn = new DATA_BLOB()
+ {
+ cbData = cbProtectedData,
+ pbData = (pbProtectedData != null) ? pbProtectedData : &dummy
+ };
+ var entropy = new DATA_BLOB()
+ {
+ cbData = cbOptionalEntropy,
+ pbData = (pbOptionalEntropy != null) ? pbOptionalEntropy : &dummy
+ };
+ var dataOut = default(DATA_BLOB);
+
+ RuntimeHelpers.PrepareConstrainedRegions();
+
+ try
+ {
+ var success = UnsafeNativeMethods.CryptUnprotectData(
+ pDataIn: &dataIn,
+ ppszDataDescr: IntPtr.Zero,
+ pOptionalEntropy: &entropy,
+ pvReserved: IntPtr.Zero,
+ pPromptStruct: IntPtr.Zero,
+ dwFlags: CRYPTPROTECT_UI_FORBIDDEN,
+ pDataOut: out dataOut);
+ if (!success)
+ {
+ var errorCode = Marshal.GetLastWin32Error();
+ throw new CryptographicException(errorCode);
+ }
+
+ return new Secret(dataOut.pbData, checked((int)dataOut.cbData));
+ }
+ finally
+ {
+ // Zero and free memory so that we don't leak secrets.
+ // FreeHGlobal actually calls LocalFree.
+ if (dataOut.pbData != null)
+ {
+ UnsafeBufferUtil.SecureZeroMemory(dataOut.pbData, dataOut.cbData);
+ Marshal.FreeHGlobal((IntPtr)dataOut.pbData);
+ }
+ }
+ }
+
+ public static Secret UnprotectWithDpapiNG(byte[] protectedData)
+ {
+ Debug.Assert(protectedData != null);
+
+ fixed (byte* pbProtectedData = protectedData)
+ {
+ byte dummy; // used to provide a valid memory address if protected data is zero-length
+ return UnprotectWithDpapiNGCore(
+ pbData: (pbProtectedData != null) ? pbProtectedData : &dummy,
+ cbData: (uint)protectedData.Length);
+ }
+ }
+
+ private static Secret UnprotectWithDpapiNGCore(byte* pbData, uint cbData)
+ {
+ Debug.Assert(pbData != null);
+
+ // First, decrypt the payload into LocalAlloc-allocated memory.
+ LocalAllocHandle unencryptedPayloadHandle;
+ uint cbUnencryptedPayload;
+ var ntstatus = UnsafeNativeMethods.NCryptUnprotectSecret(
+ phDescriptor: IntPtr.Zero,
+ dwFlags: NCRYPT_SILENT_FLAG,
+ pbProtectedBlob: pbData,
+ cbProtectedBlob: cbData,
+ pMemPara: IntPtr.Zero,
+ hWnd: IntPtr.Zero,
+ ppbData: out unencryptedPayloadHandle,
+ pcbData: out cbUnencryptedPayload);
+ UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(unencryptedPayloadHandle);
+
+ // Copy the data from LocalAlloc-allocated memory into a CryptProtectMemory-protected buffer.
+ // There's a small window between NCryptUnprotectSecret returning and the call to PrepareConstrainedRegions
+ // below where the AppDomain could rudely unload. This won't leak memory (due to the SafeHandle), but it
+ // will cause the secret not to be zeroed out before the memory is freed. We won't worry about this since
+ // the window is extremely small and AppDomain unloads should not happen here in practice.
+ using (unencryptedPayloadHandle)
+ {
+ var handleAcquired = false;
+
+ RuntimeHelpers.PrepareConstrainedRegions();
+
+ try
+ {
+ unencryptedPayloadHandle.DangerousAddRef(ref handleAcquired);
+ return new Secret((byte*)unencryptedPayloadHandle.DangerousGetHandle(), checked((int)cbUnencryptedPayload));
+ }
+ finally
+ {
+ if (handleAcquired)
+ {
+ UnsafeBufferUtil.SecureZeroMemory((byte*)unencryptedPayloadHandle.DangerousGetHandle(), cbUnencryptedPayload);
+ unencryptedPayloadHandle.DangerousRelease();
+ }
+ }
+ }
+ }
+
+ public static string GetRuleFromDpapiNGProtectedPayload(byte[] protectedData)
+ {
+ Debug.Assert(protectedData != null);
+
+ fixed (byte* pbProtectedData = protectedData)
+ {
+ byte dummy; // used to provide a valid memory address if protected data is zero-length
+ return GetRuleFromDpapiNGProtectedPayloadCore(
+ pbData: (pbProtectedData != null) ? pbProtectedData : &dummy,
+ cbData: (uint)protectedData.Length);
+ }
+ }
+
+ private static string GetRuleFromDpapiNGProtectedPayloadCore(byte* pbData, uint cbData)
+ {
+ // from ncryptprotect.h
+ const uint NCRYPT_UNPROTECT_NO_DECRYPT = 0x00000001;
+
+ NCryptDescriptorHandle descriptorHandle;
+ LocalAllocHandle unprotectedDataHandle;
+ uint cbUnprotectedData;
+ var ntstatus = UnsafeNativeMethods.NCryptUnprotectSecret(
+ phDescriptor: out descriptorHandle,
+ dwFlags: NCRYPT_UNPROTECT_NO_DECRYPT,
+ pbProtectedBlob: pbData,
+ cbProtectedBlob: cbData,
+ pMemPara: IntPtr.Zero,
+ hWnd: IntPtr.Zero,
+ ppbData: out unprotectedDataHandle,
+ pcbData: out cbUnprotectedData);
+ UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(descriptorHandle);
+
+ if (unprotectedDataHandle != null && !unprotectedDataHandle.IsInvalid)
+ {
+ // we don't care about this value
+ unprotectedDataHandle.Dispose();
+ }
+
+ using (descriptorHandle)
+ {
+ return descriptorHandle.GetProtectionDescriptorRuleString();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/GcmAuthenticatedEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/GcmAuthenticatedEncryptor.cs
new file mode 100644
index 0000000000..2e9b4ad31c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/GcmAuthenticatedEncryptor.cs
@@ -0,0 +1,289 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.Cng.Internal;
+using Microsoft.AspNetCore.DataProtection.SP800_108;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ // GCM is defined in NIST SP 800-38D (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf).
+ // Heed closely the uniqueness requirements called out in Sec. 8: the probability that the GCM encryption
+ // routine is ever invoked on two or more distinct sets of input data with the same (key, IV) shall not
+ // exceed 2^-32. If we fix the key and use a random 96-bit IV for each invocation, this means that after
+ // 2^32 encryption operations the odds of reusing any (key, IV) pair is 2^-32 (see Sec. 8.3). This won't
+ // work for our use since a high-traffic web server can go through 2^32 requests in mere days. Instead,
+ // we'll use 224 bits of entropy for each operation, with 128 bits going to the KDF and 96 bits
+ // going to the IV. This means that we'll only hit the 2^-32 probability limit after 2^96 encryption
+ // operations, which will realistically never happen. (At the absurd rate of one encryption operation
+ // per nanosecond, it would still take 180 times the age of the universe to hit 2^96 operations.)
+ internal unsafe sealed class GcmAuthenticatedEncryptor : CngAuthenticatedEncryptorBase
+ {
+ // Having a key modifier ensures with overwhelming probability that no two encryption operations
+ // will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's
+ // ability to mount a key-dependent chosen ciphertext attack. See also the class-level comment
+ // for how this is used to overcome GCM's IV limitations.
+ private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
+
+ private const uint NONCE_SIZE_IN_BYTES = 96 / 8; // GCM has a fixed 96-bit IV
+ private const uint TAG_SIZE_IN_BYTES = 128 / 8; // we're hardcoding a 128-bit authentication tag size
+
+ private readonly byte[] _contextHeader;
+ private readonly IBCryptGenRandom _genRandom;
+ private readonly ISP800_108_CTR_HMACSHA512Provider _sp800_108_ctr_hmac_provider;
+ private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle;
+ private readonly uint _symmetricAlgorithmSubkeyLengthInBytes;
+
+ public GcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, IBCryptGenRandom genRandom = null)
+ {
+ // Is the key size appropriate?
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked(symmetricAlgorithmKeySizeInBytes * 8));
+ CryptoUtil.Assert(symmetricAlgorithmHandle.GetCipherBlockLength() == 128 / 8, "GCM requires a block cipher algorithm with a 128-bit block size.");
+
+ _genRandom = genRandom ?? BCryptGenRandomImpl.Instance;
+ _sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey);
+ _symmetricAlgorithmHandle = symmetricAlgorithmHandle;
+ _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes;
+ _contextHeader = CreateContextHeader();
+ }
+
+ private byte[] CreateContextHeader()
+ {
+ var retVal = new byte[checked(
+ 1 /* KDF alg */
+ + 1 /* chaining mode */
+ + sizeof(uint) /* sym alg key size */
+ + sizeof(uint) /* GCM nonce size */
+ + sizeof(uint) /* sym alg block size */
+ + sizeof(uint) /* GCM tag size */
+ + TAG_SIZE_IN_BYTES /* tag of GCM-encrypted empty string */)];
+
+ fixed (byte* pbRetVal = retVal)
+ {
+ byte* ptr = pbRetVal;
+
+ // First is the two-byte header
+ *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF
+ *(ptr++) = 1; // 0x01 = GCM encryption + authentication
+
+ // Next is information about the symmetric algorithm (key size, nonce size, block size, tag size)
+ BitHelpers.WriteTo(ref ptr, _symmetricAlgorithmSubkeyLengthInBytes);
+ BitHelpers.WriteTo(ref ptr, NONCE_SIZE_IN_BYTES);
+ BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES); // block size = tag size
+ BitHelpers.WriteTo(ref ptr, TAG_SIZE_IN_BYTES);
+
+ // See the design document for an explanation of the following code.
+ var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
+ fixed (byte* pbTempKeys = tempKeys)
+ {
+ byte dummy;
+
+ // Derive temporary key for encryption.
+ using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider())
+ {
+ provider.DeriveKey(
+ pbLabel: &dummy,
+ cbLabel: 0,
+ pbContext: &dummy,
+ cbContext: 0,
+ pbDerivedKey: pbTempKeys,
+ cbDerivedKey: (uint)tempKeys.Length);
+ }
+
+ // Encrypt a zero-length input string with an all-zero nonce and copy the tag to the return buffer.
+ byte* pbNonce = stackalloc byte[(int)NONCE_SIZE_IN_BYTES];
+ UnsafeBufferUtil.SecureZeroMemory(pbNonce, NONCE_SIZE_IN_BYTES);
+ DoGcmEncrypt(
+ pbKey: pbTempKeys,
+ cbKey: _symmetricAlgorithmSubkeyLengthInBytes,
+ pbNonce: pbNonce,
+ pbPlaintextData: &dummy,
+ cbPlaintextData: 0,
+ pbEncryptedData: &dummy,
+ pbTag: ptr);
+ }
+
+ ptr += TAG_SIZE_IN_BYTES;
+ CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length");
+ }
+
+ // retVal := { version || chainingMode || symAlgKeySize || nonceSize || symAlgBlockSize || symAlgTagSize || TAG-of-E("") }.
+ return retVal;
+ }
+
+ protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
+ {
+ // Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag
+ if (cbCiphertext < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)
+ {
+ throw Error.CryptCommon_PayloadInvalid();
+ }
+
+ // Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag }
+
+ var cbPlaintext = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES));
+
+ var retVal = new byte[cbPlaintext];
+ fixed (byte* pbRetVal = retVal)
+ {
+ // Calculate offsets
+ byte* pbKeyModifier = pbCiphertext;
+ byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
+ byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES];
+ byte* pbAuthTag = &pbEncryptedData[cbPlaintext];
+
+ // Use the KDF to recreate the symmetric block cipher key
+ // We'll need a temporary buffer to hold the symmetric encryption subkey
+ byte* pbSymmetricDecryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)];
+ try
+ {
+ _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
+ pbLabel: pbAdditionalAuthenticatedData,
+ cbLabel: cbAdditionalAuthenticatedData,
+ contextHeader: _contextHeader,
+ pbContext: pbKeyModifier,
+ cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
+ pbDerivedKey: pbSymmetricDecryptionSubkey,
+ cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes);
+
+ // Perform the decryption operation
+
+ using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes))
+ {
+ byte dummy;
+ byte* pbPlaintext = (pbRetVal != null) ? pbRetVal : &dummy; // CLR doesn't like pinning empty buffers
+
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo;
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authInfo);
+ authInfo.pbNonce = pbNonce;
+ authInfo.cbNonce = NONCE_SIZE_IN_BYTES;
+ authInfo.pbTag = pbAuthTag;
+ authInfo.cbTag = TAG_SIZE_IN_BYTES;
+
+ // The call to BCryptDecrypt will also validate the authentication tag
+ uint cbDecryptedBytesWritten;
+ var ntstatus = UnsafeNativeMethods.BCryptDecrypt(
+ hKey: decryptionSubkeyHandle,
+ pbInput: pbEncryptedData,
+ cbInput: cbPlaintext,
+ pPaddingInfo: &authInfo,
+ pbIV: null, // IV not used; nonce provided in pPaddingInfo
+ cbIV: 0,
+ pbOutput: pbPlaintext,
+ cbOutput: cbPlaintext,
+ pcbResult: out cbDecryptedBytesWritten,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ CryptoUtil.Assert(cbDecryptedBytesWritten == cbPlaintext, "cbDecryptedBytesWritten == cbPlaintext");
+
+ // At this point, retVal := { decryptedPayload }
+ // And we're done!
+ return retVal;
+ }
+ }
+ finally
+ {
+ // The buffer contains key material, so delete it.
+ UnsafeBufferUtil.SecureZeroMemory(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes);
+ }
+ }
+ }
+
+ public override void Dispose()
+ {
+ _sp800_108_ctr_hmac_provider.Dispose();
+
+ // We don't want to dispose of the underlying algorithm instances because they
+ // might be reused.
+ }
+
+ // 'pbNonce' must point to a 96-bit buffer.
+ // 'pbTag' must point to a 128-bit buffer.
+ // 'pbEncryptedData' must point to a buffer the same length as 'pbPlaintextData'.
+ private void DoGcmEncrypt(byte* pbKey, uint cbKey, byte* pbNonce, byte* pbPlaintextData, uint cbPlaintextData, byte* pbEncryptedData, byte* pbTag)
+ {
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authCipherInfo;
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authCipherInfo);
+ authCipherInfo.pbNonce = pbNonce;
+ authCipherInfo.cbNonce = NONCE_SIZE_IN_BYTES;
+ authCipherInfo.pbTag = pbTag;
+ authCipherInfo.cbTag = TAG_SIZE_IN_BYTES;
+
+ using (var keyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbKey, cbKey))
+ {
+ uint cbResult;
+ var ntstatus = UnsafeNativeMethods.BCryptEncrypt(
+ hKey: keyHandle,
+ pbInput: pbPlaintextData,
+ cbInput: cbPlaintextData,
+ pPaddingInfo: &authCipherInfo,
+ pbIV: null,
+ cbIV: 0,
+ pbOutput: pbEncryptedData,
+ cbOutput: cbPlaintextData,
+ pcbResult: out cbResult,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+ CryptoUtil.Assert(cbResult == cbPlaintextData, "cbResult == cbPlaintextData");
+ }
+ }
+
+ protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer)
+ {
+ // Allocate a buffer to hold the key modifier, nonce, encrypted data, and tag.
+ // In GCM, the encrypted output will be the same length as the plaintext input.
+ var retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + cbPlaintext + TAG_SIZE_IN_BYTES + cbPostBuffer)];
+ fixed (byte* pbRetVal = retVal)
+ {
+ // Calculate offsets
+ byte* pbKeyModifier = &pbRetVal[cbPreBuffer];
+ byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES];
+ byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES];
+ byte* pbAuthTag = &pbEncryptedData[cbPlaintext];
+
+ // Randomly generate the key modifier and nonce
+ _genRandom.GenRandom(pbKeyModifier, KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES);
+
+ // At this point, retVal := { preBuffer | keyModifier | nonce | _____ | _____ | postBuffer }
+
+ // Use the KDF to generate a new symmetric block cipher key
+ // We'll need a temporary buffer to hold the symmetric encryption subkey
+ byte* pbSymmetricEncryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)];
+ try
+ {
+ _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader(
+ pbLabel: pbAdditionalAuthenticatedData,
+ cbLabel: cbAdditionalAuthenticatedData,
+ contextHeader: _contextHeader,
+ pbContext: pbKeyModifier,
+ cbContext: KEY_MODIFIER_SIZE_IN_BYTES,
+ pbDerivedKey: pbSymmetricEncryptionSubkey,
+ cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes);
+
+ // Perform the encryption operation
+ DoGcmEncrypt(
+ pbKey: pbSymmetricEncryptionSubkey,
+ cbKey: _symmetricAlgorithmSubkeyLengthInBytes,
+ pbNonce: pbNonce,
+ pbPlaintextData: pbPlaintext,
+ cbPlaintextData: cbPlaintext,
+ pbEncryptedData: pbEncryptedData,
+ pbTag: pbAuthTag);
+
+ // At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer }
+ // And we're done!
+ return retVal;
+ }
+ finally
+ {
+ // The buffer contains key material, so delete it.
+ UnsafeBufferUtil.SecureZeroMemory(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/IBCryptGenRandom.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/IBCryptGenRandom.cs
new file mode 100644
index 0000000000..e1cf9b7dbe
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/IBCryptGenRandom.cs
@@ -0,0 +1,12 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ internal unsafe interface IBCryptGenRandom
+ {
+ void GenRandom(byte* pbBuffer, uint cbBuffer);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/Internal/CngAuthenticatedEncryptorBase.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/Internal/CngAuthenticatedEncryptorBase.cs
new file mode 100644
index 0000000000..7b7e3e2d79
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Cng/Internal/CngAuthenticatedEncryptorBase.cs
@@ -0,0 +1,87 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng.Internal
+{
+ /// <summary>
+ /// Base class used for all CNG-related authentication encryption operations.
+ /// </summary>
+ public unsafe abstract class CngAuthenticatedEncryptorBase : IOptimizedAuthenticatedEncryptor, IDisposable
+ {
+ public byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
+ {
+ // This wrapper simply converts ArraySegment<byte> to byte* and calls the impl method.
+
+ // Input validation
+ ciphertext.Validate();
+ additionalAuthenticatedData.Validate();
+
+ byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer
+ fixed (byte* pbCiphertextArray = ciphertext.Array)
+ {
+ fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array)
+ {
+ try
+ {
+ return DecryptImpl(
+ pbCiphertext: (pbCiphertextArray != null) ? &pbCiphertextArray[ciphertext.Offset] : &dummy,
+ cbCiphertext: (uint)ciphertext.Count,
+ pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy,
+ cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count);
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize to CryptographicException.
+ throw Error.CryptCommon_GenericError(ex);
+ }
+ }
+ }
+ }
+
+ protected abstract byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData);
+
+ public abstract void Dispose();
+
+ public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
+ {
+ return Encrypt(plaintext, additionalAuthenticatedData, 0, 0);
+ }
+
+ public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize)
+ {
+ // This wrapper simply converts ArraySegment<byte> to byte* and calls the impl method.
+
+ // Input validation
+ plaintext.Validate();
+ additionalAuthenticatedData.Validate();
+
+ byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer
+ fixed (byte* pbPlaintextArray = plaintext.Array)
+ {
+ fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array)
+ {
+ try
+ {
+ return EncryptImpl(
+ pbPlaintext: (pbPlaintextArray != null) ? &pbPlaintextArray[plaintext.Offset] : &dummy,
+ cbPlaintext: (uint)plaintext.Count,
+ pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy,
+ cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count,
+ cbPreBuffer: preBufferSize,
+ cbPostBuffer: postBufferSize);
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize to CryptographicException.
+ throw Error.CryptCommon_GenericError(ex);
+ }
+ }
+ }
+ }
+
+ protected abstract byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs
new file mode 100644
index 0000000000..7789ca074f
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionBuilderExtensions.cs
@@ -0,0 +1,628 @@
+// 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.ComponentModel;
+using System.IO;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Win32;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Extensions for configuring data protection using an <see cref="IDataProtectionBuilder"/>.
+ /// </summary>
+ public static class DataProtectionBuilderExtensions
+ {
+ /// <summary>
+ /// Sets the unique name of this application within the data protection system.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="applicationName">The application name.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// This API corresponds to setting the <see cref="DataProtectionOptions.ApplicationDiscriminator"/> property
+ /// to the value of <paramref name="applicationName"/>.
+ /// </remarks>
+ public static IDataProtectionBuilder SetApplicationName(this IDataProtectionBuilder builder, string applicationName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Configure<DataProtectionOptions>(options =>
+ {
+ options.ApplicationDiscriminator = applicationName;
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Registers a <see cref="IKeyEscrowSink"/> to perform escrow before keys are persisted to storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="sink">The instance of the <see cref="IKeyEscrowSink"/> to register.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// Registrations are additive.
+ /// </remarks>
+ public static IDataProtectionBuilder AddKeyEscrowSink(this IDataProtectionBuilder builder, IKeyEscrowSink sink)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (sink == null)
+ {
+ throw new ArgumentNullException(nameof(sink));
+ }
+
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.KeyEscrowSinks.Add(sink);
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Registers a <see cref="IKeyEscrowSink"/> to perform escrow before keys are persisted to storage.
+ /// </summary>
+ /// <typeparam name="TImplementation">The concrete type of the <see cref="IKeyEscrowSink"/> to register.</typeparam>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// Registrations are additive. The factory is registered as <see cref="ServiceLifetime.Singleton"/>.
+ /// </remarks>
+ public static IDataProtectionBuilder AddKeyEscrowSink<TImplementation>(this IDataProtectionBuilder builder)
+ where TImplementation : class, IKeyEscrowSink
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var implementationInstance = services.GetRequiredService<TImplementation>();
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.KeyEscrowSinks.Add(implementationInstance);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Registers a <see cref="IKeyEscrowSink"/> to perform escrow before keys are persisted to storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="factory">A factory that creates the <see cref="IKeyEscrowSink"/> instance.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// Registrations are additive. The factory is registered as <see cref="ServiceLifetime.Singleton"/>.
+ /// </remarks>
+ public static IDataProtectionBuilder AddKeyEscrowSink(this IDataProtectionBuilder builder, Func<IServiceProvider, IKeyEscrowSink> factory)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (factory == null)
+ {
+ throw new ArgumentNullException(nameof(factory));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var instance = factory(services);
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.KeyEscrowSinks.Add(instance);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures the key management options for the data protection system.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="setupAction">An <see cref="Action{KeyManagementOptions}"/> to configure the provided <see cref="KeyManagementOptions"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder AddKeyManagementOptions(this IDataProtectionBuilder builder, Action<KeyManagementOptions> setupAction)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (setupAction == null)
+ {
+ throw new ArgumentNullException(nameof(setupAction));
+ }
+
+ builder.Services.Configure(setupAction);
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures the data protection system not to generate new keys automatically.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// Calling this API corresponds to setting <see cref="KeyManagementOptions.AutoGenerateKeys"/>
+ /// to 'false'. See that property's documentation for more information.
+ /// </remarks>
+ public static IDataProtectionBuilder DisableAutomaticKeyGeneration(this IDataProtectionBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.AutoGenerateKeys = false;
+ });
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the specified directory.
+ /// This path may be on the local machine or may point to a UNC share.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="directory">The directory in which to store keys.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder PersistKeysToFileSystem(this IDataProtectionBuilder builder, DirectoryInfo directory)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (directory == null)
+ {
+ throw new ArgumentNullException(nameof(directory));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.XmlRepository = new FileSystemXmlRepository(directory, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures the data protection system to persist keys to the Windows registry.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="registryKey">The location in the registry where keys should be stored.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder PersistKeysToRegistry(this IDataProtectionBuilder builder, RegistryKey registryKey)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (registryKey == null)
+ {
+ throw new ArgumentNullException(nameof(registryKey));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.XmlRepository = new RegistryXmlRepository(registryKey, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures keys to be encrypted to a given certificate before being persisted to storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="certificate">The certificate to use when encrypting keys.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder ProtectKeysWithCertificate(this IDataProtectionBuilder builder, X509Certificate2 certificate)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.XmlEncryptor = new CertificateXmlEncryptor(certificate, loggerFactory);
+ });
+ });
+
+ builder.Services.Configure<XmlKeyDecryptionOptions>(o => o.AddKeyDecryptionCertificate(certificate));
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures keys to be encrypted to a given certificate before being persisted to storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="thumbprint">The thumbprint of the certificate to use when encrypting keys.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder ProtectKeysWithCertificate(this IDataProtectionBuilder builder, string thumbprint)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (thumbprint == null)
+ {
+ throw new ArgumentNullException(nameof(thumbprint));
+ }
+
+ // Make sure the thumbprint corresponds to a valid certificate.
+ if (new CertificateResolver().ResolveCertificate(thumbprint) == null)
+ {
+ throw Error.CertificateXmlEncryptor_CertificateNotFound(thumbprint);
+ }
+
+ // ICertificateResolver is necessary for this type to work correctly, so register it
+ // if it doesn't already exist.
+ builder.Services.TryAddSingleton<ICertificateResolver, CertificateResolver>();
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ var certificateResolver = services.GetRequiredService<ICertificateResolver>();
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ options.XmlEncryptor = new CertificateXmlEncryptor(thumbprint, certificateResolver, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures certificates which can be used to decrypt keys loaded from storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="certificates">Certificates that can be used to decrypt key data.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder UnprotectKeysWithAnyCertificate(this IDataProtectionBuilder builder, params X509Certificate2[] certificates)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Configure<XmlKeyDecryptionOptions>(o =>
+ {
+ if (certificates != null)
+ {
+ foreach (var certificate in certificates)
+ {
+ o.AddKeyDecryptionCertificate(certificate);
+ }
+ }
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures keys to be encrypted with Windows DPAPI before being persisted to
+ /// storage. The encrypted key will only be decryptable by the current Windows user account.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// This API is only supported on Windows platforms.
+ /// </remarks>
+ public static IDataProtectionBuilder ProtectKeysWithDpapi(this IDataProtectionBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ return builder.ProtectKeysWithDpapi(protectToLocalMachine: false);
+ }
+
+ /// <summary>
+ /// Configures keys to be encrypted with Windows DPAPI before being persisted to
+ /// storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="protectToLocalMachine">'true' if the key should be decryptable by any
+ /// use on the local machine, 'false' if the key should only be decryptable by the current
+ /// Windows user account.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// This API is only supported on Windows platforms.
+ /// </remarks>
+ public static IDataProtectionBuilder ProtectKeysWithDpapi(this IDataProtectionBuilder builder, bool protectToLocalMachine)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ CryptoUtil.AssertPlatformIsWindows();
+ options.XmlEncryptor = new DpapiXmlEncryptor(protectToLocalMachine, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures keys to be encrypted with Windows CNG DPAPI before being persisted
+ /// to storage. The keys will be decryptable by the current Windows user account.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// See https://msdn.microsoft.com/en-us/library/windows/desktop/hh706794(v=vs.85).aspx
+ /// for more information on DPAPI-NG. This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ /// </remarks>
+ public static IDataProtectionBuilder ProtectKeysWithDpapiNG(this IDataProtectionBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ return builder.ProtectKeysWithDpapiNG(
+ protectionDescriptorRule: DpapiNGXmlEncryptor.GetDefaultProtectionDescriptorString(),
+ flags: DpapiNGProtectionDescriptorFlags.None);
+ }
+
+ /// <summary>
+ /// Configures keys to be encrypted with Windows CNG DPAPI before being persisted to storage.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="protectionDescriptorRule">The descriptor rule string with which to protect the key material.</param>
+ /// <param name="flags">Flags that should be passed to the call to 'NCryptCreateProtectionDescriptor'.
+ /// The default value of this parameter is <see cref="DpapiNGProtectionDescriptorFlags.None"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// See https://msdn.microsoft.com/en-us/library/windows/desktop/hh769091(v=vs.85).aspx
+ /// and https://msdn.microsoft.com/en-us/library/windows/desktop/hh706800(v=vs.85).aspx
+ /// for more information on valid values for the the <paramref name="protectionDescriptorRule"/>
+ /// and <paramref name="flags"/> arguments.
+ /// This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ /// </remarks>
+ public static IDataProtectionBuilder ProtectKeysWithDpapiNG(this IDataProtectionBuilder builder, string protectionDescriptorRule, DpapiNGProtectionDescriptorFlags flags)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (protectionDescriptorRule == null)
+ {
+ throw new ArgumentNullException(nameof(protectionDescriptorRule));
+ }
+
+ builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
+ {
+ var loggerFactory = services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions<KeyManagementOptions>(options =>
+ {
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
+ options.XmlEncryptor = new DpapiNGXmlEncryptor(protectionDescriptorRule, flags, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Sets the default lifetime of keys created by the data protection system.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="lifetime">The lifetime (time before expiration) for newly-created keys.
+ /// See <see cref="KeyManagementOptions.NewKeyLifetime"/> for more information and
+ /// usage notes.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder SetDefaultKeyLifetime(this IDataProtectionBuilder builder, TimeSpan lifetime)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (lifetime < TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(Resources.FormatLifetimeMustNotBeNegative(nameof(lifetime)));
+ }
+
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.NewKeyLifetime = lifetime;
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures the data protection system to use the specified cryptographic algorithms
+ /// by default when generating protected payloads.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="configuration">Information about what cryptographic algorithms should be used.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ public static IDataProtectionBuilder UseCryptographicAlgorithms(this IDataProtectionBuilder builder, AuthenticatedEncryptorConfiguration configuration)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ return UseCryptographicAlgorithmsCore(builder, configuration);
+ }
+
+ /// <summary>
+ /// Configures the data protection system to use custom Windows CNG algorithms.
+ /// This API is intended for advanced scenarios where the developer cannot use the
+ /// algorithms specified in the <see cref="EncryptionAlgorithm"/> and
+ /// <see cref="ValidationAlgorithm"/> enumerations.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="configuration">Information about what cryptographic algorithms should be used.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// This API is only available on Windows.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IDataProtectionBuilder UseCustomCryptographicAlgorithms(this IDataProtectionBuilder builder, CngCbcAuthenticatedEncryptorConfiguration configuration)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ return UseCryptographicAlgorithmsCore(builder, configuration);
+ }
+
+ /// <summary>
+ /// Configures the data protection system to use custom Windows CNG algorithms.
+ /// This API is intended for advanced scenarios where the developer cannot use the
+ /// algorithms specified in the <see cref="EncryptionAlgorithm"/> and
+ /// <see cref="ValidationAlgorithm"/> enumerations.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="configuration">Information about what cryptographic algorithms should be used.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// This API is only available on Windows.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IDataProtectionBuilder UseCustomCryptographicAlgorithms(this IDataProtectionBuilder builder, CngGcmAuthenticatedEncryptorConfiguration configuration)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ return UseCryptographicAlgorithmsCore(builder, configuration);
+ }
+
+ /// <summary>
+ /// Configures the data protection system to use custom algorithms.
+ /// This API is intended for advanced scenarios where the developer cannot use the
+ /// algorithms specified in the <see cref="EncryptionAlgorithm"/> and
+ /// <see cref="ValidationAlgorithm"/> enumerations.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <param name="configuration">Information about what cryptographic algorithms should be used.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IDataProtectionBuilder UseCustomCryptographicAlgorithms(this IDataProtectionBuilder builder, ManagedAuthenticatedEncryptorConfiguration configuration)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ return UseCryptographicAlgorithmsCore(builder, configuration);
+ }
+
+ private static IDataProtectionBuilder UseCryptographicAlgorithmsCore(IDataProtectionBuilder builder, AlgorithmConfiguration configuration)
+ {
+ ((IInternalAlgorithmConfiguration)configuration).Validate(); // perform self-test
+
+ builder.Services.Configure<KeyManagementOptions>(options =>
+ {
+ options.AuthenticatedEncryptorConfiguration = configuration;
+ });
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Configures the data protection system to use the <see cref="EphemeralDataProtectionProvider"/>
+ /// for data protection services.
+ /// </summary>
+ /// <param name="builder">The <see cref="IDataProtectionBuilder"/>.</param>
+ /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+ /// <remarks>
+ /// If this option is used, payloads protected by the data protection system will
+ /// be permanently undecipherable after the application exits.
+ /// </remarks>
+ public static IDataProtectionBuilder UseEphemeralDataProtectionProvider(this IDataProtectionBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Replace(ServiceDescriptor.Singleton<IDataProtectionProvider, EphemeralDataProtectionProvider>());
+
+ return builder;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionOptions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionOptions.cs
new file mode 100644
index 0000000000..c8707da1c3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionOptions.cs
@@ -0,0 +1,25 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Provides global options for the Data Protection system.
+ /// </summary>
+ public class DataProtectionOptions
+ {
+ /// <summary>
+ /// An identifier that uniquely discriminates this application from all other
+ /// applications on the machine. The discriminator value is implicitly included
+ /// in all protected payloads generated by the data protection system to isolate
+ /// multiple logical applications that all happen to be using the same key material.
+ /// </summary>
+ /// <remarks>
+ /// If two different applications need to share protected payloads, they should
+ /// ensure that this property is set to the same value across both applications.
+ /// </remarks>
+ public string ApplicationDiscriminator { get; set; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..b112e9ac68
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs
@@ -0,0 +1,106 @@
+// 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 Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.Infrastructure;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for setting up data protection services in an <see cref="IServiceCollection" />.
+ /// </summary>
+ public static class DataProtectionServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds data protection services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ public static IDataProtectionBuilder AddDataProtection(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAddSingleton<IActivator, TypeForwardingActivator>();
+ services.AddOptions();
+ AddDataProtectionServices(services);
+
+ return new DataProtectionBuilder(services);
+ }
+
+ /// <summary>
+ /// Adds data protection services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <param name="setupAction">An <see cref="Action{DataProtectionOptions}"/> to configure the provided <see cref="DataProtectionOptions"/>.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IDataProtectionBuilder AddDataProtection(this IServiceCollection services, Action<DataProtectionOptions> setupAction)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (setupAction == null)
+ {
+ throw new ArgumentNullException(nameof(setupAction));
+ }
+
+ var builder = services.AddDataProtection();
+ services.Configure(setupAction);
+ return builder;
+ }
+
+ private static void AddDataProtectionServices(IServiceCollection services)
+ {
+ if (OSVersionUtil.IsWindows())
+ {
+ services.TryAddSingleton<IRegistryPolicyResolver, RegistryPolicyResolver>();
+ }
+
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton<IConfigureOptions<KeyManagementOptions>, KeyManagementOptionsSetup>());
+ services.TryAddEnumerable(
+ ServiceDescriptor.Transient<IConfigureOptions<DataProtectionOptions>, DataProtectionOptionsSetup>());
+
+ services.TryAddSingleton<IKeyManager, XmlKeyManager>();
+ services.TryAddSingleton<IApplicationDiscriminator, HostingApplicationDiscriminator>();
+ services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, DataProtectionStartupFilter>());
+
+ // Internal services
+ services.TryAddSingleton<IDefaultKeyResolver, DefaultKeyResolver>();
+ services.TryAddSingleton<IKeyRingProvider, KeyRingProvider>();
+
+ services.TryAddSingleton<IDataProtectionProvider>(s =>
+ {
+ var dpOptions = s.GetRequiredService<IOptions<DataProtectionOptions>>();
+ var keyRingProvider = s.GetRequiredService<IKeyRingProvider>();
+ var loggerFactory = s.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
+
+ IDataProtectionProvider dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyRingProvider, loggerFactory);
+
+ // Link the provider to the supplied discriminator
+ if (!string.IsNullOrEmpty(dpOptions.Value.ApplicationDiscriminator))
+ {
+ dataProtectionProvider = dataProtectionProvider.CreateProtector(dpOptions.Value.ApplicationDiscriminator);
+ }
+
+ return dataProtectionProvider;
+ });
+
+ services.TryAddSingleton<ICertificateResolver, CertificateResolver>();
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionUtilityExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionUtilityExtensions.cs
new file mode 100644
index 0000000000..04152f3ed6
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/DataProtectionUtilityExtensions.cs
@@ -0,0 +1,46 @@
+// 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.ComponentModel;
+using Microsoft.AspNetCore.DataProtection.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public static class DataProtectionUtilityExtensions
+ {
+ /// <summary>
+ /// Returns a unique identifier for this application.
+ /// </summary>
+ /// <param name="services">The application-level <see cref="IServiceProvider"/>.</param>
+ /// <returns>A unique application identifier, or null if <paramref name="services"/> is null
+ /// or cannot provide a unique application identifier.</returns>
+ /// <remarks>
+ /// <para>
+ /// The returned identifier should be stable for repeated runs of this same application on
+ /// this machine. Additionally, the identifier is only unique within the scope of a single
+ /// machine, e.g., two different applications on two different machines may return the same
+ /// value.
+ /// </para>
+ /// <para>
+ /// This identifier may contain security-sensitive information such as physical file paths,
+ /// configuration settings, or other machine-specific information. Callers should take
+ /// special care not to disclose this information to untrusted entities.
+ /// </para>
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static string GetApplicationUniqueIdentifier(this IServiceProvider services)
+ {
+ string discriminator = null;
+ if (services != null)
+ {
+ discriminator = services.GetService<IApplicationDiscriminator>()?.Discriminator;
+ }
+
+ // Remove whitespace and homogenize empty -> null
+ discriminator = discriminator?.Trim();
+ return (string.IsNullOrEmpty(discriminator)) ? null : discriminator;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/EphemeralDataProtectionProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/EphemeralDataProtectionProvider.cs
new file mode 100644
index 0000000000..587b0ebfd4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/EphemeralDataProtectionProvider.cs
@@ -0,0 +1,124 @@
+// 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 Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// An <see cref="IDataProtectionProvider"/> that is transient.
+ /// </summary>
+ /// <remarks>
+ /// Payloads generated by a given <see cref="EphemeralDataProtectionProvider"/> instance can only
+ /// be deciphered by that same instance. Once the instance is lost, all ciphertexts
+ /// generated by that instance are permanently undecipherable.
+ /// </remarks>
+ public sealed class EphemeralDataProtectionProvider : IDataProtectionProvider
+ {
+ private readonly KeyRingBasedDataProtectionProvider _dataProtectionProvider;
+
+ /// <summary>
+ /// Creates an ephemeral <see cref="IDataProtectionProvider"/>.
+ /// </summary>
+ public EphemeralDataProtectionProvider()
+ : this (NullLoggerFactory.Instance)
+ { }
+
+ /// <summary>
+ /// Creates an ephemeral <see cref="IDataProtectionProvider"/> with logging.
+ /// </summary>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory" />.</param>
+ public EphemeralDataProtectionProvider(ILoggerFactory loggerFactory)
+ {
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ IKeyRingProvider keyringProvider;
+ if (OSVersionUtil.IsWindows())
+ {
+ // Fastest implementation: AES-256-GCM [CNG]
+ keyringProvider = new EphemeralKeyRing<CngGcmAuthenticatedEncryptorConfiguration>(loggerFactory);
+ }
+ else
+ {
+ // Slowest implementation: AES-256-CBC + HMACSHA256 [Managed]
+ keyringProvider = new EphemeralKeyRing<ManagedAuthenticatedEncryptorConfiguration>(loggerFactory);
+ }
+
+ var logger = loggerFactory.CreateLogger<EphemeralDataProtectionProvider>();
+ logger.UsingEphemeralDataProtectionProvider();
+
+ _dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider, loggerFactory);
+ }
+
+ public IDataProtector CreateProtector(string purpose)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ // just forward to the underlying provider
+ return _dataProtectionProvider.CreateProtector(purpose);
+ }
+
+ private sealed class EphemeralKeyRing<T> : IKeyRing, IKeyRingProvider
+ where T : AlgorithmConfiguration, new()
+ {
+ public EphemeralKeyRing(ILoggerFactory loggerFactory)
+ {
+ DefaultAuthenticatedEncryptor = GetDefaultEncryptor(loggerFactory);
+ }
+
+ // Currently hardcoded to a 512-bit KDK.
+ private const int NUM_BYTES_IN_KDK = 512 / 8;
+
+ public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; }
+
+ public Guid DefaultKeyId { get; } = default(Guid);
+
+ public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked)
+ {
+ isRevoked = false;
+ return (keyId == default(Guid)) ? DefaultAuthenticatedEncryptor : null;
+ }
+
+ public IKeyRing GetCurrentKeyRing()
+ {
+ return this;
+ }
+
+ private static IAuthenticatedEncryptor GetDefaultEncryptor(ILoggerFactory loggerFactory)
+ {
+ var configuration = new T();
+ if (configuration is CngGcmAuthenticatedEncryptorConfiguration)
+ {
+ var descriptor = (CngGcmAuthenticatedEncryptorDescriptor)new T().CreateNewDescriptor();
+ return new CngGcmAuthenticatedEncryptorFactory(loggerFactory)
+ .CreateAuthenticatedEncryptorInstance(
+ descriptor.MasterKey,
+ configuration as CngGcmAuthenticatedEncryptorConfiguration);
+ }
+ else if (configuration is ManagedAuthenticatedEncryptorConfiguration)
+ {
+ var descriptor = (ManagedAuthenticatedEncryptorDescriptor)new T().CreateNewDescriptor();
+ return new ManagedAuthenticatedEncryptorFactory(loggerFactory)
+ .CreateAuthenticatedEncryptorInstance(
+ descriptor.MasterKey,
+ configuration as ManagedAuthenticatedEncryptorConfiguration);
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Error.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Error.cs
new file mode 100644
index 0000000000..304f08e5c5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Error.cs
@@ -0,0 +1,95 @@
+// 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.Globalization;
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class Error
+ {
+ public static InvalidOperationException CertificateXmlEncryptor_CertificateNotFound(string thumbprint)
+ {
+ var message = Resources.FormatCertificateXmlEncryptor_CertificateNotFound(thumbprint);
+ return new InvalidOperationException(message);
+ }
+
+ public static ArgumentException Common_ArgumentCannotBeNullOrEmpty(string parameterName)
+ {
+ return new ArgumentException(Resources.Common_ArgumentCannotBeNullOrEmpty, parameterName);
+ }
+
+ public static ArgumentException Common_BufferIncorrectlySized(string parameterName, int actualSize, int expectedSize)
+ {
+ var message = Resources.FormatCommon_BufferIncorrectlySized(actualSize, expectedSize);
+ return new ArgumentException(message, parameterName);
+ }
+
+ public static CryptographicException CryptCommon_GenericError(Exception inner = null)
+ {
+ return new CryptographicException(Resources.CryptCommon_GenericError, inner);
+ }
+
+ public static CryptographicException CryptCommon_PayloadInvalid()
+ {
+ var message = Resources.CryptCommon_PayloadInvalid;
+ return new CryptographicException(message);
+ }
+
+ public static InvalidOperationException Common_PropertyCannotBeNullOrEmpty(string propertyName)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, Resources.Common_PropertyCannotBeNullOrEmpty, propertyName);
+ return new InvalidOperationException(message);
+ }
+
+ public static InvalidOperationException Common_PropertyMustBeNonNegative(string propertyName)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, Resources.Common_PropertyMustBeNonNegative, propertyName);
+ return new InvalidOperationException(message);
+ }
+
+ public static CryptographicException Common_EncryptionFailed(Exception inner = null)
+ {
+ return new CryptographicException(Resources.Common_EncryptionFailed, inner);
+ }
+
+ public static CryptographicException Common_KeyNotFound(Guid id)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, Resources.Common_KeyNotFound, id);
+ return new CryptographicException(message);
+ }
+
+ public static CryptographicException Common_KeyRevoked(Guid id)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, Resources.Common_KeyRevoked, id);
+ return new CryptographicException(message);
+ }
+
+ public static ArgumentOutOfRangeException Common_ValueMustBeNonNegative(string paramName)
+ {
+ return new ArgumentOutOfRangeException(paramName, Resources.Common_ValueMustBeNonNegative);
+ }
+
+ public static CryptographicException DecryptionFailed(Exception inner)
+ {
+ return new CryptographicException(Resources.Common_DecryptionFailed, inner);
+ }
+
+ public static CryptographicException ProtectionProvider_BadMagicHeader()
+ {
+ return new CryptographicException(Resources.ProtectionProvider_BadMagicHeader);
+ }
+
+ public static CryptographicException ProtectionProvider_BadVersion()
+ {
+ return new CryptographicException(Resources.ProtectionProvider_BadVersion);
+ }
+
+ public static InvalidOperationException XmlKeyManager_DuplicateKey(Guid keyId)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, Resources.XmlKeyManager_DuplicateKey, keyId);
+ return new InvalidOperationException(message);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IDataProtectionBuilder.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IDataProtectionBuilder.cs
new file mode 100644
index 0000000000..95c7c61f50
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IDataProtectionBuilder.cs
@@ -0,0 +1,43 @@
+// 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;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Provides access to configuration for the data protection system, which allows the
+ /// developer to configure default cryptographic algorithms, key storage locations,
+ /// and the mechanism by which keys are protected at rest.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// If the developer changes the at-rest key protection mechanism, it is intended that
+ /// he also change the key storage location, and vice versa. For instance, a call to
+ /// <see cref="DataProtectionBuilderExtensions.ProtectKeysWithCertificate(IDataProtectionBuilder,string)" /> should generally be accompanied by
+ /// a call to <see cref="DataProtectionBuilderExtensions.PersistKeysToFileSystem(IDataProtectionBuilder,DirectoryInfo)"/>, or exceptions may
+ /// occur at runtime due to the data protection system not knowing where to persist keys.
+ /// </para>
+ /// <para>
+ /// Similarly, when a developer modifies the default protected payload cryptographic
+ /// algorithms, it is intended that he also select an explitiy key storage location.
+ /// A call to <see cref="DataProtectionBuilderExtensions.UseCryptographicAlgorithms(IDataProtectionBuilder,AuthenticatedEncryptorConfiguration)"/>
+ /// should therefore generally be paired with a call to <see cref="DataProtectionBuilderExtensions.PersistKeysToFileSystem(IDataProtectionBuilder,DirectoryInfo)"/>,
+ /// for example.
+ /// </para>
+ /// <para>
+ /// When the default cryptographic algorithms or at-rest key protection mechanisms are
+ /// changed, they only affect <strong>new</strong> keys in the repository. The repository may
+ /// contain existing keys that use older algorithms or protection mechanisms.
+ /// </para>
+ /// </remarks>
+ public interface IDataProtectionBuilder
+ {
+ /// <summary>
+ /// Provides access to the <see cref="IServiceCollection"/> passed to this object's constructor.
+ /// </summary>
+ IServiceCollection Services { get; }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IPersistedDataProtector.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IPersistedDataProtector.cs
new file mode 100644
index 0000000000..0e0310cd1d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IPersistedDataProtector.cs
@@ -0,0 +1,36 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// An interface that can provide data protection services for data which has been persisted
+ /// to long-term storage.
+ /// </summary>
+ public interface IPersistedDataProtector : IDataProtector
+ {
+ /// <summary>
+ /// Cryptographically unprotects a piece of data, optionally ignoring failures due to
+ /// revocation of the cryptographic keys used to protect the payload.
+ /// </summary>
+ /// <param name="protectedData">The protected data to unprotect.</param>
+ /// <param name="ignoreRevocationErrors">'true' if the payload should be unprotected even
+ /// if the cryptographic key used to protect it has been revoked (due to potential compromise),
+ /// 'false' if revocation should fail the unprotect operation.</param>
+ /// <param name="requiresMigration">'true' if the data should be reprotected before being
+ /// persisted back to long-term storage, 'false' otherwise. Migration might be requested
+ /// when the default protection key has changed, for instance.</param>
+ /// <param name="wasRevoked">'true' if the cryptographic key used to protect this payload
+ /// has been revoked, 'false' otherwise. Payloads whose keys have been revoked should be
+ /// treated as suspect unless the application has separate assurance that the payload
+ /// has not been tampered with.</param>
+ /// <returns>The plaintext form of the protected data.</returns>
+ /// <remarks>
+ /// Implementations should throw CryptographicException if the protected data is
+ /// invalid or malformed.
+ /// </remarks>
+ byte[] DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IRegistryPolicyResolver.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IRegistryPolicyResolver.cs
new file mode 100644
index 0000000000..b188bf40f7
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/IRegistryPolicyResolver.cs
@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ // Single implementation of this interface is conditionally added to DI on Windows
+ // We have to use interface because some DI implementations would try to activate class
+ // even if it was not registered causing problems crossplat
+ internal interface IRegistryPolicyResolver
+ {
+ RegistryPolicy ResolvePolicy();
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ISecret.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ISecret.cs
new file mode 100644
index 0000000000..4010bc6445
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/ISecret.cs
@@ -0,0 +1,27 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Represents a secret value.
+ /// </summary>
+ public interface ISecret : IDisposable
+ {
+ /// <summary>
+ /// The length (in bytes) of the secret value.
+ /// </summary>
+ int Length { get; }
+
+ /// <summary>
+ /// Writes the secret value to the specified buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer which should receive the secret value.</param>
+ /// <remarks>
+ /// The buffer size must exactly match the length of the secret value.
+ /// </remarks>
+ void WriteSecretIntoBuffer(ArraySegment<byte> buffer);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionBuilder.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionBuilder.cs
new file mode 100644
index 0000000000..bc8908c9c4
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionBuilder.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ /// <summary>
+ /// Default implementation of <see cref="IDataProtectionBuilder"/>.
+ /// </summary>
+ public class DataProtectionBuilder : IDataProtectionBuilder
+ {
+ /// <summary>
+ /// Creates a new configuration object linked to a <see cref="IServiceCollection"/>.
+ /// </summary>
+ public DataProtectionBuilder(IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ Services = services;
+ }
+
+ /// <inheritdoc />
+ public IServiceCollection Services { get; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionOptionsSetup.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionOptionsSetup.cs
new file mode 100644
index 0000000000..d5e25b7586
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionOptionsSetup.cs
@@ -0,0 +1,23 @@
+// 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 Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ internal class DataProtectionOptionsSetup : IConfigureOptions<DataProtectionOptions>
+ {
+ private readonly IServiceProvider _services;
+
+ public DataProtectionOptionsSetup(IServiceProvider provider)
+ {
+ _services = provider;
+ }
+
+ public void Configure(DataProtectionOptions options)
+ {
+ options.ApplicationDiscriminator = _services.GetApplicationUniqueIdentifier();
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionStartupFilter.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionStartupFilter.cs
new file mode 100644
index 0000000000..d9faa5b0f8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DataProtectionStartupFilter.cs
@@ -0,0 +1,48 @@
+// 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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ internal class DataProtectionStartupFilter : IStartupFilter
+ {
+ private readonly IKeyRingProvider _keyRingProvider;
+ private readonly ILogger<DataProtectionStartupFilter> _logger;
+
+ public DataProtectionStartupFilter(IKeyRingProvider keyRingProvider)
+ : this(keyRingProvider, NullLoggerFactory.Instance)
+ { }
+
+ public DataProtectionStartupFilter(IKeyRingProvider keyRingProvider, ILoggerFactory loggerFactory)
+ {
+ _keyRingProvider = keyRingProvider;
+ _logger = loggerFactory.CreateLogger<DataProtectionStartupFilter>();
+ }
+
+ public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
+ {
+ try
+ {
+ // It doesn't look like much, but this preloads the key ring,
+ // which in turn may load data from remote stores like Redis or Azure.
+ var keyRing = _keyRingProvider.GetCurrentKeyRing();
+
+ _logger.KeyRingWasLoadedOnStartup(keyRing.DefaultKeyId);
+ }
+ catch (Exception ex)
+ {
+ // This should be non-fatal, so swallow, log, and allow server startup to continue.
+ // The KeyRingProvider may be able to try again on the first request.
+ _logger.KeyRingFailedToLoadOnStartup(ex);
+ }
+
+ return next;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DockerUtils.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DockerUtils.cs
new file mode 100644
index 0000000000..7a1ede17e0
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/DockerUtils.cs
@@ -0,0 +1,99 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ internal static class DockerUtils
+ {
+ private static Lazy<bool> _isDocker = new Lazy<bool>(IsProcessRunningInDocker);
+
+ public static bool IsDocker => _isDocker.Value;
+
+ public static bool IsVolumeMountedFolder(DirectoryInfo directory)
+ {
+ if (!IsDocker)
+ {
+ return false;
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // we currently don't have a good way to detect mounted file systems within Windows ctonainers
+ return false;
+ }
+
+ const string mountsFile = "/proc/self/mounts";
+ if (!File.Exists(mountsFile))
+ {
+ return false;
+ }
+
+ var lines = File.ReadAllLines(mountsFile);
+ return IsDirectoryMounted(directory, lines);
+ }
+
+ // internal for testing. Don't use directly
+ internal static bool IsDirectoryMounted(DirectoryInfo directory, IEnumerable<string> fstab)
+ {
+ // Expected file format: http://man7.org/linux/man-pages/man5/fstab.5.html
+ foreach (var line in fstab)
+ {
+ if (line == null || line.Length == 0 || line[0] == '#')
+ {
+ // skip empty and commented-out lines
+ continue;
+ }
+
+ var fields = line.Split(new[] { '\t', ' ' });
+
+ if (fields.Length < 2 // line had too few fields
+ || fields[1].Length <= 1 // fs_file empty or is the root directory '/'
+ || fields[1][0] != '/') // fs_file was not a file path
+ {
+ continue;
+ }
+
+ // check if directory is a subdirectory of this location
+ var fs_file = new DirectoryInfo(fields[1].TrimEnd(Path.DirectorySeparatorChar)).FullName;
+ var dir = directory;
+ while (dir != null)
+ {
+ // filesystems on Linux are case sensitive
+ if (fs_file.Equals(dir.FullName.TrimEnd(Path.DirectorySeparatorChar), StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ dir = dir.Parent;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsProcessRunningInDocker()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // we currently don't have a good way to detect if running in a Windows container
+ return false;
+ }
+
+ const string procFile = "/proc/1/cgroup";
+ if (!File.Exists(procFile))
+ {
+ return false;
+ }
+
+ var lines = File.ReadAllLines(procFile);
+ // typically the last line in the file is "1:name=openrc:/docker"
+ return lines.Reverse().Any(l => l.EndsWith("name=openrc:/docker", StringComparison.Ordinal));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/HostingApplicationDiscriminator.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/HostingApplicationDiscriminator.cs
new file mode 100644
index 0000000000..400d372418
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/HostingApplicationDiscriminator.cs
@@ -0,0 +1,25 @@
+// 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 Microsoft.AspNetCore.DataProtection.Infrastructure;
+using Microsoft.AspNetCore.Hosting;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ internal class HostingApplicationDiscriminator : IApplicationDiscriminator
+ {
+ private readonly IHostingEnvironment _hosting;
+
+ // the optional constructor for when IHostingEnvironment is not available from DI
+ public HostingApplicationDiscriminator()
+ {
+ }
+
+ public HostingApplicationDiscriminator(IHostingEnvironment hosting)
+ {
+ _hosting = hosting;
+ }
+
+ public string Discriminator => _hosting?.ContentRootPath;
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/IActivator.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/IActivator.cs
new file mode 100644
index 0000000000..189e2ab303
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/IActivator.cs
@@ -0,0 +1,20 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ /// <summary>
+ /// An interface into <see cref="Activator.CreateInstance{T}"/> that also supports
+ /// limited dependency injection (of <see cref="IServiceProvider"/>).
+ /// </summary>
+ public interface IActivator
+ {
+ /// <summary>
+ /// Creates an instance of <paramref name="implementationTypeName"/> and ensures
+ /// that it is assignable to <paramref name="expectedBaseType"/>.
+ /// </summary>
+ object CreateInstance(Type expectedBaseType, string implementationTypeName);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/KeyManagementOptionsSetup.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/KeyManagementOptionsSetup.cs
new file mode 100644
index 0000000000..10707c9cab
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Internal/KeyManagementOptionsSetup.cs
@@ -0,0 +1,78 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ internal class KeyManagementOptionsSetup : IConfigureOptions<KeyManagementOptions>
+ {
+ private readonly IRegistryPolicyResolver _registryPolicyResolver;
+ private readonly ILoggerFactory _loggerFactory;
+
+ public KeyManagementOptionsSetup()
+ : this(NullLoggerFactory.Instance, registryPolicyResolver: null)
+ {
+ }
+
+ public KeyManagementOptionsSetup(ILoggerFactory loggerFactory)
+ : this(loggerFactory, registryPolicyResolver: null)
+ {
+ }
+
+ public KeyManagementOptionsSetup(IRegistryPolicyResolver registryPolicyResolver)
+ : this(NullLoggerFactory.Instance, registryPolicyResolver)
+ {
+ }
+
+ public KeyManagementOptionsSetup(ILoggerFactory loggerFactory, IRegistryPolicyResolver registryPolicyResolver)
+ {
+ _loggerFactory = loggerFactory;
+ _registryPolicyResolver = registryPolicyResolver;
+ }
+
+ public void Configure(KeyManagementOptions options)
+ {
+ RegistryPolicy context = null;
+ if (_registryPolicyResolver != null)
+ {
+ context = _registryPolicyResolver.ResolvePolicy();
+ }
+
+ if (context != null)
+ {
+ if (context.DefaultKeyLifetime.HasValue)
+ {
+ options.NewKeyLifetime = TimeSpan.FromDays(context.DefaultKeyLifetime.Value);
+ }
+
+ options.AuthenticatedEncryptorConfiguration = context.EncryptorConfiguration;
+
+ var escrowSinks = context.KeyEscrowSinks;
+ if (escrowSinks != null)
+ {
+ foreach (var escrowSink in escrowSinks)
+ {
+ options.KeyEscrowSinks.Add(escrowSink);
+ }
+ }
+ }
+
+ if (options.AuthenticatedEncryptorConfiguration == null)
+ {
+ options.AuthenticatedEncryptorConfiguration = new AuthenticatedEncryptorConfiguration();
+ }
+
+ options.AuthenticatedEncryptorFactories.Add(new CngGcmAuthenticatedEncryptorFactory(_loggerFactory));
+ options.AuthenticatedEncryptorFactories.Add(new CngCbcAuthenticatedEncryptorFactory(_loggerFactory));
+ options.AuthenticatedEncryptorFactories.Add(new ManagedAuthenticatedEncryptorFactory(_loggerFactory));
+ options.AuthenticatedEncryptorFactories.Add(new AuthenticatedEncryptorFactory(_loggerFactory));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DefaultKeyResolver.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DefaultKeyResolver.cs
new file mode 100644
index 0000000000..b4f686c9f3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DefaultKeyResolver.cs
@@ -0,0 +1,146 @@
+// 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.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// Implements policy for resolving the default key from a candidate keyring.
+ /// </summary>
+ internal sealed class DefaultKeyResolver : IDefaultKeyResolver
+ {
+ /// <summary>
+ /// The window of time before the key expires when a new key should be created
+ /// and persisted to the keyring to ensure uninterrupted service.
+ /// </summary>
+ /// <remarks>
+ /// If the propagation time is 5 days and the current key expires within 5 days,
+ /// a new key will be generated.
+ /// </remarks>
+ private readonly TimeSpan _keyPropagationWindow;
+
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The maximum skew that is allowed between servers.
+ /// This is used to allow newly-created keys to be used across servers even though
+ /// their activation dates might be a few minutes into the future.
+ /// </summary>
+ /// <remarks>
+ /// If the max skew is 5 minutes and the best matching candidate default key has
+ /// an activation date of less than 5 minutes in the future, we'll use it.
+ /// </remarks>
+ private readonly TimeSpan _maxServerToServerClockSkew;
+
+ public DefaultKeyResolver(IOptions<KeyManagementOptions> keyManagementOptions)
+ : this(keyManagementOptions, NullLoggerFactory.Instance)
+ { }
+
+ public DefaultKeyResolver(IOptions<KeyManagementOptions> keyManagementOptions, ILoggerFactory loggerFactory)
+ {
+ _keyPropagationWindow = keyManagementOptions.Value.KeyPropagationWindow;
+ _maxServerToServerClockSkew = keyManagementOptions.Value.MaxServerClockSkew;
+ _logger = loggerFactory.CreateLogger<DefaultKeyResolver>();
+ }
+
+ private bool CanCreateAuthenticatedEncryptor(IKey key)
+ {
+ try
+ {
+ var encryptorInstance = key.CreateEncryptor();
+ if (encryptorInstance == null)
+ {
+ CryptoUtil.Fail<IAuthenticatedEncryptor>("CreateEncryptorInstance returned null.");
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.KeyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed(key.KeyId, nameof(IKey.CreateEncryptor), ex);
+ return false;
+ }
+ }
+
+ private IKey FindDefaultKey(DateTimeOffset now, IEnumerable<IKey> allKeys, out IKey fallbackKey, out bool callerShouldGenerateNewKey)
+ {
+ // find the preferred default key (allowing for server-to-server clock skew)
+ var preferredDefaultKey = (from key in allKeys
+ where key.ActivationDate <= now + _maxServerToServerClockSkew
+ orderby key.ActivationDate descending, key.KeyId ascending
+ select key).FirstOrDefault();
+
+ if (preferredDefaultKey != null)
+ {
+ _logger.ConsideringKeyWithExpirationDateAsDefaultKey(preferredDefaultKey.KeyId, preferredDefaultKey.ExpirationDate);
+
+ // if the key has been revoked or is expired, it is no longer a candidate
+ if (preferredDefaultKey.IsRevoked || preferredDefaultKey.IsExpired(now) || !CanCreateAuthenticatedEncryptor(preferredDefaultKey))
+ {
+ _logger.KeyIsNoLongerUnderConsiderationAsDefault(preferredDefaultKey.KeyId);
+ preferredDefaultKey = null;
+ }
+ }
+
+ // Only the key that has been most recently activated is eligible to be the preferred default,
+ // and only if it hasn't expired or been revoked. This is intentional: generating a new key is
+ // an implicit signal that we should stop using older keys (even if they're not revoked), so
+ // activating a new key should permanently mark all older keys as non-preferred.
+
+ if (preferredDefaultKey != null)
+ {
+ // Does *any* key in the key ring fulfill the requirement that its activation date is prior
+ // to the preferred default key's expiration date (allowing for skew) and that it will
+ // remain valid one propagation cycle from now? If so, the caller doesn't need to add a
+ // new key.
+ callerShouldGenerateNewKey = !allKeys.Any(key =>
+ key.ActivationDate <= (preferredDefaultKey.ExpirationDate + _maxServerToServerClockSkew)
+ && !key.IsExpired(now + _keyPropagationWindow)
+ && !key.IsRevoked);
+
+ if (callerShouldGenerateNewKey)
+ {
+ _logger.DefaultKeyExpirationImminentAndRepository();
+ }
+
+ fallbackKey = null;
+ return preferredDefaultKey;
+ }
+
+ // If we got this far, the caller must generate a key now.
+ // We should locate a fallback key, which is a key that can be used to protect payloads if
+ // the caller is configured not to generate a new key. We should try to make sure the fallback
+ // key has propagated to all callers (so its creation date should be before the previous
+ // propagation period), and we cannot use revoked keys. The fallback key may be expired.
+ fallbackKey = (from key in (from key in allKeys
+ where key.CreationDate <= now - _keyPropagationWindow
+ orderby key.CreationDate descending
+ select key).Concat(from key in allKeys
+ orderby key.CreationDate ascending
+ select key)
+ where !key.IsRevoked && CanCreateAuthenticatedEncryptor(key)
+ select key).FirstOrDefault();
+
+ _logger.RepositoryContainsNoViableDefaultKey();
+
+ callerShouldGenerateNewKey = true;
+ return null;
+ }
+
+ public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys)
+ {
+ var retVal = default(DefaultKeyResolution);
+ retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey);
+ return retVal;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DeferredKey.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DeferredKey.cs
new file mode 100644
index 0000000000..a21210aceb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/DeferredKey.cs
@@ -0,0 +1,55 @@
+// 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.Collections.Generic;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// The basic implementation of <see cref="IKey"/>, where the incoming XML element
+ /// hasn't yet been fully processed.
+ /// </summary>
+ internal sealed class DeferredKey : KeyBase
+ {
+ public DeferredKey(
+ Guid keyId,
+ DateTimeOffset creationDate,
+ DateTimeOffset activationDate,
+ DateTimeOffset expirationDate,
+ IInternalXmlKeyManager keyManager,
+ XElement keyElement,
+ IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories)
+ : base(keyId,
+ creationDate,
+ activationDate,
+ expirationDate,
+ new Lazy<IAuthenticatedEncryptorDescriptor>(GetLazyDescriptorDelegate(keyManager, keyElement)),
+ encryptorFactories)
+ {
+ }
+
+ private static Func<IAuthenticatedEncryptorDescriptor> GetLazyDescriptorDelegate(IInternalXmlKeyManager keyManager, XElement keyElement)
+ {
+ // The <key> element will be held around in memory for a potentially lengthy period
+ // of time. Since it might contain sensitive information, we should protect it.
+ var encryptedKeyElement = keyElement.ToSecret();
+
+ try
+ {
+ return () => keyManager.DeserializeDescriptorFromKeyElement(encryptedKeyElement.ToXElement());
+ }
+ finally
+ {
+ // It's important that the lambda above doesn't capture 'descriptorElement'. Clearing the reference here
+ // helps us detect if we've done this by causing a null ref at runtime.
+ keyElement = null;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKey.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKey.cs
new file mode 100644
index 0000000000..f590c01c1b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKey.cs
@@ -0,0 +1,60 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// The basic interface for representing an authenticated encryption key.
+ /// </summary>
+ public interface IKey
+ {
+ /// <summary>
+ /// The date at which encryptions with this key can begin taking place.
+ /// </summary>
+ DateTimeOffset ActivationDate { get; }
+
+ /// <summary>
+ /// The date on which this key was created.
+ /// </summary>
+ DateTimeOffset CreationDate { get; }
+
+ /// <summary>
+ /// The date after which encryptions with this key may no longer take place.
+ /// </summary>
+ /// <remarks>
+ /// An expired key may still be used to decrypt existing payloads.
+ /// </remarks>
+ DateTimeOffset ExpirationDate { get; }
+
+ /// <summary>
+ /// Returns a value stating whether this key was revoked.
+ /// </summary>
+ /// <remarks>
+ /// A revoked key may still be used to decrypt existing payloads, but the payloads
+ /// must be treated as tampered unless the application has some other assurance
+ /// that the payloads are authentic.
+ /// </remarks>
+ bool IsRevoked { get; }
+
+ /// <summary>
+ /// The id of the key.
+ /// </summary>
+ Guid KeyId { get; }
+
+ /// <summary>
+ /// Gets the <see cref="IAuthenticatedEncryptorDescriptor"/> instance associated with this key.
+ /// </summary>
+ IAuthenticatedEncryptorDescriptor Descriptor { get; }
+
+ /// <summary>
+ /// Creates an <see cref="IAuthenticatedEncryptor"/> instance that can be used to encrypt data
+ /// to and decrypt data from this key.
+ /// </summary>
+ /// <returns>An <see cref="IAuthenticatedEncryptor"/>.</returns>
+ IAuthenticatedEncryptor CreateEncryptor();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyEscrowSink.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyEscrowSink.cs
new file mode 100644
index 0000000000..64b94e844e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyEscrowSink.cs
@@ -0,0 +1,27 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// The basic interface for implementing a key escrow sink.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="IKeyEscrowSink"/> is distinct from <see cref="IXmlRepository"/> in that
+ /// <see cref="IKeyEscrowSink"/> provides a write-only interface and instances handle unencrypted key material,
+ /// while <see cref="IXmlRepository"/> provides a read+write interface and instances handle encrypted key material.
+ /// </remarks>
+ public interface IKeyEscrowSink
+ {
+ /// <summary>
+ /// Stores the given key material to the escrow service.
+ /// </summary>
+ /// <param name="keyId">The id of the key being persisted to escrow.</param>
+ /// <param name="element">The unencrypted XML element that comprises the key material.</param>
+ void Store(Guid keyId, XElement element);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyManager.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyManager.cs
new file mode 100644
index 0000000000..6debf4ac96
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/IKeyManager.cs
@@ -0,0 +1,77 @@
+// 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.Collections.Generic;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// The basic interface for performing key management operations.
+ /// </summary>
+ /// <remarks>
+ /// Instantiations of this interface are expected to be thread-safe.
+ /// </remarks>
+ public interface IKeyManager
+ {
+ /// <summary>
+ /// Creates a new key with the specified activation and expiration dates and persists
+ /// the new key to the underlying repository.
+ /// </summary>
+ /// <param name="activationDate">The date on which encryptions to this key may begin.</param>
+ /// <param name="expirationDate">The date after which encryptions to this key may no longer take place.</param>
+ /// <returns>The newly-created IKey instance.</returns>
+ IKey CreateNewKey(DateTimeOffset activationDate, DateTimeOffset expirationDate);
+
+ /// <summary>
+ /// Fetches all keys from the underlying repository.
+ /// </summary>
+ /// <returns>The collection of all keys.</returns>
+ IReadOnlyCollection<IKey> GetAllKeys();
+
+ /// <summary>
+ /// Retrieves a token that signals that callers who have cached the return value of
+ /// GetAllKeys should clear their caches. This could be in response to a call to
+ /// CreateNewKey or RevokeKey, or it could be in response to some other external notification.
+ /// Callers who are interested in observing this token should call this method before the
+ /// corresponding call to GetAllKeys.
+ /// </summary>
+ /// <returns>
+ /// The cache expiration token. When an expiration notification is triggered, any
+ /// tokens previously returned by this method will become canceled, and tokens returned by
+ /// future invocations of this method will themselves not trigger until the next expiration
+ /// event.
+ /// </returns>
+ /// <remarks>
+ /// Implementations are free to return 'CancellationToken.None' from this method.
+ /// Since this token is never guaranteed to fire, callers should still manually
+ /// clear their caches at a regular interval.
+ /// </remarks>
+ CancellationToken GetCacheExpirationToken();
+
+ /// <summary>
+ /// Revokes a specific key and persists the revocation to the underlying repository.
+ /// </summary>
+ /// <param name="keyId">The id of the key to revoke.</param>
+ /// <param name="reason">An optional human-readable reason for revocation.</param>
+ /// <remarks>
+ /// This method will not mutate existing IKey instances. After calling this method,
+ /// all existing IKey instances should be discarded, and GetAllKeys should be called again.
+ /// </remarks>
+ void RevokeKey(Guid keyId, string reason = null);
+
+ /// <summary>
+ /// Revokes all keys created before a specified date and persists the revocation to the
+ /// underlying repository.
+ /// </summary>
+ /// <param name="revocationDate">The revocation date. All keys with a creation date before
+ /// this value will be revoked.</param>
+ /// <param name="reason">An optional human-readable reason for revocation.</param>
+ /// <remarks>
+ /// This method will not mutate existing IKey instances. After calling this method,
+ /// all existing IKey instances should be discarded, and GetAllKeys should be called again.
+ /// </remarks>
+ void RevokeAllKeys(DateTimeOffset revocationDate, string reason = null);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/CacheableKeyRing.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/CacheableKeyRing.cs
new file mode 100644
index 0000000000..ff6fa87fce
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/CacheableKeyRing.cs
@@ -0,0 +1,52 @@
+// 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.Collections.Generic;
+using System.Threading;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ /// <summary>
+ /// Wraps both a keyring and its expiration policy.
+ /// </summary>
+ public sealed class CacheableKeyRing
+ {
+ private readonly CancellationToken _expirationToken;
+
+ internal CacheableKeyRing(CancellationToken expirationToken, DateTimeOffset expirationTime, IKey defaultKey, IEnumerable<IKey> allKeys)
+ : this(expirationToken, expirationTime, keyRing: new KeyRing(defaultKey, allKeys))
+ {
+ }
+
+ internal CacheableKeyRing(CancellationToken expirationToken, DateTimeOffset expirationTime, IKeyRing keyRing)
+ {
+ _expirationToken = expirationToken;
+ ExpirationTimeUtc = expirationTime.UtcDateTime;
+ KeyRing = keyRing;
+ }
+
+ internal DateTime ExpirationTimeUtc { get; }
+
+ internal IKeyRing KeyRing { get; }
+
+ internal static bool IsValid(CacheableKeyRing keyRing, DateTime utcNow)
+ {
+ return keyRing != null
+ && !keyRing._expirationToken.IsCancellationRequested
+ && keyRing.ExpirationTimeUtc > utcNow;
+ }
+
+ /// <summary>
+ /// Returns a new <see cref="CacheableKeyRing"/> which is identical to 'this' but with a
+ /// lifetime extended 2 minutes from <paramref name="now"/>. The inner cancellation token
+ /// is also disconnected.
+ /// </summary>
+ internal CacheableKeyRing WithTemporaryExtendedLifetime(DateTimeOffset now)
+ {
+ var extension = TimeSpan.FromMinutes(2);
+ return new CacheableKeyRing(CancellationToken.None, now + extension, KeyRing);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/DefaultKeyResolution.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/DefaultKeyResolution.cs
new file mode 100644
index 0000000000..1c4170607b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/DefaultKeyResolution.cs
@@ -0,0 +1,36 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ public struct DefaultKeyResolution
+ {
+ /// <summary>
+ /// The default key, may be null if no key is a good default candidate.
+ /// </summary>
+ /// <remarks>
+ /// If this property is non-null, its <see cref="IKey.CreateEncryptor()"/> method will succeed
+ /// so is appropriate for use with deferred keys.
+ /// </remarks>
+ public IKey DefaultKey;
+
+ /// <summary>
+ /// The fallback key, which should be used only if the caller is configured not to
+ /// honor the <see cref="ShouldGenerateNewKey"/> property. This property may
+ /// be null if there is no viable fallback key.
+ /// </summary>
+ /// <remarks>
+ /// If this property is non-null, its <see cref="IKey.CreateEncryptor()"/> method will succeed
+ /// so is appropriate for use with deferred keys.
+ /// </remarks>
+ public IKey FallbackKey;
+
+ /// <summary>
+ /// 'true' if a new key should be persisted to the keyring, 'false' otherwise.
+ /// This value may be 'true' even if a valid default key was found.
+ /// </summary>
+ public bool ShouldGenerateNewKey;
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/ICacheableKeyRingProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/ICacheableKeyRingProvider.cs
new file mode 100644
index 0000000000..367080f2b8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/ICacheableKeyRingProvider.cs
@@ -0,0 +1,12 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ public interface ICacheableKeyRingProvider
+ {
+ CacheableKeyRing GetCacheableKeyRing(DateTimeOffset now);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IDefaultKeyResolver.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IDefaultKeyResolver.cs
new file mode 100644
index 0000000000..f891d0d4fb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IDefaultKeyResolver.cs
@@ -0,0 +1,19 @@
+// 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.Collections.Generic;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ /// <summary>
+ /// Implements policy for resolving the default key from a candidate keyring.
+ /// </summary>
+ public interface IDefaultKeyResolver
+ {
+ /// <summary>
+ /// Locates the default key from the keyring.
+ /// </summary>
+ DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable<IKey> allKeys);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IInternalXmlKeyManager.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IInternalXmlKeyManager.cs
new file mode 100644
index 0000000000..9ebaa4c63c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IInternalXmlKeyManager.cs
@@ -0,0 +1,18 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ public interface IInternalXmlKeyManager
+ {
+ IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate);
+
+ IAuthenticatedEncryptorDescriptor DeserializeDescriptorFromKeyElement(XElement keyElement);
+
+ void RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRing.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRing.cs
new file mode 100644
index 0000000000..60ff02f2ed
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRing.cs
@@ -0,0 +1,36 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ /// <summary>
+ /// The basic interface for accessing a read-only keyring.
+ /// </summary>
+ public interface IKeyRing
+ {
+ /// <summary>
+ /// The authenticated encryptor that shall be used for new encryption operations.
+ /// </summary>
+ /// <remarks>
+ /// Activation of the encryptor instance is deferred until first access.
+ /// </remarks>
+ IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; }
+
+ /// <summary>
+ /// The id of the key associated with <see cref="DefaultAuthenticatedEncryptor"/>.
+ /// </summary>
+ Guid DefaultKeyId { get; }
+
+ /// <summary>
+ /// Returns an encryptor instance for the given key, or 'null' if the key with the
+ /// specified id cannot be found in the keyring.
+ /// </summary>
+ /// <remarks>
+ /// Activation of the encryptor instance is deferred until first access.
+ /// </remarks>
+ IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRingProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRingProvider.cs
new file mode 100644
index 0000000000..3a507f1250
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Internal/IKeyRingProvider.cs
@@ -0,0 +1,10 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement.Internal
+{
+ public interface IKeyRingProvider
+ {
+ IKeyRing GetCurrentKeyRing();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Key.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Key.cs
new file mode 100644
index 0000000000..84569a8e1b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/Key.cs
@@ -0,0 +1,33 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// The basic implementation of <see cref="IKey"/>, where the <see cref="IAuthenticatedEncryptorDescriptor"/>
+ /// has already been created.
+ /// </summary>
+ internal sealed class Key : KeyBase
+ {
+ public Key(
+ Guid keyId,
+ DateTimeOffset creationDate,
+ DateTimeOffset activationDate,
+ DateTimeOffset expirationDate,
+ IAuthenticatedEncryptorDescriptor descriptor,
+ IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories)
+ : base(keyId,
+ creationDate,
+ activationDate,
+ expirationDate,
+ new Lazy<IAuthenticatedEncryptorDescriptor>(() => descriptor),
+ encryptorFactories)
+ {
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyBase.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyBase.cs
new file mode 100644
index 0000000000..005a6ea9d5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyBase.cs
@@ -0,0 +1,78 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// The basic implementation of <see cref="IKey"/>.
+ /// </summary>
+ internal abstract class KeyBase : IKey
+ {
+ private readonly Lazy<IAuthenticatedEncryptorDescriptor> _lazyDescriptor;
+ private readonly IEnumerable<IAuthenticatedEncryptorFactory> _encryptorFactories;
+
+ private IAuthenticatedEncryptor _encryptor;
+
+ public KeyBase(
+ Guid keyId,
+ DateTimeOffset creationDate,
+ DateTimeOffset activationDate,
+ DateTimeOffset expirationDate,
+ Lazy<IAuthenticatedEncryptorDescriptor> lazyDescriptor,
+ IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories)
+ {
+ KeyId = keyId;
+ CreationDate = creationDate;
+ ActivationDate = activationDate;
+ ExpirationDate = expirationDate;
+ _lazyDescriptor = lazyDescriptor;
+ _encryptorFactories = encryptorFactories;
+ }
+
+ public DateTimeOffset ActivationDate { get; }
+
+ public DateTimeOffset CreationDate { get; }
+
+ public DateTimeOffset ExpirationDate { get; }
+
+ public bool IsRevoked { get; private set; }
+
+ public Guid KeyId { get; }
+
+ public IAuthenticatedEncryptorDescriptor Descriptor
+ {
+ get
+ {
+ return _lazyDescriptor.Value;
+ }
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptor()
+ {
+ if (_encryptor == null)
+ {
+ foreach (var factory in _encryptorFactories)
+ {
+ var encryptor = factory.CreateEncryptorInstance(this);
+ if (encryptor != null)
+ {
+ _encryptor = encryptor;
+ break;
+ }
+ }
+ }
+
+ return _encryptor;
+ }
+
+ internal void SetRevoked()
+ {
+ IsRevoked = true;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs
new file mode 100644
index 0000000000..85f1f62451
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs
@@ -0,0 +1,42 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ internal static class KeyEscrowServiceProviderExtensions
+ {
+ /// <summary>
+ /// Gets an aggregate <see cref="IKeyEscrowSink"/> from the underlying <see cref="IServiceProvider"/>.
+ /// This method may return null if no sinks are registered.
+ /// </summary>
+ public static IKeyEscrowSink GetKeyEscrowSink(this IServiceProvider services)
+ {
+ var escrowSinks = services?.GetService<IEnumerable<IKeyEscrowSink>>()?.ToList();
+ return (escrowSinks != null && escrowSinks.Count > 0) ? new AggregateKeyEscrowSink(escrowSinks) : null;
+ }
+
+ private sealed class AggregateKeyEscrowSink : IKeyEscrowSink
+ {
+ private readonly List<IKeyEscrowSink> _sinks;
+
+ public AggregateKeyEscrowSink(List<IKeyEscrowSink> sinks)
+ {
+ _sinks = sinks;
+ }
+
+ public void Store(Guid keyId, XElement element)
+ {
+ foreach (var sink in _sinks)
+ {
+ sink.Store(keyId, element);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyExtensions.cs
new file mode 100644
index 0000000000..5cd05bdb9b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyExtensions.cs
@@ -0,0 +1,15 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ internal static class KeyExtensions
+ {
+ public static bool IsExpired(this IKey key, DateTimeOffset now)
+ {
+ return (key.ExpirationDate <= now);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyManagementOptions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyManagementOptions.cs
new file mode 100644
index 0000000000..0680239f6b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyManagementOptions.cs
@@ -0,0 +1,168 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// Options that control how an <see cref="IKeyManager"/> should behave.
+ /// </summary>
+ public class KeyManagementOptions
+ {
+ private static readonly TimeSpan _keyPropagationWindow = TimeSpan.FromDays(2);
+ private static readonly TimeSpan _keyRingRefreshPeriod = TimeSpan.FromHours(24);
+ private static readonly TimeSpan _maxServerClockSkew = TimeSpan.FromMinutes(5);
+ private TimeSpan _newKeyLifetime = TimeSpan.FromDays(90);
+
+ public KeyManagementOptions()
+ {
+ }
+
+ // copy ctor
+ internal KeyManagementOptions(KeyManagementOptions other)
+ {
+ if (other != null)
+ {
+ AutoGenerateKeys = other.AutoGenerateKeys;
+ _newKeyLifetime = other._newKeyLifetime;
+ XmlEncryptor = other.XmlEncryptor;
+ XmlRepository = other.XmlRepository;
+ AuthenticatedEncryptorConfiguration = other.AuthenticatedEncryptorConfiguration;
+
+ foreach (var keyEscrowSink in other.KeyEscrowSinks)
+ {
+ KeyEscrowSinks.Add(keyEscrowSink);
+ }
+
+ foreach (var encryptorFactory in other.AuthenticatedEncryptorFactories)
+ {
+ AuthenticatedEncryptorFactories.Add(encryptorFactory);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Specifies whether the data protection system should auto-generate keys.
+ /// </summary>
+ /// <remarks>
+ /// If this value is 'false', the system will not generate new keys automatically.
+ /// The key ring must contain at least one active non-revoked key, otherwise calls
+ /// to <see cref="IDataProtector.Protect(byte[])"/> may fail. The system may end up
+ /// protecting payloads to expired keys if this property is set to 'false'.
+ /// The default value is 'true'.
+ /// </remarks>
+ public bool AutoGenerateKeys { get; set; } = true;
+
+ /// <summary>
+ /// Specifies the period before key expiration in which a new key should be generated
+ /// so that it has time to propagate fully throughout the key ring. For example, if this
+ /// period is 72 hours, then a new key will be created and persisted to storage
+ /// approximately 72 hours before expiration.
+ /// </summary>
+ /// <remarks>
+ /// This value is currently fixed at 48 hours.
+ /// </remarks>
+ internal TimeSpan KeyPropagationWindow
+ {
+ get
+ {
+ // This value is not settable since there's a complex interaction between
+ // it and the key ring refresh period.
+ return _keyPropagationWindow;
+ }
+ }
+
+ /// <summary>
+ /// Controls the auto-refresh period where the key ring provider will
+ /// flush its collection of cached keys and reread the collection from
+ /// backing storage.
+ /// </summary>
+ /// <remarks>
+ /// This value is currently fixed at 24 hours.
+ /// </remarks>
+ internal TimeSpan KeyRingRefreshPeriod
+ {
+ get
+ {
+ // This value is not settable since there's a complex interaction between
+ // it and the key expiration safety period.
+ return _keyRingRefreshPeriod;
+ }
+ }
+
+ /// <summary>
+ /// Specifies the maximum clock skew allowed between servers when reading
+ /// keys from the key ring. The key ring may use a key which has not yet
+ /// been activated or which has expired if the key's valid lifetime is within
+ /// the allowed clock skew window. This value can be set to <see cref="TimeSpan.Zero"/>
+ /// if key activation and expiration times should be strictly honored by this server.
+ /// </summary>
+ /// <remarks>
+ /// This value is currently fixed at 5 minutes.
+ /// </remarks>
+ internal TimeSpan MaxServerClockSkew
+ {
+ get
+ {
+ return _maxServerClockSkew;
+ }
+ }
+
+ /// <summary>
+ /// Controls the lifetime (number of days before expiration)
+ /// for newly-generated keys.
+ /// </summary>
+ /// <remarks>
+ /// The lifetime cannot be less than one week.
+ /// The default value is 90 days.
+ /// </remarks>
+ public TimeSpan NewKeyLifetime
+ {
+ get
+ {
+ return _newKeyLifetime;
+ }
+ set
+ {
+ if (value < TimeSpan.FromDays(7))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), Resources.KeyManagementOptions_MinNewKeyLifetimeViolated);
+ }
+ _newKeyLifetime = value;
+ }
+ }
+
+ /// <summary>
+ /// The <see cref="AlgorithmConfiguration"/> instance that can be used to create
+ /// the <see cref="IAuthenticatedEncryptorDescriptor"/> instance.
+ /// </summary>
+ public AlgorithmConfiguration AuthenticatedEncryptorConfiguration { get; set; }
+
+ /// <summary>
+ /// The list of <see cref="IKeyEscrowSink"/> to store the key material in.
+ /// </summary>
+ public IList<IKeyEscrowSink> KeyEscrowSinks { get; } = new List<IKeyEscrowSink>();
+
+ /// <summary>
+ /// The <see cref="IXmlRepository"/> to use for storing and retrieving XML elements.
+ /// </summary>
+ public IXmlRepository XmlRepository { get; set; }
+
+ /// <summary>
+ /// The <see cref="IXmlEncryptor"/> to use for encrypting XML elements.
+ /// </summary>
+ public IXmlEncryptor XmlEncryptor { get; set; }
+
+ /// <summary>
+ /// The list of <see cref="IAuthenticatedEncryptorFactory"/> that will be used for creating
+ /// <see cref="IAuthenticatedEncryptor"/>s.
+ /// </summary>
+ public IList<IAuthenticatedEncryptorFactory> AuthenticatedEncryptorFactories { get; } = new List<IAuthenticatedEncryptorFactory>();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRing.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRing.cs
new file mode 100644
index 0000000000..2bbba031a6
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRing.cs
@@ -0,0 +1,92 @@
+// 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.Collections.Generic;
+using System.Threading;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// A basic implementation of <see cref="IKeyRing"/>.
+ /// </summary>
+ internal sealed class KeyRing : IKeyRing
+ {
+ private readonly KeyHolder _defaultKeyHolder;
+ private readonly Dictionary<Guid, KeyHolder> _keyIdToKeyHolderMap;
+
+ public KeyRing(IKey defaultKey, IEnumerable<IKey> allKeys)
+ {
+ _keyIdToKeyHolderMap = new Dictionary<Guid, KeyHolder>();
+ foreach (IKey key in allKeys)
+ {
+ _keyIdToKeyHolderMap.Add(key.KeyId, new KeyHolder(key));
+ }
+
+ // It's possible under some circumstances that the default key won't be part of 'allKeys',
+ // such as if the key manager is forced to use the key it just generated even if such key
+ // wasn't in the underlying repository. In this case, we just add it now.
+ if (!_keyIdToKeyHolderMap.ContainsKey(defaultKey.KeyId))
+ {
+ _keyIdToKeyHolderMap.Add(defaultKey.KeyId, new KeyHolder(defaultKey));
+ }
+
+ DefaultKeyId = defaultKey.KeyId;
+ _defaultKeyHolder = _keyIdToKeyHolderMap[DefaultKeyId];
+ }
+
+ public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor
+ {
+ get
+ {
+ bool unused;
+ return _defaultKeyHolder.GetEncryptorInstance(out unused);
+ }
+ }
+
+ public Guid DefaultKeyId { get; }
+
+ public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked)
+ {
+ isRevoked = false;
+ KeyHolder holder;
+ _keyIdToKeyHolderMap.TryGetValue(keyId, out holder);
+ return holder?.GetEncryptorInstance(out isRevoked);
+ }
+
+ // used for providing lazy activation of the authenticated encryptor instance
+ private sealed class KeyHolder
+ {
+ private readonly IKey _key;
+ private IAuthenticatedEncryptor _encryptor;
+
+ internal KeyHolder(IKey key)
+ {
+ _key = key;
+ }
+
+ internal IAuthenticatedEncryptor GetEncryptorInstance(out bool isRevoked)
+ {
+ // simple double-check lock pattern
+ // we can't use LazyInitializer<T> because we don't have a simple value factory
+ IAuthenticatedEncryptor encryptor = Volatile.Read(ref _encryptor);
+ if (encryptor == null)
+ {
+ lock (this)
+ {
+ encryptor = Volatile.Read(ref _encryptor);
+ if (encryptor == null)
+ {
+ encryptor = _key.CreateEncryptor();
+ Volatile.Write(ref _encryptor, encryptor);
+ }
+ }
+ }
+ isRevoked = _key.IsRevoked;
+ return encryptor;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs
new file mode 100644
index 0000000000..f7f785cc3b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs
@@ -0,0 +1,35 @@
+// 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 Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ internal unsafe sealed class KeyRingBasedDataProtectionProvider : IDataProtectionProvider
+ {
+ private readonly IKeyRingProvider _keyRingProvider;
+ private readonly ILogger _logger;
+
+ public KeyRingBasedDataProtectionProvider(IKeyRingProvider keyRingProvider, ILoggerFactory loggerFactory)
+ {
+ _keyRingProvider = keyRingProvider;
+ _logger = loggerFactory.CreateLogger<KeyRingBasedDataProtector>(); // note: for protector (not provider!) type
+ }
+
+ public IDataProtector CreateProtector(string purpose)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ return new KeyRingBasedDataProtector(
+ logger: _logger,
+ keyRingProvider: _keyRingProvider,
+ originalPurposes: null,
+ newPurpose: purpose);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs
new file mode 100644
index 0000000000..e0157e66fe
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs
@@ -0,0 +1,396 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ internal unsafe sealed class KeyRingBasedDataProtector : IDataProtector, IPersistedDataProtector
+ {
+ // This magic header identifies a v0 protected data blob. It's the high 28 bits of the SHA1 hash of
+ // "Microsoft.AspNet.DataProtection.KeyManagement.KeyRingBasedDataProtector" [US-ASCII], big-endian.
+ // The last nibble reserved for version information. There's also the nice property that "F0 C9"
+ // can never appear in a well-formed UTF8 sequence, so attempts to treat a protected payload as a
+ // UTF8-encoded string will fail, and devs can catch the mistake early.
+ private const uint MAGIC_HEADER_V0 = 0x09F0C9F0;
+
+ private AdditionalAuthenticatedDataTemplate _aadTemplate;
+ private readonly IKeyRingProvider _keyRingProvider;
+ private readonly ILogger _logger;
+
+ public KeyRingBasedDataProtector(IKeyRingProvider keyRingProvider, ILogger logger, string[] originalPurposes, string newPurpose)
+ {
+ Debug.Assert(keyRingProvider != null);
+
+ Purposes = ConcatPurposes(originalPurposes, newPurpose);
+ _logger = logger; // can be null
+ _keyRingProvider = keyRingProvider;
+ _aadTemplate = new AdditionalAuthenticatedDataTemplate(Purposes);
+ }
+
+ internal string[] Purposes { get; }
+
+ private static string[] ConcatPurposes(string[] originalPurposes, string newPurpose)
+ {
+ if (originalPurposes != null && originalPurposes.Length > 0)
+ {
+ var newPurposes = new string[originalPurposes.Length + 1];
+ Array.Copy(originalPurposes, 0, newPurposes, 0, originalPurposes.Length);
+ newPurposes[originalPurposes.Length] = newPurpose;
+ return newPurposes;
+ }
+ else
+ {
+ return new string[] { newPurpose };
+ }
+ }
+
+ public IDataProtector CreateProtector(string purpose)
+ {
+ if (purpose == null)
+ {
+ throw new ArgumentNullException(nameof(purpose));
+ }
+
+ return new KeyRingBasedDataProtector(
+ logger: _logger,
+ keyRingProvider: _keyRingProvider,
+ originalPurposes: Purposes,
+ newPurpose: purpose);
+ }
+
+ private static string JoinPurposesForLog(IEnumerable<string> purposes)
+ {
+ return "(" + String.Join(", ", purposes.Select(p => "'" + p + "'")) + ")";
+ }
+
+ // allows decrypting payloads whose keys have been revoked
+ public byte[] DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked)
+ {
+ // argument & state checking
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ UnprotectStatus status;
+ var retVal = UnprotectCore(protectedData, ignoreRevocationErrors, status: out status);
+ requiresMigration = (status != UnprotectStatus.Ok);
+ wasRevoked = (status == UnprotectStatus.DecryptionKeyWasRevoked);
+ return retVal;
+ }
+
+ public byte[] Protect(byte[] plaintext)
+ {
+ if (plaintext == null)
+ {
+ throw new ArgumentNullException(nameof(plaintext));
+ }
+
+ try
+ {
+ // Perform the encryption operation using the current default encryptor.
+ var currentKeyRing = _keyRingProvider.GetCurrentKeyRing();
+ var defaultKeyId = currentKeyRing.DefaultKeyId;
+ var defaultEncryptorInstance = currentKeyRing.DefaultAuthenticatedEncryptor;
+ CryptoUtil.Assert(defaultEncryptorInstance != null, "defaultEncryptorInstance != null");
+
+ if (_logger.IsDebugLevelEnabled())
+ {
+ _logger.PerformingProtectOperationToKeyWithPurposes(defaultKeyId, JoinPurposesForLog(Purposes));
+ }
+
+ // We'll need to apply the default key id to the template if it hasn't already been applied.
+ // If the default key id has been updated since the last call to Protect, also write back the updated template.
+ var aad = _aadTemplate.GetAadForKey(defaultKeyId, isProtecting: true);
+
+ // We allocate a 20-byte pre-buffer so that we can inject the magic header and key id into the return value.
+ var retVal = defaultEncryptorInstance.Encrypt(
+ plaintext: new ArraySegment<byte>(plaintext),
+ additionalAuthenticatedData: new ArraySegment<byte>(aad),
+ preBufferSize: (uint)(sizeof(uint) + sizeof(Guid)),
+ postBufferSize: 0);
+ CryptoUtil.Assert(retVal != null && retVal.Length >= sizeof(uint) + sizeof(Guid), "retVal != null && retVal.Length >= sizeof(uint) + sizeof(Guid)");
+
+ // At this point: retVal := { 000..000 || encryptorSpecificProtectedPayload },
+ // where 000..000 is a placeholder for our magic header and key id.
+
+ // Write out the magic header and key id
+ fixed (byte* pbRetVal = retVal)
+ {
+ WriteBigEndianInteger(pbRetVal, MAGIC_HEADER_V0);
+ Write32bitAlignedGuid(&pbRetVal[sizeof(uint)], defaultKeyId);
+ }
+
+ // At this point, retVal := { magicHeader || keyId || encryptorSpecificProtectedPayload }
+ // And we're done!
+ return retVal;
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // homogenize all errors to CryptographicException
+ throw Error.Common_EncryptionFailed(ex);
+ }
+ }
+
+ // Helper function to read a GUID from a 32-bit alignment; useful on architectures where unaligned reads
+ // can result in weird behaviors at runtime.
+ private static Guid Read32bitAlignedGuid(void* ptr)
+ {
+ Debug.Assert((long)ptr % 4 == 0);
+
+ Guid retVal;
+ ((int*)&retVal)[0] = ((int*)ptr)[0];
+ ((int*)&retVal)[1] = ((int*)ptr)[1];
+ ((int*)&retVal)[2] = ((int*)ptr)[2];
+ ((int*)&retVal)[3] = ((int*)ptr)[3];
+ return retVal;
+ }
+
+ private static uint ReadBigEndian32BitInteger(byte* ptr)
+ {
+ return ((uint)ptr[0] << 24)
+ | ((uint)ptr[1] << 16)
+ | ((uint)ptr[2] << 8)
+ | ((uint)ptr[3]);
+ }
+
+ private static bool TryGetVersionFromMagicHeader(uint magicHeader, out int version)
+ {
+ const uint MAGIC_HEADER_VERSION_MASK = 0xFU;
+ if ((magicHeader & ~MAGIC_HEADER_VERSION_MASK) == MAGIC_HEADER_V0)
+ {
+ version = (int)(magicHeader & MAGIC_HEADER_VERSION_MASK);
+ return true;
+ }
+ else
+ {
+ version = default(int);
+ return false;
+ }
+ }
+
+ public byte[] Unprotect(byte[] protectedData)
+ {
+ if (protectedData == null)
+ {
+ throw new ArgumentNullException(nameof(protectedData));
+ }
+
+ // Argument checking will be done by the callee
+ bool requiresMigration, wasRevoked; // unused
+ return DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: false,
+ requiresMigration: out requiresMigration,
+ wasRevoked: out wasRevoked);
+ }
+
+ private byte[] UnprotectCore(byte[] protectedData, bool allowOperationsOnRevokedKeys, out UnprotectStatus status)
+ {
+ Debug.Assert(protectedData != null);
+
+ try
+ {
+ // argument & state checking
+ if (protectedData.Length < sizeof(uint) /* magic header */ + sizeof(Guid) /* key id */)
+ {
+ // payload must contain at least the magic header and key id
+ throw Error.ProtectionProvider_BadMagicHeader();
+ }
+
+ // Need to check that protectedData := { magicHeader || keyId || encryptorSpecificProtectedPayload }
+
+ // Parse the payload version number and key id.
+ uint magicHeaderFromPayload;
+ Guid keyIdFromPayload;
+ fixed (byte* pbInput = protectedData)
+ {
+ magicHeaderFromPayload = ReadBigEndian32BitInteger(pbInput);
+ keyIdFromPayload = Read32bitAlignedGuid(&pbInput[sizeof(uint)]);
+ }
+
+ // Are the magic header and version information correct?
+ int payloadVersion;
+ if (!TryGetVersionFromMagicHeader(magicHeaderFromPayload, out payloadVersion))
+ {
+ throw Error.ProtectionProvider_BadMagicHeader();
+ }
+ else if (payloadVersion != 0)
+ {
+ throw Error.ProtectionProvider_BadVersion();
+ }
+
+ if (_logger.IsDebugLevelEnabled())
+ {
+ _logger.PerformingUnprotectOperationToKeyWithPurposes(keyIdFromPayload, JoinPurposesForLog(Purposes));
+ }
+
+ // Find the correct encryptor in the keyring.
+ bool keyWasRevoked;
+ var currentKeyRing = _keyRingProvider.GetCurrentKeyRing();
+ var requestedEncryptor = currentKeyRing.GetAuthenticatedEncryptorByKeyId(keyIdFromPayload, out keyWasRevoked);
+ if (requestedEncryptor == null)
+ {
+ _logger.KeyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(keyIdFromPayload);
+ throw Error.Common_KeyNotFound(keyIdFromPayload);
+ }
+
+ // Do we need to notify the caller that he should reprotect the data?
+ status = UnprotectStatus.Ok;
+ if (keyIdFromPayload != currentKeyRing.DefaultKeyId)
+ {
+ status = UnprotectStatus.DefaultEncryptionKeyChanged;
+ }
+
+ // Do we need to notify the caller that this key was revoked?
+ if (keyWasRevoked)
+ {
+ if (allowOperationsOnRevokedKeys)
+ {
+ _logger.KeyWasRevokedCallerRequestedUnprotectOperationProceedRegardless(keyIdFromPayload);
+ status = UnprotectStatus.DecryptionKeyWasRevoked;
+ }
+ else
+ {
+ _logger.KeyWasRevokedUnprotectOperationCannotProceed(keyIdFromPayload);
+ throw Error.Common_KeyRevoked(keyIdFromPayload);
+ }
+ }
+
+ // Perform the decryption operation.
+ ArraySegment<byte> ciphertext = new ArraySegment<byte>(protectedData, sizeof(uint) + sizeof(Guid), protectedData.Length - (sizeof(uint) + sizeof(Guid))); // chop off magic header + encryptor id
+ ArraySegment<byte> additionalAuthenticatedData = new ArraySegment<byte>(_aadTemplate.GetAadForKey(keyIdFromPayload, isProtecting: false));
+
+ // At this point, cipherText := { encryptorSpecificPayload },
+ // so all that's left is to invoke the decryption routine directly.
+ return requestedEncryptor.Decrypt(ciphertext, additionalAuthenticatedData)
+ ?? CryptoUtil.Fail<byte[]>("IAuthenticatedEncryptor.Decrypt returned null.");
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // homogenize all failures to CryptographicException
+ throw Error.DecryptionFailed(ex);
+ }
+ }
+
+ // Helper function to write a GUID to a 32-bit alignment; useful on ARM where unaligned reads
+ // can result in weird behaviors at runtime.
+ private static void Write32bitAlignedGuid(void* ptr, Guid value)
+ {
+ Debug.Assert((long)ptr % 4 == 0);
+
+ ((int*)ptr)[0] = ((int*)&value)[0];
+ ((int*)ptr)[1] = ((int*)&value)[1];
+ ((int*)ptr)[2] = ((int*)&value)[2];
+ ((int*)ptr)[3] = ((int*)&value)[3];
+ }
+
+ private static void WriteBigEndianInteger(byte* ptr, uint value)
+ {
+ ptr[0] = (byte)(value >> 24);
+ ptr[1] = (byte)(value >> 16);
+ ptr[2] = (byte)(value >> 8);
+ ptr[3] = (byte)(value);
+ }
+
+ private struct AdditionalAuthenticatedDataTemplate
+ {
+ private byte[] _aadTemplate;
+
+ public AdditionalAuthenticatedDataTemplate(IEnumerable<string> purposes)
+ {
+ const int MEMORYSTREAM_DEFAULT_CAPACITY = 0x100; // matches MemoryStream.EnsureCapacity
+ var ms = new MemoryStream(MEMORYSTREAM_DEFAULT_CAPACITY);
+
+ // additionalAuthenticatedData := { magicHeader (32-bit) || keyId || purposeCount (32-bit) || (purpose)* }
+ // purpose := { utf8ByteCount (7-bit encoded) || utf8Text }
+
+ using (var writer = new PurposeBinaryWriter(ms))
+ {
+ writer.WriteBigEndian(MAGIC_HEADER_V0);
+ Debug.Assert(ms.Position == sizeof(uint));
+ var posPurposeCount = writer.Seek(sizeof(Guid), SeekOrigin.Current); // skip over where the key id will be stored; we'll fill it in later
+ writer.Seek(sizeof(uint), SeekOrigin.Current); // skip over where the purposeCount will be stored; we'll fill it in later
+
+ uint purposeCount = 0;
+ foreach (string purpose in purposes)
+ {
+ Debug.Assert(purpose != null);
+ writer.Write(purpose); // prepends length as a 7-bit encoded integer
+ purposeCount++;
+ }
+
+ // Once we have written all the purposes, go back and fill in 'purposeCount'
+ writer.Seek(checked((int)posPurposeCount), SeekOrigin.Begin);
+ writer.WriteBigEndian(purposeCount);
+ }
+
+ _aadTemplate = ms.ToArray();
+ }
+
+ public byte[] GetAadForKey(Guid keyId, bool isProtecting)
+ {
+ // Multiple threads might be trying to read and write the _aadTemplate field
+ // simultaneously. We need to make sure all accesses to it are thread-safe.
+ var existingTemplate = Volatile.Read(ref _aadTemplate);
+ Debug.Assert(existingTemplate.Length >= sizeof(uint) /* MAGIC_HEADER */ + sizeof(Guid) /* keyId */);
+
+ // If the template is already initialized to this key id, return it.
+ // The caller will not mutate it.
+ fixed (byte* pExistingTemplate = existingTemplate)
+ {
+ if (Read32bitAlignedGuid(&pExistingTemplate[sizeof(uint)]) == keyId)
+ {
+ return existingTemplate;
+ }
+ }
+
+ // Clone since we're about to make modifications.
+ // If this is an encryption operation, we only ever encrypt to the default key,
+ // so we should replace the existing template. This could occur after the protector
+ // has already been created, such as when the underlying key ring has been modified.
+ byte[] newTemplate = (byte[])existingTemplate.Clone();
+ fixed (byte* pNewTemplate = newTemplate)
+ {
+ Write32bitAlignedGuid(&pNewTemplate[sizeof(uint)], keyId);
+ if (isProtecting)
+ {
+ Volatile.Write(ref _aadTemplate, newTemplate);
+ }
+ return newTemplate;
+ }
+ }
+
+ private sealed class PurposeBinaryWriter : BinaryWriter
+ {
+ public PurposeBinaryWriter(MemoryStream stream) : base(stream, EncodingUtil.SecureUtf8Encoding, leaveOpen: true) { }
+
+ // Writes a big-endian 32-bit integer to the underlying stream.
+ public void WriteBigEndian(uint value)
+ {
+ var outStream = BaseStream; // property accessor also performs a flush
+ outStream.WriteByte((byte)(value >> 24));
+ outStream.WriteByte((byte)(value >> 16));
+ outStream.WriteByte((byte)(value >> 8));
+ outStream.WriteByte((byte)(value));
+ }
+ }
+ }
+
+ private enum UnprotectStatus
+ {
+ Ok,
+ DefaultEncryptionKeyChanged,
+ DecryptionKeyWasRevoked
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingProvider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingProvider.cs
new file mode 100644
index 0000000000..e407ae62dd
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/KeyRingProvider.cs
@@ -0,0 +1,257 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ internal sealed class KeyRingProvider : ICacheableKeyRingProvider, IKeyRingProvider
+ {
+ private CacheableKeyRing _cacheableKeyRing;
+ private readonly object _cacheableKeyRingLockObj = new object();
+ private readonly IDefaultKeyResolver _defaultKeyResolver;
+ private readonly KeyManagementOptions _keyManagementOptions;
+ private readonly IKeyManager _keyManager;
+ private readonly ILogger _logger;
+
+ public KeyRingProvider(
+ IKeyManager keyManager,
+ IOptions<KeyManagementOptions> keyManagementOptions,
+ IDefaultKeyResolver defaultKeyResolver)
+ : this(
+ keyManager,
+ keyManagementOptions,
+ defaultKeyResolver,
+ NullLoggerFactory.Instance)
+ {
+ }
+
+ public KeyRingProvider(
+ IKeyManager keyManager,
+ IOptions<KeyManagementOptions> keyManagementOptions,
+ IDefaultKeyResolver defaultKeyResolver,
+ ILoggerFactory loggerFactory)
+ {
+ _keyManagementOptions = new KeyManagementOptions(keyManagementOptions.Value); // clone so new instance is immutable
+ _keyManager = keyManager;
+ CacheableKeyRingProvider = this;
+ _defaultKeyResolver = defaultKeyResolver;
+ _logger = loggerFactory.CreateLogger<KeyRingProvider>();
+ }
+
+ // for testing
+ internal ICacheableKeyRingProvider CacheableKeyRingProvider { get; set; }
+
+ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey keyJustAdded)
+ {
+ // Refresh the list of all keys
+ var cacheExpirationToken = _keyManager.GetCacheExpirationToken();
+ var allKeys = _keyManager.GetAllKeys();
+
+ // Fetch the current default key from the list of all keys
+ var defaultKeyPolicy = _defaultKeyResolver.ResolveDefaultKeyPolicy(now, allKeys);
+ if (!defaultKeyPolicy.ShouldGenerateNewKey)
+ {
+ CryptoUtil.Assert(defaultKeyPolicy.DefaultKey != null, "Expected to see a default key.");
+ return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, defaultKeyPolicy.DefaultKey, allKeys);
+ }
+
+ _logger.PolicyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing();
+
+ // We shouldn't call CreateKey more than once, else we risk stack diving. This code path shouldn't
+ // get hit unless there was an ineligible key with an activation date slightly later than the one we
+ // just added. If this does happen, then we'll just use whatever key we can instead of creating
+ // new keys endlessly, eventually falling back to the one we just added if all else fails.
+ if (keyJustAdded != null)
+ {
+ var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded;
+ return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
+ }
+
+ // At this point, we know we need to generate a new key.
+
+ // We have been asked to generate a new key, but auto-generation of keys has been disabled.
+ // We need to use the fallback key or fail.
+ if (!_keyManagementOptions.AutoGenerateKeys)
+ {
+ var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey;
+ if (keyToUse == null)
+ {
+ _logger.KeyRingDoesNotContainValidDefaultKey();
+ throw new InvalidOperationException(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled);
+ }
+ else
+ {
+ _logger.UsingFallbackKeyWithExpirationAsDefaultKey(keyToUse.KeyId, keyToUse.ExpirationDate);
+ return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys);
+ }
+ }
+
+ if (defaultKeyPolicy.DefaultKey == null)
+ {
+ // The case where there's no default key is the easiest scenario, since it
+ // means that we need to create a new key with immediate activation.
+ var newKey = _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
+ return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
+ }
+ else
+ {
+ // If there is a default key, then the new key we generate should become active upon
+ // expiration of the default key. The new key lifetime is measured from the creation
+ // date (now), not the activation date.
+ var newKey = _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime);
+ return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call
+ }
+ }
+
+ private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, CancellationToken cacheExpirationToken, IKey defaultKey, IEnumerable<IKey> allKeys)
+ {
+ Debug.Assert(defaultKey != null);
+
+ // Invariant: our caller ensures that CreateEncryptorInstance succeeded at least once
+ Debug.Assert(defaultKey.CreateEncryptor() != null);
+
+ _logger.UsingKeyAsDefaultKey(defaultKey.KeyId);
+
+ var nextAutoRefreshTime = now + GetRefreshPeriodWithJitter(_keyManagementOptions.KeyRingRefreshPeriod);
+
+ // The cached keyring should expire at the earliest of (default key expiration, next auto-refresh time).
+ // Since the refresh period and safety window are not user-settable, we can guarantee that there's at
+ // least one auto-refresh between the start of the safety window and the key's expiration date.
+ // This gives us an opportunity to update the key ring before expiration, and it prevents multiple
+ // servers in a cluster from trying to update the key ring simultaneously. Special case: if the default
+ // key's expiration date is in the past, then we know we're using a fallback key and should disregard
+ // its expiration date in favor of the next auto-refresh time.
+ return new CacheableKeyRing(
+ expirationToken: cacheExpirationToken,
+ expirationTime: (defaultKey.ExpirationDate <= now) ? nextAutoRefreshTime : Min(defaultKey.ExpirationDate, nextAutoRefreshTime),
+ defaultKey: defaultKey,
+ allKeys: allKeys);
+ }
+
+ public IKeyRing GetCurrentKeyRing()
+ {
+ return GetCurrentKeyRingCore(DateTime.UtcNow);
+ }
+
+ internal IKeyRing GetCurrentKeyRingCore(DateTime utcNow)
+ {
+ Debug.Assert(utcNow.Kind == DateTimeKind.Utc);
+
+ // Can we return the cached keyring to the caller?
+ var existingCacheableKeyRing = Volatile.Read(ref _cacheableKeyRing);
+ if (CacheableKeyRing.IsValid(existingCacheableKeyRing, utcNow))
+ {
+ return existingCacheableKeyRing.KeyRing;
+ }
+
+ // The cached keyring hasn't been created or must be refreshed. We'll allow one thread to
+ // update the keyring, and all other threads will continue to use the existing cached
+ // keyring while the first thread performs the update. There is an exception: if there
+ // is no usable existing cached keyring, all callers must block until the keyring exists.
+ var acquiredLock = false;
+ try
+ {
+ Monitor.TryEnter(_cacheableKeyRingLockObj, (existingCacheableKeyRing != null) ? 0 : Timeout.Infinite, ref acquiredLock);
+ if (acquiredLock)
+ {
+ // This thread acquired the critical section and is responsible for updating the
+ // cached keyring. But first, let's make sure that somebody didn't sneak in before
+ // us and update the keyring on our behalf.
+ existingCacheableKeyRing = Volatile.Read(ref _cacheableKeyRing);
+ if (CacheableKeyRing.IsValid(existingCacheableKeyRing, utcNow))
+ {
+ return existingCacheableKeyRing.KeyRing;
+ }
+
+ if (existingCacheableKeyRing != null)
+ {
+ _logger.ExistingCachedKeyRingIsExpired();
+ }
+
+ // It's up to us to refresh the cached keyring.
+ // This call is performed *under lock*.
+ CacheableKeyRing newCacheableKeyRing;
+
+ try
+ {
+ newCacheableKeyRing = CacheableKeyRingProvider.GetCacheableKeyRing(utcNow);
+ }
+ catch (Exception ex)
+ {
+ if (existingCacheableKeyRing != null)
+ {
+ _logger.ErrorOccurredWhileRefreshingKeyRing(ex);
+ }
+ else
+ {
+ _logger.ErrorOccurredWhileReadingKeyRing(ex);
+ }
+
+ // Failures that occur while refreshing the keyring are most likely transient, perhaps due to a
+ // temporary network outage. Since we don't want every subsequent call to result in failure, we'll
+ // create a new keyring object whose expiration is now + some short period of time (currently 2 min),
+ // and after this period has elapsed the next caller will try refreshing. If we don't have an
+ // existing keyring (perhaps because this is the first call), then there's nothing to extend, so
+ // each subsequent caller will keep going down this code path until one succeeds.
+ if (existingCacheableKeyRing != null)
+ {
+ Volatile.Write(ref _cacheableKeyRing, existingCacheableKeyRing.WithTemporaryExtendedLifetime(utcNow));
+ }
+
+ // The immediate caller should fail so that he can report the error up his chain. This makes it more likely
+ // that an administrator can see the error and react to it as appropriate. The caller can retry the operation
+ // and will probably have success as long as he falls within the temporary extension mentioned above.
+ throw;
+ }
+
+ Volatile.Write(ref _cacheableKeyRing, newCacheableKeyRing);
+ return newCacheableKeyRing.KeyRing;
+ }
+ else
+ {
+ // We didn't acquire the critical section. This should only occur if we passed
+ // zero for the Monitor.TryEnter timeout, which implies that we had an existing
+ // (but outdated) keyring that we can use as a fallback.
+ Debug.Assert(existingCacheableKeyRing != null);
+ return existingCacheableKeyRing.KeyRing;
+ }
+ }
+ finally
+ {
+ if (acquiredLock)
+ {
+ Monitor.Exit(_cacheableKeyRingLockObj);
+ }
+ }
+ }
+
+ private static TimeSpan GetRefreshPeriodWithJitter(TimeSpan refreshPeriod)
+ {
+ // We'll fudge the refresh period up to -20% so that multiple applications don't try to
+ // hit a single repository simultaneously. For instance, if the refresh period is 1 hour,
+ // we'll return a value in the vicinity of 48 - 60 minutes. We use the Random class since
+ // we don't need a secure PRNG for this.
+ return TimeSpan.FromTicks((long)(refreshPeriod.Ticks * (1.0d - (new Random().NextDouble() / 5))));
+ }
+
+ private static DateTimeOffset Min(DateTimeOffset a, DateTimeOffset b)
+ {
+ return (a < b) ? a : b;
+ }
+
+ CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now)
+ {
+ // the entry point allows one recursive call
+ return CreateCacheableKeyRingCore(now, keyJustAdded: null);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs
new file mode 100644
index 0000000000..06baad13ed
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/KeyManagement/XmlKeyManager.cs
@@ -0,0 +1,564 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Win32;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ /// <summary>
+ /// A key manager backed by an <see cref="IXmlRepository"/>.
+ /// </summary>
+ public sealed class XmlKeyManager : IKeyManager, IInternalXmlKeyManager
+ {
+ // Used for serializing elements to persistent storage
+ internal static readonly XName KeyElementName = "key";
+ internal static readonly XName IdAttributeName = "id";
+ internal static readonly XName VersionAttributeName = "version";
+ internal static readonly XName CreationDateElementName = "creationDate";
+ internal static readonly XName ActivationDateElementName = "activationDate";
+ internal static readonly XName ExpirationDateElementName = "expirationDate";
+ internal static readonly XName DescriptorElementName = "descriptor";
+ internal static readonly XName DeserializerTypeAttributeName = "deserializerType";
+ internal static readonly XName RevocationElementName = "revocation";
+ internal static readonly XName RevocationDateElementName = "revocationDate";
+ internal static readonly XName ReasonElementName = "reason";
+
+ private const string RevokeAllKeysValue = "*";
+
+ private readonly IActivator _activator;
+ private readonly AlgorithmConfiguration _authenticatedEncryptorConfiguration;
+ private readonly IKeyEscrowSink _keyEscrowSink;
+ private readonly IInternalXmlKeyManager _internalKeyManager;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly ILogger _logger;
+ private readonly IEnumerable<IAuthenticatedEncryptorFactory> _encryptorFactories;
+ private readonly IDefaultKeyStorageDirectories _keyStorageDirectories;
+
+ private CancellationTokenSource _cacheExpirationTokenSource;
+
+ /// <summary>
+ /// Creates an <see cref="XmlKeyManager"/>.
+ /// </summary>
+ /// <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)
+ { }
+
+ /// <summary>
+ /// Creates an <see cref="XmlKeyManager"/>.
+ /// </summary>
+ /// <param name="keyManagementOptions">The <see cref="IOptions{KeyManagementOptions}"/> instance that provides the configuration.</param>
+ /// <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;
+ if (KeyRepository == null)
+ {
+ if (KeyEncryptor != null)
+ {
+ throw new InvalidOperationException(
+ Resources.FormatXmlKeyManager_IXmlRepositoryNotFound(nameof(IXmlRepository), nameof(IXmlEncryptor)));
+ }
+ else
+ {
+ var keyRepositoryEncryptorPair = GetFallbackKeyRepositoryEncryptorPair();
+ KeyRepository = keyRepositoryEncryptorPair.Key;
+ KeyEncryptor = keyRepositoryEncryptorPair.Value;
+ }
+ }
+
+ _authenticatedEncryptorConfiguration = keyManagementOptions.Value.AuthenticatedEncryptorConfiguration;
+
+ var escrowSinks = keyManagementOptions.Value.KeyEscrowSinks;
+ _keyEscrowSink = escrowSinks.Count > 0 ? new AggregateKeyEscrowSink(escrowSinks) : null;
+ _activator = activator;
+ TriggerAndResetCacheExpirationToken(suppressLogging: true);
+ _internalKeyManager = _internalKeyManager ?? this;
+ _encryptorFactories = keyManagementOptions.Value.AuthenticatedEncryptorFactories;
+ }
+
+ // Internal for testing.
+ internal XmlKeyManager(
+ IOptions<KeyManagementOptions> keyManagementOptions,
+ IActivator activator,
+ ILoggerFactory loggerFactory,
+ IInternalXmlKeyManager internalXmlKeyManager)
+ : this(keyManagementOptions, activator, loggerFactory)
+ {
+ _internalKeyManager = internalXmlKeyManager;
+ }
+
+ internal IXmlEncryptor KeyEncryptor { get; }
+
+ internal IXmlRepository KeyRepository { get; }
+
+ public IKey CreateNewKey(DateTimeOffset activationDate, DateTimeOffset expirationDate)
+ {
+ return _internalKeyManager.CreateNewKey(
+ keyId: Guid.NewGuid(),
+ creationDate: DateTimeOffset.UtcNow,
+ activationDate: activationDate,
+ expirationDate: expirationDate);
+ }
+
+ private static string DateTimeOffsetToFilenameSafeString(DateTimeOffset dateTime)
+ {
+ // similar to the XML format for dates, but with punctuation stripped
+ return dateTime.UtcDateTime.ToString("yyyyMMddTHHmmssFFFFFFFZ");
+ }
+
+ public IReadOnlyCollection<IKey> GetAllKeys()
+ {
+ var allElements = KeyRepository.GetAllElements();
+
+ // We aggregate all the information we read into three buckets
+ Dictionary<Guid, KeyBase> keyIdToKeyMap = new Dictionary<Guid, KeyBase>();
+ HashSet<Guid> revokedKeyIds = null;
+ DateTimeOffset? mostRecentMassRevocationDate = null;
+
+ foreach (var element in allElements)
+ {
+ if (element.Name == KeyElementName)
+ {
+ // ProcessKeyElement can return null in the case of failure, and if this happens we'll move on.
+ // Still need to throw if we see duplicate keys with the same id.
+ var key = ProcessKeyElement(element);
+ if (key != null)
+ {
+ if (keyIdToKeyMap.ContainsKey(key.KeyId))
+ {
+ throw Error.XmlKeyManager_DuplicateKey(key.KeyId);
+ }
+ keyIdToKeyMap[key.KeyId] = key;
+ }
+ }
+ else if (element.Name == RevocationElementName)
+ {
+ var revocationInfo = ProcessRevocationElement(element);
+ if (revocationInfo is Guid)
+ {
+ // a single key was revoked
+ if (revokedKeyIds == null)
+ {
+ revokedKeyIds = new HashSet<Guid>();
+ }
+ revokedKeyIds.Add((Guid)revocationInfo);
+ }
+ else
+ {
+ // all keys as of a certain date were revoked
+ DateTimeOffset thisMassRevocationDate = (DateTimeOffset)revocationInfo;
+ if (!mostRecentMassRevocationDate.HasValue || mostRecentMassRevocationDate < thisMassRevocationDate)
+ {
+ mostRecentMassRevocationDate = thisMassRevocationDate;
+ }
+ }
+ }
+ else
+ {
+ // Skip unknown elements.
+ _logger.UnknownElementWithNameFoundInKeyringSkipping(element.Name);
+ }
+ }
+
+ // Apply individual revocations
+ if (revokedKeyIds != null)
+ {
+ foreach (Guid revokedKeyId in revokedKeyIds)
+ {
+ KeyBase key;
+ keyIdToKeyMap.TryGetValue(revokedKeyId, out key);
+ if (key != null)
+ {
+ key.SetRevoked();
+ _logger.MarkedKeyAsRevokedInTheKeyring(revokedKeyId);
+ }
+ else
+ {
+ _logger.TriedToProcessRevocationOfKeyButNoSuchKeyWasFound(revokedKeyId);
+ }
+ }
+ }
+
+ // Apply mass revocations
+ if (mostRecentMassRevocationDate.HasValue)
+ {
+ foreach (var key in keyIdToKeyMap.Values)
+ {
+ // The contract of IKeyManager.RevokeAllKeys is that keys created *strictly before* the
+ // revocation date are revoked. The system clock isn't very granular, and if this were
+ // a less-than-or-equal check we could end up with the weird case where a revocation
+ // immediately followed by a key creation results in a newly-created revoked key (since
+ // the clock hasn't yet stepped).
+ if (key.CreationDate < mostRecentMassRevocationDate)
+ {
+ key.SetRevoked();
+ _logger.MarkedKeyAsRevokedInTheKeyring(key.KeyId);
+ }
+ }
+ }
+
+ // And we're finished!
+ return keyIdToKeyMap.Values.ToList().AsReadOnly();
+ }
+
+ public CancellationToken GetCacheExpirationToken()
+ {
+ return Interlocked.CompareExchange(ref _cacheExpirationTokenSource, null, null).Token;
+ }
+
+ private KeyBase ProcessKeyElement(XElement keyElement)
+ {
+ Debug.Assert(keyElement.Name == KeyElementName);
+
+ try
+ {
+ // Read metadata and prepare the key for deferred instantiation
+ Guid keyId = (Guid)keyElement.Attribute(IdAttributeName);
+ DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName);
+ DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName);
+ DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName);
+
+ _logger.FoundKey(keyId);
+
+ return new DeferredKey(
+ keyId: keyId,
+ creationDate: creationDate,
+ activationDate: activationDate,
+ expirationDate: expirationDate,
+ keyManager: this,
+ keyElement: keyElement,
+ encryptorFactories: _encryptorFactories);
+ }
+ catch (Exception ex)
+ {
+ WriteKeyDeserializationErrorToLog(ex, keyElement);
+
+ // Don't include this key in the key ring
+ return null;
+ }
+ }
+
+ // returns a Guid (for specific keys) or a DateTimeOffset (for all keys created on or before a specific date)
+ private object ProcessRevocationElement(XElement revocationElement)
+ {
+ Debug.Assert(revocationElement.Name == RevocationElementName);
+
+ try
+ {
+ string keyIdAsString = (string)revocationElement.Element(KeyElementName).Attribute(IdAttributeName);
+ if (keyIdAsString == RevokeAllKeysValue)
+ {
+ // this is a mass revocation of all keys as of the specified revocation date
+ DateTimeOffset massRevocationDate = (DateTimeOffset)revocationElement.Element(RevocationDateElementName);
+ _logger.FoundRevocationOfAllKeysCreatedPriorTo(massRevocationDate);
+ return massRevocationDate;
+ }
+ else
+ {
+ // only one key is being revoked
+ var keyId = XmlConvert.ToGuid(keyIdAsString);
+ _logger.FoundRevocationOfKey(keyId);
+ return keyId;
+ }
+ }
+ catch (Exception ex)
+ {
+ // Any exceptions that occur are fatal - we don't want to continue if we cannot process
+ // revocation information.
+ _logger.ExceptionWhileProcessingRevocationElement(revocationElement, ex);
+ throw;
+ }
+ }
+
+ public void RevokeAllKeys(DateTimeOffset revocationDate, string reason = null)
+ {
+ // <revocation version="1">
+ // <revocationDate>...</revocationDate>
+ // <!-- ... -->
+ // <key id="*" />
+ // <reason>...</reason>
+ // </revocation>
+
+ _logger.RevokingAllKeysAsOfForReason(revocationDate, reason);
+
+ var revocationElement = new XElement(RevocationElementName,
+ new XAttribute(VersionAttributeName, 1),
+ new XElement(RevocationDateElementName, revocationDate),
+ new XComment(" All keys created before the revocation date are revoked. "),
+ new XElement(KeyElementName,
+ new XAttribute(IdAttributeName, RevokeAllKeysValue)),
+ new XElement(ReasonElementName, reason));
+
+ // Persist it to the underlying repository and trigger the cancellation token
+ string friendlyName = "revocation-" + DateTimeOffsetToFilenameSafeString(revocationDate);
+ KeyRepository.StoreElement(revocationElement, friendlyName);
+ TriggerAndResetCacheExpirationToken();
+ }
+
+ public void RevokeKey(Guid keyId, string reason = null)
+ {
+ _internalKeyManager.RevokeSingleKey(
+ keyId: keyId,
+ revocationDate: DateTimeOffset.UtcNow,
+ reason: reason);
+ }
+
+ private void TriggerAndResetCacheExpirationToken([CallerMemberName] string opName = null, bool suppressLogging = false)
+ {
+ if (!suppressLogging)
+ {
+ _logger.KeyCacheExpirationTokenTriggeredByOperation(opName);
+ }
+
+ Interlocked.Exchange(ref _cacheExpirationTokenSource, new CancellationTokenSource())?.Cancel();
+ }
+
+ private void WriteKeyDeserializationErrorToLog(Exception error, XElement keyElement)
+ {
+ // Ideally we'd suppress the error since it might contain sensitive information, but it would be too difficult for
+ // an administrator to diagnose the issue if we hide this information. Instead we'll log the error to the error
+ // log and the raw <key> element to the debug log. This works for our out-of-box XML decryptors since they don't
+ // include sensitive information in the exception message.
+
+ // write sanitized <key> element
+ _logger.ExceptionWhileProcessingKeyElement(keyElement.WithoutChildNodes(), error);
+
+ // write full <key> element
+ _logger.AnExceptionOccurredWhileProcessingElementDebug(keyElement, error);
+
+ }
+
+ IKey IInternalXmlKeyManager.CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate)
+ {
+ // <key id="{guid}" version="1">
+ // <creationDate>...</creationDate>
+ // <activationDate>...</activationDate>
+ // <expirationDate>...</expirationDate>
+ // <descriptor deserializerType="{typeName}">
+ // ...
+ // </descriptor>
+ // </key>
+
+ _logger.CreatingKey(keyId, creationDate, activationDate, expirationDate);
+
+ var newDescriptor = _authenticatedEncryptorConfiguration.CreateNewDescriptor()
+ ?? CryptoUtil.Fail<IAuthenticatedEncryptorDescriptor>("CreateNewDescriptor returned null.");
+ var descriptorXmlInfo = newDescriptor.ExportToXml();
+
+ _logger.DescriptorDeserializerTypeForKeyIs(keyId, descriptorXmlInfo.DeserializerType.AssemblyQualifiedName);
+
+ // build the <key> element
+ var keyElement = new XElement(KeyElementName,
+ new XAttribute(IdAttributeName, keyId),
+ new XAttribute(VersionAttributeName, 1),
+ new XElement(CreationDateElementName, creationDate),
+ new XElement(ActivationDateElementName, activationDate),
+ new XElement(ExpirationDateElementName, expirationDate),
+ new XElement(DescriptorElementName,
+ new XAttribute(DeserializerTypeAttributeName, descriptorXmlInfo.DeserializerType.AssemblyQualifiedName),
+ descriptorXmlInfo.SerializedDescriptorElement));
+
+ // If key escrow policy is in effect, write the *unencrypted* key now.
+ if (_keyEscrowSink != null)
+ {
+ _logger.KeyEscrowSinkFoundWritingKeyToEscrow(keyId);
+ }
+ else
+ {
+ _logger.NoKeyEscrowSinkFoundNotWritingKeyToEscrow(keyId);
+ }
+ _keyEscrowSink?.Store(keyId, keyElement);
+
+ // If an XML encryptor has been configured, protect secret key material now.
+ if (KeyEncryptor == null)
+ {
+ _logger.NoXMLEncryptorConfiguredKeyMayBePersistedToStorageInUnencryptedForm(keyId);
+ }
+ var possiblyEncryptedKeyElement = KeyEncryptor?.EncryptIfNecessary(keyElement) ?? keyElement;
+
+ // Persist it to the underlying repository and trigger the cancellation token.
+ var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0:D}", keyId);
+ KeyRepository.StoreElement(possiblyEncryptedKeyElement, friendlyName);
+ TriggerAndResetCacheExpirationToken();
+
+ // And we're done!
+ return new Key(
+ keyId: keyId,
+ creationDate: creationDate,
+ activationDate: activationDate,
+ expirationDate: expirationDate,
+ descriptor: newDescriptor,
+ encryptorFactories: _encryptorFactories);
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalXmlKeyManager.DeserializeDescriptorFromKeyElement(XElement keyElement)
+ {
+ try
+ {
+ // Figure out who will be deserializing this
+ var descriptorElement = keyElement.Element(DescriptorElementName);
+ string descriptorDeserializerTypeName = (string)descriptorElement.Attribute(DeserializerTypeAttributeName);
+
+ // Decrypt the descriptor element and pass it to the descriptor for consumption
+ var unencryptedInputToDeserializer = descriptorElement.Elements().Single().DecryptElement(_activator);
+ var deserializerInstance = _activator.CreateInstance<IAuthenticatedEncryptorDescriptorDeserializer>(descriptorDeserializerTypeName);
+ var descriptorInstance = deserializerInstance.ImportFromXml(unencryptedInputToDeserializer);
+
+ return descriptorInstance ?? CryptoUtil.Fail<IAuthenticatedEncryptorDescriptor>("ImportFromXml returned null.");
+ }
+ catch (Exception ex)
+ {
+ WriteKeyDeserializationErrorToLog(ex, keyElement);
+ throw;
+ }
+ }
+
+ void IInternalXmlKeyManager.RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason)
+ {
+ // <revocation version="1">
+ // <revocationDate>...</revocationDate>
+ // <key id="{guid}" />
+ // <reason>...</reason>
+ // </revocation>
+
+ _logger.RevokingKeyForReason(keyId, revocationDate, reason);
+
+ var revocationElement = new XElement(RevocationElementName,
+ new XAttribute(VersionAttributeName, 1),
+ new XElement(RevocationDateElementName, revocationDate),
+ new XElement(KeyElementName,
+ new XAttribute(IdAttributeName, keyId)),
+ new XElement(ReasonElementName, reason));
+
+ // Persist it to the underlying repository and trigger the cancellation token
+ var friendlyName = string.Format(CultureInfo.InvariantCulture, "revocation-{0:D}", keyId);
+ KeyRepository.StoreElement(revocationElement, friendlyName);
+ TriggerAndResetCacheExpirationToken();
+ }
+
+ internal KeyValuePair<IXmlRepository, IXmlEncryptor> GetFallbackKeyRepositoryEncryptorPair()
+ {
+ IXmlRepository repository = null;
+ IXmlEncryptor encryptor = null;
+
+ // If we're running in Azure Web Sites, the key repository goes in the %HOME% directory.
+ var azureWebSitesKeysFolder = _keyStorageDirectories.GetKeyStorageDirectoryForAzureWebSites();
+ if (azureWebSitesKeysFolder != null)
+ {
+ _logger.UsingAzureAsKeyRepository(azureWebSitesKeysFolder.FullName);
+
+ // Cloud DPAPI isn't yet available, so we don't encrypt keys at rest.
+ // This isn't all that different than what Azure Web Sites does today, and we can always add this later.
+ repository = new FileSystemXmlRepository(azureWebSitesKeysFolder, _loggerFactory);
+ }
+ else
+ {
+ // If the user profile is available, store keys in the user profile directory.
+ var localAppDataKeysFolder = _keyStorageDirectories.GetKeyStorageDirectory();
+ if (localAppDataKeysFolder != null)
+ {
+ if (OSVersionUtil.IsWindows())
+ {
+ // If the user profile is available, we can protect using DPAPI.
+ // Probe to see if protecting to local user is available, and use it as the default if so.
+ encryptor = new DpapiXmlEncryptor(
+ protectToLocalMachine: !DpapiSecretSerializerHelper.CanProtectToCurrentUserAccount(),
+ loggerFactory: _loggerFactory);
+ }
+ repository = new FileSystemXmlRepository(localAppDataKeysFolder, _loggerFactory);
+
+ if (encryptor != null)
+ {
+ _logger.UsingProfileAsKeyRepositoryWithDPAPI(localAppDataKeysFolder.FullName);
+ }
+ else
+ {
+ _logger.UsingProfileAsKeyRepository(localAppDataKeysFolder.FullName);
+ }
+ }
+ else
+ {
+ // Use profile isn't available - can we use the HKLM registry?
+ RegistryKey regKeyStorageKey = null;
+ if (OSVersionUtil.IsWindows())
+ {
+ regKeyStorageKey = RegistryXmlRepository.DefaultRegistryKey;
+ }
+ if (regKeyStorageKey != null)
+ {
+ // If the user profile isn't available, we can protect using DPAPI (to machine).
+ encryptor = new DpapiXmlEncryptor(protectToLocalMachine: true, loggerFactory: _loggerFactory);
+ repository = new RegistryXmlRepository(regKeyStorageKey, _loggerFactory);
+
+ _logger.UsingRegistryAsKeyRepositoryWithDPAPI(regKeyStorageKey.Name);
+ }
+ else
+ {
+ // Final fallback - use an ephemeral repository since we don't know where else to go.
+ // This can only be used for development scenarios.
+ repository = new EphemeralXmlRepository(_loggerFactory);
+
+ _logger.UsingEphemeralKeyRepository();
+ }
+ }
+ }
+
+ return new KeyValuePair<IXmlRepository, IXmlEncryptor>(repository, encryptor);
+ }
+
+ private sealed class AggregateKeyEscrowSink : IKeyEscrowSink
+ {
+ private readonly IList<IKeyEscrowSink> _sinks;
+
+ public AggregateKeyEscrowSink(IList<IKeyEscrowSink> sinks)
+ {
+ _sinks = sinks;
+ }
+
+ public void Store(Guid keyId, XElement element)
+ {
+ foreach (var sink in _sinks)
+ {
+ sink.Store(keyId, element);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingExtensions.cs
new file mode 100644
index 0000000000..7792d48dbe
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingExtensions.cs
@@ -0,0 +1,804 @@
+// 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.Runtime.CompilerServices;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Win32;
+
+namespace Microsoft.Extensions.Logging
+{
+ /// <summary>
+ /// Helpful extension methods on <see cref="ILogger"/>.
+ /// </summary>
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Guid, DateTimeOffset, Exception> _usingFallbackKeyWithExpirationAsDefaultKey;
+
+ private static Action<ILogger, Guid, Exception> _usingKeyAsDefaultKey;
+
+ private static Action<ILogger, string, string, Exception> _openingCNGAlgorithmFromProviderWithHMAC;
+
+ private static Action<ILogger, string, string, Exception> _openingCNGAlgorithmFromProviderWithChainingModeCBC;
+
+ private static Action<ILogger, Guid, string, Exception> _performingUnprotectOperationToKeyWithPurposes;
+
+ private static Action<ILogger, Guid, Exception> _keyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed;
+
+ private static Action<ILogger, Guid, Exception> _keyWasRevokedCallerRequestedUnprotectOperationProceedRegardless;
+
+ private static Action<ILogger, Guid, Exception> _keyWasRevokedUnprotectOperationCannotProceed;
+
+ private static Action<ILogger, string, string, Exception> _openingCNGAlgorithmFromProviderWithChainingModeGCM;
+
+ private static Action<ILogger, string, Exception> _usingManagedKeyedHashAlgorithm;
+
+ private static Action<ILogger, string, Exception> _usingManagedSymmetricAlgorithm;
+
+ private static Action<ILogger, Guid, string, Exception> _keyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed;
+
+ private static Action<ILogger, Guid, DateTimeOffset, Exception> _consideringKeyWithExpirationDateAsDefaultKey;
+
+ private static Action<ILogger, Guid, Exception> _keyIsNoLongerUnderConsiderationAsDefault;
+
+ private static Action<ILogger, XName, Exception> _unknownElementWithNameFoundInKeyringSkipping;
+
+ private static Action<ILogger, Guid, Exception> _markedKeyAsRevokedInTheKeyring;
+
+ private static Action<ILogger, Guid, Exception> _triedToProcessRevocationOfKeyButNoSuchKeyWasFound;
+
+ private static Action<ILogger, Guid, Exception> _foundKey;
+
+ private static Action<ILogger, DateTimeOffset, Exception> _foundRevocationOfAllKeysCreatedPriorTo;
+
+ private static Action<ILogger, Guid, Exception> _foundRevocationOfKey;
+
+ private static Action<ILogger, XElement, Exception> _exceptionWhileProcessingRevocationElement;
+
+ private static Action<ILogger, DateTimeOffset, string, Exception> _revokingAllKeysAsOfForReason;
+
+ private static Action<ILogger, string, Exception> _keyCacheExpirationTokenTriggeredByOperation;
+
+ private static Action<ILogger, XElement, Exception> _anExceptionOccurredWhileProcessingTheKeyElement;
+
+ private static Action<ILogger, XElement, Exception> _anExceptionOccurredWhileProcessingTheKeyElementDebug;
+
+ private static Action<ILogger, string, Exception> _encryptingToWindowsDPAPIForCurrentUserAccount;
+
+ private static Action<ILogger, string, Exception> _encryptingToWindowsDPAPINGUsingProtectionDescriptorRule;
+
+ private static Action<ILogger, string, Exception> _anErrorOccurredWhileEncryptingToX509CertificateWithThumbprint;
+
+ private static Action<ILogger, string, Exception> _encryptingToX509CertificateWithThumbprint;
+
+ private static Action<ILogger, string, Exception> _exceptionOccurredWhileTryingToResolveCertificateWithThumbprint;
+
+ private static Action<ILogger, Guid, string, Exception> _performingProtectOperationToKeyWithPurposes;
+
+ private static Action<ILogger, Guid, DateTimeOffset, DateTimeOffset, DateTimeOffset, Exception> _creatingKey;
+
+ private static Action<ILogger, Guid, string, Exception> _descriptorDeserializerTypeForKeyIs;
+
+ private static Action<ILogger, Guid, Exception> _keyEscrowSinkFoundWritingKeyToEscrow;
+
+ private static Action<ILogger, Guid, Exception> _noKeyEscrowSinkFoundNotWritingKeyToEscrow;
+
+ private static Action<ILogger, Guid, Exception> _noXMLEncryptorConfiguredKeyMayBePersistedToStorageInUnencryptedForm;
+
+ private static Action<ILogger, Guid, DateTimeOffset, string, Exception> _revokingKeyForReason;
+
+ private static Action<ILogger, string, Exception> _readingDataFromFile;
+
+ private static Action<ILogger, string, string, Exception> _nameIsNotSafeFileName;
+
+ private static Action<ILogger, string, Exception> _writingDataToFile;
+
+ private static Action<ILogger, RegistryKey, string, Exception> _readingDataFromRegistryKeyValue;
+
+ private static Action<ILogger, string, string, Exception> _nameIsNotSafeRegistryValueName;
+
+ private static Action<ILogger, string, Exception> _decryptingSecretElementUsingWindowsDPAPING;
+
+ private static Action<ILogger, Exception> _exceptionOccurredTryingToDecryptElement;
+
+ private static Action<ILogger, Exception> _encryptingUsingNullEncryptor;
+
+ private static Action<ILogger, Exception> _usingEphemeralDataProtectionProvider;
+
+ private static Action<ILogger, Exception> _existingCachedKeyRingIsExpiredRefreshing;
+
+ private static Action<ILogger, Exception> _errorOccurredWhileRefreshingKeyRing;
+
+ private static Action<ILogger, Exception> _errorOccurredWhileReadingKeyRing;
+
+ private static Action<ILogger, Exception> _keyRingDoesNotContainValidDefaultKey;
+
+ private static Action<ILogger, Exception> _usingInmemoryRepository;
+
+ private static Action<ILogger, Exception> _decryptingSecretElementUsingWindowsDPAPI;
+
+ private static Action<ILogger, Exception> _defaultKeyExpirationImminentAndRepository;
+
+ private static Action<ILogger, Exception> _repositoryContainsNoViableDefaultKey;
+
+ private static Action<ILogger, Exception> _errorOccurredWhileEncryptingToWindowsDPAPI;
+
+ private static Action<ILogger, Exception> _encryptingToWindowsDPAPIForLocalMachineAccount;
+
+ private static Action<ILogger, Exception> _errorOccurredWhileEncryptingToWindowsDPAPING;
+
+ private static Action<ILogger, Exception> _policyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing;
+
+ private static Action<ILogger, Guid, Exception> _keyRingWasLoadedOnStartup;
+
+ private static Action<ILogger, Exception> _keyRingFailedToLoadOnStartup;
+
+ private static Action<ILogger, Exception> _usingEphemeralKeyRepository;
+
+ private static Action<ILogger, string, Exception> _usingRegistryAsKeyRepositoryWithDPAPI;
+
+ private static Action<ILogger, string, Exception> _usingProfileAsKeyRepository;
+
+ private static Action<ILogger, string, Exception> _usingProfileAsKeyRepositoryWithDPAPI;
+
+ private static Action<ILogger, string, Exception> _usingAzureAsKeyRepository;
+
+ private static Action<ILogger, string, Exception> _usingEphemeralFileSystemLocationInContainer;
+
+ static LoggingExtensions()
+ {
+ _usingFallbackKeyWithExpirationAsDefaultKey = LoggerMessage.Define<Guid, DateTimeOffset>(
+ eventId: 1,
+ logLevel: LogLevel.Warning,
+ formatString: "Policy resolution states that a new key should be added to the key ring, but automatic generation of keys is disabled. Using fallback key {KeyId:B} with expiration {ExpirationDate:u} as default key.");
+ _usingKeyAsDefaultKey = LoggerMessage.Define<Guid>(
+ eventId: 2,
+ logLevel: LogLevel.Debug,
+ formatString: "Using key {KeyId:B} as the default key.");
+ _openingCNGAlgorithmFromProviderWithHMAC = LoggerMessage.Define<string, string>(
+ eventId: 3,
+ logLevel: LogLevel.Debug,
+ formatString: "Opening CNG algorithm '{HashAlgorithm}' from provider '{HashAlgorithmProvider}' with HMAC.");
+ _openingCNGAlgorithmFromProviderWithChainingModeCBC = LoggerMessage.Define<string, string>(
+ eventId: 4,
+ logLevel: LogLevel.Debug,
+ formatString: "Opening CNG algorithm '{EncryptionAlgorithm}' from provider '{EncryptionAlgorithmProvider}' with chaining mode CBC.");
+ _performingUnprotectOperationToKeyWithPurposes = LoggerMessage.Define<Guid, string>(
+ eventId: 5,
+ logLevel: LogLevel.Trace,
+ formatString: "Performing unprotect operation to key {KeyId:B} with purposes {Purposes}.");
+ _keyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed = LoggerMessage.Define<Guid>(
+ eventId: 6,
+ logLevel: LogLevel.Trace,
+ formatString: "Key {KeyId:B} was not found in the key ring. Unprotect operation cannot proceed.");
+ _keyWasRevokedCallerRequestedUnprotectOperationProceedRegardless = LoggerMessage.Define<Guid>(
+ eventId: 7,
+ logLevel: LogLevel.Debug,
+ formatString: "Key {KeyId:B} was revoked. Caller requested unprotect operation proceed regardless.");
+ _keyWasRevokedUnprotectOperationCannotProceed = LoggerMessage.Define<Guid>(
+ eventId: 8,
+ logLevel: LogLevel.Debug,
+ formatString: "Key {KeyId:B} was revoked. Unprotect operation cannot proceed.");
+ _openingCNGAlgorithmFromProviderWithChainingModeGCM = LoggerMessage.Define<string, string>(
+ eventId: 9,
+ logLevel: LogLevel.Debug,
+ formatString: "Opening CNG algorithm '{EncryptionAlgorithm}' from provider '{EncryptionAlgorithmProvider}' with chaining mode GCM.");
+ _usingManagedKeyedHashAlgorithm = LoggerMessage.Define<string>(
+ eventId: 10,
+ logLevel: LogLevel.Debug,
+ formatString: "Using managed keyed hash algorithm '{FullName}'.");
+ _usingManagedSymmetricAlgorithm = LoggerMessage.Define<string>(
+ eventId: 11,
+ logLevel: LogLevel.Debug,
+ formatString: "Using managed symmetric algorithm '{FullName}'.");
+ _keyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed = LoggerMessage.Define<Guid, string>(
+ eventId: 12,
+ logLevel: LogLevel.Warning,
+ formatString: "Key {KeyId:B} is ineligible to be the default key because its {MethodName} method failed.");
+ _consideringKeyWithExpirationDateAsDefaultKey = LoggerMessage.Define<Guid, DateTimeOffset>(
+ eventId: 13,
+ logLevel: LogLevel.Debug,
+ formatString: "Considering key {KeyId:B} with expiration date {ExpirationDate:u} as default key.");
+ _keyIsNoLongerUnderConsiderationAsDefault = LoggerMessage.Define<Guid>(
+ eventId: 14,
+ logLevel: LogLevel.Debug,
+ formatString: "Key {KeyId:B} is no longer under consideration as default key because it is expired, revoked, or cannot be deciphered.");
+ _unknownElementWithNameFoundInKeyringSkipping = LoggerMessage.Define<XName>(
+ eventId: 15,
+ logLevel: LogLevel.Warning,
+ formatString: "Unknown element with name '{Name}' found in keyring, skipping.");
+ _markedKeyAsRevokedInTheKeyring = LoggerMessage.Define<Guid>(
+ eventId: 16,
+ logLevel: LogLevel.Debug,
+ formatString: "Marked key {KeyId:B} as revoked in the keyring.");
+ _triedToProcessRevocationOfKeyButNoSuchKeyWasFound = LoggerMessage.Define<Guid>(
+ eventId: 17,
+ logLevel: LogLevel.Warning,
+ formatString: "Tried to process revocation of key {KeyId:B}, but no such key was found in keyring. Skipping.");
+ _foundKey = LoggerMessage.Define<Guid>(
+ eventId: 18,
+ logLevel: LogLevel.Debug,
+ formatString: "Found key {KeyId:B}.");
+ _foundRevocationOfAllKeysCreatedPriorTo = LoggerMessage.Define<DateTimeOffset>(
+ eventId: 19,
+ logLevel: LogLevel.Debug,
+ formatString: "Found revocation of all keys created prior to {RevocationDate:u}.");
+ _foundRevocationOfKey = LoggerMessage.Define<Guid>(
+ eventId: 20,
+ logLevel: LogLevel.Debug,
+ formatString: "Found revocation of key {KeyId:B}.");
+ _exceptionWhileProcessingRevocationElement = LoggerMessage.Define<XElement>(
+ eventId: 21,
+ logLevel: LogLevel.Error,
+ formatString: "An exception occurred while processing the revocation element '{RevocationElement}'. Cannot continue keyring processing.");
+ _revokingAllKeysAsOfForReason = LoggerMessage.Define<DateTimeOffset, string>(
+ eventId: 22,
+ logLevel: LogLevel.Information,
+ formatString: "Revoking all keys as of {RevocationDate:u} for reason '{Reason}'.");
+ _keyCacheExpirationTokenTriggeredByOperation = LoggerMessage.Define<string>(
+ eventId: 23,
+ logLevel: LogLevel.Debug,
+ formatString: "Key cache expiration token triggered by '{OperationName}' operation.");
+ _anExceptionOccurredWhileProcessingTheKeyElement = LoggerMessage.Define<XElement>(
+ eventId: 24,
+ logLevel: LogLevel.Error,
+ formatString: "An exception occurred while processing the key element '{Element}'.");
+ _anExceptionOccurredWhileProcessingTheKeyElementDebug = LoggerMessage.Define<XElement>(
+ eventId: 25,
+ logLevel: LogLevel.Trace,
+ formatString: "An exception occurred while processing the key element '{Element}'.");
+ _encryptingToWindowsDPAPIForCurrentUserAccount = LoggerMessage.Define<string>(
+ eventId: 26,
+ logLevel: LogLevel.Debug,
+ formatString: "Encrypting to Windows DPAPI for current user account ({Name}).");
+ _encryptingToWindowsDPAPINGUsingProtectionDescriptorRule = LoggerMessage.Define<string>(
+ eventId: 27,
+ logLevel: LogLevel.Debug,
+ formatString: "Encrypting to Windows DPAPI-NG using protection descriptor rule '{DescriptorRule}'.");
+ _anErrorOccurredWhileEncryptingToX509CertificateWithThumbprint = LoggerMessage.Define<string>(
+ eventId: 28,
+ logLevel: LogLevel.Error,
+ formatString: "An error occurred while encrypting to X.509 certificate with thumbprint '{Thumbprint}'.");
+ _encryptingToX509CertificateWithThumbprint = LoggerMessage.Define<string>(
+ eventId: 29,
+ logLevel: LogLevel.Debug,
+ formatString: "Encrypting to X.509 certificate with thumbprint '{Thumbprint}'.");
+ _exceptionOccurredWhileTryingToResolveCertificateWithThumbprint = LoggerMessage.Define<string>(
+ eventId: 30,
+ logLevel: LogLevel.Error,
+ formatString: "An exception occurred while trying to resolve certificate with thumbprint '{Thumbprint}'.");
+ _performingProtectOperationToKeyWithPurposes = LoggerMessage.Define<Guid, string>(
+ eventId: 31,
+ logLevel: LogLevel.Trace,
+ formatString: "Performing protect operation to key {KeyId:B} with purposes {Purposes}.");
+ _descriptorDeserializerTypeForKeyIs = LoggerMessage.Define<Guid, string>(
+ eventId: 32,
+ logLevel: LogLevel.Debug,
+ formatString: "Descriptor deserializer type for key {KeyId:B} is '{AssemblyQualifiedName}'.");
+ _keyEscrowSinkFoundWritingKeyToEscrow = LoggerMessage.Define<Guid>(
+ eventId: 33,
+ logLevel: LogLevel.Debug,
+ formatString: "Key escrow sink found. Writing key {KeyId:B} to escrow.");
+ _noKeyEscrowSinkFoundNotWritingKeyToEscrow = LoggerMessage.Define<Guid>(
+ eventId: 34,
+ logLevel: LogLevel.Debug,
+ formatString: "No key escrow sink found. Not writing key {KeyId:B} to escrow.");
+ _noXMLEncryptorConfiguredKeyMayBePersistedToStorageInUnencryptedForm = LoggerMessage.Define<Guid>(
+ eventId: 35,
+ logLevel: LogLevel.Warning,
+ formatString: "No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form.");
+ _revokingKeyForReason = LoggerMessage.Define<Guid, DateTimeOffset, string>(
+ eventId: 36,
+ logLevel: LogLevel.Information,
+ formatString: "Revoking key {KeyId:B} at {RevocationDate:u} for reason '{Reason}'.");
+ _readingDataFromFile = LoggerMessage.Define<string>(
+ eventId: 37,
+ logLevel: LogLevel.Debug,
+ formatString: "Reading data from file '{FullPath}'.");
+ _nameIsNotSafeFileName = LoggerMessage.Define<string, string>(
+ eventId: 38,
+ logLevel: LogLevel.Debug,
+ formatString: "The name '{FriendlyName}' is not a safe file name, using '{NewFriendlyName}' instead.");
+ _writingDataToFile = LoggerMessage.Define<string>(
+ eventId: 39,
+ logLevel: LogLevel.Information,
+ formatString: "Writing data to file '{FileName}'.");
+ _readingDataFromRegistryKeyValue = LoggerMessage.Define<RegistryKey, string>(
+ eventId: 40,
+ logLevel: LogLevel.Debug,
+ formatString: "Reading data from registry key '{RegistryKeyName}', value '{Value}'.");
+ _nameIsNotSafeRegistryValueName = LoggerMessage.Define<string, string>(
+ eventId: 41,
+ logLevel: LogLevel.Debug,
+ formatString: "The name '{FriendlyName}' is not a safe registry value name, using '{NewFriendlyName}' instead.");
+ _decryptingSecretElementUsingWindowsDPAPING = LoggerMessage.Define<string>(
+ eventId: 42,
+ logLevel: LogLevel.Debug,
+ formatString: "Decrypting secret element using Windows DPAPI-NG with protection descriptor rule '{DescriptorRule}'.");
+ _exceptionOccurredTryingToDecryptElement = LoggerMessage.Define(
+ eventId: 43,
+ logLevel: LogLevel.Error,
+ formatString: "An exception occurred while trying to decrypt the element.");
+ _encryptingUsingNullEncryptor = LoggerMessage.Define(
+ eventId: 44,
+ logLevel: LogLevel.Warning,
+ formatString: "Encrypting using a null encryptor; secret information isn't being protected.");
+ _usingEphemeralDataProtectionProvider = LoggerMessage.Define(
+ eventId: 45,
+ logLevel: LogLevel.Warning,
+ formatString: "Using ephemeral data protection provider. Payloads will be undecipherable upon application shutdown.");
+ _existingCachedKeyRingIsExpiredRefreshing = LoggerMessage.Define(
+ eventId: 46,
+ logLevel: LogLevel.Debug,
+ formatString: "Existing cached key ring is expired. Refreshing.");
+ _errorOccurredWhileRefreshingKeyRing = LoggerMessage.Define(
+ eventId: 47,
+ logLevel: LogLevel.Error,
+ formatString: "An error occurred while refreshing the key ring. Will try again in 2 minutes.");
+ _errorOccurredWhileReadingKeyRing = LoggerMessage.Define(
+ eventId: 48,
+ logLevel: LogLevel.Error,
+ formatString: "An error occurred while reading the key ring.");
+ _keyRingDoesNotContainValidDefaultKey = LoggerMessage.Define(
+ eventId: 49,
+ logLevel: LogLevel.Error,
+ formatString: "The key ring does not contain a valid default key, and the key manager is configured with auto-generation of keys disabled.");
+ _usingInmemoryRepository = LoggerMessage.Define(
+ eventId: 50,
+ logLevel: LogLevel.Warning,
+ formatString: "Using an in-memory repository. Keys will not be persisted to storage.");
+ _decryptingSecretElementUsingWindowsDPAPI = LoggerMessage.Define(
+ eventId: 51,
+ logLevel: LogLevel.Debug,
+ formatString: "Decrypting secret element using Windows DPAPI.");
+ _defaultKeyExpirationImminentAndRepository = LoggerMessage.Define(
+ eventId: 52,
+ logLevel: LogLevel.Debug,
+ formatString: "Default key expiration imminent and repository contains no viable successor. Caller should generate a successor.");
+ _repositoryContainsNoViableDefaultKey = LoggerMessage.Define(
+ eventId: 53,
+ logLevel: LogLevel.Debug,
+ formatString: "Repository contains no viable default key. Caller should generate a key with immediate activation.");
+ _errorOccurredWhileEncryptingToWindowsDPAPI = LoggerMessage.Define(
+ eventId: 54,
+ logLevel: LogLevel.Error,
+ formatString: "An error occurred while encrypting to Windows DPAPI.");
+ _encryptingToWindowsDPAPIForLocalMachineAccount = LoggerMessage.Define(
+ eventId: 55,
+ logLevel: LogLevel.Debug,
+ formatString: "Encrypting to Windows DPAPI for local machine account.");
+ _errorOccurredWhileEncryptingToWindowsDPAPING = LoggerMessage.Define(
+ eventId: 56,
+ logLevel: LogLevel.Error,
+ formatString: "An error occurred while encrypting to Windows DPAPI-NG.");
+ _policyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing = LoggerMessage.Define(
+ eventId: 57,
+ logLevel: LogLevel.Debug,
+ formatString: "Policy resolution states that a new key should be added to the key ring.");
+ _creatingKey = LoggerMessage.Define<Guid, DateTimeOffset, DateTimeOffset, DateTimeOffset>(
+ eventId: 58,
+ logLevel: LogLevel.Information,
+ formatString: "Creating key {KeyId:B} with creation date {CreationDate:u}, activation date {ActivationDate:u}, and expiration date {ExpirationDate:u}.");
+ _usingEphemeralKeyRepository = LoggerMessage.Define(
+ eventId: 59,
+ logLevel: LogLevel.Warning,
+ formatString: "Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.");
+ _usingEphemeralFileSystemLocationInContainer = LoggerMessage.Define<string>(
+ eventId: 60,
+ logLevel: LogLevel.Warning,
+ formatString: Resources.FileSystem_EphemeralKeysLocationInContainer);
+
+ _usingRegistryAsKeyRepositoryWithDPAPI = LoggerMessage.Define<string>(
+ eventId: 0,
+ logLevel: LogLevel.Information,
+ formatString: "User profile not available. Using '{Name}' as key repository and Windows DPAPI to encrypt keys at rest.");
+ _usingProfileAsKeyRepository = LoggerMessage.Define<string>(
+ eventId: 0,
+ logLevel: LogLevel.Information,
+ formatString: "User profile is available. Using '{FullName}' as key repository; keys will not be encrypted at rest.");
+ _usingProfileAsKeyRepositoryWithDPAPI = LoggerMessage.Define<string>(
+ eventId: 0,
+ logLevel: LogLevel.Information,
+ formatString: "User profile is available. Using '{FullName}' as key repository and Windows DPAPI to encrypt keys at rest.");
+ _usingAzureAsKeyRepository = LoggerMessage.Define<string>(
+ eventId: 0,
+ logLevel: LogLevel.Information,
+ formatString: "Azure Web Sites environment detected. Using '{FullName}' as key repository; keys will not be encrypted at rest.");
+ _keyRingWasLoadedOnStartup = LoggerMessage.Define<Guid>(
+ eventId: 0,
+ logLevel: LogLevel.Debug,
+ formatString: "Key ring with default key {KeyId:B} was loaded during application startup.");
+ _keyRingFailedToLoadOnStartup = LoggerMessage.Define(
+ eventId: 0,
+ logLevel: LogLevel.Information,
+ formatString: "Key ring failed to load during application startup.");
+ }
+
+ /// <summary>
+ /// Returns a value stating whether the 'debug' log level is enabled.
+ /// Returns false if the logger instance is null.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsDebugLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Debug);
+ }
+
+ /// <summary>
+ /// Returns a value stating whether the 'error' log level is enabled.
+ /// Returns false if the logger instance is null.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsErrorLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Error);
+ }
+
+ /// <summary>
+ /// Returns a value stating whether the 'information' log level is enabled.
+ /// Returns false if the logger instance is null.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsInformationLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Information);
+ }
+
+ /// <summary>
+ /// Returns a value stating whether the 'trace' log level is enabled.
+ /// Returns false if the logger instance is null.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsTraceLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Trace);
+ }
+
+ /// <summary>
+ /// Returns a value stating whether the 'warning' log level is enabled.
+ /// Returns false if the logger instance is null.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsWarningLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Warning);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsLogLevelEnabledCore(ILogger logger, LogLevel level)
+ {
+ return (logger != null && logger.IsEnabled(level));
+ }
+
+ public static void UsingFallbackKeyWithExpirationAsDefaultKey(this ILogger logger, Guid keyId, DateTimeOffset expirationDate)
+ {
+ _usingFallbackKeyWithExpirationAsDefaultKey(logger, keyId, expirationDate, null);
+ }
+
+ public static void UsingKeyAsDefaultKey(this ILogger logger, Guid keyId)
+ {
+ _usingKeyAsDefaultKey(logger, keyId, null);
+ }
+
+ public static void OpeningCNGAlgorithmFromProviderWithHMAC(this ILogger logger, string hashAlgorithm, string hashAlgorithmProvider)
+ {
+ _openingCNGAlgorithmFromProviderWithHMAC(logger, hashAlgorithm, hashAlgorithmProvider, null);
+ }
+
+ public static void OpeningCNGAlgorithmFromProviderWithChainingModeCBC(this ILogger logger, string encryptionAlgorithm, string encryptionAlgorithmProvider)
+ {
+ _openingCNGAlgorithmFromProviderWithChainingModeCBC(logger, encryptionAlgorithm, encryptionAlgorithmProvider, null);
+ }
+
+ public static void PerformingUnprotectOperationToKeyWithPurposes(this ILogger logger, Guid keyIdFromPayload, string p0)
+ {
+ _performingUnprotectOperationToKeyWithPurposes(logger, keyIdFromPayload, p0, null);
+ }
+
+ public static void KeyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(this ILogger logger, Guid keyIdFromPayload)
+ {
+ _keyWasNotFoundInTheKeyRingUnprotectOperationCannotProceed(logger, keyIdFromPayload, null);
+ }
+
+ public static void KeyWasRevokedCallerRequestedUnprotectOperationProceedRegardless(this ILogger logger, Guid keyIdFromPayload)
+ {
+ _keyWasRevokedCallerRequestedUnprotectOperationProceedRegardless(logger, keyIdFromPayload, null);
+ }
+
+ public static void KeyWasRevokedUnprotectOperationCannotProceed(this ILogger logger, Guid keyIdFromPayload)
+ {
+ _keyWasRevokedUnprotectOperationCannotProceed(logger, keyIdFromPayload, null);
+ }
+
+ public static void OpeningCNGAlgorithmFromProviderWithChainingModeGCM(this ILogger logger, string encryptionAlgorithm, string encryptionAlgorithmProvider)
+ {
+ _openingCNGAlgorithmFromProviderWithChainingModeGCM(logger, encryptionAlgorithm, encryptionAlgorithmProvider, null);
+ }
+
+ public static void UsingManagedKeyedHashAlgorithm(this ILogger logger, string fullName)
+ {
+ _usingManagedKeyedHashAlgorithm(logger, fullName, null);
+ }
+
+ public static void UsingManagedSymmetricAlgorithm(this ILogger logger, string fullName)
+ {
+ _usingManagedSymmetricAlgorithm(logger, fullName, null);
+ }
+
+ public static void KeyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed(this ILogger logger, Guid keyId, string p0, Exception exception)
+ {
+ _keyIsIneligibleToBeTheDefaultKeyBecauseItsMethodFailed(logger, keyId, p0, exception);
+ }
+
+ public static void ConsideringKeyWithExpirationDateAsDefaultKey(this ILogger logger, Guid keyId, DateTimeOffset expirationDate)
+ {
+ _consideringKeyWithExpirationDateAsDefaultKey(logger, keyId, expirationDate, null);
+ }
+
+ public static void KeyIsNoLongerUnderConsiderationAsDefault(this ILogger logger, Guid keyId)
+ {
+ _keyIsNoLongerUnderConsiderationAsDefault(logger, keyId, null);
+ }
+
+ public static void UnknownElementWithNameFoundInKeyringSkipping(this ILogger logger, XName name)
+ {
+ _unknownElementWithNameFoundInKeyringSkipping(logger, name, null);
+ }
+
+ public static void MarkedKeyAsRevokedInTheKeyring(this ILogger logger, Guid revokedKeyId)
+ {
+ _markedKeyAsRevokedInTheKeyring(logger, revokedKeyId, null);
+ }
+
+ public static void TriedToProcessRevocationOfKeyButNoSuchKeyWasFound(this ILogger logger, Guid revokedKeyId)
+ {
+ _triedToProcessRevocationOfKeyButNoSuchKeyWasFound(logger, revokedKeyId, null);
+ }
+
+ public static void FoundKey(this ILogger logger, Guid keyId)
+ {
+ _foundKey(logger, keyId, null);
+ }
+
+ public static void FoundRevocationOfAllKeysCreatedPriorTo(this ILogger logger, DateTimeOffset massRevocationDate)
+ {
+ _foundRevocationOfAllKeysCreatedPriorTo(logger, massRevocationDate, null);
+ }
+
+ public static void FoundRevocationOfKey(this ILogger logger, Guid keyId)
+ {
+ _foundRevocationOfKey(logger, keyId, null);
+ }
+
+ public static void ExceptionWhileProcessingRevocationElement(this ILogger logger, XElement revocationElement, Exception exception)
+ {
+ _exceptionWhileProcessingRevocationElement(logger, revocationElement, exception);
+ }
+
+ public static void RevokingAllKeysAsOfForReason(this ILogger logger, DateTimeOffset revocationDate, string reason)
+ {
+ _revokingAllKeysAsOfForReason(logger, revocationDate, reason, null);
+ }
+
+ public static void KeyCacheExpirationTokenTriggeredByOperation(this ILogger logger, string opName)
+ {
+ _keyCacheExpirationTokenTriggeredByOperation(logger, opName, null);
+ }
+
+ public static void ExceptionWhileProcessingKeyElement(this ILogger logger, XElement keyElement, Exception exception)
+ {
+ _anExceptionOccurredWhileProcessingTheKeyElement(logger, keyElement, exception);
+ }
+
+ public static void AnExceptionOccurredWhileProcessingElementDebug(this ILogger logger, XElement keyElement, Exception exception)
+ {
+ _anExceptionOccurredWhileProcessingTheKeyElementDebug(logger, keyElement, exception);
+ }
+
+ public static void EncryptingToWindowsDPAPIForCurrentUserAccount(this ILogger logger, string name)
+ {
+ _encryptingToWindowsDPAPIForCurrentUserAccount(logger, name, null);
+ }
+
+ public static void AnErrorOccurredWhileEncryptingToX509CertificateWithThumbprint(this ILogger logger, string thumbprint, Exception exception)
+ {
+ _anErrorOccurredWhileEncryptingToX509CertificateWithThumbprint(logger, thumbprint, exception);
+ }
+
+ public static void EncryptingToX509CertificateWithThumbprint(this ILogger logger, string thumbprint)
+ {
+ _encryptingToX509CertificateWithThumbprint(logger, thumbprint, null);
+ }
+
+ public static void ExceptionWhileTryingToResolveCertificateWithThumbprint(this ILogger logger, string thumbprint, Exception exception)
+ {
+ _exceptionOccurredWhileTryingToResolveCertificateWithThumbprint(logger, thumbprint, exception);
+ }
+
+ public static void PerformingProtectOperationToKeyWithPurposes(this ILogger logger, Guid defaultKeyId, string p0)
+ {
+ _performingProtectOperationToKeyWithPurposes(logger, defaultKeyId, p0, null);
+ }
+
+ public static void DescriptorDeserializerTypeForKeyIs(this ILogger logger, Guid keyId, string assemblyQualifiedName)
+ {
+ _descriptorDeserializerTypeForKeyIs(logger, keyId, assemblyQualifiedName, null);
+ }
+
+ public static void KeyEscrowSinkFoundWritingKeyToEscrow(this ILogger logger, Guid keyId)
+ {
+ _keyEscrowSinkFoundWritingKeyToEscrow(logger, keyId, null);
+ }
+
+ public static void NoKeyEscrowSinkFoundNotWritingKeyToEscrow(this ILogger logger, Guid keyId)
+ {
+ _noKeyEscrowSinkFoundNotWritingKeyToEscrow(logger, keyId, null);
+ }
+
+ public static void NoXMLEncryptorConfiguredKeyMayBePersistedToStorageInUnencryptedForm(this ILogger logger, Guid keyId)
+ {
+ _noXMLEncryptorConfiguredKeyMayBePersistedToStorageInUnencryptedForm(logger, keyId, null);
+ }
+
+ public static void RevokingKeyForReason(this ILogger logger, Guid keyId, DateTimeOffset revocationDate, string reason)
+ {
+ _revokingKeyForReason(logger, keyId, revocationDate, reason, null);
+ }
+
+ public static void ReadingDataFromFile(this ILogger logger, string fullPath)
+ {
+ _readingDataFromFile(logger, fullPath, null);
+ }
+
+ public static void NameIsNotSafeFileName(this ILogger logger, string friendlyName, string newFriendlyName)
+ {
+ _nameIsNotSafeFileName(logger, friendlyName, newFriendlyName, null);
+ }
+
+ public static void WritingDataToFile(this ILogger logger, string finalFilename)
+ {
+ _writingDataToFile(logger, finalFilename, null);
+ }
+
+ public static void ReadingDataFromRegistryKeyValue(this ILogger logger, RegistryKey regKey, string valueName)
+ {
+ _readingDataFromRegistryKeyValue(logger, regKey, valueName, null);
+ }
+
+ public static void NameIsNotSafeRegistryValueName(this ILogger logger, string friendlyName, string newFriendlyName)
+ {
+ _nameIsNotSafeRegistryValueName(logger, friendlyName, newFriendlyName, null);
+ }
+
+ public static void DecryptingSecretElementUsingWindowsDPAPING(this ILogger logger, string protectionDescriptorRule)
+ {
+ _decryptingSecretElementUsingWindowsDPAPING(logger, protectionDescriptorRule, null);
+ }
+
+ public static void EncryptingToWindowsDPAPINGUsingProtectionDescriptorRule(this ILogger logger, string protectionDescriptorRuleString)
+ {
+ _encryptingToWindowsDPAPINGUsingProtectionDescriptorRule(logger, protectionDescriptorRuleString, null);
+ }
+
+ public static void ExceptionOccurredTryingToDecryptElement(this ILogger logger, Exception exception)
+ {
+ _exceptionOccurredTryingToDecryptElement(logger, exception);
+ }
+
+ public static void EncryptingUsingNullEncryptor(this ILogger logger)
+ {
+ _encryptingUsingNullEncryptor(logger, null);
+ }
+
+ public static void UsingEphemeralDataProtectionProvider(this ILogger logger)
+ {
+ _usingEphemeralDataProtectionProvider(logger, null);
+ }
+
+ public static void ExistingCachedKeyRingIsExpired(this ILogger logger)
+ {
+ _existingCachedKeyRingIsExpiredRefreshing(logger, null);
+ }
+
+ public static void ErrorOccurredWhileRefreshingKeyRing(this ILogger logger, Exception exception)
+ {
+ _errorOccurredWhileRefreshingKeyRing(logger, exception);
+ }
+
+ public static void ErrorOccurredWhileReadingKeyRing(this ILogger logger, Exception exception)
+ {
+ _errorOccurredWhileReadingKeyRing(logger, exception);
+ }
+
+ public static void KeyRingDoesNotContainValidDefaultKey(this ILogger logger)
+ {
+ _keyRingDoesNotContainValidDefaultKey(logger, null);
+ }
+
+ public static void UsingInmemoryRepository(this ILogger logger)
+ {
+ _usingInmemoryRepository(logger, null);
+ }
+
+ public static void DecryptingSecretElementUsingWindowsDPAPI(this ILogger logger)
+ {
+ _decryptingSecretElementUsingWindowsDPAPI(logger, null);
+ }
+
+ public static void DefaultKeyExpirationImminentAndRepository(this ILogger logger)
+ {
+ _defaultKeyExpirationImminentAndRepository(logger, null);
+ }
+
+ public static void RepositoryContainsNoViableDefaultKey(this ILogger logger)
+ {
+ _repositoryContainsNoViableDefaultKey(logger, null);
+ }
+
+ public static void ErrorOccurredWhileEncryptingToWindowsDPAPI(this ILogger logger, Exception exception)
+ {
+ _errorOccurredWhileEncryptingToWindowsDPAPI(logger, exception);
+ }
+
+ public static void EncryptingToWindowsDPAPIForLocalMachineAccount(this ILogger logger)
+ {
+ _encryptingToWindowsDPAPIForLocalMachineAccount(logger, null);
+ }
+
+ public static void ErrorOccurredWhileEncryptingToWindowsDPAPING(this ILogger logger, Exception exception)
+ {
+ _errorOccurredWhileEncryptingToWindowsDPAPING(logger, exception);
+ }
+
+ public static void PolicyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing(this ILogger logger)
+ {
+ _policyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing(logger, null);
+ }
+
+ public static void CreatingKey(this ILogger logger, Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate)
+ {
+ _creatingKey(logger, keyId, creationDate, activationDate, expirationDate, null);
+ }
+
+ public static void UsingEphemeralKeyRepository(this ILogger logger)
+ {
+ _usingEphemeralKeyRepository(logger, null);
+ }
+
+ public static void UsingRegistryAsKeyRepositoryWithDPAPI(this ILogger logger, string name)
+ {
+ _usingRegistryAsKeyRepositoryWithDPAPI(logger, name, null);
+ }
+
+ public static void UsingProfileAsKeyRepository(this ILogger logger, string fullName)
+ {
+ _usingProfileAsKeyRepository(logger, fullName, null);
+ }
+
+ public static void UsingProfileAsKeyRepositoryWithDPAPI(this ILogger logger, string fullName)
+ {
+ _usingProfileAsKeyRepositoryWithDPAPI(logger, fullName, null);
+ }
+
+ public static void UsingAzureAsKeyRepository(this ILogger logger, string fullName)
+ {
+ _usingAzureAsKeyRepository(logger, fullName, null);
+ }
+
+ public static void KeyRingWasLoadedOnStartup(this ILogger logger, Guid defaultKeyId)
+ {
+ _keyRingWasLoadedOnStartup(logger, defaultKeyId, null);
+ }
+
+ public static void KeyRingFailedToLoadOnStartup(this ILogger logger, Exception innerException)
+ {
+ _keyRingFailedToLoadOnStartup(logger, innerException);
+ }
+
+ public static void UsingEphemeralFileSystemLocationInContainer(this ILogger logger, string path)
+ {
+ _usingEphemeralFileSystemLocationInContainer(logger, path, null);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingServiceProviderExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingServiceProviderExtensions.cs
new file mode 100644
index 0000000000..199f9fd35c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/LoggingServiceProviderExtensions.cs
@@ -0,0 +1,43 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace System
+{
+ /// <summary>
+ /// Helpful logging-related extension methods on <see cref="IServiceProvider"/>.
+ /// </summary>
+ internal static class LoggingServiceProviderExtensions
+ {
+ /// <summary>
+ /// Retrieves an instance of <see cref="ILogger"/> given the type name <typeparamref name="T"/>.
+ /// This is equivalent to <see cref="LoggerFactoryExtensions.CreateLogger{T}(ILoggerFactory)"/>.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="ILogger"/> instance, or null if <paramref name="services"/> is null or the
+ /// <see cref="IServiceProvider"/> cannot produce an <see cref="ILoggerFactory"/>.
+ /// </returns>
+ public static ILogger GetLogger<T>(this IServiceProvider services)
+ {
+ return GetLogger(services, typeof(T));
+ }
+
+ /// <summary>
+ /// Retrieves an instance of <see cref="ILogger"/> given the type name <paramref name="type"/>.
+ /// This is equivalent to <see cref="LoggerFactoryExtensions.CreateLogger{T}(ILoggerFactory)"/>.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="ILogger"/> instance, or null if <paramref name="services"/> is null or the
+ /// <see cref="IServiceProvider"/> cannot produce an <see cref="ILoggerFactory"/>.
+ /// </returns>
+ public static ILogger GetLogger(this IServiceProvider services, Type type)
+ {
+ // Compiler won't allow us to use static types as the type parameter
+ // for the call to CreateLogger<T>, so we'll duplicate its logic here.
+ return services?.GetService<ILoggerFactory>()?.CreateLogger(type.FullName);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/HashAlgorithmExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/HashAlgorithmExtensions.cs
new file mode 100644
index 0000000000..af854158ec
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/HashAlgorithmExtensions.cs
@@ -0,0 +1,19 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.Managed
+{
+ internal static class HashAlgorithmExtensions
+ {
+ public static int GetDigestSizeInBytes(this HashAlgorithm hashAlgorithm)
+ {
+ var hashSizeInBits = hashAlgorithm.HashSize;
+ CryptoUtil.Assert(hashSizeInBits >= 0 && hashSizeInBits % 8 == 0, "hashSizeInBits >= 0 && hashSizeInBits % 8 == 0");
+ return hashSizeInBits / 8;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/IManagedGenRandom.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/IManagedGenRandom.cs
new file mode 100644
index 0000000000..1d08f1e7d8
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/IManagedGenRandom.cs
@@ -0,0 +1,12 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.Managed
+{
+ internal interface IManagedGenRandom
+ {
+ byte[] GenRandom(int numBytes);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs
new file mode 100644
index 0000000000..0d93955d75
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs
@@ -0,0 +1,375 @@
+// 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.IO;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.SP800_108;
+
+namespace Microsoft.AspNetCore.DataProtection.Managed
+{
+ // An encryptor which does Encrypt(CBC) + HMAC using SymmetricAlgorithm and HashAlgorithm.
+ // The payloads produced by this encryptor should be compatible with the payloads
+ // produced by the CNG-based Encrypt(CBC) + HMAC authenticated encryptor.
+ internal unsafe sealed class ManagedAuthenticatedEncryptor : IAuthenticatedEncryptor, IDisposable
+ {
+ // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single
+ // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block
+ // encryption operations, which a high-traffic web server might perform in mere hours.
+ // AES and other 128-bit block ciphers are less susceptible to this due to the larger IV
+ // space, but unfortunately some organizations require older 64-bit block ciphers. To address
+ // the collision issue, we'll feed 128 bits of entropy to the KDF when performing subkey
+ // generation. This creates >= 192 bits total entropy for each operation, so we shouldn't
+ // expect a collision until >= 2^96 operations. Even 2^80 operations still maintains a <= 2^-32
+ // probability of collision, and this is acceptable for the expected KDK lifetime.
+ private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
+
+ private static readonly Func<byte[], HashAlgorithm> _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
+
+ private readonly byte[] _contextHeader;
+ private readonly IManagedGenRandom _genRandom;
+ private readonly Secret _keyDerivationKey;
+ private readonly Func<SymmetricAlgorithm> _symmetricAlgorithmFactory;
+ private readonly int _symmetricAlgorithmBlockSizeInBytes;
+ private readonly int _symmetricAlgorithmSubkeyLengthInBytes;
+ private readonly int _validationAlgorithmDigestLengthInBytes;
+ private readonly int _validationAlgorithmSubkeyLengthInBytes;
+ private readonly Func<KeyedHashAlgorithm> _validationAlgorithmFactory;
+
+ public ManagedAuthenticatedEncryptor(Secret keyDerivationKey, Func<SymmetricAlgorithm> symmetricAlgorithmFactory, int symmetricAlgorithmKeySizeInBytes, Func<KeyedHashAlgorithm> validationAlgorithmFactory, IManagedGenRandom genRandom = null)
+ {
+ _genRandom = genRandom ?? ManagedGenRandomImpl.Instance;
+ _keyDerivationKey = keyDerivationKey;
+
+ // Validate that the symmetric algorithm has the properties we require
+ using (var symmetricAlgorithm = symmetricAlgorithmFactory())
+ {
+ _symmetricAlgorithmFactory = symmetricAlgorithmFactory;
+ _symmetricAlgorithmBlockSizeInBytes = symmetricAlgorithm.GetBlockSizeInBytes();
+ _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes;
+ }
+
+ // Validate that the MAC algorithm has the properties we require
+ using (var validationAlgorithm = validationAlgorithmFactory())
+ {
+ _validationAlgorithmFactory = validationAlgorithmFactory;
+ _validationAlgorithmDigestLengthInBytes = validationAlgorithm.GetDigestSizeInBytes();
+ _validationAlgorithmSubkeyLengthInBytes = _validationAlgorithmDigestLengthInBytes; // for simplicity we'll generate MAC subkeys with a length equal to the digest length
+ }
+
+ // Argument checking on the algorithms and lengths passed in to us
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmBlockSize(checked((uint)_symmetricAlgorithmBlockSizeInBytes * 8));
+ AlgorithmAssert.IsAllowableSymmetricAlgorithmKeySize(checked((uint)_symmetricAlgorithmSubkeyLengthInBytes * 8));
+ AlgorithmAssert.IsAllowableValidationAlgorithmDigestSize(checked((uint)_validationAlgorithmDigestLengthInBytes * 8));
+
+ _contextHeader = CreateContextHeader();
+ }
+
+ private byte[] CreateContextHeader()
+ {
+ var EMPTY_ARRAY = new byte[0];
+ var EMPTY_ARRAY_SEGMENT = new ArraySegment<byte>(EMPTY_ARRAY);
+
+ var retVal = new byte[checked(
+ 1 /* KDF alg */
+ + 1 /* chaining mode */
+ + sizeof(uint) /* sym alg key size */
+ + sizeof(uint) /* sym alg block size */
+ + sizeof(uint) /* hmac alg key size */
+ + sizeof(uint) /* hmac alg digest size */
+ + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */
+ + _validationAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)];
+
+ var idx = 0;
+
+ // First is the two-byte header
+ retVal[idx++] = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF
+ retVal[idx++] = 0; // 0x00 = CBC encryption + HMAC authentication
+
+ // Next is information about the symmetric algorithm (key size followed by block size)
+ BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmSubkeyLengthInBytes);
+ BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmBlockSizeInBytes);
+
+ // Next is information about the keyed hash algorithm (key size followed by digest size)
+ BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmSubkeyLengthInBytes);
+ BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmDigestLengthInBytes);
+
+ // See the design document for an explanation of the following code.
+ var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _validationAlgorithmSubkeyLengthInBytes];
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
+ kdk: EMPTY_ARRAY,
+ label: EMPTY_ARRAY_SEGMENT,
+ context: EMPTY_ARRAY_SEGMENT,
+ prfFactory: _kdkPrfFactory,
+ output: new ArraySegment<byte>(tempKeys));
+
+ // At this point, tempKeys := { K_E || K_H }.
+
+ // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer.
+ using (var symmetricAlg = CreateSymmetricAlgorithm())
+ {
+ using (var cryptoTransform = symmetricAlg.CreateEncryptor(
+ rgbKey: new ArraySegment<byte>(tempKeys, 0, _symmetricAlgorithmSubkeyLengthInBytes).AsStandaloneArray(),
+ rgbIV: new byte[_symmetricAlgorithmBlockSizeInBytes]))
+ {
+ var ciphertext = cryptoTransform.TransformFinalBlock(EMPTY_ARRAY, 0, 0);
+ CryptoUtil.Assert(ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes, "ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes");
+ Buffer.BlockCopy(ciphertext, 0, retVal, idx, ciphertext.Length);
+ }
+ }
+
+ idx += _symmetricAlgorithmBlockSizeInBytes;
+
+ // MAC a zero-length input string and copy the digest to the return buffer.
+ using (var hashAlg = CreateValidationAlgorithm(new ArraySegment<byte>(tempKeys, _symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes).AsStandaloneArray()))
+ {
+ var digest = hashAlg.ComputeHash(EMPTY_ARRAY);
+ CryptoUtil.Assert(digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes, "digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes");
+ Buffer.BlockCopy(digest, 0, retVal, idx, digest.Length);
+ }
+
+ idx += _validationAlgorithmDigestLengthInBytes;
+ CryptoUtil.Assert(idx == retVal.Length, "idx == retVal.Length");
+
+ // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || macAlgKeySize || macAlgDigestSize || E("") || MAC("") }.
+ return retVal;
+ }
+
+ private SymmetricAlgorithm CreateSymmetricAlgorithm()
+ {
+ var retVal = _symmetricAlgorithmFactory();
+ CryptoUtil.Assert(retVal != null, "retVal != null");
+
+ retVal.Mode = CipherMode.CBC;
+ retVal.Padding = PaddingMode.PKCS7;
+ return retVal;
+ }
+
+ private KeyedHashAlgorithm CreateValidationAlgorithm(byte[] key)
+ {
+ var retVal = _validationAlgorithmFactory();
+ CryptoUtil.Assert(retVal != null, "retVal != null");
+
+ retVal.Key = key;
+ return retVal;
+ }
+
+ public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> additionalAuthenticatedData)
+ {
+ protectedPayload.Validate();
+ additionalAuthenticatedData.Validate();
+
+ // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC
+ if (protectedPayload.Count < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes))
+ {
+ throw Error.CryptCommon_PayloadInvalid();
+ }
+
+ // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
+
+ try
+ {
+ // Step 1: Extract the key modifier and IV from the payload.
+
+ int keyModifierOffset; // position in protectedPayload.Array where key modifier begins
+ int ivOffset; // position in protectedPayload.Array where key modifier ends / IV begins
+ int ciphertextOffset; // position in protectedPayload.Array where IV ends / ciphertext begins
+ int macOffset; // position in protectedPayload.Array where ciphertext ends / MAC begins
+ int eofOffset; // position in protectedPayload.Array where MAC ends
+
+ checked
+ {
+ keyModifierOffset = protectedPayload.Offset;
+ ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
+ ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes;
+ }
+
+ ArraySegment<byte> keyModifier = new ArraySegment<byte>(protectedPayload.Array, keyModifierOffset, ivOffset - keyModifierOffset);
+ var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
+ Buffer.BlockCopy(protectedPayload.Array, ivOffset, iv, 0, iv.Length);
+
+ // Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys.
+ // We pin all unencrypted keys to limit their exposure via GC relocation.
+
+ var decryptedKdk = new byte[_keyDerivationKey.Length];
+ var decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
+ var validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes];
+ var derivedKeysBuffer = new byte[checked(decryptionSubkey.Length + validationSubkey.Length)];
+
+ fixed (byte* __unused__1 = decryptedKdk)
+ fixed (byte* __unused__2 = decryptionSubkey)
+ fixed (byte* __unused__3 = validationSubkey)
+ fixed (byte* __unused__4 = derivedKeysBuffer)
+ {
+ try
+ {
+ _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+ kdk: decryptedKdk,
+ label: additionalAuthenticatedData,
+ contextHeader: _contextHeader,
+ context: keyModifier,
+ prfFactory: _kdkPrfFactory,
+ output: new ArraySegment<byte>(derivedKeysBuffer));
+
+ Buffer.BlockCopy(derivedKeysBuffer, 0, decryptionSubkey, 0, decryptionSubkey.Length);
+ Buffer.BlockCopy(derivedKeysBuffer, decryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
+
+ // Step 3: Calculate the correct MAC for this payload.
+ // correctHash := MAC(IV || ciphertext)
+ byte[] correctHash;
+
+ using (var hashAlgorithm = CreateValidationAlgorithm(validationSubkey))
+ {
+ checked
+ {
+ eofOffset = protectedPayload.Offset + protectedPayload.Count;
+ macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes;
+ }
+
+ correctHash = hashAlgorithm.ComputeHash(protectedPayload.Array, ivOffset, macOffset - ivOffset);
+ }
+
+ // Step 4: Validate the MAC provided as part of the payload.
+
+ if (!CryptoUtil.TimeConstantBuffersAreEqual(correctHash, 0, correctHash.Length, protectedPayload.Array, macOffset, eofOffset - macOffset))
+ {
+ throw Error.CryptCommon_PayloadInvalid(); // integrity check failure
+ }
+
+ // Step 5: Decipher the ciphertext and return it to the caller.
+
+ using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
+ using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv))
+ {
+ var outputStream = new MemoryStream();
+ using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write))
+ {
+ cryptoStream.Write(protectedPayload.Array, ciphertextOffset, macOffset - ciphertextOffset);
+ cryptoStream.FlushFinalBlock();
+
+ // At this point, outputStream := { plaintext }, and we're done!
+ return outputStream.ToArray();
+ }
+ }
+ }
+ finally
+ {
+ // delete since these contain secret material
+ Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
+ Array.Clear(decryptionSubkey, 0, decryptionSubkey.Length);
+ Array.Clear(validationSubkey, 0, validationSubkey.Length);
+ Array.Clear(derivedKeysBuffer, 0, derivedKeysBuffer.Length);
+ }
+ }
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize all exceptions to CryptographicException.
+ throw Error.CryptCommon_GenericError(ex);
+ }
+ }
+
+ public void Dispose()
+ {
+ _keyDerivationKey.Dispose();
+ }
+
+ public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
+ {
+ plaintext.Validate();
+ additionalAuthenticatedData.Validate();
+
+ try
+ {
+ var outputStream = new MemoryStream();
+
+ // Step 1: Generate a random key modifier and IV for this operation.
+ // Both will be equal to the block size of the block cipher algorithm.
+
+ var keyModifier = _genRandom.GenRandom(KEY_MODIFIER_SIZE_IN_BYTES);
+ var iv = _genRandom.GenRandom(_symmetricAlgorithmBlockSizeInBytes);
+
+ // Step 2: Copy the key modifier and the IV to the output stream since they'll act as a header.
+
+ outputStream.Write(keyModifier, 0, keyModifier.Length);
+ outputStream.Write(iv, 0, iv.Length);
+
+ // At this point, outputStream := { keyModifier || IV }.
+
+ // Step 3: Decrypt the KDK, and use it to generate new encryption and HMAC keys.
+ // We pin all unencrypted keys to limit their exposure via GC relocation.
+
+ var decryptedKdk = new byte[_keyDerivationKey.Length];
+ var encryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
+ var validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes];
+ var derivedKeysBuffer = new byte[checked(encryptionSubkey.Length + validationSubkey.Length)];
+
+ fixed (byte* __unused__1 = decryptedKdk)
+ fixed (byte* __unused__2 = encryptionSubkey)
+ fixed (byte* __unused__3 = validationSubkey)
+ fixed (byte* __unused__4 = derivedKeysBuffer)
+ {
+ try
+ {
+ _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
+ kdk: decryptedKdk,
+ label: additionalAuthenticatedData,
+ contextHeader: _contextHeader,
+ context: new ArraySegment<byte>(keyModifier),
+ prfFactory: _kdkPrfFactory,
+ output: new ArraySegment<byte>(derivedKeysBuffer));
+
+ Buffer.BlockCopy(derivedKeysBuffer, 0, encryptionSubkey, 0, encryptionSubkey.Length);
+ Buffer.BlockCopy(derivedKeysBuffer, encryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
+
+ // Step 4: Perform the encryption operation.
+
+ using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
+ using (var cryptoTransform = symmetricAlgorithm.CreateEncryptor(encryptionSubkey, iv))
+ using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write))
+ {
+ cryptoStream.Write(plaintext.Array, plaintext.Offset, plaintext.Count);
+ cryptoStream.FlushFinalBlock();
+
+ // At this point, outputStream := { keyModifier || IV || ciphertext }
+
+ // Step 5: Calculate the digest over the IV and ciphertext.
+ // We don't need to calculate the digest over the key modifier since that
+ // value has already been mixed into the KDF used to generate the MAC key.
+
+ using (var validationAlgorithm = CreateValidationAlgorithm(validationSubkey))
+ {
+ // As an optimization, avoid duplicating the underlying buffer
+ var underlyingBuffer = outputStream.GetBuffer();
+
+ var mac = validationAlgorithm.ComputeHash(underlyingBuffer, KEY_MODIFIER_SIZE_IN_BYTES, checked((int)outputStream.Length - KEY_MODIFIER_SIZE_IN_BYTES));
+ outputStream.Write(mac, 0, mac.Length);
+
+ // At this point, outputStream := { keyModifier || IV || ciphertext || MAC(IV || ciphertext) }
+ // And we're done!
+ return outputStream.ToArray();
+ }
+ }
+ }
+ finally
+ {
+ // delete since these contain secret material
+ Array.Clear(decryptedKdk, 0, decryptedKdk.Length);
+ Array.Clear(encryptionSubkey, 0, encryptionSubkey.Length);
+ Array.Clear(validationSubkey, 0, validationSubkey.Length);
+ Array.Clear(derivedKeysBuffer, 0, derivedKeysBuffer.Length);
+ }
+ }
+ }
+ catch (Exception ex) when (ex.RequiresHomogenization())
+ {
+ // Homogenize all exceptions to CryptographicException.
+ throw Error.CryptCommon_GenericError(ex);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedGenRandomImpl.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedGenRandomImpl.cs
new file mode 100644
index 0000000000..d334f36672
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/ManagedGenRandomImpl.cs
@@ -0,0 +1,25 @@
+// 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.Security.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.Managed
+{
+ internal unsafe sealed class ManagedGenRandomImpl : IManagedGenRandom
+ {
+ private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
+ public static readonly ManagedGenRandomImpl Instance = new ManagedGenRandomImpl();
+
+ private ManagedGenRandomImpl()
+ {
+ }
+
+ public byte[] GenRandom(int numBytes)
+ {
+ var bytes = new byte[numBytes];
+ _rng.GetBytes(bytes);
+ return bytes;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/SymmetricAlgorithmExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/SymmetricAlgorithmExtensions.cs
new file mode 100644
index 0000000000..d411ce26c0
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Managed/SymmetricAlgorithmExtensions.cs
@@ -0,0 +1,19 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.Managed
+{
+ internal static class SymmetricAlgorithmExtensions
+ {
+ public static int GetBlockSizeInBytes(this SymmetricAlgorithm symmetricAlgorithm)
+ {
+ var blockSizeInBits = symmetricAlgorithm.BlockSize;
+ CryptoUtil.Assert(blockSizeInBits >= 0 && blockSizeInBits % 8 == 0, "blockSizeInBits >= 0 && blockSizeInBits % 8 == 0");
+ return blockSizeInBits / 8;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/MemoryProtection.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/MemoryProtection.cs
new file mode 100644
index 0000000000..be87e3cde5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/MemoryProtection.cs
@@ -0,0 +1,42 @@
+// 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.Runtime.InteropServices;
+using Microsoft.AspNetCore.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Wrappers around CryptProtectMemory / CryptUnprotectMemory.
+ /// </summary>
+ internal unsafe static class MemoryProtection
+ {
+ // from dpapi.h
+ private const uint CRYPTPROTECTMEMORY_SAME_PROCESS = 0x00;
+
+ public static void CryptProtectMemory(SafeHandle pBuffer, uint byteCount)
+ {
+ if (!UnsafeNativeMethods.CryptProtectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS))
+ {
+ UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error();
+ }
+ }
+
+ public static void CryptUnprotectMemory(byte* pBuffer, uint byteCount)
+ {
+ if (!UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS))
+ {
+ UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error();
+ }
+ }
+
+ public static void CryptUnprotectMemory(SafeHandle pBuffer, uint byteCount)
+ {
+ if (!UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS))
+ {
+ UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Microsoft.AspNetCore.DataProtection.csproj b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Microsoft.AspNetCore.DataProtection.csproj
new file mode 100644
index 0000000000..3d3d87f25b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Microsoft.AspNetCore.DataProtection.csproj
@@ -0,0 +1,31 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core logic to protect and unprotect data, similar to DPAPI.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;dataprotection</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Cryptography.Internal\Microsoft.AspNetCore.Cryptography.Internal.csproj" />
+ <ProjectReference Include="..\Microsoft.AspNetCore.DataProtection.Abstractions\Microsoft.AspNetCore.DataProtection.Abstractions.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Win32.Registry" Version="$(MicrosoftWin32RegistryPackageVersion)" />
+ <PackageReference Include="System.Security.Cryptography.Xml" Version="$(SystemSecurityCryptographyXmlPackageVersion)" />
+ <PackageReference Include="System.Security.Principal.Windows" Version="$(SystemSecurityPrincipalWindowsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..614112bd73
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+// 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.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/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/Resources.Designer.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..c570287f84
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Properties/Resources.Designer.cs
@@ -0,0 +1,394 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.DataProtection
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.DataProtection.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// An error occurred during a cryptographic operation.
+ /// </summary>
+ internal static string CryptCommon_GenericError
+ {
+ get => GetString("CryptCommon_GenericError");
+ }
+
+ /// <summary>
+ /// An error occurred during a cryptographic operation.
+ /// </summary>
+ internal static string FormatCryptCommon_GenericError()
+ => GetString("CryptCommon_GenericError");
+
+ /// <summary>
+ /// The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length.
+ /// </summary>
+ internal static string Common_BufferIncorrectlySized
+ {
+ get => GetString("Common_BufferIncorrectlySized");
+ }
+
+ /// <summary>
+ /// The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length.
+ /// </summary>
+ internal static string FormatCommon_BufferIncorrectlySized(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Common_BufferIncorrectlySized"), p0, p1);
+
+ /// <summary>
+ /// The payload was invalid.
+ /// </summary>
+ internal static string CryptCommon_PayloadInvalid
+ {
+ get => GetString("CryptCommon_PayloadInvalid");
+ }
+
+ /// <summary>
+ /// The payload was invalid.
+ /// </summary>
+ internal static string FormatCryptCommon_PayloadInvalid()
+ => GetString("CryptCommon_PayloadInvalid");
+
+ /// <summary>
+ /// Property {0} cannot be null or empty.
+ /// </summary>
+ internal static string Common_PropertyCannotBeNullOrEmpty
+ {
+ get => GetString("Common_PropertyCannotBeNullOrEmpty");
+ }
+
+ /// <summary>
+ /// Property {0} cannot be null or empty.
+ /// </summary>
+ internal static string FormatCommon_PropertyCannotBeNullOrEmpty(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyCannotBeNullOrEmpty"), p0);
+
+ /// <summary>
+ /// The provided payload could not be decrypted. Refer to the inner exception for more information.
+ /// </summary>
+ internal static string Common_DecryptionFailed
+ {
+ get => GetString("Common_DecryptionFailed");
+ }
+
+ /// <summary>
+ /// The provided payload could not be decrypted. Refer to the inner exception for more information.
+ /// </summary>
+ internal static string FormatCommon_DecryptionFailed()
+ => GetString("Common_DecryptionFailed");
+
+ /// <summary>
+ /// An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information.
+ /// </summary>
+ internal static string Common_EncryptionFailed
+ {
+ get => GetString("Common_EncryptionFailed");
+ }
+
+ /// <summary>
+ /// An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information.
+ /// </summary>
+ internal static string FormatCommon_EncryptionFailed()
+ => GetString("Common_EncryptionFailed");
+
+ /// <summary>
+ /// The key {0:B} was not found in the key ring.
+ /// </summary>
+ internal static string Common_KeyNotFound
+ {
+ get => GetString("Common_KeyNotFound");
+ }
+
+ /// <summary>
+ /// The key {0:B} was not found in the key ring.
+ /// </summary>
+ internal static string FormatCommon_KeyNotFound()
+ => GetString("Common_KeyNotFound");
+
+ /// <summary>
+ /// The key {0:B} has been revoked.
+ /// </summary>
+ internal static string Common_KeyRevoked
+ {
+ get => GetString("Common_KeyRevoked");
+ }
+
+ /// <summary>
+ /// The key {0:B} has been revoked.
+ /// </summary>
+ internal static string FormatCommon_KeyRevoked()
+ => GetString("Common_KeyRevoked");
+
+ /// <summary>
+ /// The provided payload cannot be decrypted because it was not protected with this protection provider.
+ /// </summary>
+ internal static string ProtectionProvider_BadMagicHeader
+ {
+ get => GetString("ProtectionProvider_BadMagicHeader");
+ }
+
+ /// <summary>
+ /// The provided payload cannot be decrypted because it was not protected with this protection provider.
+ /// </summary>
+ internal static string FormatProtectionProvider_BadMagicHeader()
+ => GetString("ProtectionProvider_BadMagicHeader");
+
+ /// <summary>
+ /// The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
+ /// </summary>
+ internal static string ProtectionProvider_BadVersion
+ {
+ get => GetString("ProtectionProvider_BadVersion");
+ }
+
+ /// <summary>
+ /// The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
+ /// </summary>
+ internal static string FormatProtectionProvider_BadVersion()
+ => GetString("ProtectionProvider_BadVersion");
+
+ /// <summary>
+ /// Value must be non-negative.
+ /// </summary>
+ internal static string Common_ValueMustBeNonNegative
+ {
+ get => GetString("Common_ValueMustBeNonNegative");
+ }
+
+ /// <summary>
+ /// Value must be non-negative.
+ /// </summary>
+ internal static string FormatCommon_ValueMustBeNonNegative()
+ => GetString("Common_ValueMustBeNonNegative");
+
+ /// <summary>
+ /// The type '{1}' is not assignable to '{0}'.
+ /// </summary>
+ internal static string TypeExtensions_BadCast
+ {
+ get => GetString("TypeExtensions_BadCast");
+ }
+
+ /// <summary>
+ /// The type '{1}' is not assignable to '{0}'.
+ /// </summary>
+ internal static string FormatTypeExtensions_BadCast(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TypeExtensions_BadCast"), p0, p1);
+
+ /// <summary>
+ /// The new key lifetime must be at least one week.
+ /// </summary>
+ internal static string KeyManagementOptions_MinNewKeyLifetimeViolated
+ {
+ get => GetString("KeyManagementOptions_MinNewKeyLifetimeViolated");
+ }
+
+ /// <summary>
+ /// The new key lifetime must be at least one week.
+ /// </summary>
+ internal static string FormatKeyManagementOptions_MinNewKeyLifetimeViolated()
+ => GetString("KeyManagementOptions_MinNewKeyLifetimeViolated");
+
+ /// <summary>
+ /// The key {0:B} already exists in the keyring.
+ /// </summary>
+ internal static string XmlKeyManager_DuplicateKey
+ {
+ get => GetString("XmlKeyManager_DuplicateKey");
+ }
+
+ /// <summary>
+ /// The key {0:B} already exists in the keyring.
+ /// </summary>
+ internal static string FormatXmlKeyManager_DuplicateKey()
+ => GetString("XmlKeyManager_DuplicateKey");
+
+ /// <summary>
+ /// Argument cannot be null or empty.
+ /// </summary>
+ internal static string Common_ArgumentCannotBeNullOrEmpty
+ {
+ get => GetString("Common_ArgumentCannotBeNullOrEmpty");
+ }
+
+ /// <summary>
+ /// Argument cannot be null or empty.
+ /// </summary>
+ internal static string FormatCommon_ArgumentCannotBeNullOrEmpty()
+ => GetString("Common_ArgumentCannotBeNullOrEmpty");
+
+ /// <summary>
+ /// Property {0} must have a non-negative value.
+ /// </summary>
+ internal static string Common_PropertyMustBeNonNegative
+ {
+ get => GetString("Common_PropertyMustBeNonNegative");
+ }
+
+ /// <summary>
+ /// Property {0} must have a non-negative value.
+ /// </summary>
+ internal static string FormatCommon_PropertyMustBeNonNegative(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyMustBeNonNegative"), p0);
+
+ /// <summary>
+ /// GCM algorithms require the Windows platform.
+ /// </summary>
+ internal static string Platform_WindowsRequiredForGcm
+ {
+ get => GetString("Platform_WindowsRequiredForGcm");
+ }
+
+ /// <summary>
+ /// GCM algorithms require the Windows platform.
+ /// </summary>
+ internal static string FormatPlatform_WindowsRequiredForGcm()
+ => GetString("Platform_WindowsRequiredForGcm");
+
+ /// <summary>
+ /// A certificate with the thumbprint '{0}' could not be found.
+ /// </summary>
+ internal static string CertificateXmlEncryptor_CertificateNotFound
+ {
+ get => GetString("CertificateXmlEncryptor_CertificateNotFound");
+ }
+
+ /// <summary>
+ /// A certificate with the thumbprint '{0}' could not be found.
+ /// </summary>
+ internal static string FormatCertificateXmlEncryptor_CertificateNotFound(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("CertificateXmlEncryptor_CertificateNotFound"), p0);
+
+ /// <summary>
+ /// Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
+ /// </summary>
+ internal static string EncryptedXmlDecryptor_DoesNotWorkOnCoreClr
+ {
+ get => GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr");
+ }
+
+ /// <summary>
+ /// Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
+ /// </summary>
+ internal static string FormatEncryptedXmlDecryptor_DoesNotWorkOnCoreClr()
+ => GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr");
+
+ /// <summary>
+ /// The symmetric algorithm block size of {0} bits is invalid. The block size must be between 64 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
+ /// </summary>
+ internal static string AlgorithmAssert_BadBlockSize
+ {
+ get => GetString("AlgorithmAssert_BadBlockSize");
+ }
+
+ /// <summary>
+ /// The symmetric algorithm block size of {0} bits is invalid. The block size must be between 64 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
+ /// </summary>
+ internal static string FormatAlgorithmAssert_BadBlockSize(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadBlockSize"), p0);
+
+ /// <summary>
+ /// The validation algorithm digest size of {0} bits is invalid. The digest size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
+ /// </summary>
+ internal static string AlgorithmAssert_BadDigestSize
+ {
+ get => GetString("AlgorithmAssert_BadDigestSize");
+ }
+
+ /// <summary>
+ /// The validation algorithm digest size of {0} bits is invalid. The digest size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
+ /// </summary>
+ internal static string FormatAlgorithmAssert_BadDigestSize(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadDigestSize"), p0);
+
+ /// <summary>
+ /// The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
+ /// </summary>
+ internal static string AlgorithmAssert_BadKeySize
+ {
+ get => GetString("AlgorithmAssert_BadKeySize");
+ }
+
+ /// <summary>
+ /// The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.
+ /// </summary>
+ internal static string FormatAlgorithmAssert_BadKeySize(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0);
+
+ /// <summary>
+ /// The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.
+ /// </summary>
+ internal static string KeyRingProvider_NoDefaultKey_AutoGenerateDisabled
+ {
+ get => GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled");
+ }
+
+ /// <summary>
+ /// The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.
+ /// </summary>
+ internal static string FormatKeyRingProvider_NoDefaultKey_AutoGenerateDisabled()
+ => GetString("KeyRingProvider_NoDefaultKey_AutoGenerateDisabled");
+
+ /// <summary>
+ /// {0} must not be negative
+ /// </summary>
+ internal static string LifetimeMustNotBeNegative
+ {
+ get => GetString("LifetimeMustNotBeNegative");
+ }
+
+ /// <summary>
+ /// {0} must not be negative
+ /// </summary>
+ internal static string FormatLifetimeMustNotBeNegative(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("LifetimeMustNotBeNegative"), p0);
+
+ /// <summary>
+ /// The '{0}' instance could not be found. When an '{1}' instance is set, a corresponding '{0}' instance must also be set.
+ /// </summary>
+ internal static string XmlKeyManager_IXmlRepositoryNotFound
+ {
+ get => GetString("XmlKeyManager_IXmlRepositoryNotFound");
+ }
+
+ /// <summary>
+ /// The '{0}' instance could not be found. When an '{1}' instance is set, a corresponding '{0}' instance must also be set.
+ /// </summary>
+ internal static string FormatXmlKeyManager_IXmlRepositoryNotFound(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("XmlKeyManager_IXmlRepositoryNotFound"), p0, p1);
+
+ /// <summary>
+ /// Storing keys in a directory '{path}' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
+ /// </summary>
+ internal static string FileSystem_EphemeralKeysLocationInContainer
+ {
+ get => GetString("FileSystem_EphemeralKeysLocationInContainer");
+ }
+
+ /// <summary>
+ /// Storing keys in a directory '{path}' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
+ /// </summary>
+ internal static string FormatFileSystem_EphemeralKeysLocationInContainer(object path)
+ => string.Format(CultureInfo.CurrentCulture, GetString("FileSystem_EphemeralKeysLocationInContainer", "path"), path);
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicy.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicy.cs
new file mode 100644
index 0000000000..5617ce78f6
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicy.cs
@@ -0,0 +1,28 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal class RegistryPolicy
+ {
+ public RegistryPolicy(
+ AlgorithmConfiguration configuration,
+ IEnumerable<IKeyEscrowSink> keyEscrowSinks,
+ int? defaultKeyLifetime)
+ {
+ EncryptorConfiguration = configuration;
+ KeyEscrowSinks = keyEscrowSinks;
+ DefaultKeyLifetime = defaultKeyLifetime;
+ }
+
+ public AlgorithmConfiguration EncryptorConfiguration { get; }
+
+ public IEnumerable<IKeyEscrowSink> KeyEscrowSinks { get; }
+
+ public int? DefaultKeyLifetime { get; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicyResolver.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicyResolver.cs
new file mode 100644
index 0000000000..d3357fa34d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/RegistryPolicyResolver.cs
@@ -0,0 +1,140 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Win32;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// A type which allows reading policy from the system registry.
+ /// </summary>
+ internal sealed class RegistryPolicyResolver: IRegistryPolicyResolver
+ {
+ private readonly Func<RegistryKey> _getPolicyRegKey;
+ private readonly IActivator _activator;
+
+ public RegistryPolicyResolver(IActivator activator)
+ {
+ _getPolicyRegKey = () => Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\DotNetPackages\Microsoft.AspNetCore.DataProtection");
+ _activator = activator;
+ }
+
+ internal RegistryPolicyResolver(RegistryKey policyRegKey, IActivator activator)
+ {
+ _getPolicyRegKey = () => policyRegKey;
+ _activator = activator;
+ }
+
+ // populates an options object from values stored in the registry
+ private static void PopulateOptions(object options, RegistryKey key)
+ {
+ foreach (PropertyInfo propInfo in options.GetType().GetProperties())
+ {
+ if (propInfo.IsDefined(typeof(ApplyPolicyAttribute)))
+ {
+ var valueFromRegistry = key.GetValue(propInfo.Name);
+ if (valueFromRegistry != null)
+ {
+ if (propInfo.PropertyType == typeof(string))
+ {
+ propInfo.SetValue(options, Convert.ToString(valueFromRegistry, CultureInfo.InvariantCulture));
+ }
+ else if (propInfo.PropertyType == typeof(int))
+ {
+ propInfo.SetValue(options, Convert.ToInt32(valueFromRegistry, CultureInfo.InvariantCulture));
+ }
+ else if (propInfo.PropertyType == typeof(Type))
+ {
+ propInfo.SetValue(options, Type.GetType(Convert.ToString(valueFromRegistry, CultureInfo.InvariantCulture), throwOnError: true));
+ }
+ else
+ {
+ throw CryptoUtil.Fail("Unexpected type on property: " + propInfo.Name);
+ }
+ }
+ }
+ }
+ }
+
+ private static List<string> ReadKeyEscrowSinks(RegistryKey key)
+ {
+ var sinks = new List<string>();
+
+ // The format of this key is "type1; type2; ...".
+ // We call Type.GetType to perform an eager check that the type exists.
+ var sinksFromRegistry = (string)key.GetValue("KeyEscrowSinks");
+ if (sinksFromRegistry != null)
+ {
+ foreach (string sinkFromRegistry in sinksFromRegistry.Split(';'))
+ {
+ var candidate = sinkFromRegistry.Trim();
+ if (!String.IsNullOrEmpty(candidate))
+ {
+ typeof(IKeyEscrowSink).AssertIsAssignableFrom(Type.GetType(candidate, throwOnError: true));
+ sinks.Add(candidate);
+ }
+ }
+ }
+
+ return sinks;
+ }
+
+ public RegistryPolicy ResolvePolicy()
+ {
+ using (var registryKey = _getPolicyRegKey())
+ {
+ return ResolvePolicyCore(registryKey); // fully evaluate enumeration while the reg key is open
+ }
+ }
+
+ private RegistryPolicy ResolvePolicyCore(RegistryKey policyRegKey)
+ {
+ if (policyRegKey == null)
+ {
+ return null;
+ }
+
+ // Read the encryption options type: CNG-CBC, CNG-GCM, Managed
+ AlgorithmConfiguration configuration = null;
+
+ var encryptionType = (string)policyRegKey.GetValue("EncryptionType");
+ if (String.Equals(encryptionType, "CNG-CBC", StringComparison.OrdinalIgnoreCase))
+ {
+ configuration = new CngCbcAuthenticatedEncryptorConfiguration();
+ }
+ else if (String.Equals(encryptionType, "CNG-GCM", StringComparison.OrdinalIgnoreCase))
+ {
+ configuration = new CngGcmAuthenticatedEncryptorConfiguration();
+ }
+ else if (String.Equals(encryptionType, "Managed", StringComparison.OrdinalIgnoreCase))
+ {
+ configuration = new ManagedAuthenticatedEncryptorConfiguration();
+ }
+ else if (!String.IsNullOrEmpty(encryptionType))
+ {
+ throw CryptoUtil.Fail("Unrecognized EncryptionType: " + encryptionType);
+ }
+ if (configuration != null)
+ {
+ PopulateOptions(configuration, policyRegKey);
+ }
+
+ // Read ancillary data
+
+ var defaultKeyLifetime = (int?)policyRegKey.GetValue("DefaultKeyLifetime");
+
+ var keyEscrowSinks = ReadKeyEscrowSinks(policyRegKey).Select(item => _activator.CreateInstance<IKeyEscrowSink>(item));
+
+ return new RegistryPolicy(configuration, keyEscrowSinks, defaultKeyLifetime);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/DefaultKeyStorageDirectories.cs
new file mode 100644
index 0000000000..a0717263fb
--- /dev/null
+++ b/src/DataProtection/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<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));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/EphemeralXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/EphemeralXmlRepository.cs
new file mode 100644
index 0000000000..17c7156b8d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/EphemeralXmlRepository.cs
@@ -0,0 +1,60 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ /// <summary>
+ /// An ephemeral XML repository backed by process memory. This class must not be used for
+ /// anything other than dev scenarios as the keys will not be persisted to storage.
+ /// </summary>
+ internal class EphemeralXmlRepository : IXmlRepository
+ {
+ private readonly List<XElement> _storedElements = new List<XElement>();
+
+ public EphemeralXmlRepository(ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger<EphemeralXmlRepository>();
+ logger.UsingInmemoryRepository();
+ }
+
+ public virtual IReadOnlyCollection<XElement> GetAllElements()
+ {
+ // force complete enumeration under lock for thread safety
+ lock (_storedElements)
+ {
+ return GetAllElementsCore().ToList().AsReadOnly();
+ }
+ }
+
+ private IEnumerable<XElement> GetAllElementsCore()
+ {
+ // this method must be called under lock
+ foreach (XElement element in _storedElements)
+ {
+ yield return new XElement(element); // makes a deep copy so caller doesn't inadvertently modify it
+ }
+ }
+
+ public virtual void StoreElement(XElement element, string friendlyName)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ var cloned = new XElement(element); // makes a deep copy so caller doesn't inadvertently modify it
+
+ // under lock for thread safety
+ lock (_storedElements)
+ {
+ _storedElements.Add(cloned);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs
new file mode 100644
index 0000000000..7ceede33d1
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/FileSystemXmlRepository.cs
@@ -0,0 +1,155 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ /// <summary>
+ /// An XML repository backed by a file system.
+ /// </summary>
+ public class FileSystemXmlRepository : IXmlRepository
+ {
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a <see cref="FileSystemXmlRepository"/> with keys stored at the given directory.
+ /// </summary>
+ /// <param name="directory">The directory in which to persist key material.</param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ public FileSystemXmlRepository(DirectoryInfo directory, ILoggerFactory loggerFactory)
+ {
+ Directory = directory ?? throw new ArgumentNullException(nameof(directory));
+
+ _logger = loggerFactory.CreateLogger<FileSystemXmlRepository>();
+
+ try
+ {
+ if (DockerUtils.IsDocker && !DockerUtils.IsVolumeMountedFolder(Directory))
+ {
+ // warn users that keys may be lost when running in docker without a volume mounted folder
+ _logger.UsingEphemeralFileSystemLocationInContainer(Directory.FullName);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Treat exceptions as non-fatal when attempting to detect docker.
+ // These might occur if fstab is an unrecognized format, or if there are other unusual
+ // file IO errors.
+ _logger.LogTrace(ex, "Failure occurred while attempting to detect docker.");
+ }
+ }
+
+ /// <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 static DirectoryInfo DefaultKeyStorageDirectory => DefaultKeyStorageDirectories.Instance.GetKeyStorageDirectory();
+
+ /// <summary>
+ /// The directory into which key material will be written.
+ /// </summary>
+ public DirectoryInfo Directory { get; }
+
+ public virtual IReadOnlyCollection<XElement> GetAllElements()
+ {
+ // forces complete enumeration
+ return GetAllElementsCore().ToList().AsReadOnly();
+ }
+
+ private IEnumerable<XElement> GetAllElementsCore()
+ {
+ Directory.Create(); // won't throw if the directory already exists
+
+ // Find all files matching the pattern "*.xml".
+ // Note: Inability to read any file is considered a fatal error (since the file may contain
+ // revocation information), and we'll fail the entire operation rather than return a partial
+ // set of elements. If a file contains well-formed XML but its contents are meaningless, we
+ // won't fail that operation here. The caller is responsible for failing as appropriate given
+ // that scenario.
+ foreach (var fileSystemInfo in Directory.EnumerateFileSystemInfos("*.xml", SearchOption.TopDirectoryOnly))
+ {
+ yield return ReadElementFromFile(fileSystemInfo.FullName);
+ }
+ }
+
+ private static bool IsSafeFilename(string filename)
+ {
+ // Must be non-empty and contain only a-zA-Z0-9, hyphen, and underscore.
+ return (!String.IsNullOrEmpty(filename) && filename.All(c =>
+ c == '-'
+ || c == '_'
+ || ('0' <= c && c <= '9')
+ || ('A' <= c && c <= 'Z')
+ || ('a' <= c && c <= 'z')));
+ }
+
+ private XElement ReadElementFromFile(string fullPath)
+ {
+ _logger.ReadingDataFromFile(fullPath);
+
+ using (var fileStream = File.OpenRead(fullPath))
+ {
+ return XElement.Load(fileStream);
+ }
+ }
+
+ public virtual void StoreElement(XElement element, string friendlyName)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ if (!IsSafeFilename(friendlyName))
+ {
+ var newFriendlyName = Guid.NewGuid().ToString();
+ _logger.NameIsNotSafeFileName(friendlyName, newFriendlyName);
+ friendlyName = newFriendlyName;
+ }
+
+ StoreElementCore(element, friendlyName);
+ }
+
+ private void StoreElementCore(XElement element, string filename)
+ {
+ // We're first going to write the file to a temporary location. This way, another consumer
+ // won't try reading the file in the middle of us writing it. Additionally, if our process
+ // crashes mid-write, we won't end up with a corrupt .xml file.
+
+ Directory.Create(); // won't throw if the directory already exists
+ var tempFilename = Path.Combine(Directory.FullName, Guid.NewGuid().ToString() + ".tmp");
+ var finalFilename = Path.Combine(Directory.FullName, filename + ".xml");
+
+ try
+ {
+ using (var tempFileStream = File.OpenWrite(tempFilename))
+ {
+ element.Save(tempFileStream);
+ }
+
+ // Once the file has been fully written, perform the rename.
+ // Renames are atomic operations on the file systems we support.
+ _logger.WritingDataToFile(finalFilename);
+
+ // Use File.Copy because File.Move on NFS shares has issues in .NET Core 2.0
+ File.Copy(tempFilename, finalFilename);
+ }
+ finally
+ {
+ File.Delete(tempFilename); // won't throw if the file doesn't exist
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IDefaultKeyStorageDirectory.cs
new file mode 100644
index 0000000000..e7e1410e79
--- /dev/null
+++ b/src/DataProtection/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
+{
+ /// <summary>
+ /// This interface enables overridding the default storage location of keys on disk
+ /// </summary>
+ internal interface IDefaultKeyStorageDirectories
+ {
+ DirectoryInfo GetKeyStorageDirectory();
+
+ DirectoryInfo GetKeyStorageDirectoryForAzureWebSites();
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IXmlRepository.cs
new file mode 100644
index 0000000000..d62422d55e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/IXmlRepository.cs
@@ -0,0 +1,37 @@
+// 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.Collections.Generic;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ /// <summary>
+ /// The basic interface for storing and retrieving XML elements.
+ /// </summary>
+ public interface IXmlRepository
+ {
+ /// <summary>
+ /// Gets all top-level XML elements in the repository.
+ /// </summary>
+ /// <remarks>
+ /// All top-level elements in the repository.
+ /// </remarks>
+ IReadOnlyCollection<XElement> GetAllElements();
+
+ /// <summary>
+ /// Adds a top-level XML element to the repository.
+ /// </summary>
+ /// <param name="element">The element to add.</param>
+ /// <param name="friendlyName">An optional name to be associated with the XML element.
+ /// For instance, if this repository stores XML files on disk, the friendly name may
+ /// be used as part of the file name. Repository implementations are not required to
+ /// observe this parameter even if it has been provided by the caller.</param>
+ /// <remarks>
+ /// The 'friendlyName' parameter must be unique if specified. For instance, it could
+ /// be the id of the key being stored.
+ /// </remarks>
+ void StoreElement(XElement element, string friendlyName);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/RegistryXmlRepository.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/RegistryXmlRepository.cs
new file mode 100644
index 0000000000..7692d1ccb5
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Repositories/RegistryXmlRepository.cs
@@ -0,0 +1,160 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Security.Principal;
+using System.Xml.Linq;
+using Microsoft.Extensions.Logging;
+using Microsoft.Win32;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ /// <summary>
+ /// An XML repository backed by the Windows registry.
+ /// </summary>
+ public class RegistryXmlRepository : IXmlRepository
+ {
+ private static readonly Lazy<RegistryKey> _defaultRegistryKeyLazy = new Lazy<RegistryKey>(GetDefaultHklmStorageKey);
+
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a <see cref="RegistryXmlRepository"/> with keys stored in the given registry key.
+ /// </summary>
+ /// <param name="registryKey">The registry key in which to persist key material.</param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ public RegistryXmlRepository(RegistryKey registryKey, ILoggerFactory loggerFactory)
+ {
+ if (registryKey == null)
+ {
+ throw new ArgumentNullException(nameof(registryKey));
+ }
+
+ RegistryKey = registryKey;
+ _logger = loggerFactory.CreateLogger<RegistryXmlRepository>();
+ }
+
+ /// <summary>
+ /// The default key storage directory, which currently corresponds to
+ /// "HKLM\SOFTWARE\Microsoft\ASP.NET\4.0.30319.0\AutoGenKeys\{SID}".
+ /// </summary>
+ /// <remarks>
+ /// This property can return null if no suitable default registry key can
+ /// be found, such as the case when this application is not hosted inside IIS.
+ /// </remarks>
+ public static RegistryKey DefaultRegistryKey => _defaultRegistryKeyLazy.Value;
+
+ /// <summary>
+ /// The registry key into which key material will be written.
+ /// </summary>
+ public RegistryKey RegistryKey { get; }
+
+ public virtual IReadOnlyCollection<XElement> GetAllElements()
+ {
+ // forces complete enumeration
+ return GetAllElementsCore().ToList().AsReadOnly();
+ }
+
+ private IEnumerable<XElement> GetAllElementsCore()
+ {
+ // Note: Inability to parse any value is considered a fatal error (since the value may contain
+ // revocation information), and we'll fail the entire operation rather than return a partial
+ // set of elements. If a file contains well-formed XML but its contents are meaningless, we
+ // won't fail that operation here. The caller is responsible for failing as appropriate given
+ // that scenario.
+
+ foreach (string valueName in RegistryKey.GetValueNames())
+ {
+ var element = ReadElementFromRegKey(RegistryKey, valueName);
+ if (element != null)
+ {
+ yield return element;
+ }
+ }
+ }
+
+ private static RegistryKey GetDefaultHklmStorageKey()
+ {
+ try
+ {
+ var registryView = IntPtr.Size == 4 ? RegistryView.Registry32 : RegistryView.Registry64;
+ // Try reading the auto-generated machine key from HKLM
+ using (var hklmBaseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, registryView))
+ {
+ // Even though this is in HKLM, WAS ensures that applications hosted in IIS are properly isolated.
+ // See APP_POOL::EnsureSharedMachineKeyStorage in WAS source for more info.
+ // The version number will need to change if IIS hosts Core CLR directly.
+ var aspnetAutoGenKeysBaseKeyName = string.Format(
+ CultureInfo.InvariantCulture,
+ @"SOFTWARE\Microsoft\ASP.NET\4.0.30319.0\AutoGenKeys\{0}",
+ WindowsIdentity.GetCurrent().User.Value);
+
+ var aspnetBaseKey = hklmBaseKey.OpenSubKey(aspnetAutoGenKeysBaseKeyName, writable: true);
+ if (aspnetBaseKey != null)
+ {
+ using (aspnetBaseKey)
+ {
+ // We'll create a 'DataProtection' subkey under the auto-gen keys base
+ return aspnetBaseKey.OpenSubKey("DataProtection", writable: true)
+ ?? aspnetBaseKey.CreateSubKey("DataProtection");
+ }
+ }
+ return null; // couldn't find the auto-generated machine key
+ }
+ }
+ catch
+ {
+ // swallow all errors; they're not fatal
+ return null;
+ }
+ }
+
+ private static bool IsSafeRegistryValueName(string filename)
+ {
+ // Must be non-empty and contain only a-zA-Z0-9, hyphen, and underscore.
+ return (!String.IsNullOrEmpty(filename) && filename.All(c =>
+ c == '-'
+ || c == '_'
+ || ('0' <= c && c <= '9')
+ || ('A' <= c && c <= 'Z')
+ || ('a' <= c && c <= 'z')));
+ }
+
+ private XElement ReadElementFromRegKey(RegistryKey regKey, string valueName)
+ {
+ _logger.ReadingDataFromRegistryKeyValue(regKey, valueName);
+
+ var data = regKey.GetValue(valueName) as string;
+ return (!String.IsNullOrEmpty(data)) ? XElement.Parse(data) : null;
+ }
+
+ public virtual void StoreElement(XElement element, string friendlyName)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ if (!IsSafeRegistryValueName(friendlyName))
+ {
+ var newFriendlyName = Guid.NewGuid().ToString();
+ _logger.NameIsNotSafeRegistryValueName(friendlyName, newFriendlyName);
+ friendlyName = newFriendlyName;
+ }
+
+ StoreElementCore(element, friendlyName);
+ }
+
+ private void StoreElementCore(XElement element, string valueName)
+ {
+ // Technically calls to RegSetValue* and RegGetValue* are atomic, so we don't have to worry about
+ // another thread trying to read this value while we're writing it. There's still a small risk of
+ // data corruption if power is lost while the registry file is being flushed to the file system,
+ // but the window for that should be small enough that we shouldn't have to worry about it.
+ RegistryKey.SetValue(valueName, element.ToString(), RegistryValueKind.String);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Resources.resx b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Resources.resx
new file mode 100644
index 0000000000..9540aa54fa
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Resources.resx
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CryptCommon_GenericError" xml:space="preserve">
+ <value>An error occurred during a cryptographic operation.</value>
+ </data>
+ <data name="Common_BufferIncorrectlySized" xml:space="preserve">
+ <value>The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length.</value>
+ </data>
+ <data name="CryptCommon_PayloadInvalid" xml:space="preserve">
+ <value>The payload was invalid.</value>
+ </data>
+ <data name="Common_PropertyCannotBeNullOrEmpty" xml:space="preserve">
+ <value>Property {0} cannot be null or empty.</value>
+ </data>
+ <data name="Common_DecryptionFailed" xml:space="preserve">
+ <value>The provided payload could not be decrypted. Refer to the inner exception for more information.</value>
+ </data>
+ <data name="Common_EncryptionFailed" xml:space="preserve">
+ <value>An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information.</value>
+ </data>
+ <data name="Common_KeyNotFound" xml:space="preserve">
+ <value>The key {0:B} was not found in the key ring.</value>
+ </data>
+ <data name="Common_KeyRevoked" xml:space="preserve">
+ <value>The key {0:B} has been revoked.</value>
+ </data>
+ <data name="ProtectionProvider_BadMagicHeader" xml:space="preserve">
+ <value>The provided payload cannot be decrypted because it was not protected with this protection provider.</value>
+ </data>
+ <data name="ProtectionProvider_BadVersion" xml:space="preserve">
+ <value>The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.</value>
+ </data>
+ <data name="Common_ValueMustBeNonNegative" xml:space="preserve">
+ <value>Value must be non-negative.</value>
+ </data>
+ <data name="TypeExtensions_BadCast" xml:space="preserve">
+ <value>The type '{1}' is not assignable to '{0}'.</value>
+ </data>
+ <data name="KeyManagementOptions_MinNewKeyLifetimeViolated" xml:space="preserve">
+ <value>The new key lifetime must be at least one week.</value>
+ </data>
+ <data name="XmlKeyManager_DuplicateKey" xml:space="preserve">
+ <value>The key {0:B} already exists in the keyring.</value>
+ </data>
+ <data name="Common_ArgumentCannotBeNullOrEmpty" xml:space="preserve">
+ <value>Argument cannot be null or empty.</value>
+ </data>
+ <data name="Common_PropertyMustBeNonNegative" xml:space="preserve">
+ <value>Property {0} must have a non-negative value.</value>
+ </data>
+ <data name="Platform_WindowsRequiredForGcm" xml:space="preserve">
+ <value>GCM algorithms require the Windows platform.</value>
+ </data>
+ <data name="CertificateXmlEncryptor_CertificateNotFound" xml:space="preserve">
+ <value>A certificate with the thumbprint '{0}' could not be found.</value>
+ </data>
+ <data name="EncryptedXmlDecryptor_DoesNotWorkOnCoreClr" xml:space="preserve">
+ <value>Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.</value>
+ </data>
+ <data name="AlgorithmAssert_BadBlockSize" xml:space="preserve">
+ <value>The symmetric algorithm block size of {0} bits is invalid. The block size must be between 64 and 2048 bits, inclusive, and it must be a multiple of 8 bits.</value>
+ </data>
+ <data name="AlgorithmAssert_BadDigestSize" xml:space="preserve">
+ <value>The validation algorithm digest size of {0} bits is invalid. The digest size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.</value>
+ </data>
+ <data name="AlgorithmAssert_BadKeySize" xml:space="preserve">
+ <value>The symmetric algorithm key size of {0} bits is invalid. The key size must be between 128 and 2048 bits, inclusive, and it must be a multiple of 8 bits.</value>
+ </data>
+ <data name="KeyRingProvider_NoDefaultKey_AutoGenerateDisabled" xml:space="preserve">
+ <value>The key ring does not contain a valid default protection key. The data protection system cannot create a new key because auto-generation of keys is disabled.</value>
+ </data>
+ <data name="LifetimeMustNotBeNegative" xml:space="preserve">
+ <value>{0} must not be negative</value>
+ </data>
+ <data name="XmlKeyManager_IXmlRepositoryNotFound" xml:space="preserve">
+ <value>The '{0}' instance could not be found. When an '{1}' instance is set, a corresponding '{0}' instance must also be set.</value>
+ </data>
+ <data name="FileSystem_EphemeralKeysLocationInContainer" xml:space="preserve">
+ <value>Storing keys in a directory '{path}' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs
new file mode 100644
index 0000000000..f7e6aecdb1
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs
@@ -0,0 +1,12 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ internal unsafe interface ISP800_108_CTR_HMACSHA512Provider : IDisposable
+ {
+ void DeriveKey(byte* pbLabel, uint cbLabel, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
new file mode 100644
index 0000000000..57e8f0472c
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs
@@ -0,0 +1,66 @@
+// 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.Security.Cryptography;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Managed;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ internal static class ManagedSP800_108_CTR_HMACSHA512
+ {
+ public static void DeriveKeys(byte[] kdk, ArraySegment<byte> label, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
+ {
+ // make copies so we can mutate these local vars
+ var outputOffset = output.Offset;
+ var outputCount = output.Count;
+
+ using (var prf = prfFactory(kdk))
+ {
+ // See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
+ var prfInput = new byte[checked(sizeof(uint) /* [i]_2 */ + label.Count + 1 /* 0x00 */ + context.Count + sizeof(uint) /* [K]_2 */)];
+
+ // Copy [L]_2 to prfInput since it's stable over all iterations
+ uint outputSizeInBits = (uint)checked((int)outputCount * 8);
+ prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
+ prfInput[prfInput.Length - 3] = (byte)(outputSizeInBits >> 16);
+ prfInput[prfInput.Length - 2] = (byte)(outputSizeInBits >> 8);
+ prfInput[prfInput.Length - 1] = (byte)(outputSizeInBits);
+
+ // Copy label and context to prfInput since they're stable over all iterations
+ Buffer.BlockCopy(label.Array, label.Offset, prfInput, sizeof(uint), label.Count);
+ Buffer.BlockCopy(context.Array, context.Offset, prfInput, sizeof(int) + label.Count + 1, context.Count);
+
+ var prfOutputSizeInBytes = prf.GetDigestSizeInBytes();
+ for (uint i = 1; outputCount > 0; i++)
+ {
+ // Copy [i]_2 to prfInput since it mutates with each iteration
+ prfInput[0] = (byte)(i >> 24);
+ prfInput[1] = (byte)(i >> 16);
+ prfInput[2] = (byte)(i >> 8);
+ prfInput[3] = (byte)(i);
+
+ // Run the PRF and copy the results to the output buffer
+ var prfOutput = prf.ComputeHash(prfInput);
+ CryptoUtil.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
+ var numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount);
+ Buffer.BlockCopy(prfOutput, 0, output.Array, outputOffset, numBytesToCopyThisIteration);
+ Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so delete it
+
+ // adjust offsets
+ outputOffset += numBytesToCopyThisIteration;
+ outputCount -= numBytesToCopyThisIteration;
+ }
+ }
+ }
+
+ public static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
+ {
+ var combinedContext = new byte[checked(contextHeader.Length + context.Count)];
+ Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length);
+ Buffer.BlockCopy(context.Array, context.Offset, combinedContext, contextHeader.Length, context.Count);
+ DeriveKeys(kdk, label, new ArraySegment<byte>(combinedContext), prfFactory, output);
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs
new file mode 100644
index 0000000000..adb084a0c9
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs
@@ -0,0 +1,37 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ internal unsafe static class SP800_108_CTR_HMACSHA512Extensions
+ {
+ public static void DeriveKeyWithContextHeader(this ISP800_108_CTR_HMACSHA512Provider provider, byte* pbLabel, uint cbLabel, byte[] contextHeader, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey)
+ {
+ var cbCombinedContext = checked((uint)contextHeader.Length + cbContext);
+
+ // Try allocating the combined context on the stack to avoid temporary managed objects; only fall back to heap if buffers are too large.
+ byte[] heapAllocatedCombinedContext = (cbCombinedContext > Constants.MAX_STACKALLOC_BYTES) ? new byte[cbCombinedContext] : null;
+ fixed (byte* pbHeapAllocatedCombinedContext = heapAllocatedCombinedContext)
+ {
+ byte* pbCombinedContext = pbHeapAllocatedCombinedContext;
+ if (pbCombinedContext == null)
+ {
+ byte* pbStackAllocatedCombinedContext = stackalloc byte[(int)cbCombinedContext]; // will be released when frame pops
+ pbCombinedContext = pbStackAllocatedCombinedContext;
+ }
+
+ fixed (byte* pbContextHeader = contextHeader)
+ {
+ UnsafeBufferUtil.BlockCopy(from: pbContextHeader, to: pbCombinedContext, byteCount: contextHeader.Length);
+ }
+ UnsafeBufferUtil.BlockCopy(from: pbContext, to: &pbCombinedContext[contextHeader.Length], byteCount: cbContext);
+
+ // At this point, combinedContext := { contextHeader || context }
+ provider.DeriveKey(pbLabel, cbLabel, pbCombinedContext, cbCombinedContext, pbDerivedKey, cbDerivedKey);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs
new file mode 100644
index 0000000000..c28af6f0a3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs
@@ -0,0 +1,64 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ /// <summary>
+ /// Provides an implementation of the SP800-108-CTR-HMACSHA512 key derivation function.
+ /// This class assumes at least Windows 7 / Server 2008 R2.
+ /// </summary>
+ /// <remarks>
+ /// More info at http://csrc.nist.gov/publications/nistpubs/800-108/sp800-108.pdf, Sec. 5.1.
+ /// </remarks>
+ internal unsafe static class SP800_108_CTR_HMACSHA512Util
+ {
+ // Creates a provider with an empty key.
+ public static ISP800_108_CTR_HMACSHA512Provider CreateEmptyProvider()
+ {
+ byte dummy;
+ return CreateProvider(pbKdk: &dummy, cbKdk: 0);
+ }
+
+ // Creates a provider from the given key.
+ public static ISP800_108_CTR_HMACSHA512Provider CreateProvider(byte* pbKdk, uint cbKdk)
+ {
+ if (OSVersionUtil.IsWindows8OrLater())
+ {
+ return new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk);
+ }
+ else
+ {
+ return new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk);
+ }
+ }
+
+ // Creates a provider from the given secret.
+ public static ISP800_108_CTR_HMACSHA512Provider CreateProvider(Secret kdk)
+ {
+ var secretLengthInBytes = checked((uint)kdk.Length);
+ if (secretLengthInBytes == 0)
+ {
+ return CreateEmptyProvider();
+ }
+ else
+ {
+ fixed (byte* pbPlaintextSecret = new byte[secretLengthInBytes])
+ {
+ try
+ {
+ kdk.WriteSecretIntoBuffer(pbPlaintextSecret, checked((int)secretLengthInBytes));
+ return CreateProvider(pbPlaintextSecret, secretLengthInBytes);
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbPlaintextSecret, secretLengthInBytes);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs
new file mode 100644
index 0000000000..a2143ff872
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs
@@ -0,0 +1,80 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ internal unsafe sealed class Win7SP800_108_CTR_HMACSHA512Provider : ISP800_108_CTR_HMACSHA512Provider
+ {
+ private readonly BCryptHashHandle _hashHandle;
+
+ public Win7SP800_108_CTR_HMACSHA512Provider(byte* pbKdk, uint cbKdk)
+ {
+ _hashHandle = CachedAlgorithmHandles.HMAC_SHA512.CreateHmac(pbKdk, cbKdk);
+ }
+
+ public void DeriveKey(byte* pbLabel, uint cbLabel, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey)
+ {
+ const uint SHA512_DIGEST_SIZE_IN_BYTES = 512 / 8;
+ byte* pbHashDigest = stackalloc byte[(int)SHA512_DIGEST_SIZE_IN_BYTES];
+
+ // NOTE: pbDerivedKey and cbDerivedKey are modified as data is copied to the output buffer.
+
+ // this will be zero-inited
+ var tempInputBuffer = new byte[checked(
+ sizeof(int) /* [i] */
+ + cbLabel /* Label */
+ + 1 /* 0x00 */
+ + cbContext /* Context */
+ + sizeof(int) /* [L] */)];
+
+ fixed (byte* pbTempInputBuffer = tempInputBuffer)
+ {
+ // Step 1: Calculate all necessary offsets into the temp input & output buffer.
+ byte* pbTempInputCounter = pbTempInputBuffer;
+ byte* pbTempInputLabel = &pbTempInputCounter[sizeof(int)];
+ byte* pbTempInputContext = &pbTempInputLabel[cbLabel + 1 /* 0x00 */];
+ byte* pbTempInputBitlengthIndicator = &pbTempInputContext[cbContext];
+
+ // Step 2: Copy Label and Context into the temp input buffer.
+ UnsafeBufferUtil.BlockCopy(from: pbLabel, to: pbTempInputLabel, byteCount: cbLabel);
+ UnsafeBufferUtil.BlockCopy(from: pbContext, to: pbTempInputContext, byteCount: cbContext);
+
+ // Step 3: copy [L] into last part of data to be hashed, big-endian
+ BitHelpers.WriteTo(pbTempInputBitlengthIndicator, checked(cbDerivedKey * 8));
+
+ // Step 4: iterate until all desired bytes have been generated
+ for (uint i = 1; cbDerivedKey > 0; i++)
+ {
+ // Step 4a: Copy [i] into the first part of data to be hashed, big-endian
+ BitHelpers.WriteTo(pbTempInputCounter, i);
+
+ // Step 4b: Hash. Win7 doesn't allow reusing hash algorithm objects after the final hash
+ // has been computed, so we'll just keep calling DuplicateHash on the original
+ // hash handle. This offers a slight performance increase over allocating a new hash
+ // handle for each iteration. We don't need to mess with any of this on Win8 since on
+ // that platform we use BCryptKeyDerivation directly, which offers superior performance.
+ using (var hashHandle = _hashHandle.DuplicateHash())
+ {
+ hashHandle.HashData(pbTempInputBuffer, (uint)tempInputBuffer.Length, pbHashDigest, SHA512_DIGEST_SIZE_IN_BYTES);
+ }
+
+ // Step 4c: Copy bytes from the temporary buffer to the output buffer.
+ uint numBytesToCopy = Math.Min(cbDerivedKey, SHA512_DIGEST_SIZE_IN_BYTES);
+ UnsafeBufferUtil.BlockCopy(from: pbHashDigest, to: pbDerivedKey, byteCount: numBytesToCopy);
+ pbDerivedKey += numBytesToCopy;
+ cbDerivedKey -= numBytesToCopy;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ _hashHandle.Dispose();
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs
new file mode 100644
index 0000000000..be7fe7c917
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs
@@ -0,0 +1,108 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ internal unsafe sealed class Win8SP800_108_CTR_HMACSHA512Provider : ISP800_108_CTR_HMACSHA512Provider
+ {
+ private readonly BCryptKeyHandle _keyHandle;
+
+ public Win8SP800_108_CTR_HMACSHA512Provider(byte* pbKdk, uint cbKdk)
+ {
+ _keyHandle = ImportKey(pbKdk, cbKdk);
+ }
+
+ public void DeriveKey(byte* pbLabel, uint cbLabel, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey)
+ {
+ const int SHA512_ALG_CHAR_COUNT = 7;
+ char* pszHashAlgorithm = stackalloc char[SHA512_ALG_CHAR_COUNT /* includes terminating null */];
+ pszHashAlgorithm[0] = 'S';
+ pszHashAlgorithm[1] = 'H';
+ pszHashAlgorithm[2] = 'A';
+ pszHashAlgorithm[3] = '5';
+ pszHashAlgorithm[4] = '1';
+ pszHashAlgorithm[5] = '2';
+ pszHashAlgorithm[6] = (char)0;
+
+ // First, build the buffers necessary to pass (label, context, PRF algorithm) into the KDF
+ BCryptBuffer* pBuffers = stackalloc BCryptBuffer[3];
+
+ pBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_LABEL;
+ pBuffers[0].pvBuffer = (IntPtr)pbLabel;
+ pBuffers[0].cbBuffer = cbLabel;
+
+ pBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_CONTEXT;
+ pBuffers[1].pvBuffer = (IntPtr)pbContext;
+ pBuffers[1].cbBuffer = cbContext;
+
+ pBuffers[2].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM;
+ pBuffers[2].pvBuffer = (IntPtr)pszHashAlgorithm;
+ pBuffers[2].cbBuffer = checked(SHA512_ALG_CHAR_COUNT * sizeof(char));
+
+ // Add the header which points to the buffers
+ var bufferDesc = default(BCryptBufferDesc);
+ BCryptBufferDesc.Initialize(ref bufferDesc);
+ bufferDesc.cBuffers = 3;
+ bufferDesc.pBuffers = pBuffers;
+
+ // Finally, invoke the KDF
+ uint numBytesDerived;
+ var ntstatus = UnsafeNativeMethods.BCryptKeyDerivation(
+ hKey: _keyHandle,
+ pParameterList: &bufferDesc,
+ pbDerivedKey: pbDerivedKey,
+ cbDerivedKey: cbDerivedKey,
+ pcbResult: out numBytesDerived,
+ dwFlags: 0);
+ UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
+
+ // Final sanity checks before returning control to caller.
+ CryptoUtil.Assert(numBytesDerived == cbDerivedKey, "numBytesDerived == cbDerivedKey");
+ }
+
+ public void Dispose()
+ {
+ _keyHandle.Dispose();
+ }
+
+ private static BCryptKeyHandle ImportKey(byte* pbKdk, uint cbKdk)
+ {
+ // The MS implementation of SP800_108_CTR_HMAC has a limit on the size of the key it can accept.
+ // If the incoming key is too long, we'll hash it using SHA512 to bring it back to a manageable
+ // length. This transform is appropriate since SP800_108_CTR_HMAC is just a glorified HMAC under
+ // the covers, and the HMAC algorithm allows hashing the key using the underlying PRF if the key
+ // is greater than the PRF's block length.
+
+ const uint SHA512_BLOCK_SIZE_IN_BYTES = 1024 / 8;
+ const uint SHA512_DIGEST_SIZE_IN_BYTES = 512 / 8;
+
+ if (cbKdk > SHA512_BLOCK_SIZE_IN_BYTES)
+ {
+ // Hash key.
+ byte* pbHashedKey = stackalloc byte[(int)SHA512_DIGEST_SIZE_IN_BYTES];
+ try
+ {
+ using (var hashHandle = CachedAlgorithmHandles.SHA512.CreateHash())
+ {
+ hashHandle.HashData(pbKdk, cbKdk, pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES);
+ }
+ return CachedAlgorithmHandles.SP800_108_CTR_HMAC.GenerateSymmetricKey(pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES);
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES);
+ }
+ }
+ else
+ {
+ // Use key directly.
+ return CachedAlgorithmHandles.SP800_108_CTR_HMAC.GenerateSymmetricKey(pbKdk, cbKdk);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Secret.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Secret.cs
new file mode 100644
index 0000000000..05c1c212bd
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/Secret.cs
@@ -0,0 +1,284 @@
+// 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 Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.Managed;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Represents a secret value stored in memory.
+ /// </summary>
+ public unsafe sealed class Secret : IDisposable, ISecret
+ {
+ // from wincrypt.h
+ private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16;
+
+ private readonly SecureLocalAllocHandle _localAllocHandle;
+ private readonly uint _plaintextLength;
+
+ /// <summary>
+ /// Creates a new Secret from the provided input value, where the input value
+ /// is specified as an array segment.
+ /// </summary>
+ public Secret(ArraySegment<byte> value)
+ {
+ value.Validate();
+
+ _localAllocHandle = Protect(value);
+ _plaintextLength = (uint)value.Count;
+ }
+
+ /// <summary>
+ /// Creates a new Secret from the provided input value, where the input value
+ /// is specified as an array.
+ /// </summary>
+ public Secret(byte[] value)
+ : this(new ArraySegment<byte>(value))
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ }
+
+ /// <summary>
+ /// Creates a new Secret from the provided input value, where the input value
+ /// is specified as a pointer to unmanaged memory.
+ /// </summary>
+ public Secret(byte* secret, int secretLength)
+ {
+ if (secret == null)
+ {
+ throw new ArgumentNullException(nameof(secret));
+ }
+ if (secretLength < 0)
+ {
+ throw Error.Common_ValueMustBeNonNegative(nameof(secretLength));
+ }
+
+ _localAllocHandle = Protect(secret, (uint)secretLength);
+ _plaintextLength = (uint)secretLength;
+ }
+
+ /// <summary>
+ /// Creates a new Secret from another secret object.
+ /// </summary>
+ public Secret(ISecret secret)
+ {
+ if (secret == null)
+ {
+ throw new ArgumentNullException(nameof(secret));
+ }
+
+ var other = secret as Secret;
+ if (other != null)
+ {
+ // Fast-track: simple deep copy scenario.
+ this._localAllocHandle = other._localAllocHandle.Duplicate();
+ this._plaintextLength = other._plaintextLength;
+ }
+ else
+ {
+ // Copy the secret to a temporary managed buffer, then protect the buffer.
+ // We pin the temp buffer and zero it out when we're finished to limit exposure of the secret.
+ var tempPlaintextBuffer = new byte[secret.Length];
+ fixed (byte* pbTempPlaintextBuffer = tempPlaintextBuffer)
+ {
+ try
+ {
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer));
+ _localAllocHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length);
+ _plaintextLength = (uint)tempPlaintextBuffer.Length;
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbTempPlaintextBuffer, tempPlaintextBuffer.Length);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// The length (in bytes) of the secret value.
+ /// </summary>
+ public int Length
+ {
+ get
+ {
+ return (int)_plaintextLength; // ctor guarantees the length fits into a signed int
+ }
+ }
+
+ /// <summary>
+ /// Wipes the secret from memory.
+ /// </summary>
+ public void Dispose()
+ {
+ _localAllocHandle.Dispose();
+ }
+
+ private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext)
+ {
+ fixed (byte* pbPlaintextArray = plaintext.Array)
+ {
+ return Protect(&pbPlaintextArray[plaintext.Offset], (uint)plaintext.Count);
+ }
+ }
+
+ private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext)
+ {
+ // If we're not running on a platform that supports CryptProtectMemory,
+ // shove the plaintext directly into a LocalAlloc handle. Ideally we'd
+ // mark this memory page as non-pageable, but this is fraught with peril.
+ if (!OSVersionUtil.IsWindows())
+ {
+ var handle = SecureLocalAllocHandle.Allocate((IntPtr)checked((int)cbPlaintext));
+ UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: handle, byteCount: cbPlaintext);
+ return handle;
+ }
+
+ // We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE.
+ var numTotalBytesToAllocate = cbPlaintext;
+ var numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE);
+ if (numBytesPaddingRequired == CRYPTPROTECTMEMORY_BLOCK_SIZE)
+ {
+ numBytesPaddingRequired = 0; // we're already a proper multiple of the block size
+ }
+ checked { numTotalBytesToAllocate += numBytesPaddingRequired; }
+ CryptoUtil.Assert(numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0");
+
+ // Allocate and copy plaintext data; padding is uninitialized / undefined.
+ var encryptedMemoryHandle = SecureLocalAllocHandle.Allocate((IntPtr)numTotalBytesToAllocate);
+ UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: encryptedMemoryHandle, byteCount: cbPlaintext);
+
+ // Finally, CryptProtectMemory the whole mess.
+ if (numTotalBytesToAllocate != 0)
+ {
+ MemoryProtection.CryptProtectMemory(encryptedMemoryHandle, byteCount: numTotalBytesToAllocate);
+ }
+ return encryptedMemoryHandle;
+ }
+
+ /// <summary>
+ /// Returns a Secret comprised entirely of random bytes retrieved from
+ /// a cryptographically secure RNG.
+ /// </summary>
+ public static Secret Random(int numBytes)
+ {
+ if (numBytes < 0)
+ {
+ throw Error.Common_ValueMustBeNonNegative(nameof(numBytes));
+ }
+
+ if (numBytes == 0)
+ {
+ byte dummy;
+ return new Secret(&dummy, 0);
+ }
+ else
+ {
+ // Don't use CNG if we're not on Windows.
+ if (!OSVersionUtil.IsWindows())
+ {
+ return new Secret(ManagedGenRandomImpl.Instance.GenRandom(numBytes));
+ }
+
+ var bytes = new byte[numBytes];
+ fixed (byte* pbBytes = bytes)
+ {
+ try
+ {
+ BCryptUtil.GenRandom(pbBytes, (uint)numBytes);
+ return new Secret(pbBytes, numBytes);
+ }
+ finally
+ {
+ UnsafeBufferUtil.SecureZeroMemory(pbBytes, numBytes);
+ }
+ }
+ }
+ }
+
+ private void UnprotectInto(byte* pbBuffer)
+ {
+ // If we're not running on a platform that supports CryptProtectMemory,
+ // the handle contains plaintext bytes.
+ if (!OSVersionUtil.IsWindows())
+ {
+ UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
+ return;
+ }
+
+ if (_plaintextLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0)
+ {
+ // Case 1: Secret length is an exact multiple of the block size. Copy directly to the buffer and decrypt there.
+ UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
+ MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength);
+ }
+ else
+ {
+ // Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and
+ // perform the decryption in the duplicate buffer, then copy the plaintext data over.
+ using (var duplicateHandle = _localAllocHandle.Duplicate())
+ {
+ MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length));
+ UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Writes the secret value to the specified buffer.
+ /// </summary>
+ /// <remarks>
+ /// The buffer size must exactly match the length of the secret value.
+ /// </remarks>
+ public void WriteSecretIntoBuffer(ArraySegment<byte> buffer)
+ {
+ // Parameter checking
+ buffer.Validate();
+ if (buffer.Count != Length)
+ {
+ throw Error.Common_BufferIncorrectlySized(nameof(buffer), actualSize: buffer.Count, expectedSize: Length);
+ }
+
+ // only unprotect if the secret is zero-length, as CLR doesn't like pinning zero-length buffers
+ if (Length != 0)
+ {
+ fixed (byte* pbBufferArray = buffer.Array)
+ {
+ UnprotectInto(&pbBufferArray[buffer.Offset]);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Writes the secret value to the specified buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer into which to write the secret value.</param>
+ /// <param name="bufferLength">The size (in bytes) of the provided buffer.</param>
+ /// <remarks>
+ /// The 'bufferLength' parameter must exactly match the length of the secret value.
+ /// </remarks>
+ public void WriteSecretIntoBuffer(byte* buffer, int bufferLength)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (bufferLength != Length)
+ {
+ throw Error.Common_BufferIncorrectlySized(nameof(bufferLength), actualSize: bufferLength, expectedSize: Length);
+ }
+
+ if (Length != 0)
+ {
+ UnprotectInto(buffer);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SimpleActivator.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SimpleActivator.cs
new file mode 100644
index 0000000000..54eac601bb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/SimpleActivator.cs
@@ -0,0 +1,56 @@
+// 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.Reflection;
+using Microsoft.AspNetCore.DataProtection.Internal;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// A simplified default implementation of <see cref="IActivator"/> that understands
+ /// how to call ctors which take <see cref="IServiceProvider"/>.
+ /// </summary>
+ internal class SimpleActivator : IActivator
+ {
+ /// <summary>
+ /// A default <see cref="SimpleActivator"/> whose wrapped <see cref="IServiceProvider"/> is null.
+ /// </summary>
+ internal static readonly SimpleActivator DefaultWithoutServices = new SimpleActivator(null);
+
+ private readonly IServiceProvider _services;
+
+ public SimpleActivator(IServiceProvider services)
+ {
+ _services = services;
+ }
+
+ public virtual object CreateInstance(Type expectedBaseType, string implementationTypeName)
+ {
+ // Would the assignment even work?
+ var implementationType = Type.GetType(implementationTypeName, throwOnError: true);
+ expectedBaseType.AssertIsAssignableFrom(implementationType);
+
+ // If no IServiceProvider was specified, prefer .ctor() [if it exists]
+ if (_services == null)
+ {
+ var ctorParameterless = implementationType.GetConstructor(Type.EmptyTypes);
+ if (ctorParameterless != null)
+ {
+ return Activator.CreateInstance(implementationType);
+ }
+ }
+
+ // If an IServiceProvider was specified or if .ctor() doesn't exist, prefer .ctor(IServiceProvider) [if it exists]
+ var ctorWhichTakesServiceProvider = implementationType.GetConstructor(new Type[] { typeof(IServiceProvider) });
+ if (ctorWhichTakesServiceProvider != null)
+ {
+ return ctorWhichTakesServiceProvider.Invoke(new[] { _services });
+ }
+
+ // Finally, prefer .ctor() as an ultimate fallback.
+ // This will throw if the ctor cannot be called.
+ return Activator.CreateInstance(implementationType);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeExtensions.cs
new file mode 100644
index 0000000000..0e35c06a6b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeExtensions.cs
@@ -0,0 +1,30 @@
+// 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.Reflection;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Helpful extension methods on <see cref="Type"/>.
+ /// </summary>
+ internal static class TypeExtensions
+ {
+ /// <summary>
+ /// Throws <see cref="InvalidCastException"/> if <paramref name="implementationType"/>
+ /// is not assignable to <paramref name="expectedBaseType"/>.
+ /// </summary>
+ public static void AssertIsAssignableFrom(this Type expectedBaseType, Type implementationType)
+ {
+ if (!expectedBaseType.IsAssignableFrom(implementationType))
+ {
+ // It might seem a bit weird to throw an InvalidCastException explicitly rather than
+ // to let the CLR generate one, but searching through NetFX there is indeed precedent
+ // for this pattern when the caller knows ahead of time the operation will fail.
+ throw new InvalidCastException(Resources.FormatTypeExtensions_BadCast(
+ expectedBaseType.AssemblyQualifiedName, implementationType.AssemblyQualifiedName));
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs
new file mode 100644
index 0000000000..bf3113eada
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs
@@ -0,0 +1,69 @@
+// 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.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal class TypeForwardingActivator : SimpleActivator
+ {
+ private const string OldNamespace = "Microsoft.AspNet.DataProtection";
+ private const string CurrentNamespace = "Microsoft.AspNetCore.DataProtection";
+ private readonly ILogger _logger;
+ private static readonly Regex _versionPattern = new Regex(@",\s?Version=[0-9]+(\.[0-9]+){0,3}", RegexOptions.Compiled, TimeSpan.FromSeconds(2));
+
+ public TypeForwardingActivator(IServiceProvider services)
+ : this(services, NullLoggerFactory.Instance)
+ {
+ }
+
+ public TypeForwardingActivator(IServiceProvider services, ILoggerFactory loggerFactory)
+ : base(services)
+ {
+ _logger = loggerFactory.CreateLogger(typeof(TypeForwardingActivator));
+ }
+
+ public override object CreateInstance(Type expectedBaseType, string originalTypeName)
+ => CreateInstance(expectedBaseType, originalTypeName, out var _);
+
+ // for testing
+ internal object CreateInstance(Type expectedBaseType, string originalTypeName, out bool forwarded)
+ {
+ var forwardedTypeName = originalTypeName;
+ var candidate = false;
+ if (originalTypeName.Contains(OldNamespace))
+ {
+ candidate = true;
+ forwardedTypeName = originalTypeName.Replace(OldNamespace, CurrentNamespace);
+ }
+
+ if (candidate || forwardedTypeName.StartsWith(CurrentNamespace + ".", StringComparison.Ordinal))
+ {
+ candidate = true;
+ forwardedTypeName = RemoveVersionFromAssemblyName(forwardedTypeName);
+ }
+
+ if (candidate)
+ {
+ var type = Type.GetType(forwardedTypeName, false);
+ if (type != null)
+ {
+ _logger.LogDebug("Forwarded activator type request from {FromType} to {ToType}",
+ originalTypeName,
+ forwardedTypeName);
+ forwarded = true;
+ return base.CreateInstance(expectedBaseType, forwardedTypeName);
+ }
+ }
+
+ forwarded = false;
+ return base.CreateInstance(expectedBaseType, originalTypeName);
+ }
+
+ protected string RemoveVersionFromAssemblyName(string forwardedTypeName)
+ => _versionPattern.Replace(forwardedTypeName, "");
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlConstants.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlConstants.cs
new file mode 100644
index 0000000000..9908e8e138
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlConstants.cs
@@ -0,0 +1,39 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Contains XLinq constants.
+ /// </summary>
+ internal static class XmlConstants
+ {
+ /// <summary>
+ /// The root namespace used for all DataProtection-specific XML elements and attributes.
+ /// </summary>
+ private static readonly XNamespace RootNamespace = XNamespace.Get("http://schemas.asp.net/2015/03/dataProtection");
+
+ /// <summary>
+ /// Represents the type of decryptor that can be used when reading 'encryptedSecret' elements.
+ /// </summary>
+ internal static readonly XName DecryptorTypeAttributeName = "decryptorType";
+
+ /// <summary>
+ /// Elements with this attribute will be read with the specified deserializer type.
+ /// </summary>
+ internal static readonly XName DeserializerTypeAttributeName = "deserializerType";
+
+ /// <summary>
+ /// Elements with this name will be automatically decrypted when read by the XML key manager.
+ /// </summary>
+ internal static readonly XName EncryptedSecretElementName = RootNamespace.GetName("encryptedSecret");
+
+ /// <summary>
+ /// Elements where this attribute has a value of 'true' should be encrypted before storage.
+ /// </summary>
+ internal static readonly XName RequiresEncryptionAttributeName = RootNamespace.GetName("requiresEncryption");
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateResolver.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateResolver.cs
new file mode 100644
index 0000000000..36ff53a6f3
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateResolver.cs
@@ -0,0 +1,63 @@
+// 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.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// A default implementation of <see cref="ICertificateResolver"/> that looks in the current user
+ /// and local machine certificate stores.
+ /// </summary>
+ public class CertificateResolver : ICertificateResolver
+ {
+ /// <summary>
+ /// Locates an <see cref="X509Certificate2"/> given its thumbprint.
+ /// </summary>
+ /// <param name="thumbprint">The thumbprint (as a hex string) of the certificate to resolve.</param>
+ /// <returns>The resolved <see cref="X509Certificate2"/>, or null if the certificate cannot be found.</returns>
+ public virtual X509Certificate2 ResolveCertificate(string thumbprint)
+ {
+ if (thumbprint == null)
+ {
+ throw new ArgumentNullException(nameof(thumbprint));
+ }
+
+ if (String.IsNullOrEmpty(thumbprint))
+ {
+ throw Error.Common_ArgumentCannotBeNullOrEmpty(nameof(thumbprint));
+ }
+
+ return GetCertificateFromStore(StoreLocation.CurrentUser, thumbprint)
+ ?? GetCertificateFromStore(StoreLocation.LocalMachine, thumbprint);
+ }
+
+ private static X509Certificate2 GetCertificateFromStore(StoreLocation location, string thumbprint)
+ {
+ var store = new X509Store(location);
+ try
+ {
+ store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
+ var matchingCerts = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: true);
+ return (matchingCerts != null && matchingCerts.Count > 0)
+ ? matchingCerts[0]
+ : null;
+ }
+ catch (CryptographicException)
+ {
+ // Suppress first-chance exceptions when opening the store.
+ // For example, LocalMachine\My is not supported on Linux yet and will throw on Open(),
+ // but there isn't a good way to detect this without attempting to open the store.
+ // See https://github.com/dotnet/corefx/issues/3690.
+ return null;
+ }
+ finally
+ {
+ store.Close();
+ }
+ }
+ }
+}
+
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs
new file mode 100644
index 0000000000..ee1342df94
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs
@@ -0,0 +1,147 @@
+// 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.Security.Cryptography.X509Certificates;
+using System.Security.Cryptography.Xml;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlEncryptor"/> that can perform XML encryption by using an X.509 certificate.
+ /// </summary>
+ public sealed class CertificateXmlEncryptor : IInternalCertificateXmlEncryptor, IXmlEncryptor
+ {
+ private readonly Func<X509Certificate2> _certFactory;
+ private readonly IInternalCertificateXmlEncryptor _encryptor;
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a <see cref="CertificateXmlEncryptor"/> given a certificate's thumbprint, an
+ /// <see cref="ICertificateResolver"/> that can be used to resolve the certificate, and
+ /// an <see cref="IServiceProvider"/>.
+ /// </summary>
+ public CertificateXmlEncryptor(string thumbprint, ICertificateResolver certificateResolver, ILoggerFactory loggerFactory)
+ : this(loggerFactory, encryptor: null)
+ {
+ if (thumbprint == null)
+ {
+ throw new ArgumentNullException(nameof(thumbprint));
+ }
+
+ if (certificateResolver == null)
+ {
+ throw new ArgumentNullException(nameof(certificateResolver));
+ }
+
+ _certFactory = CreateCertFactory(thumbprint, certificateResolver);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="CertificateXmlEncryptor"/> given an <see cref="X509Certificate2"/> instance
+ /// and an <see cref="IServiceProvider"/>.
+ /// </summary>
+ public CertificateXmlEncryptor(X509Certificate2 certificate, ILoggerFactory loggerFactory)
+ : this(loggerFactory, encryptor: null)
+ {
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ _certFactory = () => certificate;
+ }
+
+ internal CertificateXmlEncryptor(ILoggerFactory loggerFactory, IInternalCertificateXmlEncryptor encryptor)
+ {
+ _encryptor = encryptor ?? this;
+ _logger = loggerFactory.CreateLogger<CertificateXmlEncryptor>();
+ }
+
+ /// <summary>
+ /// Encrypts the specified <see cref="XElement"/> with an X.509 certificate.
+ /// </summary>
+ /// <param name="plaintextElement">The plaintext to encrypt.</param>
+ /// <returns>
+ /// An <see cref="EncryptedXmlInfo"/> that contains the encrypted value of
+ /// <paramref name="plaintextElement"/> along with information about how to
+ /// decrypt it.
+ /// </returns>
+ public EncryptedXmlInfo Encrypt(XElement plaintextElement)
+ {
+ if (plaintextElement == null)
+ {
+ throw new ArgumentNullException(nameof(plaintextElement));
+ }
+
+ // <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">
+ // ...
+ // </EncryptedData>
+
+ var encryptedElement = EncryptElement(plaintextElement);
+ return new EncryptedXmlInfo(encryptedElement, typeof(EncryptedXmlDecryptor));
+ }
+
+ private XElement EncryptElement(XElement plaintextElement)
+ {
+ // EncryptedXml works with XmlDocument, not XLinq. When we perform the conversion
+ // we'll wrap the incoming element in a dummy <root /> element since encrypted XML
+ // doesn't handle encrypting the root element all that well.
+ var xmlDocument = new XmlDocument();
+ xmlDocument.Load(new XElement("root", plaintextElement).CreateReader());
+ var elementToEncrypt = (XmlElement)xmlDocument.DocumentElement.FirstChild;
+
+ // Perform the encryption and update the document in-place.
+ var encryptedXml = new EncryptedXml(xmlDocument);
+ var encryptedData = _encryptor.PerformEncryption(encryptedXml, elementToEncrypt);
+ EncryptedXml.ReplaceElement(elementToEncrypt, encryptedData, content: false);
+
+ // Strip the <root /> element back off and convert the XmlDocument to an XElement.
+ return XElement.Load(xmlDocument.DocumentElement.FirstChild.CreateNavigator().ReadSubtree());
+ }
+
+ private Func<X509Certificate2> CreateCertFactory(string thumbprint, ICertificateResolver resolver)
+ {
+ return () =>
+ {
+ try
+ {
+ var cert = resolver.ResolveCertificate(thumbprint);
+ if (cert == null)
+ {
+ throw Error.CertificateXmlEncryptor_CertificateNotFound(thumbprint);
+ }
+ return cert;
+ }
+ catch (Exception ex)
+ {
+ _logger.ExceptionWhileTryingToResolveCertificateWithThumbprint(thumbprint, ex);
+
+ throw;
+ }
+ };
+ }
+
+ EncryptedData IInternalCertificateXmlEncryptor.PerformEncryption(EncryptedXml encryptedXml, XmlElement elementToEncrypt)
+ {
+ var cert = _certFactory()
+ ?? CryptoUtil.Fail<X509Certificate2>("Cert factory returned null.");
+
+ _logger.EncryptingToX509CertificateWithThumbprint(cert.Thumbprint);
+
+ try
+ {
+ return encryptedXml.Encrypt(elementToEncrypt, cert);
+ }
+ catch (Exception ex)
+ {
+ _logger.AnErrorOccurredWhileEncryptingToX509CertificateWithThumbprint(cert.Thumbprint, ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs
new file mode 100644
index 0000000000..e0d3fafe62
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs
@@ -0,0 +1,35 @@
+// 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;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// Flags used to control the creation of protection descriptors.
+ /// </summary>
+ /// <remarks>
+ /// These values correspond to the 'dwFlags' parameter on NCryptCreateProtectionDescriptor.
+ /// See https://msdn.microsoft.com/en-us/library/windows/desktop/hh706800(v=vs.85).aspx for more information.
+ /// </remarks>
+ [Flags]
+ public enum DpapiNGProtectionDescriptorFlags
+ {
+ /// <summary>
+ /// No special handling is necessary.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// The provided descriptor is a reference to a full descriptor stored
+ /// in the system registry.
+ /// </summary>
+ NamedDescriptor = 0x00000001,
+
+ /// <summary>
+ /// When combined with <see cref="NamedDescriptor"/>, uses the HKLM registry
+ /// instead of the HKCU registry when locating the full descriptor.
+ /// </summary>
+ MachineKey = 0x00000020,
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs
new file mode 100644
index 0000000000..653beffad1
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs
@@ -0,0 +1,91 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlDecryptor"/> that decrypts XML elements that were encrypted with <see cref="DpapiNGXmlEncryptor"/>.
+ /// </summary>
+ /// <remarks>
+ /// This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ /// </remarks>
+ public sealed class DpapiNGXmlDecryptor : IXmlDecryptor
+ {
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a new instance of a <see cref="DpapiNGXmlDecryptor"/>.
+ /// </summary>
+ public DpapiNGXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of a <see cref="DpapiNGXmlDecryptor"/>.
+ /// </summary>
+ /// <param name="services">An optional <see cref="IServiceProvider"/> to provide ancillary services.</param>
+ public DpapiNGXmlDecryptor(IServiceProvider services)
+ {
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
+
+ _logger = services.GetLogger<DpapiNGXmlDecryptor>();
+ }
+
+ /// <summary>
+ /// Decrypts the specified XML element.
+ /// </summary>
+ /// <param name="encryptedElement">An encrypted XML element.</param>
+ /// <returns>The decrypted form of <paramref name="encryptedElement"/>.</returns>
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ if (encryptedElement == null)
+ {
+ throw new ArgumentNullException(nameof(encryptedElement));
+ }
+
+ try
+ {
+ // <encryptedKey>
+ // <!-- This key is encrypted with {provider}. -->
+ // <!-- rule string -->
+ // <value>{base64}</value>
+ // </encryptedKey>
+
+ var protectedSecret = Convert.FromBase64String((string)encryptedElement.Element("value"));
+ if (_logger.IsDebugLevelEnabled())
+ {
+ string protectionDescriptorRule;
+ try
+ {
+ protectionDescriptorRule = DpapiSecretSerializerHelper.GetRuleFromDpapiNGProtectedPayload(protectedSecret);
+ }
+ catch
+ {
+ // swallow all errors - it's just a log
+ protectionDescriptorRule = null;
+ }
+ _logger.DecryptingSecretElementUsingWindowsDPAPING(protectionDescriptorRule);
+ }
+
+ using (var secret = DpapiSecretSerializerHelper.UnprotectWithDpapiNG(protectedSecret))
+ {
+ return secret.ToXElement();
+ }
+ }
+ catch (Exception ex)
+ {
+ // It's OK for us to log the error, as we control the exception, and it doesn't contain
+ // sensitive information.
+ _logger?.ExceptionOccurredTryingToDecryptElement(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs
new file mode 100644
index 0000000000..f5162496bb
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs
@@ -0,0 +1,114 @@
+// 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.Globalization;
+using System.Security.Principal;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// A class that can encrypt XML elements using Windows DPAPI:NG.
+ /// </summary>
+ /// <remarks>
+ /// This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ /// </remarks>
+ public sealed class DpapiNGXmlEncryptor : IXmlEncryptor
+ {
+ private readonly ILogger _logger;
+ private readonly NCryptDescriptorHandle _protectionDescriptorHandle;
+
+ /// <summary>
+ /// Creates a new instance of a <see cref="DpapiNGXmlEncryptor"/>.
+ /// </summary>
+ /// <param name="protectionDescriptorRule">The rule string from which to create the protection descriptor.</param>
+ /// <param name="flags">Flags controlling the creation of the protection descriptor.</param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ public DpapiNGXmlEncryptor(string protectionDescriptorRule, DpapiNGProtectionDescriptorFlags flags, ILoggerFactory loggerFactory)
+ {
+ if (protectionDescriptorRule == null)
+ {
+ throw new ArgumentNullException(nameof(protectionDescriptorRule));
+ }
+
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
+
+ var ntstatus = UnsafeNativeMethods.NCryptCreateProtectionDescriptor(protectionDescriptorRule, (uint)flags, out _protectionDescriptorHandle);
+ UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(_protectionDescriptorHandle);
+
+ _logger = loggerFactory.CreateLogger<DpapiNGXmlEncryptor>();
+ }
+
+ /// <summary>
+ /// Encrypts the specified <see cref="XElement"/>.
+ /// </summary>
+ /// <param name="plaintextElement">The plaintext to encrypt.</param>
+ /// <returns>
+ /// An <see cref="EncryptedXmlInfo"/> that contains the encrypted value of
+ /// <paramref name="plaintextElement"/> along with information about how to
+ /// decrypt it.
+ /// </returns>
+ public EncryptedXmlInfo Encrypt(XElement plaintextElement)
+ {
+ if (plaintextElement == null)
+ {
+ throw new ArgumentNullException(nameof(plaintextElement));
+ }
+
+ var protectionDescriptorRuleString = _protectionDescriptorHandle.GetProtectionDescriptorRuleString();
+ _logger.EncryptingToWindowsDPAPINGUsingProtectionDescriptorRule(protectionDescriptorRuleString);
+
+ // Convert the XML element to a binary secret so that it can be run through DPAPI
+ byte[] cngDpapiEncryptedData;
+ try
+ {
+ using (var plaintextElementAsSecret = plaintextElement.ToSecret())
+ {
+ cngDpapiEncryptedData = DpapiSecretSerializerHelper.ProtectWithDpapiNG(plaintextElementAsSecret, _protectionDescriptorHandle);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorOccurredWhileEncryptingToWindowsDPAPING(ex);
+ throw;
+ }
+
+ // <encryptedKey>
+ // <!-- This key is encrypted with {provider}. -->
+ // <!-- rule string -->
+ // <value>{base64}</value>
+ // </encryptedKey>
+
+ var element = new XElement("encryptedKey",
+ new XComment(" This key is encrypted with Windows DPAPI-NG. "),
+ new XComment(" Rule: " + protectionDescriptorRuleString + " "),
+ new XElement("value",
+ Convert.ToBase64String(cngDpapiEncryptedData)));
+
+ return new EncryptedXmlInfo(element, typeof(DpapiNGXmlDecryptor));
+ }
+
+ /// <summary>
+ /// Creates a rule string tied to the current Windows user and which is transferrable
+ /// across machines (backed up in AD).
+ /// </summary>
+ internal static string GetDefaultProtectionDescriptorString()
+ {
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
+
+ // Creates a SID=... protection descriptor string for the current user.
+ // Reminder: DPAPI:NG provides only encryption, not authentication.
+ using (var currentIdentity = WindowsIdentity.GetCurrent())
+ {
+ // use the SID to create an SDDL string
+ return string.Format(CultureInfo.InvariantCulture, "SID={0}", currentIdentity.User.Value);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs
new file mode 100644
index 0000000000..6241263350
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs
@@ -0,0 +1,74 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlDecryptor"/> that decrypts XML elements that were encrypted with <see cref="DpapiXmlEncryptor"/>.
+ /// </summary>
+ public sealed class DpapiXmlDecryptor : IXmlDecryptor
+ {
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a new instance of a <see cref="DpapiXmlDecryptor"/>.
+ /// </summary>
+ public DpapiXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of a <see cref="DpapiXmlDecryptor"/>.
+ /// </summary>
+ /// <param name="services">An optional <see cref="IServiceProvider"/> to provide ancillary services.</param>
+ public DpapiXmlDecryptor(IServiceProvider services)
+ {
+ CryptoUtil.AssertPlatformIsWindows();
+
+ _logger = services.GetLogger<DpapiXmlDecryptor>();
+ }
+
+ /// <summary>
+ /// Decrypts the specified XML element.
+ /// </summary>
+ /// <param name="encryptedElement">An encrypted XML element.</param>
+ /// <returns>The decrypted form of <paramref name="encryptedElement"/>.</returns>
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ if (encryptedElement == null)
+ {
+ throw new ArgumentNullException(nameof(encryptedElement));
+ }
+
+ _logger?.DecryptingSecretElementUsingWindowsDPAPI();
+
+ try
+ {
+ // <encryptedKey>
+ // <!-- This key is encrypted with {provider}. -->
+ // <value>{base64}</value>
+ // </encryptedKey>
+
+ var protectedSecret = Convert.FromBase64String((string)encryptedElement.Element("value"));
+ using (var secret = DpapiSecretSerializerHelper.UnprotectWithDpapi(protectedSecret))
+ {
+ return secret.ToXElement();
+ }
+ }
+ catch (Exception ex)
+ {
+ // It's OK for us to log the error, as we control the exception, and it doesn't contain
+ // sensitive information.
+ _logger?.ExceptionOccurredTryingToDecryptElement(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs
new file mode 100644
index 0000000000..d7fa2d7b1b
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs
@@ -0,0 +1,90 @@
+// 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.Security.Principal;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlEncryptor"/> that encrypts XML by using Windows DPAPI.
+ /// </summary>
+ /// <remarks>
+ /// This API is only supported on Windows platforms.
+ /// </remarks>
+ public sealed class DpapiXmlEncryptor : IXmlEncryptor
+ {
+ private readonly ILogger _logger;
+ private readonly bool _protectToLocalMachine;
+
+ /// <summary>
+ /// Creates a <see cref="DpapiXmlEncryptor"/> given a protection scope and an <see cref="IServiceProvider"/>.
+ /// </summary>
+ /// <param name="protectToLocalMachine">'true' if the data should be decipherable by anybody on the local machine,
+ /// 'false' if the data should only be decipherable by the current Windows user account.</param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ public DpapiXmlEncryptor(bool protectToLocalMachine, ILoggerFactory loggerFactory)
+ {
+ CryptoUtil.AssertPlatformIsWindows();
+
+ _protectToLocalMachine = protectToLocalMachine;
+ _logger = loggerFactory.CreateLogger<DpapiXmlEncryptor>();
+ }
+
+ /// <summary>
+ /// Encrypts the specified <see cref="XElement"/>.
+ /// </summary>
+ /// <param name="plaintextElement">The plaintext to encrypt.</param>
+ /// <returns>
+ /// An <see cref="EncryptedXmlInfo"/> that contains the encrypted value of
+ /// <paramref name="plaintextElement"/> along with information about how to
+ /// decrypt it.
+ /// </returns>
+ public EncryptedXmlInfo Encrypt(XElement plaintextElement)
+ {
+ if (plaintextElement == null)
+ {
+ throw new ArgumentNullException(nameof(plaintextElement));
+ }
+ if (_protectToLocalMachine)
+ {
+ _logger.EncryptingToWindowsDPAPIForLocalMachineAccount();
+ }
+ else
+ {
+ _logger.EncryptingToWindowsDPAPIForCurrentUserAccount(WindowsIdentity.GetCurrent().Name);
+ }
+
+ // Convert the XML element to a binary secret so that it can be run through DPAPI
+ byte[] dpapiEncryptedData;
+ try
+ {
+ using (var plaintextElementAsSecret = plaintextElement.ToSecret())
+ {
+ dpapiEncryptedData = DpapiSecretSerializerHelper.ProtectWithDpapi(plaintextElementAsSecret, protectToLocalMachine: _protectToLocalMachine);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorOccurredWhileEncryptingToWindowsDPAPI(ex);
+ throw;
+ }
+
+ // <encryptedKey>
+ // <!-- This key is encrypted with {provider}. -->
+ // <value>{base64}</value>
+ // </encryptedKey>
+
+ var element = new XElement("encryptedKey",
+ new XComment(" This key is encrypted with Windows DPAPI. "),
+ new XElement("value",
+ Convert.ToBase64String(dpapiEncryptedData)));
+
+ return new EncryptedXmlInfo(element, typeof(DpapiXmlDecryptor));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
new file mode 100644
index 0000000000..fee981b2d7
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
@@ -0,0 +1,162 @@
+// 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.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Security.Cryptography.Xml;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlDecryptor"/> that decrypts XML elements by using the <see cref="EncryptedXml"/> class.
+ /// </summary>
+ public sealed class EncryptedXmlDecryptor : IInternalEncryptedXmlDecryptor, IXmlDecryptor
+ {
+ private readonly IInternalEncryptedXmlDecryptor _decryptor;
+ private readonly XmlKeyDecryptionOptions _options;
+
+ /// <summary>
+ /// Creates a new instance of an <see cref="EncryptedXmlDecryptor"/>.
+ /// </summary>
+ public EncryptedXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of an <see cref="EncryptedXmlDecryptor"/>.
+ /// </summary>
+ /// <param name="services">An optional <see cref="IServiceProvider"/> to provide ancillary services.</param>
+ public EncryptedXmlDecryptor(IServiceProvider services)
+ {
+ _decryptor = services?.GetService<IInternalEncryptedXmlDecryptor>() ?? this;
+ _options = services?.GetService<IOptions<XmlKeyDecryptionOptions>>()?.Value;
+ }
+
+ /// <summary>
+ /// Decrypts the specified XML element.
+ /// </summary>
+ /// <param name="encryptedElement">An encrypted XML element.</param>
+ /// <returns>The decrypted form of <paramref name="encryptedElement"/>.</returns>
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ if (encryptedElement == null)
+ {
+ throw new ArgumentNullException(nameof(encryptedElement));
+ }
+
+ // <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">
+ // ...
+ // </EncryptedData>
+
+ // EncryptedXml works with XmlDocument, not XLinq. When we perform the conversion
+ // we'll wrap the incoming element in a dummy <root /> element since encrypted XML
+ // doesn't handle encrypting the root element all that well.
+ var xmlDocument = new XmlDocument();
+ xmlDocument.Load(new XElement("root", encryptedElement).CreateReader());
+ var elementToDecrypt = (XmlElement)xmlDocument.DocumentElement.FirstChild;
+
+ // Perform the decryption and update the document in-place.
+ var encryptedXml = new EncryptedXmlWithCertificateKeys(_options, xmlDocument);
+ _decryptor.PerformPreDecryptionSetup(encryptedXml);
+
+ encryptedXml.DecryptDocument();
+
+ // Strip the <root /> element back off and convert the XmlDocument to an XElement.
+ return XElement.Load(xmlDocument.DocumentElement.FirstChild.CreateNavigator().ReadSubtree());
+ }
+
+ void IInternalEncryptedXmlDecryptor.PerformPreDecryptionSetup(EncryptedXml encryptedXml)
+ {
+ // no-op
+ }
+
+ /// <summary>
+ /// Can decrypt the XML key data from an <see cref="X509Certificate2"/> that is not in stored in <see cref="X509Store"/>.
+ /// </summary>
+ private class EncryptedXmlWithCertificateKeys : EncryptedXml
+ {
+ private readonly XmlKeyDecryptionOptions _options;
+
+ public EncryptedXmlWithCertificateKeys(XmlKeyDecryptionOptions options, XmlDocument document)
+ : base(document)
+ {
+ _options = options;
+ }
+
+ public override byte[] DecryptEncryptedKey(EncryptedKey encryptedKey)
+ {
+ if (_options != null && _options.KeyDecryptionCertificateCount > 0)
+ {
+ var keyInfoEnum = encryptedKey.KeyInfo?.GetEnumerator();
+ if (keyInfoEnum == null)
+ {
+ return null;
+ }
+
+ while (keyInfoEnum.MoveNext())
+ {
+ if (!(keyInfoEnum.Current is KeyInfoX509Data kiX509Data))
+ {
+ continue;
+ }
+
+ byte[] key = GetKeyFromCert(encryptedKey, kiX509Data);
+ if (key != null)
+ {
+ return key;
+ }
+ }
+ }
+
+ return base.DecryptEncryptedKey(encryptedKey);
+ }
+
+ private byte[] GetKeyFromCert(EncryptedKey encryptedKey, KeyInfoX509Data keyInfo)
+ {
+ var certEnum = keyInfo.Certificates?.GetEnumerator();
+ if (certEnum == null)
+ {
+ return null;
+ }
+
+ while (certEnum.MoveNext())
+ {
+ if (!(certEnum.Current is X509Certificate2 certInfo))
+ {
+ continue;
+ }
+
+ if (!_options.TryGetKeyDecryptionCertificates(certInfo, out var keyDecryptionCerts))
+ {
+ continue;
+ }
+
+ foreach (var keyDecryptionCert in keyDecryptionCerts)
+ {
+ if (!keyDecryptionCert.HasPrivateKey)
+ {
+ continue;
+ }
+
+ using (RSA privateKey = keyDecryptionCert.GetRSAPrivateKey())
+ {
+ if (privateKey != null)
+ {
+ var useOAEP = encryptedKey.EncryptionMethod?.KeyAlgorithm == XmlEncRSAOAEPUrl;
+ return DecryptKey(encryptedKey.CipherData.CipherValue, privateKey, useOAEP);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlInfo.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlInfo.cs
new file mode 100644
index 0000000000..17e2a01e4e
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/EncryptedXmlInfo.cs
@@ -0,0 +1,56 @@
+// 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.Reflection;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// Wraps an <see cref="XElement"/> that contains a blob of encrypted XML
+ /// and information about the class which can be used to decrypt it.
+ /// </summary>
+ public sealed class EncryptedXmlInfo
+ {
+ /// <summary>
+ /// Creates an instance of an <see cref="EncryptedXmlInfo"/>.
+ /// </summary>
+ /// <param name="encryptedElement">A piece of encrypted XML.</param>
+ /// <param name="decryptorType">The class whose <see cref="IXmlDecryptor.Decrypt(XElement)"/>
+ /// method can be used to decrypt <paramref name="encryptedElement"/>.</param>
+ public EncryptedXmlInfo(XElement encryptedElement, Type decryptorType)
+ {
+ if (encryptedElement == null)
+ {
+ throw new ArgumentNullException(nameof(encryptedElement));
+ }
+
+ if (decryptorType == null)
+ {
+ throw new ArgumentNullException(nameof(decryptorType));
+ }
+
+ if (!typeof(IXmlDecryptor).IsAssignableFrom(decryptorType))
+ {
+ throw new ArgumentException(
+ Resources.FormatTypeExtensions_BadCast(decryptorType.FullName, typeof(IXmlDecryptor).FullName),
+ nameof(decryptorType));
+ }
+
+ EncryptedElement = encryptedElement;
+ DecryptorType = decryptorType;
+ }
+
+ /// <summary>
+ /// The class whose <see cref="IXmlDecryptor.Decrypt(XElement)"/> method can be used to
+ /// decrypt the value stored in <see cref="EncryptedElement"/>.
+ /// </summary>
+ public Type DecryptorType { get; }
+
+ /// <summary>
+ /// A piece of encrypted XML.
+ /// </summary>
+ public XElement EncryptedElement { get; }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/ICertificateResolver.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/ICertificateResolver.cs
new file mode 100644
index 0000000000..1be22dfbce
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/ICertificateResolver.cs
@@ -0,0 +1,20 @@
+// 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.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// Provides services for locating <see cref="X509Certificate2"/> instances.
+ /// </summary>
+ public interface ICertificateResolver
+ {
+ /// <summary>
+ /// Locates an <see cref="X509Certificate2"/> given its thumbprint.
+ /// </summary>
+ /// <param name="thumbprint">The thumbprint (as a hex string) of the certificate to resolve.</param>
+ /// <returns>The resolved <see cref="X509Certificate2"/>, or null if the certificate cannot be found.</returns>
+ X509Certificate2 ResolveCertificate(string thumbprint);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.cs
new file mode 100644
index 0000000000..ef9fe71648
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.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;
+using System.Xml;
+using System.Security.Cryptography.Xml;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// Internal implementation details of <see cref="CertificateXmlEncryptor"/> for unit testing.
+ /// </summary>
+ internal interface IInternalCertificateXmlEncryptor
+ {
+ EncryptedData PerformEncryption(EncryptedXml encryptedXml, XmlElement elementToEncrypt);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs
new file mode 100644
index 0000000000..79fc0481ed
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs
@@ -0,0 +1,16 @@
+// 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.Security.Cryptography.Xml;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// Internal implementation details of <see cref="EncryptedXmlDecryptor"/> for unit testing.
+ /// </summary>
+ internal interface IInternalEncryptedXmlDecryptor
+ {
+ void PerformPreDecryptionSetup(EncryptedXml encryptedXml);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlDecryptor.cs
new file mode 100644
index 0000000000..1ada323d21
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlDecryptor.cs
@@ -0,0 +1,24 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// The basic interface for decrypting an XML element.
+ /// </summary>
+ public interface IXmlDecryptor
+ {
+ /// <summary>
+ /// Decrypts the specified XML element.
+ /// </summary>
+ /// <param name="encryptedElement">An encrypted XML element.</param>
+ /// <returns>The decrypted form of <paramref name="encryptedElement"/>.</returns>
+ /// <remarks>
+ /// Implementations of this method must not mutate the <see cref="XElement"/>
+ /// instance provided by <paramref name="encryptedElement"/>.
+ /// </remarks>
+ XElement Decrypt(XElement encryptedElement);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlEncryptor.cs
new file mode 100644
index 0000000000..40a87d1a8d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/IXmlEncryptor.cs
@@ -0,0 +1,28 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// The basic interface for encrypting XML elements.
+ /// </summary>
+ public interface IXmlEncryptor
+ {
+ /// <summary>
+ /// Encrypts the specified <see cref="XElement"/>.
+ /// </summary>
+ /// <param name="plaintextElement">The plaintext to encrypt.</param>
+ /// <returns>
+ /// An <see cref="EncryptedXmlInfo"/> that contains the encrypted value of
+ /// <paramref name="plaintextElement"/> along with information about how to
+ /// decrypt it.
+ /// </returns>
+ /// <remarks>
+ /// Implementations of this method must not mutate the <see cref="XElement"/>
+ /// instance provided by <paramref name="plaintextElement"/>.
+ /// </remarks>
+ EncryptedXmlInfo Encrypt(XElement plaintextElement);
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlDecryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlDecryptor.cs
new file mode 100644
index 0000000000..a63c0f2963
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlDecryptor.cs
@@ -0,0 +1,36 @@
+// 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.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlDecryptor"/> that decrypts XML elements with a null decryptor.
+ /// </summary>
+ public sealed class NullXmlDecryptor : IXmlDecryptor
+ {
+ /// <summary>
+ /// Decrypts the specified XML element.
+ /// </summary>
+ /// <param name="encryptedElement">An encrypted XML element.</param>
+ /// <returns>The decrypted form of <paramref name="encryptedElement"/>.</returns>
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ if (encryptedElement == null)
+ {
+ throw new ArgumentNullException(nameof(encryptedElement));
+ }
+
+ // <unencryptedKey>
+ // <!-- This key is not encrypted. -->
+ // <plaintextElement />
+ // </unencryptedKey>
+
+ // Return a clone of the single child node.
+ return new XElement(encryptedElement.Elements().Single());
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlEncryptor.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlEncryptor.cs
new file mode 100644
index 0000000000..0f3100b859
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/NullXmlEncryptor.cs
@@ -0,0 +1,65 @@
+// 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.Xml.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// An <see cref="IXmlEncryptor"/> that encrypts XML elements with a null encryptor.
+ /// </summary>
+ public sealed class NullXmlEncryptor : IXmlEncryptor
+ {
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="NullXmlEncryptor"/>.
+ /// </summary>
+ public NullXmlEncryptor()
+ : this(services: null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of <see cref="NullXmlEncryptor"/>.
+ /// </summary>
+ /// <param name="services">An optional <see cref="IServiceProvider"/> to provide ancillary services.</param>
+ public NullXmlEncryptor(IServiceProvider services)
+ {
+ _logger = services.GetLogger<NullXmlEncryptor>();
+ }
+
+ /// <summary>
+ /// Encrypts the specified <see cref="XElement"/> with a null encryptor, i.e.,
+ /// by returning the original value of <paramref name="plaintextElement"/> unencrypted.
+ /// </summary>
+ /// <param name="plaintextElement">The plaintext to echo back.</param>
+ /// <returns>
+ /// An <see cref="EncryptedXmlInfo"/> that contains the null-encrypted value of
+ /// <paramref name="plaintextElement"/> along with information about how to
+ /// decrypt it.
+ /// </returns>
+ public EncryptedXmlInfo Encrypt(XElement plaintextElement)
+ {
+ if (plaintextElement == null)
+ {
+ throw new ArgumentNullException(nameof(plaintextElement));
+ }
+
+ _logger?.EncryptingUsingNullEncryptor();
+
+ // <unencryptedKey>
+ // <!-- This key is not encrypted. -->
+ // <plaintextElement />
+ // </unencryptedKey>
+
+ var newElement = new XElement("unencryptedKey",
+ new XComment(" This key is not encrypted. "),
+ new XElement(plaintextElement) /* copy ctor */);
+
+ return new EncryptedXmlInfo(newElement, typeof(NullXmlDecryptor));
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs
new file mode 100644
index 0000000000..cfc65a44a2
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs
@@ -0,0 +1,185 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Internal;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ internal unsafe static class XmlEncryptionExtensions
+ {
+ public static XElement DecryptElement(this XElement element, IActivator activator)
+ {
+ // If no decryption necessary, return original element.
+ if (!DoesElementOrDescendentRequireDecryption(element))
+ {
+ return element;
+ }
+
+ // Deep copy the element (since we're going to mutate) and put
+ // it into a document to guarantee it has a parent.
+ var doc = new XDocument(new XElement(element));
+
+ // We remove elements from the document as we decrypt them and perform
+ // fix-up later. This keeps us from going into an infinite loop in
+ // the case of a null decryptor (which returns its original input which
+ // is still marked as 'requires decryption').
+ var placeholderReplacements = new Dictionary<XElement, XElement>();
+
+ while (true)
+ {
+ var elementWhichRequiresDecryption = doc.Descendants(XmlConstants.EncryptedSecretElementName).FirstOrDefault();
+ if (elementWhichRequiresDecryption == null)
+ {
+ // All encryption is finished.
+ break;
+ }
+
+ // Decrypt the clone so that the decryptor doesn't inadvertently modify
+ // the original document or other data structures. The element we pass to
+ // the decryptor should be the child of the 'encryptedSecret' element.
+ var clonedElementWhichRequiresDecryption = new XElement(elementWhichRequiresDecryption);
+ string decryptorTypeName = (string)clonedElementWhichRequiresDecryption.Attribute(XmlConstants.DecryptorTypeAttributeName);
+ var decryptorInstance = activator.CreateInstance<IXmlDecryptor>(decryptorTypeName);
+ var decryptedElement = decryptorInstance.Decrypt(clonedElementWhichRequiresDecryption.Elements().Single());
+
+ // Put a placeholder into the original document so that we can continue our
+ // search for elements which need to be decrypted.
+ var newPlaceholder = new XElement("placeholder");
+ placeholderReplacements[newPlaceholder] = decryptedElement;
+ elementWhichRequiresDecryption.ReplaceWith(newPlaceholder);
+ }
+
+ // Finally, perform fixup.
+ Debug.Assert(placeholderReplacements.Count > 0);
+ foreach (var entry in placeholderReplacements)
+ {
+ entry.Key.ReplaceWith(entry.Value);
+ }
+ return doc.Root;
+ }
+
+ public static XElement EncryptIfNecessary(this IXmlEncryptor encryptor, XElement element)
+ {
+ // If no encryption is necessary, return null.
+ if (!DoesElementOrDescendentRequireEncryption(element))
+ {
+ return null;
+ }
+
+ // Deep copy the element (since we're going to mutate) and put
+ // it into a document to guarantee it has a parent.
+ var doc = new XDocument(new XElement(element));
+
+ // We remove elements from the document as we encrypt them and perform
+ // fix-up later. This keeps us from going into an infinite loop in
+ // the case of a null encryptor (which returns its original input which
+ // is still marked as 'requires encryption').
+ var placeholderReplacements = new Dictionary<XElement, EncryptedXmlInfo>();
+
+ while (true)
+ {
+ var elementWhichRequiresEncryption = doc.Descendants().FirstOrDefault(DoesSingleElementRequireEncryption);
+ if (elementWhichRequiresEncryption == null)
+ {
+ // All encryption is finished.
+ break;
+ }
+
+ // Encrypt the clone so that the encryptor doesn't inadvertently modify
+ // the original document or other data structures.
+ var clonedElementWhichRequiresEncryption = new XElement(elementWhichRequiresEncryption);
+ var innerDoc = new XDocument(clonedElementWhichRequiresEncryption);
+ var encryptedXmlInfo = encryptor.Encrypt(clonedElementWhichRequiresEncryption);
+ CryptoUtil.Assert(encryptedXmlInfo != null, "IXmlEncryptor.Encrypt returned null.");
+
+ // Put a placeholder into the original document so that we can continue our
+ // search for elements which need to be encrypted.
+ var newPlaceholder = new XElement("placeholder");
+ placeholderReplacements[newPlaceholder] = encryptedXmlInfo;
+ elementWhichRequiresEncryption.ReplaceWith(newPlaceholder);
+ }
+
+ // Finally, perform fixup.
+ Debug.Assert(placeholderReplacements.Count > 0);
+ foreach (var entry in placeholderReplacements)
+ {
+ // <enc:encryptedSecret decryptorType="{type}" xmlns:enc="{ns}">
+ // <element />
+ // </enc:encryptedSecret>
+ entry.Key.ReplaceWith(
+ new XElement(XmlConstants.EncryptedSecretElementName,
+ new XAttribute(XmlConstants.DecryptorTypeAttributeName, entry.Value.DecryptorType.AssemblyQualifiedName),
+ entry.Value.EncryptedElement));
+ }
+ return doc.Root;
+ }
+
+ /// <summary>
+ /// Converts an <see cref="XElement"/> to a <see cref="Secret"/> so that it can be kept in memory
+ /// securely or run through the DPAPI routines.
+ /// </summary>
+ public static Secret ToSecret(this XElement element)
+ {
+ const int DEFAULT_BUFFER_SIZE = 16 * 1024; // 16k buffer should be large enough to encrypt any realistic secret
+ var memoryStream = new MemoryStream(DEFAULT_BUFFER_SIZE);
+ element.Save(memoryStream);
+
+ var underlyingBuffer = memoryStream.GetBuffer();
+ fixed (byte* __unused__ = underlyingBuffer) // try to limit this moving around in memory while we allocate
+ {
+ try
+ {
+ return new Secret(new ArraySegment<byte>(underlyingBuffer, 0, checked((int)memoryStream.Length)));
+ }
+ finally
+ {
+ Array.Clear(underlyingBuffer, 0, underlyingBuffer.Length);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Converts a <see cref="Secret"/> back into an <see cref="XElement"/>.
+ /// </summary>
+ public static XElement ToXElement(this Secret secret)
+ {
+ var plaintextSecret = new byte[secret.Length];
+ fixed (byte* __unused__ = plaintextSecret) // try to keep the GC from moving it around
+ {
+ try
+ {
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(plaintextSecret));
+ var memoryStream = new MemoryStream(plaintextSecret, writable: false);
+ return XElement.Load(memoryStream);
+ }
+ finally
+ {
+ Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
+ }
+ }
+ }
+
+ private static bool DoesElementOrDescendentRequireDecryption(XElement element)
+ {
+ return element.DescendantsAndSelf(XmlConstants.EncryptedSecretElementName).Any();
+ }
+
+ private static bool DoesElementOrDescendentRequireEncryption(XElement element)
+ {
+ return element.DescendantsAndSelf().Any(DoesSingleElementRequireEncryption);
+ }
+
+ private static bool DoesSingleElementRequireEncryption(XElement element)
+ {
+ return element.IsMarkedAsRequiringEncryption();
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs
new file mode 100644
index 0000000000..7da598816f
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlKeyDecryptionOptions.cs
@@ -0,0 +1,39 @@
+// 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.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ /// <summary>
+ /// Specifies settings for how to decrypt XML keys.
+ /// </summary>
+ internal class XmlKeyDecryptionOptions
+ {
+ private readonly Dictionary<string, List<X509Certificate2>> _certs = new Dictionary<string, List<X509Certificate2>>(StringComparer.Ordinal);
+
+ public int KeyDecryptionCertificateCount => _certs.Count;
+
+ public bool TryGetKeyDecryptionCertificates(X509Certificate2 certInfo, out IReadOnlyList<X509Certificate2> keyDecryptionCerts)
+ {
+ var key = GetKey(certInfo);
+ var retVal = _certs.TryGetValue(key, out var keyDecryptionCertsRetVal);
+ keyDecryptionCerts = keyDecryptionCertsRetVal;
+ return retVal;
+ }
+
+ public void AddKeyDecryptionCertificate(X509Certificate2 certificate)
+ {
+ var key = GetKey(certificate);
+ if (!_certs.TryGetValue(key, out var certificates))
+ {
+ certificates = _certs[key] = new List<X509Certificate2>();
+ }
+ certificates.Add(certificate);
+ }
+
+ private string GetKey(X509Certificate2 cert) => cert.Thumbprint;
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlExtensions.cs b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlExtensions.cs
new file mode 100644
index 0000000000..bc08eb2b3d
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/XmlExtensions.cs
@@ -0,0 +1,30 @@
+// 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.Xml.Linq;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Contains helpers to work with XElement objects.
+ /// </summary>
+ internal static class XmlExtensions
+ {
+ /// <summary>
+ /// Returns a new XElement which is a carbon copy of the provided element,
+ /// but with no child nodes. Useful for writing exception messages without
+ /// inadvertently disclosing secret key material. It is assumed that the
+ /// element name itself and its attribute values are not secret.
+ /// </summary>
+ public static XElement WithoutChildNodes(this XElement element)
+ {
+ var newElement = new XElement(element.Name);
+ foreach (var attr in element.Attributes())
+ {
+ newElement.SetAttributeValue(attr.Name, attr.Value);
+ }
+ return newElement;
+ }
+ }
+}
diff --git a/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/baseline.netcore.json b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/baseline.netcore.json
new file mode 100644
index 0000000000..6c7f96a387
--- /dev/null
+++ b/src/DataProtection/src/Microsoft.AspNetCore.DataProtection/baseline.netcore.json
@@ -0,0 +1,3071 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.DataProtectionServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddDataProtection",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddDataProtection",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "setupAction",
+ "Type": "System.Action<Microsoft.AspNetCore.DataProtection.DataProtectionOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.DataProtectionBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "SetApplicationName",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "applicationName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddKeyEscrowSink",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "sink",
+ "Type": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyEscrowSink"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddKeyEscrowSink<T0>",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TImplementation",
+ "ParameterPosition": 0,
+ "Class": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyEscrowSink"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddKeyEscrowSink",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "factory",
+ "Type": "System.Func<System.IServiceProvider, Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyEscrowSink>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddKeyManagementOptions",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "setupAction",
+ "Type": "System.Action<Microsoft.AspNetCore.DataProtection.KeyManagement.KeyManagementOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DisableAutomaticKeyGeneration",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToFileSystem",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "directory",
+ "Type": "System.IO.DirectoryInfo"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToRegistry",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "registryKey",
+ "Type": "Microsoft.Win32.RegistryKey"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProtectKeysWithCertificate",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "certificate",
+ "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProtectKeysWithCertificate",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "thumbprint",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UnprotectKeysWithAnyCertificate",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "certificates",
+ "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProtectKeysWithDpapi",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProtectKeysWithDpapi",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "protectToLocalMachine",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProtectKeysWithDpapiNG",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ProtectKeysWithDpapiNG",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "protectionDescriptorRule",
+ "Type": "System.String"
+ },
+ {
+ "Name": "flags",
+ "Type": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiNGProtectionDescriptorFlags"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SetDefaultKeyLifetime",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "lifetime",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCryptographicAlgorithms",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorConfiguration"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCustomCryptographicAlgorithms",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngCbcAuthenticatedEncryptorConfiguration"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCustomCryptographicAlgorithms",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngGcmAuthenticatedEncryptorConfiguration"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCustomCryptographicAlgorithms",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ },
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.ManagedAuthenticatedEncryptorConfiguration"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseEphemeralDataProtectionProvider",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.DataProtectionOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ApplicationDiscriminator",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ApplicationDiscriminator",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.DataProtectionUtilityExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetApplicationUniqueIdentifier",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EphemeralDataProtectionProvider",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateProtector",
+ "Parameters": [
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtector",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Services",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.IPersistedDataProtector",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "DangerousUnprotect",
+ "Parameters": [
+ {
+ "Name": "protectedData",
+ "Type": "System.Byte[]"
+ },
+ {
+ "Name": "ignoreRevocationErrors",
+ "Type": "System.Boolean"
+ },
+ {
+ "Name": "requiresMigration",
+ "Type": "System.Boolean",
+ "Direction": "Out"
+ },
+ {
+ "Name": "wasRevoked",
+ "Type": "System.Boolean",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.ISecret",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "System.IDisposable"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Length",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteSecretIntoBuffer",
+ "Parameters": [
+ {
+ "Name": "buffer",
+ "Type": "System.ArraySegment<System.Byte>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.Secret",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.ISecret"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Dispose",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.IDisposable",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Length",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.ISecret",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Random",
+ "Parameters": [
+ {
+ "Name": "numBytes",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.Secret",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteSecretIntoBuffer",
+ "Parameters": [
+ {
+ "Name": "buffer",
+ "Type": "System.ArraySegment<System.Byte>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.ISecret",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteSecretIntoBuffer",
+ "Parameters": [
+ {
+ "Name": "buffer",
+ "Type": "System.Byte*"
+ },
+ {
+ "Name": "bufferLength",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.ArraySegment<System.Byte>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "secret",
+ "Type": "System.Byte*"
+ },
+ {
+ "Name": "secretLength",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "secret",
+ "Type": "Microsoft.AspNetCore.DataProtection.ISecret"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.CertificateResolver",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.ICertificateResolver"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ResolveCertificate",
+ "Parameters": [
+ {
+ "Name": "thumbprint",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.ICertificateResolver",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.CertificateXmlEncryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IInternalCertificateXmlEncryptor",
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Encrypt",
+ "Parameters": [
+ {
+ "Name": "plaintextElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "thumbprint",
+ "Type": "System.String"
+ },
+ {
+ "Name": "certificateResolver",
+ "Type": "Microsoft.AspNetCore.DataProtection.XmlEncryption.ICertificateResolver"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "certificate",
+ "Type": "System.Security.Cryptography.X509Certificates.X509Certificate2"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiNGProtectionDescriptorFlags",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "None",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "NamedDescriptor",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ },
+ {
+ "Kind": "Field",
+ "Name": "MachineKey",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "32"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiNGXmlDecryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Decrypt",
+ "Parameters": [
+ {
+ "Name": "encryptedElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiNGXmlEncryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Encrypt",
+ "Parameters": [
+ {
+ "Name": "plaintextElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "protectionDescriptorRule",
+ "Type": "System.String"
+ },
+ {
+ "Name": "flags",
+ "Type": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiNGProtectionDescriptorFlags"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiXmlDecryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Decrypt",
+ "Parameters": [
+ {
+ "Name": "encryptedElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiXmlEncryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Encrypt",
+ "Parameters": [
+ {
+ "Name": "plaintextElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "protectToLocalMachine",
+ "Type": "System.Boolean"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlDecryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IInternalEncryptedXmlDecryptor",
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Decrypt",
+ "Parameters": [
+ {
+ "Name": "encryptedElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlInfo",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DecryptorType",
+ "Parameters": [],
+ "ReturnType": "System.Type",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptedElement",
+ "Parameters": [],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "encryptedElement",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "decryptorType",
+ "Type": "System.Type"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.ICertificateResolver",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ResolveCertificate",
+ "Parameters": [
+ {
+ "Name": "thumbprint",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Security.Cryptography.X509Certificates.X509Certificate2",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Decrypt",
+ "Parameters": [
+ {
+ "Name": "encryptedElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Encrypt",
+ "Parameters": [
+ {
+ "Name": "plaintextElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlInfo",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.NullXmlDecryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Decrypt",
+ "Parameters": [
+ {
+ "Name": "encryptedElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlDecryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.XmlEncryption.NullXmlEncryptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Encrypt",
+ "Parameters": [
+ {
+ "Name": "plaintextElement",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultKeyStorageDirectory",
+ "Parameters": [],
+ "ReturnType": "System.IO.DirectoryInfo",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Directory",
+ "Parameters": [],
+ "ReturnType": "System.IO.DirectoryInfo",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllElements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<System.Xml.Linq.XElement>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "StoreElement",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "friendlyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "directory",
+ "Type": "System.IO.DirectoryInfo"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetAllElements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<System.Xml.Linq.XElement>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "StoreElement",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "friendlyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.Repositories.RegistryXmlRepository",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultRegistryKey",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Win32.RegistryKey",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RegistryKey",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Win32.RegistryKey",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllElements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<System.Xml.Linq.XElement>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "StoreElement",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "friendlyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "registryKey",
+ "Type": "Microsoft.Win32.RegistryKey"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ActivationDate",
+ "Parameters": [],
+ "ReturnType": "System.DateTimeOffset",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CreationDate",
+ "Parameters": [],
+ "ReturnType": "System.DateTimeOffset",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ExpirationDate",
+ "Parameters": [],
+ "ReturnType": "System.DateTimeOffset",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsRevoked",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_KeyId",
+ "Parameters": [],
+ "ReturnType": "System.Guid",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Descriptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEncryptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyEscrowSink",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Store",
+ "Parameters": [
+ {
+ "Name": "keyId",
+ "Type": "System.Guid"
+ },
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateNewKey",
+ "Parameters": [
+ {
+ "Name": "activationDate",
+ "Type": "System.DateTimeOffset"
+ },
+ {
+ "Name": "expirationDate",
+ "Type": "System.DateTimeOffset"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllKeys",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<Microsoft.AspNetCore.DataProtection.KeyManagement.IKey>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetCacheExpirationToken",
+ "Parameters": [],
+ "ReturnType": "System.Threading.CancellationToken",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RevokeKey",
+ "Parameters": [
+ {
+ "Name": "keyId",
+ "Type": "System.Guid"
+ },
+ {
+ "Name": "reason",
+ "Type": "System.String",
+ "DefaultValue": "null"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RevokeAllKeys",
+ "Parameters": [
+ {
+ "Name": "revocationDate",
+ "Type": "System.DateTimeOffset"
+ },
+ {
+ "Name": "reason",
+ "Type": "System.String",
+ "DefaultValue": "null"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.KeyManagement.KeyManagementOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AutoGenerateKeys",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AutoGenerateKeys",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_NewKeyLifetime",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_NewKeyLifetime",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticatedEncryptorConfiguration",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthenticatedEncryptorConfiguration",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_KeyEscrowSinks",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyEscrowSink>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_XmlRepository",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_XmlRepository",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_XmlEncryptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_XmlEncryptor",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.XmlEncryption.IXmlEncryptor"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticatedEncryptorFactories",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Microsoft.AspNetCore.DataProtection.KeyManagement.Internal.IInternalXmlKeyManager"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateNewKey",
+ "Parameters": [
+ {
+ "Name": "activationDate",
+ "Type": "System.DateTimeOffset"
+ },
+ {
+ "Name": "expirationDate",
+ "Type": "System.DateTimeOffset"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllKeys",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection<Microsoft.AspNetCore.DataProtection.KeyManagement.IKey>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetCacheExpirationToken",
+ "Parameters": [],
+ "ReturnType": "System.Threading.CancellationToken",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RevokeAllKeys",
+ "Parameters": [
+ {
+ "Name": "revocationDate",
+ "Type": "System.DateTimeOffset"
+ },
+ {
+ "Name": "reason",
+ "Type": "System.String",
+ "DefaultValue": "null"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RevokeKey",
+ "Parameters": [
+ {
+ "Name": "keyId",
+ "Type": "System.Guid"
+ },
+ {
+ "Name": "reason",
+ "Type": "System.String",
+ "DefaultValue": "null"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKeyManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "keyManagementOptions",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.DataProtection.KeyManagement.KeyManagementOptions>"
+ },
+ {
+ "Name": "activator",
+ "Type": "Microsoft.AspNetCore.DataProtection.Internal.IActivator"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "keyManagementOptions",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.DataProtection.KeyManagement.KeyManagementOptions>"
+ },
+ {
+ "Name": "activator",
+ "Type": "Microsoft.AspNetCore.DataProtection.Internal.IActivator"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.AuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateEncryptorInstance",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.CngCbcAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateEncryptorInstance",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.CngGcmAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateEncryptorInstance",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "AES_128_CBC",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "AES_192_CBC",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ },
+ {
+ "Kind": "Field",
+ "Name": "AES_256_CBC",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "2"
+ },
+ {
+ "Kind": "Field",
+ "Name": "AES_128_GCM",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "3"
+ },
+ {
+ "Kind": "Field",
+ "Name": "AES_192_GCM",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "4"
+ },
+ {
+ "Kind": "Field",
+ "Name": "AES_256_GCM",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "5"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Decrypt",
+ "Parameters": [
+ {
+ "Name": "ciphertext",
+ "Type": "System.ArraySegment<System.Byte>"
+ },
+ {
+ "Name": "additionalAuthenticatedData",
+ "Type": "System.ArraySegment<System.Byte>"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Encrypt",
+ "Parameters": [
+ {
+ "Name": "plaintext",
+ "Type": "System.ArraySegment<System.Byte>"
+ },
+ {
+ "Name": "additionalAuthenticatedData",
+ "Type": "System.ArraySegment<System.Byte>"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateEncryptorInstance",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ManagedAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateEncryptorInstance",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "Microsoft.AspNetCore.DataProtection.KeyManagement.IKey"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.IAuthenticatedEncryptorFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ValidationAlgorithm",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "HMACSHA256",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "HMACSHA512",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateNewDescriptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorConfiguration",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "BaseType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IInternalAlgorithmConfiguration"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithm",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithm",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ValidationAlgorithm",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ValidationAlgorithm",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ValidationAlgorithm",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ValidationAlgorithm"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateNewDescriptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ExportToXml",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlSerializedDescriptorInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorConfiguration"
+ },
+ {
+ "Name": "masterKey",
+ "Type": "Microsoft.AspNetCore.DataProtection.ISecret"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ImportFromXml",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngCbcAuthenticatedEncryptorConfiguration",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "BaseType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IInternalAlgorithmConfiguration"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithm",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithm",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithmProvider",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithmProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithmKeySize",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithmKeySize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HashAlgorithm",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_HashAlgorithm",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HashAlgorithmProvider",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_HashAlgorithmProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateNewDescriptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngCbcAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ExportToXml",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlSerializedDescriptorInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngCbcAuthenticatedEncryptorConfiguration"
+ },
+ {
+ "Name": "masterKey",
+ "Type": "Microsoft.AspNetCore.DataProtection.ISecret"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngCbcAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ImportFromXml",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngGcmAuthenticatedEncryptorConfiguration",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "BaseType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IInternalAlgorithmConfiguration"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithm",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithm",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithmProvider",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithmProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithmKeySize",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithmKeySize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateNewDescriptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngGcmAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ExportToXml",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlSerializedDescriptorInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngGcmAuthenticatedEncryptorConfiguration"
+ },
+ {
+ "Name": "masterKey",
+ "Type": "Microsoft.AspNetCore.DataProtection.ISecret"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.CngGcmAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ImportFromXml",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ExportToXml",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlSerializedDescriptorInfo",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ImportFromXml",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.ManagedAuthenticatedEncryptorConfiguration",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "BaseType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AlgorithmConfiguration",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IInternalAlgorithmConfiguration"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithmType",
+ "Parameters": [],
+ "ReturnType": "System.Type",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithmType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Type"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EncryptionAlgorithmKeySize",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EncryptionAlgorithmKeySize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ValidationAlgorithmType",
+ "Parameters": [],
+ "ReturnType": "System.Type",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ValidationAlgorithmType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Type"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateNewDescriptor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.ManagedAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ExportToXml",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlSerializedDescriptorInfo",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "configuration",
+ "Type": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.ManagedAuthenticatedEncryptorConfiguration"
+ },
+ {
+ "Name": "masterKey",
+ "Type": "Microsoft.AspNetCore.DataProtection.ISecret"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.ManagedAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ImportFromXml",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptor",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.IAuthenticatedEncryptorDescriptorDeserializer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "MarkAsRequiresEncryption",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.XmlSerializedDescriptorInfo",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DeserializerType",
+ "Parameters": [],
+ "ReturnType": "System.Type",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SerializedDescriptorElement",
+ "Parameters": [],
+ "ReturnType": "System.Xml.Linq.XElement",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "serializedDescriptorElement",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "deserializerType",
+ "Type": "System.Type"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/DataProtection/test/CreateTestCert.ps1 b/src/DataProtection/test/CreateTestCert.ps1
new file mode 100644
index 0000000000..a85a040f05
--- /dev/null
+++ b/src/DataProtection/test/CreateTestCert.ps1
@@ -0,0 +1,14 @@
+#
+# Generates a new test cert in a .pfx file
+# Obviously, don't actually use this to produce production certs
+#
+
+param(
+ [Parameter(Mandatory = $true)]
+ $OutFile
+)
+
+$password = ConvertTo-SecureString -Force -AsPlainText -String "password"
+$cert = New-SelfSignedCertificate -DnsName "localhost" -CertStoreLocation Cert:\CurrentUser\My\
+Export-PfxCertificate -Cert $cert -Password $password -FilePath $OutFile
+Remove-Item "Cert:\CurrentUser\My\$($cert.Thumbprint)"
diff --git a/src/DataProtection/test/Directory.Build.props b/src/DataProtection/test/Directory.Build.props
new file mode 100644
index 0000000000..f4a350c9a4
--- /dev/null
+++ b/src/DataProtection/test/Directory.Build.props
@@ -0,0 +1,19 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp3.0</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs
new file mode 100644
index 0000000000..69dfcdfe03
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs
@@ -0,0 +1,33 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ public unsafe class BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests
+ {
+ [Fact]
+ public void Init_SetsProperties()
+ {
+ // Act
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out var cipherModeInfo);
+
+ // Assert
+ Assert.Equal((uint)sizeof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO), cipherModeInfo.cbSize);
+ Assert.Equal(1U, cipherModeInfo.dwInfoVersion);
+ Assert.Equal(IntPtr.Zero, (IntPtr)cipherModeInfo.pbNonce);
+ Assert.Equal(0U, cipherModeInfo.cbNonce);
+ Assert.Equal(IntPtr.Zero, (IntPtr)cipherModeInfo.pbAuthData);
+ Assert.Equal(0U, cipherModeInfo.cbAuthData);
+ Assert.Equal(IntPtr.Zero, (IntPtr)cipherModeInfo.pbTag);
+ Assert.Equal(0U, cipherModeInfo.cbTag);
+ Assert.Equal(IntPtr.Zero, (IntPtr)cipherModeInfo.pbMacContext);
+ Assert.Equal(0U, cipherModeInfo.cbMacContext);
+ Assert.Equal(0U, cipherModeInfo.cbAAD);
+ Assert.Equal(0UL, cipherModeInfo.cbData);
+ Assert.Equal(0U, cipherModeInfo.dwFlags);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs
new file mode 100644
index 0000000000..34192eb758
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs
@@ -0,0 +1,58 @@
+// 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 Microsoft.AspNetCore.Cryptography.Internal;
+using Microsoft.AspNetCore.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ public class BCRYPT_KEY_LENGTHS_STRUCT_Tests
+ {
+ [Theory]
+ [InlineData(128, 128, 0, 128)]
+ [InlineData(128, 256, 64, 128)]
+ [InlineData(128, 256, 64, 192)]
+ [InlineData(128, 256, 64, 256)]
+ public void EnsureValidKeyLength_SuccessCases(int minLength, int maxLength, int increment, int testValue)
+ {
+ // Arrange
+ var keyLengthsStruct = new BCRYPT_KEY_LENGTHS_STRUCT
+ {
+ dwMinLength = (uint)minLength,
+ dwMaxLength = (uint)maxLength,
+ dwIncrement = (uint)increment
+ };
+
+ // Act
+ keyLengthsStruct.EnsureValidKeyLength((uint)testValue);
+
+ // Assert
+ // Nothing to do - if we got this far without throwing, success!
+ }
+
+ [Theory]
+ [InlineData(128, 128, 0, 192)]
+ [InlineData(128, 256, 64, 64)]
+ [InlineData(128, 256, 64, 512)]
+ [InlineData(128, 256, 64, 160)]
+ [InlineData(128, 256, 64, 129)]
+ public void EnsureValidKeyLength_FailureCases(int minLength, int maxLength, int increment, int testValue)
+ {
+ // Arrange
+ var keyLengthsStruct = new BCRYPT_KEY_LENGTHS_STRUCT
+ {
+ dwMinLength = (uint)minLength,
+ dwMaxLength = (uint)maxLength,
+ dwIncrement = (uint)increment
+ };
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => keyLengthsStruct.EnsureValidKeyLength((uint)testValue),
+ paramName: "keyLengthInBits",
+ exceptionMessage: Resources.FormatBCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength(testValue, minLength, maxLength, increment));
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs
new file mode 100644
index 0000000000..286bca18f4
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs
@@ -0,0 +1,61 @@
+// 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.Linq;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ public unsafe class BCryptUtilTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void GenRandom_PopulatesBuffer()
+ {
+ // Arrange
+ byte[] bytes = new byte[sizeof(Guid) + 6];
+ bytes[0] = 0x04; // leading canary
+ bytes[1] = 0x10;
+ bytes[2] = 0xE4;
+ bytes[sizeof(Guid) + 3] = 0xEA; // trailing canary
+ bytes[sizeof(Guid) + 4] = 0xF2;
+ bytes[sizeof(Guid) + 5] = 0x6A;
+
+ fixed (byte* pBytes = &bytes[3])
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ // Act
+ BCryptUtil.GenRandom(pBytes, (uint)sizeof(Guid));
+
+ // Check that the canaries haven't changed
+ Assert.Equal(0x04, bytes[0]);
+ Assert.Equal(0x10, bytes[1]);
+ Assert.Equal(0xE4, bytes[2]);
+ Assert.Equal(0xEA, bytes[sizeof(Guid) + 3]);
+ Assert.Equal(0xF2, bytes[sizeof(Guid) + 4]);
+ Assert.Equal(0x6A, bytes[sizeof(Guid) + 5]);
+
+ // Check that the buffer was actually filled.
+ // This check will fail once every 2**128 runs, which is insignificant.
+ Guid newGuid = new Guid(bytes.Skip(3).Take(sizeof(Guid)).ToArray());
+ Assert.NotEqual(Guid.Empty, newGuid);
+
+ // Check that the first and last bytes of the buffer are not zero, which indicates that they
+ // were in fact filled. This check will fail around 0.8% of the time, so we'll iterate up
+ // to 100 times, which puts the total failure rate at once every 2**700 runs,
+ // which is insignificant.
+ if (bytes[3] != 0x00 && bytes[18] != 0x00)
+ {
+ return; // success!
+ }
+ }
+ }
+
+ Assert.True(false, "Buffer was not filled as expected.");
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs
new file mode 100644
index 0000000000..de601a12d5
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs
@@ -0,0 +1,189 @@
+// 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.Text;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography.Cng
+{
+ // This class tests both the properties and the output of hash algorithms.
+ // It only tests the properties of the encryption algorithms.
+ // Output of the encryption and key derivatoin functions are tested by other projects.
+ public unsafe class CachedAlgorithmHandlesTests
+ {
+ private static readonly byte[] _dataToHash = Encoding.UTF8.GetBytes("Sample input data.");
+ private static readonly byte[] _hmacKey = Encoding.UTF8.GetBytes("Secret key material.");
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void AES_CBC_Cached_Handle()
+ {
+ RunAesBlockCipherAlgorithmTest(() => CachedAlgorithmHandles.AES_CBC);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void AES_GCM_Cached_Handle()
+ {
+ RunAesBlockCipherAlgorithmTest(() => CachedAlgorithmHandles.AES_GCM);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void SHA1_Cached_Handle_No_HMAC()
+ {
+ RunHashAlgorithmTest_No_HMAC(
+ getter: () => CachedAlgorithmHandles.SHA1,
+ expectedAlgorithmName: "SHA1",
+ expectedBlockSizeInBytes: 512 / 8,
+ expectedDigestSizeInBytes: 160 / 8,
+ expectedDigest: "MbYo3dZmXtgUZcUoWoxkCDKFvkk=");
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void SHA1_Cached_Handle_With_HMAC()
+ {
+ RunHashAlgorithmTest_With_HMAC(
+ getter: () => CachedAlgorithmHandles.HMAC_SHA1,
+ expectedAlgorithmName: "SHA1",
+ expectedBlockSizeInBytes: 512 / 8,
+ expectedDigestSizeInBytes: 160 / 8,
+ expectedDigest: "PjYTgLTWkt6NeH0NudIR7N47Ipg=");
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void SHA256_Cached_Handle_No_HMAC()
+ {
+ RunHashAlgorithmTest_No_HMAC(
+ getter: () => CachedAlgorithmHandles.SHA256,
+ expectedAlgorithmName: "SHA256",
+ expectedBlockSizeInBytes: 512 / 8,
+ expectedDigestSizeInBytes: 256 / 8,
+ expectedDigest: "5uRfQadsrnUTa3/TEo5PP6SDZQkb9AcE4wNXDVcM0Fo=");
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void SHA256_Cached_Handle_With_HMAC()
+ {
+ RunHashAlgorithmTest_With_HMAC(
+ getter: () => CachedAlgorithmHandles.HMAC_SHA256,
+ expectedAlgorithmName: "SHA256",
+ expectedBlockSizeInBytes: 512 / 8,
+ expectedDigestSizeInBytes: 256 / 8,
+ expectedDigest: "KLzo0lVg5gZkpL5D6Ck7QT8w4iuPCe/pGCrMcOXWbKY=");
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void SHA512_Cached_Handle_No_HMAC()
+ {
+ RunHashAlgorithmTest_No_HMAC(
+ getter: () => CachedAlgorithmHandles.SHA512,
+ expectedAlgorithmName: "SHA512",
+ expectedBlockSizeInBytes: 1024 / 8,
+ expectedDigestSizeInBytes: 512 / 8,
+ expectedDigest: "jKI7WrcgPP7n2HAYOb8uFRi7xEsNG/BmdGd18dwwkIpqJ4Vmlk2b+8hssLyMQlprTSKVJNObSiYUqW5THS7okw==");
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void SHA512_Cached_Handle_With_HMAC()
+ {
+ RunHashAlgorithmTest_With_HMAC(
+ getter: () => CachedAlgorithmHandles.HMAC_SHA512,
+ expectedAlgorithmName: "SHA512",
+ expectedBlockSizeInBytes: 1024 / 8,
+ expectedDigestSizeInBytes: 512 / 8,
+ expectedDigest: "pKTX5vtPtbsn7pX9ISDlOYr1NFklTBIPYAFICy0ZQbFc0QVzGaTUvtqTOi91I0sHa1DIod6uIogux5iLdHjfcA==");
+ }
+
+ private static void RunAesBlockCipherAlgorithmTest(Func<BCryptAlgorithmHandle> getter)
+ {
+ // Getter must return the same instance of the cached handle
+ var algorithmHandle = getter();
+ var algorithmHandleSecondAttempt = getter();
+ Assert.NotNull(algorithmHandle);
+ Assert.Same(algorithmHandle, algorithmHandleSecondAttempt);
+
+ // Validate that properties are what we expect
+ Assert.Equal("AES", algorithmHandle.GetAlgorithmName());
+ Assert.Equal((uint)(128 / 8), algorithmHandle.GetCipherBlockLength());
+ var supportedKeyLengths = algorithmHandle.GetSupportedKeyLengths();
+ Assert.Equal(128U, supportedKeyLengths.dwMinLength);
+ Assert.Equal(256U, supportedKeyLengths.dwMaxLength);
+ Assert.Equal(64U, supportedKeyLengths.dwIncrement);
+ }
+
+ private static void RunHashAlgorithmTest_No_HMAC(
+ Func<BCryptAlgorithmHandle> getter,
+ string expectedAlgorithmName,
+ uint expectedBlockSizeInBytes,
+ uint expectedDigestSizeInBytes,
+ string expectedDigest)
+ {
+ // Getter must return the same instance of the cached handle
+ var algorithmHandle = getter();
+ var algorithmHandleSecondAttempt = getter();
+ Assert.NotNull(algorithmHandle);
+ Assert.Same(algorithmHandle, algorithmHandleSecondAttempt);
+
+ // Validate that properties are what we expect
+ Assert.Equal(expectedAlgorithmName, algorithmHandle.GetAlgorithmName());
+ Assert.Equal(expectedBlockSizeInBytes, algorithmHandle.GetHashBlockLength());
+ Assert.Equal(expectedDigestSizeInBytes, algorithmHandle.GetHashDigestLength());
+
+ // Perform the digest calculation and validate against our expectation
+ var hashHandle = algorithmHandle.CreateHash();
+ byte[] outputHash = new byte[expectedDigestSizeInBytes];
+ fixed (byte* pInput = _dataToHash)
+ {
+ fixed (byte* pOutput = outputHash)
+ {
+ hashHandle.HashData(pInput, (uint)_dataToHash.Length, pOutput, (uint)outputHash.Length);
+ }
+ }
+ Assert.Equal(expectedDigest, Convert.ToBase64String(outputHash));
+ }
+
+ private static void RunHashAlgorithmTest_With_HMAC(
+ Func<BCryptAlgorithmHandle> getter,
+ string expectedAlgorithmName,
+ uint expectedBlockSizeInBytes,
+ uint expectedDigestSizeInBytes,
+ string expectedDigest)
+ {
+ // Getter must return the same instance of the cached handle
+ var algorithmHandle = getter();
+ var algorithmHandleSecondAttempt = getter();
+ Assert.NotNull(algorithmHandle);
+ Assert.Same(algorithmHandle, algorithmHandleSecondAttempt);
+
+ // Validate that properties are what we expect
+ Assert.Equal(expectedAlgorithmName, algorithmHandle.GetAlgorithmName());
+ Assert.Equal(expectedBlockSizeInBytes, algorithmHandle.GetHashBlockLength());
+ Assert.Equal(expectedDigestSizeInBytes, algorithmHandle.GetHashDigestLength());
+
+ // Perform the digest calculation and validate against our expectation
+ fixed (byte* pKey = _hmacKey)
+ {
+ var hashHandle = algorithmHandle.CreateHmac(pKey, (uint)_hmacKey.Length);
+ byte[] outputHash = new byte[expectedDigestSizeInBytes];
+ fixed (byte* pInput = _dataToHash)
+ {
+ fixed (byte* pOutput = outputHash)
+ {
+ hashHandle.HashData(pInput, (uint)_dataToHash.Length, pOutput, (uint)outputHash.Length);
+ }
+ }
+ Assert.Equal(expectedDigest, Convert.ToBase64String(outputHash));
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/CryptoUtilTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/CryptoUtilTests.cs
new file mode 100644
index 0000000000..b911ab065a
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/CryptoUtilTests.cs
@@ -0,0 +1,54 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ public unsafe class CryptoUtilTests
+ {
+ [Fact]
+ public void TimeConstantBuffersAreEqual_Array_Equal()
+ {
+ // Arrange
+ byte[] a = new byte[] { 0x01, 0x23, 0x45, 0x67 };
+ byte[] b = new byte[] { 0xAB, 0xCD, 0x23, 0x45, 0x67, 0xEF };
+
+ // Act & assert
+ Assert.True(CryptoUtil.TimeConstantBuffersAreEqual(a, 1, 3, b, 2, 3));
+ }
+
+ [Fact]
+ public void TimeConstantBuffersAreEqual_Array_Unequal()
+ {
+ byte[] a = new byte[] { 0x01, 0x23, 0x45, 0x67 };
+ byte[] b = new byte[] { 0xAB, 0xCD, 0x23, 0xFF, 0x67, 0xEF };
+
+ // Act & assert
+ Assert.False(CryptoUtil.TimeConstantBuffersAreEqual(a, 1, 3, b, 2, 3));
+ }
+
+ [Fact]
+ public void TimeConstantBuffersAreEqual_Pointers_Equal()
+ {
+ // Arrange
+ uint a = 0x01234567;
+ uint b = 0x01234567;
+
+ // Act & assert
+ Assert.True(CryptoUtil.TimeConstantBuffersAreEqual((byte*)&a, (byte*)&b, sizeof(uint)));
+ }
+
+ [Fact]
+ public void TimeConstantBuffersAreEqual_Pointers_Unequal()
+ {
+ // Arrange
+ uint a = 0x01234567;
+ uint b = 0x89ABCDEF;
+
+ // Act & assert
+ Assert.False(CryptoUtil.TimeConstantBuffersAreEqual((byte*)&a, (byte*)&b, sizeof(uint)));
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Microsoft.AspNetCore.Cryptography.Internal.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Microsoft.AspNetCore.Cryptography.Internal.Test.csproj
new file mode 100644
index 0000000000..759f10679d
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Microsoft.AspNetCore.Cryptography.Internal.Test.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\shared\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Cryptography.Internal\Microsoft.AspNetCore.Cryptography.Internal.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Properties/AssemblyInfo.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3adbc7af4e
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// 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.Runtime.CompilerServices;
+
+// for unit testing
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs
new file mode 100644
index 0000000000..cf5b8f9384
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs
@@ -0,0 +1,31 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography.SafeHandles
+{
+ public unsafe class SecureLocalAllocHandleTests
+ {
+ [Fact]
+ public void Duplicate_Copies_Data()
+ {
+ // Arrange
+ const string expected = "xyz";
+ int cbExpected = expected.Length * sizeof(char);
+ var controlHandle = SecureLocalAllocHandle.Allocate((IntPtr)cbExpected);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ ((char*)controlHandle.DangerousGetHandle())[i] = expected[i];
+ }
+
+ // Act
+ var duplicateHandle = controlHandle.Duplicate();
+
+ // Assert
+ Assert.Equal(expected, new string((char*)duplicateHandle.DangerousGetHandle(), 0, expected.Length)); // contents the same data
+ Assert.NotEqual(controlHandle.DangerousGetHandle(), duplicateHandle.DangerousGetHandle()); // shouldn't just point to the same memory location
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs
new file mode 100644
index 0000000000..359835db7e
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs
@@ -0,0 +1,162 @@
+// 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.Reflection;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ public unsafe class UnsafeBufferUtilTests
+ {
+ [Fact]
+ public void BlockCopy_PtrToPtr_IntLength()
+ {
+ // Arrange
+ long x = 0x0123456789ABCDEF;
+ long y = 0;
+
+ // Act
+ UnsafeBufferUtil.BlockCopy(from: &x, to: &y, byteCount: (int)sizeof(long));
+
+ // Assert
+ Assert.Equal(x, y);
+ }
+
+ [Fact]
+ public void BlockCopy_PtrToPtr_UIntLength()
+ {
+ // Arrange
+ long x = 0x0123456789ABCDEF;
+ long y = 0;
+
+ // Act
+ UnsafeBufferUtil.BlockCopy(from: &x, to: &y, byteCount: (uint)sizeof(long));
+
+ // Assert
+ Assert.Equal(x, y);
+ }
+
+ [Fact]
+ public void BlockCopy_HandleToHandle()
+ {
+ // Arrange
+ const string expected = "Hello there!";
+ int cbExpected = expected.Length * sizeof(char);
+ var controlHandle = LocalAlloc(cbExpected);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ ((char*)controlHandle.DangerousGetHandle())[i] = expected[i];
+ }
+ var testHandle = LocalAlloc(cbExpected);
+
+ // Act
+ UnsafeBufferUtil.BlockCopy(from: controlHandle, to: testHandle, length: (IntPtr)cbExpected);
+
+ // Assert
+ string actual = new string((char*)testHandle.DangerousGetHandle(), 0, expected.Length);
+ GC.KeepAlive(testHandle);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void BlockCopy_HandleToPtr()
+ {
+ // Arrange
+ const string expected = "Hello there!";
+ int cbExpected = expected.Length * sizeof(char);
+ var controlHandle = LocalAlloc(cbExpected);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ ((char*)controlHandle.DangerousGetHandle())[i] = expected[i];
+ }
+ char* dest = stackalloc char[expected.Length];
+
+ // Act
+ UnsafeBufferUtil.BlockCopy(from: controlHandle, to: dest, byteCount: (uint)cbExpected);
+
+ // Assert
+ string actual = new string(dest, 0, expected.Length);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void BlockCopy_PtrToHandle()
+ {
+ // Arrange
+ const string expected = "Hello there!";
+ int cbExpected = expected.Length * sizeof(char);
+ var testHandle = LocalAlloc(cbExpected);
+
+ // Act
+ fixed (char* pExpected = expected)
+ {
+ UnsafeBufferUtil.BlockCopy(from: pExpected, to: testHandle, byteCount: (uint)cbExpected);
+ }
+
+ // Assert
+ string actual = new string((char*)testHandle.DangerousGetHandle(), 0, expected.Length);
+ GC.KeepAlive(testHandle);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void SecureZeroMemory_IntLength()
+ {
+ // Arrange
+ long x = 0x0123456789ABCDEF;
+
+ // Act
+ UnsafeBufferUtil.SecureZeroMemory((byte*)&x, byteCount: (int)sizeof(long));
+
+ // Assert
+ Assert.Equal(0, x);
+ }
+
+ [Fact]
+ public void SecureZeroMemory_UIntLength()
+ {
+ // Arrange
+ long x = 0x0123456789ABCDEF;
+
+ // Act
+ UnsafeBufferUtil.SecureZeroMemory((byte*)&x, byteCount: (uint)sizeof(long));
+
+ // Assert
+ Assert.Equal(0, x);
+ }
+
+ [Fact]
+ public void SecureZeroMemory_ULongLength()
+ {
+ // Arrange
+ long x = 0x0123456789ABCDEF;
+
+ // Act
+ UnsafeBufferUtil.SecureZeroMemory((byte*)&x, byteCount: (ulong)sizeof(long));
+
+ // Assert
+ Assert.Equal(0, x);
+ }
+
+ [Fact]
+ public void SecureZeroMemory_IntPtrLength()
+ {
+ // Arrange
+ long x = 0x0123456789ABCDEF;
+
+ // Act
+ UnsafeBufferUtil.SecureZeroMemory((byte*)&x, length: (IntPtr)sizeof(long));
+
+ // Assert
+ Assert.Equal(0, x);
+ }
+
+ private static LocalAllocHandle LocalAlloc(int cb)
+ {
+ return SecureLocalAllocHandle.Allocate((IntPtr)cb);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/WeakReferenceHelpersTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/WeakReferenceHelpersTests.cs
new file mode 100644
index 0000000000..da66146b07
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.Internal.Test/WeakReferenceHelpersTests.cs
@@ -0,0 +1,84 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography
+{
+ public class WeakReferenceHelpersTests
+ {
+ [Fact]
+ public void GetSharedInstance_ExistingWeakRefHasBeenGCed_CreatesNew()
+ {
+ // Arrange
+ WeakReference<MyDisposable> wrOriginal = new WeakReference<MyDisposable>(null);
+ WeakReference<MyDisposable> wr = wrOriginal;
+ MyDisposable newInstance = new MyDisposable();
+
+ // Act
+ var retVal = WeakReferenceHelpers.GetSharedInstance(ref wr, () => newInstance);
+
+ // Assert
+ Assert.NotNull(wr);
+ Assert.NotSame(wrOriginal, wr);
+ Assert.True(wr.TryGetTarget(out var target));
+ Assert.Same(newInstance, target);
+ Assert.Same(newInstance, retVal);
+ Assert.False(newInstance.HasBeenDisposed);
+ }
+
+ [Fact]
+ public void GetSharedInstance_ExistingWeakRefIsNull_CreatesNew()
+ {
+ // Arrange
+ WeakReference<MyDisposable> wr = null;
+ MyDisposable newInstance = new MyDisposable();
+
+ // Act
+ var retVal = WeakReferenceHelpers.GetSharedInstance(ref wr, () => newInstance);
+
+ // Assert
+ Assert.NotNull(wr);
+ Assert.True(wr.TryGetTarget(out var target));
+ Assert.Same(newInstance, target);
+ Assert.Same(newInstance, retVal);
+ Assert.False(newInstance.HasBeenDisposed);
+ }
+
+ [Fact]
+ public void GetSharedInstance_ExistingWeakRefIsNull_AnotherThreadCreatesInstanceWhileOurFactoryRuns_ReturnsExistingInstanceAndDisposesNewInstance()
+ {
+ // Arrange
+ WeakReference<MyDisposable> wr = null;
+ MyDisposable instanceThatWillBeCreatedFirst = new MyDisposable();
+ MyDisposable instanceThatWillBeCreatedSecond = new MyDisposable();
+
+ // Act
+ var retVal = WeakReferenceHelpers.GetSharedInstance(ref wr, () =>
+ {
+ // mimic another thread creating the instance while our factory is being invoked
+ WeakReferenceHelpers.GetSharedInstance(ref wr, () => instanceThatWillBeCreatedFirst);
+ return instanceThatWillBeCreatedSecond;
+ });
+
+ // Assert
+ Assert.NotNull(wr);
+ Assert.True(wr.TryGetTarget(out var target));
+ Assert.Same(instanceThatWillBeCreatedFirst, target);
+ Assert.Same(instanceThatWillBeCreatedFirst, retVal);
+ Assert.False(instanceThatWillBeCreatedFirst.HasBeenDisposed);
+ Assert.True(instanceThatWillBeCreatedSecond.HasBeenDisposed);
+ }
+
+ private sealed class MyDisposable : IDisposable
+ {
+ public bool HasBeenDisposed { get; private set; }
+
+ public void Dispose()
+ {
+ HasBeenDisposed = true;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test.csproj
new file mode 100644
index 0000000000..a475ac199d
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\shared\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Cryptography.Internal\Microsoft.AspNetCore.Cryptography.Internal.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Cryptography.KeyDerivation\Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs
new file mode 100644
index 0000000000..a45f5e24ce
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs
@@ -0,0 +1,196 @@
+// 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.Text;
+using Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cryptography.KeyDerivation
+{
+ public class Pbkdf2Tests
+ {
+
+#if NET461
+#elif NETCOREAPP3_0
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF. We only use 5 iterations so
+ // that our unit tests are fast.
+
+ // This provider is only available in .NET Core because .NET Standard only supports HMACSHA1
+ [Theory]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 - 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 0, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLo=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLpk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 - 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm9")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 0, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Q==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Wk=")]
+ public void RunTest_Normal_NetCore(string password, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedValueAsBase64)
+ {
+ // Arrange
+ byte[] salt = new byte[256];
+ for (int i = 0; i < salt.Length; i++)
+ {
+ salt[i] = (byte)i;
+ }
+
+ // Act & assert
+ TestProvider<NetCorePbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
+ }
+
+ [Fact]
+ public void RunTest_WithLongPassword_NetCore_FallbackToManaged()
+ {
+ // salt is less than 8 bytes
+ byte[] salt = Encoding.UTF8.GetBytes("salt");
+ const string expectedDerivedKeyBase64 = "Sc+V/c3fiZq5Z5qH3iavAiojTsW97FAp2eBNmCQAwCNzA8hfhFFYyQLIMK65qPnBFHOHXQPwAxNQNhaEAH9hzfiaNBSRJpF9V4rpl02d5ZpI6cZbsQFF7TJW7XJzQVpYoPDgJlg0xVmYLhn1E9qMtUVUuXsBjOOdd7K1M+ZI00c=";
+
+ RunTest_WithLongPassword_Impl<NetCorePbkdf2Provider>(salt, expectedDerivedKeyBase64);
+ }
+
+ [Fact]
+ public void RunTest_WithLongPassword_NetCore()
+ {
+ // salt longer than 8 bytes
+ var salt = Encoding.UTF8.GetBytes("abcdefghijkl");
+ RunTest_WithLongPassword_Impl<NetCorePbkdf2Provider>(salt, "NGJtFzYUaaSxu+3ZsMeZO5d/qPJDUYW4caLkFlaY0cLSYdh1PN4+nHUVp4pUUubJWu3UeXNMnHKNDfnn8GMfnDVrAGTv1lldszsvUJ0JQ6p4+daQEYBc//Tj/ejuB3luwW0IinyE7U/ViOQKbfi5pCZFMQ0FFx9I+eXRlyT+I74=");
+ }
+#else
+#error Update target framework
+#endif
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF. We only use 5 iterations so
+ // that our unit tests are fast.
+ [Theory]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 - 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 0, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLo=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLpk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 - 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm9")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 0, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Q==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Wk=")]
+ public void RunTest_Normal_Managed(string password, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedValueAsBase64)
+ {
+ // Arrange
+ byte[] salt = new byte[256];
+ for (int i = 0; i < salt.Length; i++)
+ {
+ salt[i] = (byte)i;
+ }
+
+ // Act & assert
+ TestProvider<ManagedPbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF. We only use 5 iterations so
+ // that our unit tests are fast.
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 - 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 0, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLo=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLpk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 - 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm9")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 0, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Q==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Wk=")]
+ public void RunTest_Normal_Win7(string password, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedValueAsBase64)
+ {
+ // Arrange
+ byte[] salt = new byte[256];
+ for (int i = 0; i < salt.Length; i++)
+ {
+ salt[i] = (byte)i;
+ }
+
+ // Act & assert
+ TestProvider<Win7Pbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF. We only use 5 iterations so
+ // that our unit tests are fast.
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 - 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRA==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 0, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLo=")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLpk")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 - 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm9")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 0, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Q==")]
+ [InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Wk=")]
+ public void RunTest_Normal_Win8(string password, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedValueAsBase64)
+ {
+ // Arrange
+ byte[] salt = new byte[256];
+ for (int i = 0; i < salt.Length; i++)
+ {
+ salt[i] = (byte)i;
+ }
+
+ // Act & assert
+ TestProvider<Win8Pbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
+ }
+
+ [Fact]
+ public void RunTest_WithLongPassword_Managed()
+ {
+ RunTest_WithLongPassword_Impl<ManagedPbkdf2Provider>();
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void RunTest_WithLongPassword_Win7()
+ {
+ RunTest_WithLongPassword_Impl<Win7Pbkdf2Provider>();
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
+ public void RunTest_WithLongPassword_Win8()
+ {
+ RunTest_WithLongPassword_Impl<Win8Pbkdf2Provider>();
+ }
+
+ private static void RunTest_WithLongPassword_Impl<TProvider>()
+ where TProvider : IPbkdf2Provider, new()
+ {
+ byte[] salt = Encoding.UTF8.GetBytes("salt");
+ const string expectedDerivedKeyBase64 = "Sc+V/c3fiZq5Z5qH3iavAiojTsW97FAp2eBNmCQAwCNzA8hfhFFYyQLIMK65qPnBFHOHXQPwAxNQNhaEAH9hzfiaNBSRJpF9V4rpl02d5ZpI6cZbsQFF7TJW7XJzQVpYoPDgJlg0xVmYLhn1E9qMtUVUuXsBjOOdd7K1M+ZI00c=";
+ RunTest_WithLongPassword_Impl<TProvider>(salt, expectedDerivedKeyBase64);
+ }
+
+ private static void RunTest_WithLongPassword_Impl<TProvider>(byte[] salt, string expectedDerivedKeyBase64)
+ where TProvider : IPbkdf2Provider, new()
+ {
+ // Arrange
+ string password = new String('x', 50000); // 50,000 char password
+ const KeyDerivationPrf prf = KeyDerivationPrf.HMACSHA256;
+ const int iterationCount = 5;
+ const int numBytesRequested = 128;
+
+ // Act & assert
+ TestProvider<TProvider>(password, salt, prf, iterationCount, numBytesRequested, expectedDerivedKeyBase64);
+ }
+
+ private static void TestProvider<TProvider>(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedDerivedKeyAsBase64)
+ where TProvider : IPbkdf2Provider, new()
+ {
+ byte[] derivedKey = new TProvider().DeriveKey(password, salt, prf, iterationCount, numBytesRequested);
+ Assert.Equal(numBytesRequested, derivedKey.Length);
+ Assert.Equal(expectedDerivedKeyAsBase64, Convert.ToBase64String(derivedKey));
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Properties/AssemblyInfo.cs b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3adbc7af4e
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// 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.Runtime.CompilerServices;
+
+// for unit testing
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/DataProtectionCommonExtensionsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/DataProtectionCommonExtensionsTests.cs
new file mode 100644
index 0000000000..cfd4f3b41f
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/DataProtectionCommonExtensionsTests.cs
@@ -0,0 +1,313 @@
+// 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.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.DataProtection.Abstractions;
+using Microsoft.AspNetCore.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class DataProtectionCommonExtensionsTests
+ {
+ [Theory]
+ [InlineData(new object[] { new string[0] })]
+ [InlineData(new object[] { new string[] { null } })]
+ [InlineData(new object[] { new string[] { "the next value is bad", null } })]
+ public void CreateProtector_ChainedAsIEnumerable_FailureCases(string[] purposes)
+ {
+ // Arrange
+ var mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
+ var provider = mockProtector.Object;
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => provider.CreateProtector((IEnumerable<string>)purposes),
+ paramName: "purposes",
+ exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
+ }
+
+ [Theory]
+ [InlineData(new object[] { new string[] { null } })]
+ [InlineData(new object[] { new string[] { "the next value is bad", null } })]
+ public void CreateProtector_ChainedAsParams_FailureCases(string[] subPurposes)
+ {
+ // Arrange
+ var mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
+ var provider = mockProtector.Object;
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => provider.CreateProtector("primary-purpose", subPurposes),
+ paramName: "purposes",
+ exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
+ }
+
+ [Fact]
+ public void CreateProtector_ChainedAsIEnumerable_SuccessCase()
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock<IDataProtector>().Object;
+
+ var thirdMock = new Mock<IDataProtector>();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock<IDataProtector>();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock<IDataProtector>();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
+
+ // Act
+ var retVal = firstMock.Object.CreateProtector((IEnumerable<string>)new string[] { "first", "second", "third" });
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Fact]
+ public void CreateProtector_ChainedAsParams_NonEmptyParams_SuccessCase()
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock<IDataProtector>().Object;
+
+ var thirdMock = new Mock<IDataProtector>();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock<IDataProtector>();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock<IDataProtector>();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
+
+ // Act
+ var retVal = firstMock.Object.CreateProtector("first", "second", "third");
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Theory]
+ [InlineData(new object[] { null })]
+ [InlineData(new object[] { new string[0] })]
+ public void CreateProtector_ChainedAsParams_EmptyParams_SuccessCases(string[] subPurposes)
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock<IDataProtector>().Object;
+ var firstMock = new Mock<IDataProtector>();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector);
+
+ // Act
+ var retVal = firstMock.Object.CreateProtector("first", subPurposes);
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Fact]
+ public void GetDataProtectionProvider_NoServiceFound_Throws()
+ {
+ // Arrange
+ var services = new Mock<IServiceProvider>().Object;
+
+ // Act & assert
+ var ex = Assert.Throws<InvalidOperationException>(() => services.GetDataProtectionProvider());
+ Assert.Equal(Resources.FormatDataProtectionExtensions_NoService(typeof(IDataProtectionProvider).FullName), ex.Message);
+ }
+
+ [Fact]
+ public void GetDataProtectionProvider_ServiceFound_ReturnsService()
+ {
+ // Arrange
+ var expected = new Mock<IDataProtectionProvider>().Object;
+ var mockServices = new Mock<IServiceProvider>();
+ mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(expected);
+ var services = mockServices.Object;
+
+ // Act
+ var actual = services.GetDataProtectionProvider();
+
+ // Assert
+ Assert.Same(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(new object[] { new string[0] })]
+ [InlineData(new object[] { new string[] { null } })]
+ [InlineData(new object[] { new string[] { "the next value is bad", null } })]
+ public void GetDataProtector_ChainedAsIEnumerable_FailureCases(string[] purposes)
+ {
+ // Arrange
+ var mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
+ var mockServices = new Mock<IServiceProvider>();
+ mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object);
+ var services = mockServices.Object;
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => services.GetDataProtector((IEnumerable<string>)purposes),
+ paramName: "purposes",
+ exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
+ }
+
+ [Theory]
+ [InlineData(new object[] { new string[] { null } })]
+ [InlineData(new object[] { new string[] { "the next value is bad", null } })]
+ public void GetDataProtector_ChainedAsParams_FailureCases(string[] subPurposes)
+ {
+ // Arrange
+ var mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny<string>())).Returns(mockProtector.Object);
+ var mockServices = new Mock<IServiceProvider>();
+ mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(mockProtector.Object);
+ var services = mockServices.Object;
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => services.GetDataProtector("primary-purpose", subPurposes),
+ paramName: "purposes",
+ exceptionMessage: Resources.DataProtectionExtensions_NullPurposesCollection);
+ }
+
+ [Fact]
+ public void GetDataProtector_ChainedAsIEnumerable_SuccessCase()
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock<IDataProtector>().Object;
+
+ var thirdMock = new Mock<IDataProtector>();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock<IDataProtector>();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock<IDataProtector>();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
+
+ var mockServices = new Mock<IServiceProvider>();
+ mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
+ var services = mockServices.Object;
+
+ // Act
+ var retVal = services.GetDataProtector((IEnumerable<string>)new string[] { "first", "second", "third" });
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Fact]
+ public void GetDataProtector_ChainedAsParams_NonEmptyParams_SuccessCase()
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock<IDataProtector>().Object;
+
+ var thirdMock = new Mock<IDataProtector>();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock<IDataProtector>();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock<IDataProtector>();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
+
+ var mockServices = new Mock<IServiceProvider>();
+ mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
+ var services = mockServices.Object;
+
+ // Act
+ var retVal = services.GetDataProtector("first", "second", "third");
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Theory]
+ [InlineData(new object[] { null })]
+ [InlineData(new object[] { new string[0] })]
+ public void GetDataProtector_ChainedAsParams_EmptyParams_SuccessCases(string[] subPurposes)
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock<IDataProtector>().Object;
+ var firstMock = new Mock<IDataProtector>();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector);
+ var mockServices = new Mock<IServiceProvider>();
+ mockServices.Setup(o => o.GetService(typeof(IDataProtectionProvider))).Returns(firstMock.Object);
+ var services = mockServices.Object;
+
+ // Act
+ var retVal = services.GetDataProtector("first", subPurposes);
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Fact]
+ public void Protect_InvalidUtf8_Failure()
+ {
+ // Arrange
+ Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
+
+ // Act & assert
+ var ex = Assert.Throws<CryptographicException>(() =>
+ {
+ mockProtector.Object.Protect("Hello\ud800");
+ });
+ Assert.IsAssignableFrom<EncoderFallbackException>(ex.InnerException);
+ }
+
+ [Fact]
+ public void Protect_Success()
+ {
+ // Arrange
+ Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(p => p.Protect(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f })).Returns(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 });
+
+ // Act
+ string retVal = mockProtector.Object.Protect("Hello");
+
+ // Assert
+ Assert.Equal("AQIDBAU", retVal);
+ }
+
+ [Fact]
+ public void Unprotect_InvalidBase64BeforeDecryption_Failure()
+ {
+ // Arrange
+ Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
+
+ // Act & assert
+ var ex = Assert.Throws<CryptographicException>(() =>
+ {
+ mockProtector.Object.Unprotect("A");
+ });
+ }
+
+ [Fact]
+ public void Unprotect_InvalidUtf8AfterDecryption_Failure()
+ {
+ // Arrange
+ Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0xff });
+
+ // Act & assert
+ var ex = Assert.Throws<CryptographicException>(() =>
+ {
+ mockProtector.Object.Unprotect("AQIDBAU");
+ });
+ Assert.IsAssignableFrom<DecoderFallbackException>(ex.InnerException);
+ }
+
+ [Fact]
+ public void Unprotect_Success()
+ {
+ // Arrange
+ Mock<IDataProtector> mockProtector = new Mock<IDataProtector>();
+ mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f });
+
+ // Act
+ string retVal = DataProtectionCommonExtensions.Unprotect(mockProtector.Object, "AQIDBAU");
+
+ // Assert
+ Assert.Equal("Hello", retVal);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/Microsoft.AspNetCore.DataProtection.Abstractions.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/Microsoft.AspNetCore.DataProtection.Abstractions.Test.csproj
new file mode 100644
index 0000000000..1da22cec0d
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Abstractions.Test/Microsoft.AspNetCore.DataProtection.Abstractions.Test.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\common\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Cryptography.Internal\Microsoft.AspNetCore.Cryptography.Internal.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Abstractions\Microsoft.AspNetCore.DataProtection.Abstractions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs
new file mode 100644
index 0000000000..faa9bd1c96
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs
@@ -0,0 +1,78 @@
+// 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.Linq;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.Azure.KeyVault.Models;
+using Microsoft.Azure.KeyVault.WebKey;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test
+{
+ public class AzureKeyVaultXmlEncryptorTests
+ {
+ [Fact]
+ public void UsesKeyVaultToEncryptKey()
+ {
+ var mock = new Mock<IKeyVaultWrappingClient>();
+ mock.Setup(client => client.WrapKeyAsync("key", JsonWebKeyEncryptionAlgorithm.RSAOAEP, It.IsAny<byte[]>()))
+ .Returns<string, string, byte[]>((_, __, data) => Task.FromResult(new KeyOperationResult("KeyId", data.Reverse().ToArray())));
+
+ var encryptor = new AzureKeyVaultXmlEncryptor(mock.Object, "key", new MockNumberGenerator());
+ var result = encryptor.Encrypt(new XElement("Element"));
+
+ var encryptedElement = result.EncryptedElement;
+ var value = encryptedElement.Element("value");
+
+ mock.VerifyAll();
+ Assert.NotNull(result);
+ Assert.NotNull(value);
+ Assert.Equal(typeof(AzureKeyVaultXmlDecryptor), result.DecryptorType);
+ Assert.Equal("VfLYL2prdymawfucH3Goso0zkPbQ4/GKqUsj2TRtLzsBPz7p7cL1SQaY6I29xSlsPQf6IjxHSz4sDJ427GvlLQ==", encryptedElement.Element("value").Value);
+ Assert.Equal("AAECAwQFBgcICQoLDA0ODw==", encryptedElement.Element("iv").Value);
+ Assert.Equal("Dw4NDAsKCQgHBgUEAwIBAA==", encryptedElement.Element("key").Value);
+ Assert.Equal("KeyId", encryptedElement.Element("kid").Value);
+ }
+
+ [Fact]
+ public void UsesKeyVaultToDecryptKey()
+ {
+ var mock = new Mock<IKeyVaultWrappingClient>();
+ mock.Setup(client => client.UnwrapKeyAsync("KeyId", JsonWebKeyEncryptionAlgorithm.RSAOAEP, It.IsAny<byte[]>()))
+ .Returns<string, string, byte[]>((_, __, data) => Task.FromResult(new KeyOperationResult(null, data.Reverse().ToArray())))
+ .Verifiable();
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton(mock.Object);
+
+ var encryptor = new AzureKeyVaultXmlDecryptor(serviceCollection.BuildServiceProvider());
+
+ var result = encryptor.Decrypt(XElement.Parse(
+ @"<encryptedKey>
+ <kid>KeyId</kid>
+ <key>Dw4NDAsKCQgHBgUEAwIBAA==</key>
+ <iv>AAECAwQFBgcICQoLDA0ODw==</iv>
+ <value>VfLYL2prdymawfucH3Goso0zkPbQ4/GKqUsj2TRtLzsBPz7p7cL1SQaY6I29xSlsPQf6IjxHSz4sDJ427GvlLQ==</value>
+ </encryptedKey>"));
+
+ mock.VerifyAll();
+ Assert.NotNull(result);
+ Assert.Equal("<Element />", result.ToString());
+ }
+
+ private class MockNumberGenerator : RandomNumberGenerator
+ {
+ public override void GetBytes(byte[] data)
+ {
+ for (int i = 0; i < data.Length; i++)
+ {
+ data[i] = (byte)i;
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj
new file mode 100644
index 0000000000..c5ffd2f4e3
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.AzureKeyVault\Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureBlobXmlRepositoryTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureBlobXmlRepositoryTests.cs
new file mode 100644
index 0000000000..61d5d0ae78
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureBlobXmlRepositoryTests.cs
@@ -0,0 +1,111 @@
+// 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;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Blob;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureStorage.Test
+{
+ public class AzureBlobXmlRepositoryTests
+ {
+ [Fact]
+ public void StoreCreatesBlobWhenNotExist()
+ {
+ AccessCondition downloadCondition = null;
+ AccessCondition uploadCondition = null;
+ byte[] bytes = null;
+ BlobProperties properties = new BlobProperties();
+
+ var mock = new Mock<ICloudBlob>();
+ mock.SetupGet(c => c.Properties).Returns(properties);
+ mock.Setup(c => c.UploadFromByteArrayAsync(
+ It.IsAny<byte[]>(),
+ It.IsAny<int>(),
+ It.IsAny<int>(),
+ It.IsAny<AccessCondition>(),
+ It.IsAny<BlobRequestOptions>(),
+ It.IsAny<OperationContext>()))
+ .Returns(async (byte[] buffer, int index, int count, AccessCondition accessCondition, BlobRequestOptions options, OperationContext operationContext) =>
+ {
+ bytes = buffer.Skip(index).Take(count).ToArray();
+ uploadCondition = accessCondition;
+ await Task.Yield();
+ });
+
+ var repository = new AzureBlobXmlRepository(() => mock.Object);
+ repository.StoreElement(new XElement("Element"), null);
+
+ Assert.Null(downloadCondition);
+ Assert.Equal("*", uploadCondition.IfNoneMatchETag);
+ Assert.Equal("application/xml; charset=utf-8", properties.ContentType);
+ var element = "<Element />";
+
+ Assert.Equal(bytes, GetEnvelopedContent(element));
+ }
+
+ [Fact]
+ public void StoreUpdatesWhenExistsAndNewerExists()
+ {
+ AccessCondition downloadCondition = null;
+ byte[] bytes = null;
+ BlobProperties properties = new BlobProperties();
+
+ var mock = new Mock<ICloudBlob>();
+ mock.SetupGet(c => c.Properties).Returns(properties);
+ mock.Setup(c => c.DownloadToStreamAsync(
+ It.IsAny<Stream>(),
+ It.IsAny<AccessCondition>(),
+ null,
+ null))
+ .Returns(async (Stream target, AccessCondition condition, BlobRequestOptions options, OperationContext context) =>
+ {
+ var data = GetEnvelopedContent("<Element1 />");
+ await target.WriteAsync(data, 0, data.Length);
+ })
+ .Verifiable();
+
+ mock.Setup(c => c.UploadFromByteArrayAsync(
+ It.IsAny<byte[]>(),
+ It.IsAny<int>(),
+ It.IsAny<int>(),
+ It.Is((AccessCondition cond) => cond.IfNoneMatchETag == "*"),
+ It.IsAny<BlobRequestOptions>(),
+ It.IsAny<OperationContext>()))
+ .Throws(new StorageException(new RequestResult { HttpStatusCode = 412 }, null, null))
+ .Verifiable();
+
+ mock.Setup(c => c.UploadFromByteArrayAsync(
+ It.IsAny<byte[]>(),
+ It.IsAny<int>(),
+ It.IsAny<int>(),
+ It.Is((AccessCondition cond) => cond.IfNoneMatchETag != "*"),
+ It.IsAny<BlobRequestOptions>(),
+ It.IsAny<OperationContext>()))
+ .Returns(async (byte[] buffer, int index, int count, AccessCondition accessCondition, BlobRequestOptions options, OperationContext operationContext) =>
+ {
+ bytes = buffer.Skip(index).Take(count).ToArray();
+ await Task.Yield();
+ })
+ .Verifiable();
+
+ var repository = new AzureBlobXmlRepository(() => mock.Object);
+ repository.StoreElement(new XElement("Element2"), null);
+
+ mock.Verify();
+ Assert.Null(downloadCondition);
+ Assert.Equal(bytes, GetEnvelopedContent("<Element1 /><Element2 />"));
+ }
+
+ private static byte[] GetEnvelopedContent(string element)
+ {
+ return Encoding.UTF8.GetBytes($"<?xml version=\"1.0\" encoding=\"utf-8\"?><repository>{element}</repository>");
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureDataProtectionBuilderExtensionsTest.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureDataProtectionBuilderExtensionsTest.cs
new file mode 100644
index 0000000000..d386352b73
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/AzureDataProtectionBuilderExtensionsTest.cs
@@ -0,0 +1,32 @@
+// 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 Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.WindowsAzure.Storage.Blob;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AzureStorage
+{
+ public class AzureDataProtectionBuilderExtensionsTest
+ {
+ [Fact]
+ public void PersistKeysToAzureBlobStorage_UsesAzureBlobXmlRepository()
+ {
+ // Arrange
+ var container = new CloudBlobContainer(new Uri("http://www.example.com"));
+ var serviceCollection = new ServiceCollection();
+ var builder = serviceCollection.AddDataProtection();
+
+ // Act
+ builder.PersistKeysToAzureBlobStorage(container, "keys.xml");
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var options = services.GetRequiredService<IOptions<KeyManagementOptions>>();
+ Assert.IsType<AzureBlobXmlRepository>(options.Value.XmlRepository);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test.csproj
new file mode 100644
index 0000000000..8644347572
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test/Microsoft.AspNetCore.DataProtection.AzureStorage.Test.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.AzureStorage\Microsoft.AspNetCore.DataProtection.AzureStorage.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs
new file mode 100644
index 0000000000..c298d8e64f
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs
@@ -0,0 +1,69 @@
+// 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.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class DataProtectionEntityFrameworkTests
+ {
+ [Fact]
+ public void CreateRepository_ThrowsIf_ContextIsNull()
+ {
+ Assert.Throws<ArgumentNullException>(() => new EntityFrameworkCoreXmlRepository<DataProtectionKeyContext>(null, null));
+ }
+
+ [Fact]
+ public void StoreElement_PersistsData()
+ {
+ var element = XElement.Parse("<Element1/>");
+ var friendlyName = "Element1";
+ var key = new DataProtectionKey() { FriendlyName = friendlyName, Xml = element.ToString() };
+
+ var services = GetServices(nameof(StoreElement_PersistsData));
+ var service = new EntityFrameworkCoreXmlRepository<DataProtectionKeyContext>(services, NullLoggerFactory.Instance);
+ service.StoreElement(element, friendlyName);
+
+ // Use a separate instance of the context to verify correct data was saved to database
+ using (var context = services.CreateScope().ServiceProvider.GetRequiredService< DataProtectionKeyContext>())
+ {
+ Assert.Equal(1, context.DataProtectionKeys.Count());
+ Assert.Equal(key.FriendlyName, context.DataProtectionKeys.Single()?.FriendlyName);
+ Assert.Equal(key.Xml, context.DataProtectionKeys.Single()?.Xml);
+ }
+ }
+
+ [Fact]
+ public void GetAllElements_ReturnsAllElements()
+ {
+ var element1 = XElement.Parse("<Element1/>");
+ var element2 = XElement.Parse("<Element2/>");
+
+ var services = GetServices(nameof(GetAllElements_ReturnsAllElements));
+ var service1 = CreateRepo(services);
+ service1.StoreElement(element1, "element1");
+ service1.StoreElement(element2, "element2");
+
+ // Use a separate instance of the context to verify correct data was saved to database
+ var service2 = CreateRepo(services);
+ var elements = service2.GetAllElements();
+ Assert.Equal(2, elements.Count);
+ }
+
+ private EntityFrameworkCoreXmlRepository<DataProtectionKeyContext> CreateRepo(IServiceProvider services)
+ => new EntityFrameworkCoreXmlRepository<DataProtectionKeyContext>(services, NullLoggerFactory.Instance);
+
+ private IServiceProvider GetServices(string dbName)
+ => new ServiceCollection()
+ .AddDbContext<DataProtectionKeyContext>(o => o.UseInMemoryDatabase(dbName))
+ .BuildServiceProvider(validateScopes: true);
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs
new file mode 100644
index 0000000000..96151de0bb
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs
@@ -0,0 +1,14 @@
+// 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 Microsoft.EntityFrameworkCore;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test
+{
+ class DataProtectionKeyContext : DbContext, IDataProtectionKeyContext
+ {
+ public DataProtectionKeyContext(DbContextOptions<DataProtectionKeyContext> options) : base(options) { }
+
+ public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..55b67d98e3
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs
@@ -0,0 +1,26 @@
+// 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 Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test
+{
+ public class EntityFrameworkCoreDataProtectionBuilderExtensionsTests
+ {
+ [Fact]
+ public void PersistKeysToEntityFrameworkCore_UsesEntityFrameworkCoreXmlRepository()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection
+ .AddDbContext<DataProtectionKeyContext>()
+ .AddDataProtection()
+ .PersistKeysToDbContext<DataProtectionKeyContext>();
+ var serviceProvider = serviceCollection.BuildServiceProvider(validateScopes: true);
+ var keyManagementOptions = serviceProvider.GetRequiredService<IOptions<KeyManagementOptions>>();
+ Assert.IsType<EntityFrameworkCoreXmlRepository<DataProtectionKeyContext>>(keyManagementOptions.Value.XmlRepository);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj
new file mode 100644
index 0000000000..ed07b79f25
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionAdvancedExtensionsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionAdvancedExtensionsTests.cs
new file mode 100644
index 0000000000..c98aff6c8f
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionAdvancedExtensionsTests.cs
@@ -0,0 +1,101 @@
+// 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.Globalization;
+using System.Text;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class DataProtectionAdvancedExtensionsTests
+ {
+ private const string SampleEncodedString = "AQI"; // = WebEncoders.Base64UrlEncode({ 0x01, 0x02 })
+
+ [Fact]
+ public void Protect_PayloadAsString_WithExplicitExpiration()
+ {
+ // Arrange
+ var plaintextAsBytes = Encoding.UTF8.GetBytes("this is plaintext");
+ var expiration = StringToDateTime("2015-01-01 00:00:00Z");
+ var mockDataProtector = new Mock<ITimeLimitedDataProtector>();
+ mockDataProtector.Setup(o => o.Protect(plaintextAsBytes, expiration)).Returns(new byte[] { 0x01, 0x02 });
+
+ // Act
+ string protectedPayload = mockDataProtector.Object.Protect("this is plaintext", expiration);
+
+ // Assert
+ Assert.Equal(SampleEncodedString, protectedPayload);
+ }
+
+ [Fact]
+ public void Protect_PayloadAsString_WithLifetimeAsTimeSpan()
+ {
+ // Arrange
+ var plaintextAsBytes = Encoding.UTF8.GetBytes("this is plaintext");
+ DateTimeOffset actualExpiration = default(DateTimeOffset);
+ var mockDataProtector = new Mock<ITimeLimitedDataProtector>();
+ mockDataProtector.Setup(o => o.Protect(plaintextAsBytes, It.IsAny<DateTimeOffset>()))
+ .Returns<byte[], DateTimeOffset>((_, exp) =>
+ {
+ actualExpiration = exp;
+ return new byte[] { 0x01, 0x02 };
+ });
+
+ // Act
+ DateTimeOffset lowerBound = DateTimeOffset.UtcNow.AddHours(48);
+ string protectedPayload = mockDataProtector.Object.Protect("this is plaintext", TimeSpan.FromHours(48));
+ DateTimeOffset upperBound = DateTimeOffset.UtcNow.AddHours(48);
+
+ // Assert
+ Assert.Equal(SampleEncodedString, protectedPayload);
+ Assert.InRange(actualExpiration, lowerBound, upperBound);
+ }
+
+ [Fact]
+ public void Protect_PayloadAsBytes_WithLifetimeAsTimeSpan()
+ {
+ // Arrange
+ DateTimeOffset actualExpiration = default(DateTimeOffset);
+ var mockDataProtector = new Mock<ITimeLimitedDataProtector>();
+ mockDataProtector.Setup(o => o.Protect(new byte[] { 0x11, 0x22, 0x33 }, It.IsAny<DateTimeOffset>()))
+ .Returns<byte[], DateTimeOffset>((_, exp) =>
+ {
+ actualExpiration = exp;
+ return new byte[] { 0x01, 0x02 };
+ });
+
+ // Act
+ DateTimeOffset lowerBound = DateTimeOffset.UtcNow.AddHours(48);
+ byte[] protectedPayload = mockDataProtector.Object.Protect(new byte[] { 0x11, 0x22, 0x33 }, TimeSpan.FromHours(48));
+ DateTimeOffset upperBound = DateTimeOffset.UtcNow.AddHours(48);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x01, 0x02 }, protectedPayload);
+ Assert.InRange(actualExpiration, lowerBound, upperBound);
+ }
+
+ [Fact]
+ public void Unprotect_PayloadAsString()
+ {
+ // Arrange
+ var futureDate = DateTimeOffset.UtcNow.AddYears(1);
+ var controlExpiration = futureDate;
+ var mockDataProtector = new Mock<ITimeLimitedDataProtector>();
+ mockDataProtector.Setup(o => o.Unprotect(new byte[] { 0x01, 0x02 }, out controlExpiration)).Returns(Encoding.UTF8.GetBytes("this is plaintext"));
+
+ // Act
+ string unprotectedPayload = mockDataProtector.Object.Unprotect(SampleEncodedString, out var testExpiration);
+
+ // Assert
+ Assert.Equal("this is plaintext", unprotectedPayload);
+ Assert.Equal(futureDate, testExpiration);
+ }
+
+ private static DateTime StringToDateTime(string input)
+ {
+ return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
new file mode 100644
index 0000000000..a66ebec2e8
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/DataProtectionProviderTests.cs
@@ -0,0 +1,313 @@
+// 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.IO;
+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.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class DataProtectionProviderTests
+ {
+ [Fact]
+ 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 = DataProtectionProvider.Create(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.Single(allFiles);
+ 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);
+ });
+ }
+
+ [Fact]
+ public void System_NoKeysDirectoryProvided_UsesDefaultKeysDirectory()
+ {
+ var mock = new Mock<IDefaultKeyStorageDirectories>();
+ var keysPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName());
+ mock.Setup(m => m.GetKeyStorageDirectory()).Returns(new DirectoryInfo(keysPath));
+
+ // Step 1: Instantiate the system and round-trip a payload
+ var provider = DataProtectionProvider.CreateProvider(
+ keyDirectory: null,
+ certificate: null,
+ setupAction: builder =>
+ {
+ builder.SetApplicationName("TestApplication");
+ builder.Services.AddSingleton<IKeyManager>(s =>
+ new XmlKeyManager(
+ s.GetRequiredService<IOptions<KeyManagementOptions>>(),
+ s.GetRequiredService<IActivator>(),
+ NullLoggerFactory.Instance,
+ mock.Object));
+ });
+
+ var protector = provider.CreateProtector("Protector");
+ Assert.Equal("payload", protector.Unprotect(protector.Protect("payload")));
+
+ // 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);
+ }
+ else
+ {
+ Assert.Contains("Warning: the key below is in an unencrypted form.", fileText, StringComparison.Ordinal);
+ }
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ 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 = DataProtectionProvider.Create(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.Single(allFiles);
+ 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);
+ });
+ }
+
+ [ConditionalFact]
+ [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)]
+ public void System_UsesProvidedDirectoryAndCertificate()
+ {
+ var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx");
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(new X509Certificate2(filePath, "password", X509KeyStorageFlags.Exportable));
+ 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");
+ var data = protector.Protect("payload");
+
+ // add a cert without the private key to ensure the decryption will still fallback to the cert store
+ var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCertWithoutPrivateKey.pfx"), "password");
+ var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certWithoutKey)).CreateProtector("purpose");
+ Assert.Equal("payload", unprotector.Unprotect(data));
+
+ // 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.Single(allFiles);
+ 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();
+ }
+ });
+ }
+
+ [ConditionalFact]
+ [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)]
+ public void System_UsesProvidedCertificateNotFromStore()
+ {
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ var certWithoutKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3WithoutPrivateKey.pfx"), "password3", X509KeyStorageFlags.Exportable);
+ Assert.False(certWithoutKey.HasPrivateKey, "Cert should not have private key");
+ store.Add(certWithoutKey);
+ store.Close();
+ }
+
+ WithUniqueTempDirectory(directory =>
+ {
+ using (var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ certificateStore.Open(OpenFlags.ReadWrite);
+ var certInStore = certificateStore.Certificates.Find(X509FindType.FindBySubjectName, "TestCert", false)[0];
+ Assert.NotNull(certInStore);
+ Assert.False(certInStore.HasPrivateKey);
+
+ try
+ {
+ var certWithKey = new X509Certificate2(Path.Combine(GetTestFilesPath(), "TestCert3.pfx"), "password3");
+
+ var protector = DataProtectionProvider.Create(directory, certWithKey).CreateProtector("purpose");
+ var data = protector.Protect("payload");
+
+ var keylessUnprotector = DataProtectionProvider.Create(directory).CreateProtector("purpose");
+ Assert.Throws<CryptographicException>(() => keylessUnprotector.Unprotect(data));
+
+ var unprotector = DataProtectionProvider.Create(directory, o => o.UnprotectKeysWithAnyCertificate(certInStore, certWithKey)).CreateProtector("purpose");
+ Assert.Equal("payload", unprotector.Unprotect(data));
+ }
+ finally
+ {
+ certificateStore.Remove(certInStore);
+ certificateStore.Close();
+ }
+ }
+ });
+ }
+
+ [Fact]
+ public void System_UsesInMemoryCertificate()
+ {
+ var filePath = Path.Combine(GetTestFilesPath(), "TestCert2.pfx");
+ var certificate = new X509Certificate2(filePath, "password");
+
+ AssetStoreDoesNotContain(certificate);
+
+ 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 = 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.Single(allFiles);
+ 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);
+ });
+ }
+
+ private static void AssetStoreDoesNotContain(X509Certificate2 certificate)
+ {
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ try
+ {
+ store.Open(OpenFlags.ReadOnly);
+ }
+ catch
+ {
+ return;
+ }
+
+ // ensure this cert is not in the x509 store
+ Assert.Empty(store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false));
+ }
+ }
+
+ [Fact]
+ public void System_CanUnprotectWithCert()
+ {
+ var filePath = Path.Combine(GetTestFilesPath(), "TestCert2.pfx");
+ var certificate = new X509Certificate2(filePath, "password");
+
+ WithUniqueTempDirectory(directory =>
+ {
+ // Step 1: directory should be completely empty
+ directory.Create();
+ Assert.Empty(directory.GetFiles());
+
+ // Step 2: instantiate the system and create some data
+ var protector = DataProtectionProvider
+ .Create(directory, certificate)
+ .CreateProtector("purpose");
+
+ var data = 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.Single(allFiles);
+ 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);
+
+ // Step 4: setup a second system and validate it can decrypt keys and unprotect data
+ var unprotector = DataProtectionProvider.Create(directory,
+ b => b.UnprotectKeysWithAnyCertificate(certificate));
+ Assert.Equal("payload", unprotector.CreateProtector("purpose").Unprotect(data));
+ });
+ }
+
+ /// <summary>
+ /// Runs a test and cleans up the temp directory afterward.
+ /// </summary>
+ private static void WithUniqueTempDirectory(Action<DirectoryInfo> testCode)
+ {
+ string uniqueTempPath = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName());
+ var dirInfo = new DirectoryInfo(uniqueTempPath);
+ try
+ {
+ testCode(dirInfo);
+ }
+ finally
+ {
+ // clean up when test is done
+ if (dirInfo.Exists)
+ {
+ dirInfo.Delete(recursive: true);
+ }
+ }
+ }
+
+ private static string GetTestFilesPath()
+ => Path.Combine(AppContext.BaseDirectory, "TestFiles");
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj
new file mode 100644
index 0000000000..16a4f12c98
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Microsoft.AspNetCore.DataProtection.Extensions.Test.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\shared\*.cs" />
+ <Content Include="TestFiles\**\*" CopyToOutputDirectory="PreserveNewest" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Abstractions\Microsoft.AspNetCore.DataProtection.Abstractions.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Properties/AssemblyInfo.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..a613784a32
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// 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 Xunit;
+
+// Workaround for DataProtectionProviderTests.System_UsesProvidedDirectoryAndCertificate
+// https://github.com/aspnet/DataProtection/issues/160
+[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] \ No newline at end of file
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx
new file mode 100644
index 0000000000..266754e8ee
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx
new file mode 100644
index 0000000000..4ed9bbe394
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx
new file mode 100644
index 0000000000..364251ba09
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx
new file mode 100644
index 0000000000..9776e9006d
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx
new file mode 100644
index 0000000000..812374c50c
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs
new file mode 100644
index 0000000000..47dfc26fd7
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TimeLimitedDataProtectorTests.cs
@@ -0,0 +1,180 @@
+// 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.Globalization;
+using System.Security.Cryptography;
+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";
+
+ [Fact]
+ public void Protect_LifetimeSpecified()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ DateTimeOffset expiration = StringToDateTime("2000-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock<IDataProtector>();
+ mockInnerProtector.Setup(o => o.CreateProtector("new purpose").CreateProtector(TimeLimitedPurposeString).Protect(
+ new byte[] {
+ 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ })).Returns(new byte[] { 0x10, 0x11 });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act
+ var subProtector = timeLimitedProtector.CreateProtector("new purpose");
+ var protectedPayload = subProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }, expiration);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload);
+ }
+
+ [Fact]
+ public void Protect_LifetimeNotSpecified_UsesInfiniteLifetime()
+ {
+ // Arrange
+ // 0x2bca2875f4373fff is the representation of DateTimeOffset.MaxValue.
+ DateTimeOffset expiration = StringToDateTime("2000-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock<IDataProtector>();
+ mockInnerProtector.Setup(o => o.CreateProtector("new purpose").CreateProtector(TimeLimitedPurposeString).Protect(
+ new byte[] {
+ 0x2b, 0xca, 0x28, 0x75, 0xf4, 0x37, 0x3f, 0xff, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ })).Returns(new byte[] { 0x10, 0x11 });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act
+ var subProtector = timeLimitedProtector.CreateProtector("new purpose");
+ var protectedPayload = subProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 });
+
+ // Assert
+ Assert.Equal(new byte[] { 0x10, 0x11 }, protectedPayload);
+ }
+
+ [Fact]
+ public void Unprotect_WithinPayloadValidityPeriod_Success()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ DateTimeOffset expectedExpiration = StringToDateTime("2000-01-01 00:00:00Z");
+ DateTimeOffset now = StringToDateTime("1999-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock<IDataProtector>();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Returns(
+ new byte[] {
+ 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act
+ var retVal = timeLimitedProtector.UnprotectCore(new byte[] { 0x10, 0x11 }, now, out var actualExpiration);
+
+ // Assert
+ Assert.Equal(expectedExpiration, actualExpiration);
+ Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }, retVal);
+ }
+
+ [Fact]
+ public void Unprotect_PayloadHasExpired_Fails()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ DateTimeOffset expectedExpiration = StringToDateTime("2000-01-01 00:00:00Z");
+ DateTimeOffset now = StringToDateTime("2001-01-01 00:00:00Z");
+ var mockInnerProtector = new Mock<IDataProtector>();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Returns(
+ new byte[] {
+ 0x08, 0xc1, 0x22, 0x02, 0x47, 0xe4, 0x40, 0x00, /* header */
+ 0x01, 0x02, 0x03, 0x04, 0x05 /* payload */
+ });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act & assert
+ var ex = Assert.Throws<CryptographicException>(()
+ => timeLimitedProtector.UnprotectCore(new byte[] { 0x10, 0x11 }, now, out var _));
+
+ // Assert
+ Assert.Equal(ExtResources.FormatTimeLimitedDataProtector_PayloadExpired(expectedExpiration), ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_ProtectedDataMalformed_Fails()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ var mockInnerProtector = new Mock<IDataProtector>();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Returns(
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 /* header too short */
+ });
+
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act & assert
+ var ex = Assert.Throws<CryptographicException>(()
+ => timeLimitedProtector.Unprotect(new byte[] { 0x10, 0x11 }, out var _));
+
+ // Assert
+ Assert.Equal(ExtResources.TimeLimitedDataProtector_PayloadInvalid, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_UnprotectOperationFails_HomogenizesExceptionToCryptographicException()
+ {
+ // Arrange
+ // 0x08c1220247e44000 is the representation of midnight 2000-01-01 UTC.
+ var mockInnerProtector = new Mock<IDataProtector>();
+ mockInnerProtector.Setup(o => o.CreateProtector(TimeLimitedPurposeString).Unprotect(new byte[] { 0x10, 0x11 })).Throws(new Exception("How exceptional!"));
+ var timeLimitedProtector = new TimeLimitedDataProtector(mockInnerProtector.Object);
+
+ // Act & assert
+ var ex = Assert.Throws<CryptographicException>(()
+ => timeLimitedProtector.Unprotect(new byte[] { 0x10, 0x11 }, out var _));
+
+ // Assert
+ Assert.Equal(Resources.CryptCommon_GenericError, ex.Message);
+ Assert.Equal("How exceptional!", ex.InnerException.Message);
+ }
+
+ [Fact]
+ public void RoundTrip_ProtectedData()
+ {
+ // Arrange
+ var ephemeralProtector = new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("my purpose");
+ var timeLimitedProtector = new TimeLimitedDataProtector(ephemeralProtector);
+ var expectedExpiration = StringToDateTime("2020-01-01 00:00:00Z");
+
+ // Act
+ byte[] ephemeralProtectedPayload = ephemeralProtector.Protect(new byte[] { 0x01, 0x02, 0x03, 0x04 });
+ byte[] timeLimitedProtectedPayload = timeLimitedProtector.Protect(new byte[] { 0x11, 0x22, 0x33, 0x44 }, expectedExpiration);
+
+ // Assert
+ Assert.Equal(
+ new byte[] { 0x11, 0x22, 0x33, 0x44 },
+ timeLimitedProtector.UnprotectCore(timeLimitedProtectedPayload, StringToDateTime("2010-01-01 00:00:00Z"), out var actualExpiration));
+ Assert.Equal(expectedExpiration, actualExpiration);
+
+ // the two providers shouldn't be able to talk to one another (due to the purpose chaining)
+ Assert.Throws<CryptographicException>(() => ephemeralProtector.Unprotect(timeLimitedProtectedPayload));
+ Assert.Throws<CryptographicException>(() => timeLimitedProtector.Unprotect(ephemeralProtectedPayload, out actualExpiration));
+ }
+
+ private static DateTime StringToDateTime(string input)
+ {
+ return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/X509StoreIsAvailableAttribute.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/X509StoreIsAvailableAttribute.cs
new file mode 100644
index 0000000000..2181b4c24f
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/X509StoreIsAvailableAttribute.cs
@@ -0,0 +1,43 @@
+// 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.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Testing.xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class X509StoreIsAvailableAttribute : Attribute, ITestCondition
+ {
+ public X509StoreIsAvailableAttribute(StoreName name, StoreLocation location)
+ {
+ Name = name;
+ Location = location;
+ }
+
+ public bool IsMet
+ {
+ get
+ {
+ try
+ {
+ using (var store = new X509Store(Name, Location))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ return true;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ public string SkipReason => $"Skipping because the X509Store({Name}/{Location}) is not available on this machine.";
+
+ public StoreName Name { get; }
+ public StoreLocation Location { get; }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/DataProtectionRedisTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/DataProtectionRedisTests.cs
new file mode 100644
index 0000000000..a204050ad1
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/DataProtectionRedisTests.cs
@@ -0,0 +1,110 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.AspNetCore.Testing.xunit;
+using Moq;
+using StackExchange.Redis;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.DataProtection.StackExchangeRedis
+{
+ public class DataProtectionRedisTests
+ {
+ private readonly ITestOutputHelper _output;
+
+ public DataProtectionRedisTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void GetAllElements_ReturnsAllXmlValuesForGivenKey()
+ {
+ var database = new Mock<IDatabase>();
+ database.Setup(d => d.ListRange("Key", 0, -1, CommandFlags.None)).Returns(new RedisValue[]
+ {
+ "<Element1/>",
+ "<Element2/>",
+ }).Verifiable();
+ var repo = new RedisXmlRepository(() => database.Object, "Key");
+
+ var elements = repo.GetAllElements().ToArray();
+
+ database.Verify();
+ Assert.Equal(new XElement("Element1").ToString(), elements[0].ToString());
+ Assert.Equal(new XElement("Element2").ToString(), elements[1].ToString());
+ }
+
+ [Fact]
+ public void GetAllElements_ThrowsParsingException()
+ {
+ var database = new Mock<IDatabase>();
+ database.Setup(d => d.ListRange("Key", 0, -1, CommandFlags.None)).Returns(new RedisValue[]
+ {
+ "<Element1/>",
+ "<Element2",
+ }).Verifiable();
+ var repo = new RedisXmlRepository(() => database.Object, "Key");
+
+ Assert.Throws<XmlException>(() => repo.GetAllElements());
+ }
+
+ [Fact]
+ public void StoreElement_PushesValueToList()
+ {
+ var database = new Mock<IDatabase>();
+ database.Setup(d => d.ListRightPush("Key", "<Element2 />", When.Always, CommandFlags.None)).Verifiable();
+ var repo = new RedisXmlRepository(() => database.Object, "Key");
+
+ repo.StoreElement(new XElement("Element2"), null);
+
+ database.Verify();
+ }
+
+ [ConditionalFact]
+ [TestRedisServerIsAvailable]
+ public async Task XmlRoundTripsToActualRedisServer()
+ {
+ var connStr = TestRedisServer.GetConnectionString();
+
+ _output.WriteLine("Attempting to connect to " + connStr);
+
+ var guid = Guid.NewGuid().ToString();
+ RedisKey key = "Test:DP:Key" + guid;
+
+ try
+ {
+ using (var redis = await ConnectionMultiplexer.ConnectAsync(connStr).TimeoutAfter(TimeSpan.FromMinutes(1)))
+ {
+ var repo = new RedisXmlRepository(() => redis.GetDatabase(), key);
+ var element = new XElement("HelloRedis", guid);
+ repo.StoreElement(element, guid);
+ }
+
+ using (var redis = await ConnectionMultiplexer.ConnectAsync(connStr).TimeoutAfter(TimeSpan.FromMinutes(1)))
+ {
+ var repo = new RedisXmlRepository(() => redis.GetDatabase(), key);
+ var elements = repo.GetAllElements();
+
+ Assert.Contains(elements, e => e.Name == "HelloRedis" && e.Value == guid);
+ }
+ }
+ finally
+ {
+ // cleanup
+ using (var redis = await ConnectionMultiplexer.ConnectAsync(connStr).TimeoutAfter(TimeSpan.FromMinutes(1)))
+ {
+ await redis.GetDatabase().KeyDeleteAsync(key);
+ }
+ }
+
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test.csproj
new file mode 100644
index 0000000000..87f9f318bc
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test.csproj
@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\common\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="testconfig.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.Abstractions\Microsoft.AspNetCore.DataProtection.Abstractions.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.StackExchangeRedis\Microsoft.AspNetCore.DataProtection.StackExchangeRedis.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/RedisDataProtectionBuilderExtensionsTest.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/RedisDataProtectionBuilderExtensionsTest.cs
new file mode 100644
index 0000000000..2b4c2865c3
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/RedisDataProtectionBuilderExtensionsTest.cs
@@ -0,0 +1,32 @@
+// 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 Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using StackExchange.Redis;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.StackExchangeRedis
+{
+ public class RedisDataProtectionBuilderExtensionsTest
+ {
+ [Fact]
+ public void PersistKeysToRedis_UsesRedisXmlRepository()
+ {
+ // Arrange
+ var connection = Mock.Of<IConnectionMultiplexer>();
+ var serviceCollection = new ServiceCollection();
+ var builder = serviceCollection.AddDataProtection();
+
+ // Act
+ builder.PersistKeysToStackExchangeRedis(connection);
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var options = services.GetRequiredService<IOptions<KeyManagementOptions>>();
+ Assert.IsType<RedisXmlRepository>(options.Value.XmlRepository);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServer.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServer.cs
new file mode 100644
index 0000000000..dfe369625a
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServer.cs
@@ -0,0 +1,28 @@
+// 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 Microsoft.Extensions.Configuration;
+using System;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal class TestRedisServer
+ {
+ public const string ConnectionStringKeyName = "Test:Redis:Server";
+ private static readonly IConfigurationRoot _config;
+
+ static TestRedisServer()
+ {
+ _config = new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("testconfig.json")
+ .AddEnvironmentVariables()
+ .Build();
+ }
+
+ internal static string GetConnectionString()
+ {
+ return _config[ConnectionStringKeyName];
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServerIsAvailableAttribute.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServerIsAvailableAttribute.cs
new file mode 100644
index 0000000000..04857c494b
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/TestRedisServerIsAvailableAttribute.cs
@@ -0,0 +1,15 @@
+// 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 Microsoft.AspNetCore.Testing.xunit;
+using System;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal class TestRedisServerIsAvailableAttribute : Attribute, ITestCondition
+ {
+ public bool IsMet => !string.IsNullOrEmpty(TestRedisServer.GetConnectionString());
+
+ public string SkipReason => $"A test redis server must be configured to run. Set the connection string as an environment variable as {TestRedisServer.ConnectionStringKeyName.Replace(":", "__")} or in testconfig.json";
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/testconfig.json b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/testconfig.json
new file mode 100644
index 0000000000..2e2f447946
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.StackExchangeRedis.Test/testconfig.json
@@ -0,0 +1,10 @@
+{
+ "Test": {
+ "Redis": {
+ // You can setup a local Redis server easily with Docker by running
+ // docker run --rm -it -p 6379:6379 redis
+ // Then uncomment this config below
+ // "Server": "localhost:6379,127.0.0.1:6379"
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ActivatorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ActivatorTests.cs
new file mode 100644
index 0000000000..a249162706
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ActivatorTests.cs
@@ -0,0 +1,117 @@
+// 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 Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class ActivatorTests
+ {
+ [Fact]
+ public void CreateInstance_WithServiceProvider_PrefersParameterfulCtor()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var services = serviceCollection.BuildServiceProvider();
+ var activator = services.GetActivator();
+
+ // Act
+ var retVal1 = (ClassWithParameterlessCtor)activator.CreateInstance<object>(typeof(ClassWithParameterlessCtor).AssemblyQualifiedName);
+ var retVal2 = (ClassWithServiceProviderCtor)activator.CreateInstance<object>(typeof(ClassWithServiceProviderCtor).AssemblyQualifiedName);
+ var retVal3 = (ClassWithBothCtors)activator.CreateInstance<object>(typeof(ClassWithBothCtors).AssemblyQualifiedName);
+
+ // Assert
+ Assert.NotNull(services);
+ Assert.NotNull(retVal1);
+ Assert.NotNull(retVal2);
+ Assert.Same(services, retVal2.Services);
+ Assert.NotNull(retVal3);
+ Assert.False(retVal3.ParameterlessCtorCalled);
+ Assert.Same(services, retVal3.Services);
+ }
+
+ [Fact]
+ public void CreateInstance_WithoutServiceProvider_PrefersParameterlessCtor()
+ {
+ // Arrange
+ var activator = ((IServiceProvider)null).GetActivator();
+
+ // Act
+ var retVal1 = (ClassWithParameterlessCtor)activator.CreateInstance<object>(typeof(ClassWithParameterlessCtor).AssemblyQualifiedName);
+ var retVal2 = (ClassWithServiceProviderCtor)activator.CreateInstance<object>(typeof(ClassWithServiceProviderCtor).AssemblyQualifiedName);
+ var retVal3 = (ClassWithBothCtors)activator.CreateInstance<object>(typeof(ClassWithBothCtors).AssemblyQualifiedName);
+
+ // Assert
+ Assert.NotNull(retVal1);
+ Assert.NotNull(retVal2);
+ Assert.Null(retVal2.Services);
+ Assert.NotNull(retVal3);
+ Assert.True(retVal3.ParameterlessCtorCalled);
+ Assert.Null(retVal3.Services);
+ }
+
+
+ [Fact]
+ public void CreateInstance_TypeDoesNotImplementInterface_ThrowsInvalidCast()
+ {
+ // Arrange
+ var activator = ((IServiceProvider)null).GetActivator();
+
+ // Act & assert
+ var ex = Assert.Throws<InvalidCastException>(
+ () => activator.CreateInstance<IDisposable>(typeof(ClassWithParameterlessCtor).AssemblyQualifiedName));
+ Assert.Equal(Resources.FormatTypeExtensions_BadCast(typeof(IDisposable).AssemblyQualifiedName, typeof(ClassWithParameterlessCtor).AssemblyQualifiedName), ex.Message);
+ }
+
+ [Fact]
+ public void GetActivator_ServiceProviderHasActivator_ReturnsSameInstance()
+ {
+ // Arrange
+ var expectedActivator = new Mock<IActivator>().Object;
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton<IActivator>(expectedActivator);
+
+ // Act
+ var actualActivator = serviceCollection.BuildServiceProvider().GetActivator();
+
+ // Assert
+ Assert.Same(expectedActivator, actualActivator);
+ }
+
+ private class ClassWithParameterlessCtor
+ {
+ }
+
+ private class ClassWithServiceProviderCtor
+ {
+ public readonly IServiceProvider Services;
+
+ public ClassWithServiceProviderCtor(IServiceProvider services)
+ {
+ Services = services;
+ }
+ }
+
+ private class ClassWithBothCtors
+ {
+ public readonly IServiceProvider Services;
+ public readonly bool ParameterlessCtorCalled;
+
+ public ClassWithBothCtors()
+ {
+ ParameterlessCtorCalled = true;
+ Services = null;
+ }
+
+ public ClassWithBothCtors(IServiceProvider services)
+ {
+ ParameterlessCtorCalled = false;
+ Services = services;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AnonymousImpersonation.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AnonymousImpersonation.cs
new file mode 100644
index 0000000000..e142b698e7
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AnonymousImpersonation.cs
@@ -0,0 +1,92 @@
+// 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.
+
+#if NET461
+using System;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Helpers for working with the anonymous Windows identity.
+ /// </summary>
+ internal static class AnonymousImpersonation
+ {
+ /// <summary>
+ /// Performs an action while impersonated under the anonymous user (NT AUTHORITY\ANONYMOUS LOGIN).
+ /// </summary>
+ public static void Run(Action callback)
+ {
+ using (var threadHandle = ThreadHandle.OpenCurrentThreadHandle())
+ {
+ bool impersonated = false;
+ try
+ {
+ impersonated = ImpersonateAnonymousToken(threadHandle);
+ if (!impersonated)
+ {
+ Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
+ }
+ callback();
+ }
+ finally
+ {
+ if (impersonated && !RevertToSelf())
+ {
+ Environment.FailFast("RevertToSelf() returned false!");
+ }
+ }
+ }
+ }
+
+ [DllImport("advapi32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ private static extern bool ImpersonateAnonymousToken([In] ThreadHandle ThreadHandle);
+
+ [DllImport("advapi32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ private static extern bool RevertToSelf();
+
+ private sealed class ThreadHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ private ThreadHandle()
+ : base(ownsHandle: true)
+ {
+ }
+
+ public static ThreadHandle OpenCurrentThreadHandle()
+ {
+ const int THREAD_ALL_ACCESS = 0x1FFFFF;
+ var handle = OpenThread(
+ dwDesiredAccess: THREAD_ALL_ACCESS,
+ bInheritHandle: false,
+#pragma warning disable CS0618 // Type or member is obsolete
+ dwThreadId: (uint)AppDomain.GetCurrentThreadId());
+#pragma warning restore CS0618 // Type or member is obsolete
+ CryptoUtil.AssertSafeHandleIsValid(handle);
+ return handle;
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ return CloseHandle(handle);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
+ [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ private static extern bool CloseHandle(
+ [In] IntPtr hObject);
+
+ [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ private static extern ThreadHandle OpenThread(
+ [In] uint dwDesiredAccess,
+ [In] bool bInheritHandle,
+ [In] uint dwThreadId);
+ }
+ }
+}
+#elif NETCOREAPP3_0
+#else
+#error Target framework needs to be updated
+#endif
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactoryTest.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactoryTest.cs
new file mode 100644
index 0000000000..2b13d7990c
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorFactoryTest.cs
@@ -0,0 +1,52 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ public class CngCbcAuthenticatedEncryptorFactoryTest
+ {
+ [Fact]
+ public void CreateEncrptorInstance_UnknownDescriptorType_ReturnsNull()
+ {
+ // Arrange
+ var key = new Mock<IKey>();
+ key.Setup(k => k.Descriptor).Returns(new Mock<IAuthenticatedEncryptorDescriptor>().Object);
+
+ var factory = new CngCbcAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Act
+ var encryptor = factory.CreateEncryptorInstance(key.Object);
+
+ // Assert
+ Assert.Null(encryptor);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void CreateEncrptorInstance_ExpectedDescriptorType_ReturnsEncryptor()
+ {
+ // Arrange
+ var descriptor = new CngCbcAuthenticatedEncryptorConfiguration().CreateNewDescriptor();
+ var key = new Mock<IKey>();
+ key.Setup(k => k.Descriptor).Returns(descriptor);
+
+ var factory = new CngCbcAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Act
+ var encryptor = factory.CreateEncryptorInstance(key.Object);
+
+ // Assert
+ Assert.NotNull(encryptor);
+ Assert.IsType<CbcAuthenticatedEncryptor>(encryptor);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactoryTest.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactoryTest.cs
new file mode 100644
index 0000000000..e641705f3a
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorFactoryTest.cs
@@ -0,0 +1,52 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ public class CngGcmAuthenticatedEncryptorFactoryTest
+ {
+ [Fact]
+ public void CreateEncrptorInstance_UnknownDescriptorType_ReturnsNull()
+ {
+ // Arrange
+ var key = new Mock<IKey>();
+ key.Setup(k => k.Descriptor).Returns(new Mock<IAuthenticatedEncryptorDescriptor>().Object);
+
+ var factory = new CngGcmAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Act
+ var encryptor = factory.CreateEncryptorInstance(key.Object);
+
+ // Assert
+ Assert.Null(encryptor);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void CreateEncrptorInstance_ExpectedDescriptorType_ReturnsEncryptor()
+ {
+ // Arrange
+ var descriptor = new CngGcmAuthenticatedEncryptorConfiguration().CreateNewDescriptor();
+ var key = new Mock<IKey>();
+ key.Setup(k => k.Descriptor).Returns(descriptor);
+
+ var factory = new CngGcmAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Act
+ var encryptor = factory.CreateEncryptorInstance(key.Object);
+
+ // Assert
+ Assert.NotNull(encryptor);
+ Assert.IsType<GcmAuthenticatedEncryptor>(encryptor);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..e7ef5d69c7
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,59 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class AuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [Fact]
+ public void ImportFromXml_Cbc_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var descriptor = new AuthenticatedEncryptorDescriptor(
+ new AuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = EncryptionAlgorithm.AES_192_CBC,
+ ValidationAlgorithm = ValidationAlgorithm.HMACSHA512
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret());
+ var control = CreateEncryptorInstanceFromDescriptor(descriptor);
+
+ const string xml = @"
+ <encryptor version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <encryption algorithm='AES_192_CBC' />
+ <validation algorithm='HMACSHA512' />
+ <masterKey enc:requiresEncryption='true'>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</masterKey>
+ </encryptor>";
+ var deserializedDescriptor = new AuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml));
+ var test = CreateEncryptorInstanceFromDescriptor(deserializedDescriptor as AuthenticatedEncryptorDescriptor);
+
+ // Act & assert
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ private static IAuthenticatedEncryptor CreateEncryptorInstanceFromDescriptor(AuthenticatedEncryptorDescriptor descriptor)
+ {
+ var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ var key = new Key(
+ Guid.NewGuid(),
+ DateTimeOffset.Now,
+ DateTimeOffset.Now + TimeSpan.FromHours(1),
+ DateTimeOffset.Now + TimeSpan.FromDays(30),
+ descriptor,
+ new[] { encryptorFactory });
+
+
+ return key.CreateEncryptor();
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..0bed1de2e4
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,189 @@
+// 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.Globalization;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Cryptography.SafeHandles;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Managed;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class AuthenticatedEncryptorDescriptorTests
+ {
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
+ [InlineData(EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA256)]
+ [InlineData(EncryptionAlgorithm.AES_192_CBC, ValidationAlgorithm.HMACSHA256)]
+ [InlineData(EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm.HMACSHA256)]
+ [InlineData(EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA512)]
+ [InlineData(EncryptionAlgorithm.AES_192_CBC, ValidationAlgorithm.HMACSHA512)]
+ [InlineData(EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm.HMACSHA512)]
+ public void CreateAuthenticatedEncryptor_RoundTripsData_CngCbcImplementation(EncryptionAlgorithm encryptionAlgorithm, ValidationAlgorithm validationAlgorithm)
+ {
+ // Parse test input
+ int keyLengthInBits = Int32.Parse(Regex.Match(encryptionAlgorithm.ToString(), @"^AES_(?<keyLength>\d{3})_CBC$").Groups["keyLength"].Value, CultureInfo.InvariantCulture);
+ string hashAlgorithm = Regex.Match(validationAlgorithm.ToString(), @"^HMAC(?<hashAlgorithm>.*)$").Groups["hashAlgorithm"].Value;
+
+ // Arrange
+ var masterKey = Secret.Random(512 / 8);
+ var control = new CbcAuthenticatedEncryptor(
+ keyDerivationKey: masterKey,
+ symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC,
+ symmetricAlgorithmKeySizeInBytes: (uint)(keyLengthInBits / 8),
+ hmacAlgorithmHandle: BCryptAlgorithmHandle.OpenAlgorithmHandle(hashAlgorithm, hmac: true));
+ var test = CreateEncryptorInstanceFromDescriptor(CreateDescriptor(encryptionAlgorithm, validationAlgorithm, masterKey));
+
+ // Act & assert - data round trips properly from control to test
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
+ [InlineData(EncryptionAlgorithm.AES_128_GCM)]
+ [InlineData(EncryptionAlgorithm.AES_192_GCM)]
+ [InlineData(EncryptionAlgorithm.AES_256_GCM)]
+ public void CreateAuthenticatedEncryptor_RoundTripsData_CngGcmImplementation(EncryptionAlgorithm encryptionAlgorithm)
+ {
+ // Parse test input
+ int keyLengthInBits = Int32.Parse(Regex.Match(encryptionAlgorithm.ToString(), @"^AES_(?<keyLength>\d{3})_GCM$").Groups["keyLength"].Value, CultureInfo.InvariantCulture);
+
+ // Arrange
+ var masterKey = Secret.Random(512 / 8);
+ var control = new GcmAuthenticatedEncryptor(
+ keyDerivationKey: masterKey,
+ symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_GCM,
+ symmetricAlgorithmKeySizeInBytes: (uint)(keyLengthInBits / 8));
+ var test = CreateEncryptorInstanceFromDescriptor(CreateDescriptor(encryptionAlgorithm, ValidationAlgorithm.HMACSHA256 /* unused */, masterKey));
+
+ // Act & assert - data round trips properly from control to test
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ public static TheoryData CreateAuthenticatedEncryptor_RoundTripsData_ManagedImplementationData
+ => new TheoryData<EncryptionAlgorithm, ValidationAlgorithm, Func<HMAC>>
+ {
+ { EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA256, () => new HMACSHA256() },
+ { EncryptionAlgorithm.AES_192_CBC, ValidationAlgorithm.HMACSHA256, () => new HMACSHA256() },
+ { EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm.HMACSHA256, () => new HMACSHA256() },
+ { EncryptionAlgorithm.AES_128_CBC, ValidationAlgorithm.HMACSHA512, () => new HMACSHA512() },
+ { EncryptionAlgorithm.AES_192_CBC, ValidationAlgorithm.HMACSHA512, () => new HMACSHA512() },
+ { EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm.HMACSHA512, () => new HMACSHA512() },
+ };
+
+ [Theory]
+ [MemberData(nameof(CreateAuthenticatedEncryptor_RoundTripsData_ManagedImplementationData))]
+ public void CreateAuthenticatedEncryptor_RoundTripsData_ManagedImplementation(
+ EncryptionAlgorithm encryptionAlgorithm,
+ ValidationAlgorithm validationAlgorithm,
+ Func<HMAC> validationAlgorithmFactory)
+ {
+ // Parse test input
+ int keyLengthInBits = Int32.Parse(Regex.Match(encryptionAlgorithm.ToString(), @"^AES_(?<keyLength>\d{3})_CBC$").Groups["keyLength"].Value, CultureInfo.InvariantCulture);
+
+ // Arrange
+ var masterKey = Secret.Random(512 / 8);
+ var control = new ManagedAuthenticatedEncryptor(
+ keyDerivationKey: masterKey,
+ symmetricAlgorithmFactory: () => Aes.Create(),
+ symmetricAlgorithmKeySizeInBytes: keyLengthInBits / 8,
+ validationAlgorithmFactory: validationAlgorithmFactory);
+ var test = CreateEncryptorInstanceFromDescriptor(CreateDescriptor(encryptionAlgorithm, validationAlgorithm, masterKey));
+
+ // Act & assert - data round trips properly from control to test
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ [Fact]
+ public void ExportToXml_ProducesCorrectPayload_Cbc()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = CreateDescriptor(EncryptionAlgorithm.AES_192_CBC, ValidationAlgorithm.HMACSHA512, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(AuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+ <descriptor>
+ <encryption algorithm='AES_192_CBC' />
+ <validation algorithm='HMACSHA512' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ [Fact]
+ public void ExportToXml_ProducesCorrectPayload_Gcm()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = CreateDescriptor(EncryptionAlgorithm.AES_192_GCM, ValidationAlgorithm.HMACSHA512, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(AuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+ <descriptor>
+ <encryption algorithm='AES_192_GCM' />
+ <!-- some comment here -->
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ private static AuthenticatedEncryptorDescriptor CreateDescriptor(EncryptionAlgorithm encryptionAlgorithm, ValidationAlgorithm validationAlgorithm, ISecret masterKey)
+ {
+ return new AuthenticatedEncryptorDescriptor(new AuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = encryptionAlgorithm,
+ ValidationAlgorithm = validationAlgorithm
+ }, masterKey);
+ }
+
+ private static IAuthenticatedEncryptor CreateEncryptorInstanceFromDescriptor(AuthenticatedEncryptorDescriptor descriptor)
+ {
+ var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Dummy key with the specified descriptor.
+ var key = new Key(
+ Guid.NewGuid(),
+ DateTimeOffset.Now,
+ DateTimeOffset.Now + TimeSpan.FromHours(1),
+ DateTimeOffset.Now + TimeSpan.FromDays(30),
+ descriptor,
+ new[] { encryptorFactory });
+
+
+ return key.CreateEncryptor();
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs
new file mode 100644
index 0000000000..9be301495e
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs
@@ -0,0 +1,40 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngCbcAuthenticatedEncryptorConfigurationTests
+ {
+ [Fact]
+ public void CreateNewDescriptor_CreatesUniqueCorrectlySizedMasterKey()
+ {
+ // Arrange
+ var configuration = new CngCbcAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var masterKey1 = ((CngCbcAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor()).MasterKey;
+ var masterKey2 = ((CngCbcAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor()).MasterKey;
+
+ // Assert
+ SecretAssert.NotEqual(masterKey1, masterKey2);
+ SecretAssert.LengthIs(512 /* bits */, masterKey1);
+ SecretAssert.LengthIs(512 /* bits */, masterKey2);
+ }
+
+ [Fact]
+ public void CreateNewDescriptor_PropagatesOptions()
+ {
+ // Arrange
+ var configuration = new CngCbcAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var descriptor = (CngCbcAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor();
+
+ // Assert
+ Assert.Equal(configuration, descriptor.Configuration);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..eb61aaa676
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,66 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngCbcAuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void ImportFromXml_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var descriptor = new CngCbcAuthenticatedEncryptorDescriptor(
+ new CngCbcAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = Constants.BCRYPT_AES_ALGORITHM,
+ EncryptionAlgorithmKeySize = 192,
+ EncryptionAlgorithmProvider = null,
+ HashAlgorithm = Constants.BCRYPT_SHA512_ALGORITHM,
+ HashAlgorithmProvider = null
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret());
+ var control = CreateEncryptorInstanceFromDescriptor(descriptor);
+
+ const string xml = @"
+ <descriptor version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <encryption algorithm='AES' keyLength='192' />
+ <hash algorithm='SHA512' />
+ <masterKey enc:requiresEncryption='true'>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</masterKey>
+ </descriptor>";
+ var deserializedDescriptor = new CngCbcAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml));
+ var test = CreateEncryptorInstanceFromDescriptor(deserializedDescriptor as CngCbcAuthenticatedEncryptorDescriptor);
+
+ // Act & assert
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ private static IAuthenticatedEncryptor CreateEncryptorInstanceFromDescriptor(CngCbcAuthenticatedEncryptorDescriptor descriptor)
+ {
+ var encryptorFactory = new CngCbcAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ var key = new Key(
+ Guid.NewGuid(),
+ DateTimeOffset.Now,
+ DateTimeOffset.Now + TimeSpan.FromHours(1),
+ DateTimeOffset.Now + TimeSpan.FromDays(30),
+ descriptor,
+ new[] { encryptorFactory });
+
+
+ return key.CreateEncryptor();
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..090465fb13
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,69 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngCbcAuthenticatedEncryptorDescriptorTests
+ {
+ [Fact]
+ public void ExportToXml_WithProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngCbcAuthenticatedEncryptorDescriptor(new CngCbcAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048,
+ EncryptionAlgorithmProvider = "enc-alg-prov",
+ HashAlgorithm = "hash-alg",
+ HashAlgorithmProvider = "hash-alg-prov"
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(CngCbcAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+ <descriptor>
+ <encryption algorithm='enc-alg' keyLength='2048' provider='enc-alg-prov' />
+ <hash algorithm='hash-alg' provider='hash-alg-prov' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ [Fact]
+ public void ExportToXml_WithoutProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngCbcAuthenticatedEncryptorDescriptor(new CngCbcAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048,
+ HashAlgorithm = "hash-alg"
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(CngCbcAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+ <descriptor>
+ <encryption algorithm='enc-alg' keyLength='2048' />
+ <hash algorithm='hash-alg' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs
new file mode 100644
index 0000000000..e70460cf40
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs
@@ -0,0 +1,40 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngGcmAuthenticatedEncryptorConfigurationTests
+ {
+ [Fact]
+ public void CreateNewDescriptor_CreatesUniqueCorrectlySizedMasterKey()
+ {
+ // Arrange
+ var configuration = new CngGcmAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var masterKey1 = ((CngGcmAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor()).MasterKey;
+ var masterKey2 = ((CngGcmAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor()).MasterKey;
+
+ // Assert
+ SecretAssert.NotEqual(masterKey1, masterKey2);
+ SecretAssert.LengthIs(512 /* bits */, masterKey1);
+ SecretAssert.LengthIs(512 /* bits */, masterKey2);
+ }
+
+ [Fact]
+ public void CreateNewDescriptor_PropagatesOptions()
+ {
+ // Arrange
+ var configuration = new CngGcmAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var descriptor = (CngGcmAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor();
+
+ // Assert
+ Assert.Equal(configuration, descriptor.Configuration);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..05845dfde0
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,63 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngGcmAuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void ImportFromXml_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var descriptor = new CngGcmAuthenticatedEncryptorDescriptor(
+ new CngGcmAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = Constants.BCRYPT_AES_ALGORITHM,
+ EncryptionAlgorithmKeySize = 192,
+ EncryptionAlgorithmProvider = null
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret());
+ var control = CreateEncryptorInstanceFromDescriptor(descriptor);
+
+ const string xml = @"
+ <descriptor version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <encryption algorithm='AES' keyLength='192' />
+ <masterKey enc:requiresEncryption='true'>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</masterKey>
+ </descriptor>";
+ var deserializedDescriptor = new CngGcmAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml));
+ var test = CreateEncryptorInstanceFromDescriptor(deserializedDescriptor as CngGcmAuthenticatedEncryptorDescriptor);
+
+ // Act & assert
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ private static IAuthenticatedEncryptor CreateEncryptorInstanceFromDescriptor(CngGcmAuthenticatedEncryptorDescriptor descriptor)
+ {
+ var encryptorFactory = new CngGcmAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ var key = new Key(
+ keyId: Guid.NewGuid(),
+ creationDate: DateTimeOffset.Now,
+ activationDate: DateTimeOffset.Now + TimeSpan.FromHours(1),
+ expirationDate: DateTimeOffset.Now + TimeSpan.FromDays(30),
+ descriptor: descriptor,
+ encryptorFactories: new[] { encryptorFactory });
+
+
+ return key.CreateEncryptor();
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..933f7e7d85
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,64 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngGcmAuthenticatedEncryptorDescriptorTests
+ {
+ [Fact]
+ public void ExportToXml_WithProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngGcmAuthenticatedEncryptorDescriptor(new CngGcmAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048,
+ EncryptionAlgorithmProvider = "enc-alg-prov"
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+ <descriptor>
+ <encryption algorithm='enc-alg' keyLength='2048' provider='enc-alg-prov' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ [Fact]
+ public void ExportToXml_WithoutProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngGcmAuthenticatedEncryptorDescriptor(new CngGcmAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+ <descriptor>
+ <encryption algorithm='enc-alg' keyLength='2048' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs
new file mode 100644
index 0000000000..6dbc4b7fea
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs
@@ -0,0 +1,40 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class ManagedAuthenticatedEncryptorConfigurationTests
+ {
+ [Fact]
+ public void CreateNewDescriptor_CreatesUniqueCorrectlySizedMasterKey()
+ {
+ // Arrange
+ var configuration = new ManagedAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var masterKey1 = ((ManagedAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor()).MasterKey;
+ var masterKey2 = ((ManagedAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor()).MasterKey;
+
+ // Assert
+ SecretAssert.NotEqual(masterKey1, masterKey2);
+ SecretAssert.LengthIs(512 /* bits */, masterKey1);
+ SecretAssert.LengthIs(512 /* bits */, masterKey2);
+ }
+
+ [Fact]
+ public void CreateNewDescriptor_PropagatesOptions()
+ {
+ // Arrange
+ var configuration = new ManagedAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var descriptor = (ManagedAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor();
+
+ // Assert
+ Assert.Equal(configuration, descriptor.Configuration);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..69cc556e6b
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,101 @@
+// 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.Security.Cryptography;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class ManagedAuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [Theory]
+ [InlineData(typeof(Aes), typeof(HMACSHA1))]
+ [InlineData(typeof(Aes), typeof(HMACSHA256))]
+ [InlineData(typeof(Aes), typeof(HMACSHA384))]
+ [InlineData(typeof(Aes), typeof(HMACSHA512))]
+ public void ImportFromXml_BuiltInTypes_CreatesAppropriateDescriptor(Type encryptionAlgorithmType, Type validationAlgorithmType)
+ {
+ // Arrange
+ var descriptor = new ManagedAuthenticatedEncryptorDescriptor(
+ new ManagedAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithmType = encryptionAlgorithmType,
+ EncryptionAlgorithmKeySize = 192,
+ ValidationAlgorithmType = validationAlgorithmType
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret());
+ var control = CreateEncryptorInstanceFromDescriptor(descriptor);
+
+ string xml = string.Format(@"
+ <descriptor>
+ <encryption algorithm='{0}' keyLength='192' />
+ <validation algorithm='{1}' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>",
+ encryptionAlgorithmType.Name, validationAlgorithmType.Name);
+ var deserializedDescriptor = new ManagedAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml));
+ var test = CreateEncryptorInstanceFromDescriptor(deserializedDescriptor as ManagedAuthenticatedEncryptorDescriptor);
+
+ // Act & assert
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ [Fact]
+ public void ImportFromXml_CustomType_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var descriptor = new ManagedAuthenticatedEncryptorDescriptor(
+ new ManagedAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithmType = typeof(Aes),
+ EncryptionAlgorithmKeySize = 192,
+ ValidationAlgorithmType = typeof(HMACSHA384)
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret());
+ var control = CreateEncryptorInstanceFromDescriptor(descriptor);
+
+ string xml = string.Format(@"
+ <descriptor>
+ <encryption algorithm='{0}' keyLength='192' />
+ <validation algorithm='{1}' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>",
+ typeof(Aes).AssemblyQualifiedName, typeof(HMACSHA384).AssemblyQualifiedName);
+ var deserializedDescriptor = new ManagedAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml));
+ var test = CreateEncryptorInstanceFromDescriptor(deserializedDescriptor as ManagedAuthenticatedEncryptorDescriptor);
+
+ // Act & assert
+ byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
+ byte[] aad = new byte[] { 2, 4, 6, 8, 0 };
+ byte[] ciphertext = control.Encrypt(new ArraySegment<byte>(plaintext), new ArraySegment<byte>(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment<byte>(ciphertext), new ArraySegment<byte>(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ private static IAuthenticatedEncryptor CreateEncryptorInstanceFromDescriptor(ManagedAuthenticatedEncryptorDescriptor descriptor)
+ {
+ var encryptorFactory = new ManagedAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ var key = new Key(
+ Guid.NewGuid(),
+ DateTimeOffset.Now,
+ DateTimeOffset.Now + TimeSpan.FromHours(1),
+ DateTimeOffset.Now + TimeSpan.FromDays(30),
+ descriptor,
+ new[] { encryptorFactory });
+
+ return key.CreateEncryptor();
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..4e4f453448
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,115 @@
+// 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.Security.Cryptography;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class ManagedAuthenticatedEncryptorDescriptorTests
+ {
+ [Fact]
+ public void ExportToXml_CustomTypes_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new ManagedAuthenticatedEncryptorDescriptor(new ManagedAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithmType = typeof(MySymmetricAlgorithm),
+ EncryptionAlgorithmKeySize = 2048,
+ ValidationAlgorithmType = typeof(MyKeyedHashAlgorithm)
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ string expectedXml = string.Format(@"
+ <descriptor>
+ <encryption algorithm='{0}' keyLength='2048' />
+ <validation algorithm='{1}' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>",
+ typeof(MySymmetricAlgorithm).AssemblyQualifiedName, typeof(MyKeyedHashAlgorithm).AssemblyQualifiedName);
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ [Theory]
+ [InlineData(typeof(Aes), typeof(HMACSHA1))]
+ [InlineData(typeof(Aes), typeof(HMACSHA256))]
+ [InlineData(typeof(Aes), typeof(HMACSHA384))]
+ [InlineData(typeof(Aes), typeof(HMACSHA512))]
+ public void ExportToXml_BuiltInTypes_ProducesCorrectPayload(Type encryptionAlgorithmType, Type validationAlgorithmType)
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new ManagedAuthenticatedEncryptorDescriptor(new ManagedAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithmType = encryptionAlgorithmType,
+ EncryptionAlgorithmKeySize = 2048,
+ ValidationAlgorithmType = validationAlgorithmType
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ string expectedXml = string.Format(@"
+ <descriptor>
+ <encryption algorithm='{0}' keyLength='2048' />
+ <validation algorithm='{1}' />
+ <masterKey enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <value>k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==</value>
+ </masterKey>
+ </descriptor>",
+ encryptionAlgorithmType.Name, validationAlgorithmType.Name);
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ private sealed class MySymmetricAlgorithm : SymmetricAlgorithm
+ {
+ public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] rgbIV)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] rgbIV)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void GenerateIV()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void GenerateKey()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private sealed class MyKeyedHashAlgorithm : KeyedHashAlgorithm
+ {
+ public override void Initialize()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void HashCore(byte[] array, int ibStart, int cbSize)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override byte[] HashFinal()
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactoryTest.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactoryTest.cs
new file mode 100644
index 0000000000..ef5eae5d19
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/AuthenticatedEncryption/ManagedAuthenticatedEncryptorFactoryTest.cs
@@ -0,0 +1,50 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.DataProtection.Managed;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption
+{
+ public class ManagedAuthenticatedEncryptorFactoryTest
+ {
+ [Fact]
+ public void CreateEncrptorInstance_UnknownDescriptorType_ReturnsNull()
+ {
+ // Arrange
+ var key = new Mock<IKey>();
+ key.Setup(k => k.Descriptor).Returns(new Mock<IAuthenticatedEncryptorDescriptor>().Object);
+
+ var factory = new ManagedAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Act
+ var encryptor = factory.CreateEncryptorInstance(key.Object);
+
+ // Assert
+ Assert.Null(encryptor);
+ }
+
+ [Fact]
+ public void CreateEncrptorInstance_ExpectedDescriptorType_ReturnsEncryptor()
+ {
+ // Arrange
+ var descriptor = new ManagedAuthenticatedEncryptorConfiguration().CreateNewDescriptor();
+ var key = new Mock<IKey>();
+ key.Setup(k => k.Descriptor).Returns(descriptor);
+
+ var factory = new ManagedAuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // Act
+ var encryptor = factory.CreateEncryptorInstance(key.Object);
+
+ // Assert
+ Assert.NotNull(encryptor);
+ Assert.IsType<ManagedAuthenticatedEncryptor>(encryptor);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
new file mode 100644
index 0000000000..97e7d7a96d
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
@@ -0,0 +1,120 @@
+// 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.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ public class CbcAuthenticatedEncryptorTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_Decrypt_RoundTrips()
+ {
+ // Arrange
+ Secret kdk = new Secret(new byte[512 / 8]);
+ CbcAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk,
+ symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC,
+ symmetricAlgorithmKeySizeInBytes: 256 / 8,
+ hmacAlgorithmHandle: CachedAlgorithmHandles.HMAC_SHA256);
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
+ ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
+
+ // Act
+ byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
+ byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
+
+ // Assert
+ Assert.Equal(plaintext, decipheredtext);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_Decrypt_Tampering_Fails()
+ {
+ // Arrange
+ Secret kdk = new Secret(new byte[512 / 8]);
+ CbcAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk,
+ symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC,
+ symmetricAlgorithmKeySizeInBytes: 256 / 8,
+ hmacAlgorithmHandle: CachedAlgorithmHandles.HMAC_SHA256);
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
+ ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
+ byte[] validCiphertext = encryptor.Encrypt(plaintext, aad);
+
+ // Act & assert - 1
+ // Ciphertext is too short to be a valid payload
+ byte[] invalidCiphertext_tooShort = new byte[10];
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_tooShort), aad);
+ });
+
+ // Act & assert - 2
+ // Ciphertext has been manipulated
+ byte[] invalidCiphertext_manipulated = (byte[])validCiphertext.Clone();
+ invalidCiphertext_manipulated[0] ^= 0x01;
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_manipulated), aad);
+ });
+
+ // Act & assert - 3
+ // Ciphertext is too long
+ byte[] invalidCiphertext_tooLong = validCiphertext.Concat(new byte[] { 0 }).ToArray();
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_tooLong), aad);
+ });
+
+ // Act & assert - 4
+ // AAD is incorrect
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(validCiphertext), new ArraySegment<byte>(Encoding.UTF8.GetBytes("different aad")));
+ });
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_KnownKey()
+ {
+ // Arrange
+ Secret kdk = new Secret(Encoding.UTF8.GetBytes("master key"));
+ CbcAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk,
+ symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC,
+ symmetricAlgorithmKeySizeInBytes: 256 / 8,
+ hmacAlgorithmHandle: CachedAlgorithmHandles.HMAC_SHA256,
+ genRandom: new SequentialGenRandom());
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, 2, 3);
+ ArraySegment<byte> aad = new ArraySegment<byte>(new byte[] { 7, 6, 5, 4, 3, 2, 1, 0 }, 1, 4);
+
+ // Act
+ byte[] retVal = encryptor.Encrypt(
+ plaintext: plaintext,
+ additionalAuthenticatedData: aad,
+ preBufferSize: 3,
+ postBufferSize: 4);
+
+ // Assert
+
+ // retVal := 00 00 00 (preBuffer)
+ // | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (keyModifier)
+ // | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F (IV)
+ // | B7 EA 3E 32 58 93 A3 06 03 89 C6 66 03 63 08 4B (encryptedData)
+ // | 9D 8A 85 C7 0F BD 98 D8 7F 72 E7 72 3E B5 A6 26 (HMAC)
+ // | 6C 38 77 F7 66 19 A2 C9 2C BB AD DA E7 62 00 00
+ // | 00 00 00 00 (postBuffer)
+
+ string retValAsString = Convert.ToBase64String(retVal);
+ Assert.Equal("AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh+36j4yWJOjBgOJxmYDYwhLnYqFxw+9mNh/cudyPrWmJmw4d/dmGaLJLLut2udiAAAAAAAA", retValAsString);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs
new file mode 100644
index 0000000000..faedbf44e9
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs
@@ -0,0 +1,110 @@
+// 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 Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng.Internal
+{
+ public unsafe class CngAuthenticatedEncryptorBaseTests
+ {
+ [Fact]
+ public void Decrypt_ForwardsArraySegment()
+ {
+ // Arrange
+ var ciphertext = new ArraySegment<byte>(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, 3, 2);
+ var aad = new ArraySegment<byte>(new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }, 1, 4);
+
+ var encryptorMock = new Mock<MockableEncryptor>();
+ encryptorMock
+ .Setup(o => o.DecryptHook(It.IsAny<IntPtr>(), 2, It.IsAny<IntPtr>(), 4))
+ .Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) =>
+ {
+ // ensure that pointers started at the right place
+ Assert.Equal((byte)0x03, *(byte*)pbCiphertext);
+ Assert.Equal((byte)0x11, *(byte*)pbAdditionalAuthenticatedData);
+ return new byte[] { 0x20, 0x21, 0x22 };
+ });
+
+ // Act
+ var retVal = encryptorMock.Object.Decrypt(ciphertext, aad);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal);
+ }
+
+ [Fact]
+ public void Decrypt_HandlesEmptyAADPointerFixup()
+ {
+ // Arrange
+ var ciphertext = new ArraySegment<byte>(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, 3, 2);
+ var aad = new ArraySegment<byte>(new byte[0]);
+
+ var encryptorMock = new Mock<MockableEncryptor>();
+ encryptorMock
+ .Setup(o => o.DecryptHook(It.IsAny<IntPtr>(), 2, It.IsAny<IntPtr>(), 0))
+ .Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) =>
+ {
+ // ensure that pointers started at the right place
+ Assert.Equal((byte)0x03, *(byte*)pbCiphertext);
+ Assert.NotEqual(IntPtr.Zero, pbAdditionalAuthenticatedData); // CNG will complain if this pointer is zero
+ return new byte[] { 0x20, 0x21, 0x22 };
+ });
+
+ // Act
+ var retVal = encryptorMock.Object.Decrypt(ciphertext, aad);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal);
+ }
+
+ [Fact]
+ public void Decrypt_HandlesEmptyCiphertextPointerFixup()
+ {
+ // Arrange
+ var ciphertext = new ArraySegment<byte>(new byte[0]);
+ var aad = new ArraySegment<byte>(new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }, 1, 4);
+
+ var encryptorMock = new Mock<MockableEncryptor>();
+ encryptorMock
+ .Setup(o => o.DecryptHook(It.IsAny<IntPtr>(), 0, It.IsAny<IntPtr>(), 4))
+ .Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) =>
+ {
+ // ensure that pointers started at the right place
+ Assert.NotEqual(IntPtr.Zero, pbCiphertext); // CNG will complain if this pointer is zero
+ Assert.Equal((byte)0x11, *(byte*)pbAdditionalAuthenticatedData);
+ return new byte[] { 0x20, 0x21, 0x22 };
+ });
+
+ // Act
+ var retVal = encryptorMock.Object.Decrypt(ciphertext, aad);
+
+ // Assert
+ Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal);
+ }
+
+ public abstract class MockableEncryptor : CngAuthenticatedEncryptorBase
+ {
+ public override void Dispose()
+ {
+ }
+
+ public abstract byte[] DecryptHook(IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData);
+
+ protected override sealed unsafe byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
+ {
+ return DecryptHook((IntPtr)pbCiphertext, cbCiphertext, (IntPtr)pbAdditionalAuthenticatedData, cbAdditionalAuthenticatedData);
+ }
+
+ public abstract byte[] EncryptHook(IntPtr pbPlaintext, uint cbPlaintext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer);
+
+ protected override sealed unsafe byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer)
+ {
+ return EncryptHook((IntPtr)pbPlaintext, cbPlaintext, (IntPtr)pbAdditionalAuthenticatedData, cbAdditionalAuthenticatedData, cbPreBuffer, cbPostBuffer);
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
new file mode 100644
index 0000000000..b01058e5b4
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
@@ -0,0 +1,109 @@
+// 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.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Cng
+{
+ public class GcmAuthenticatedEncryptorTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_Decrypt_RoundTrips()
+ {
+ // Arrange
+ Secret kdk = new Secret(new byte[512 / 8]);
+ GcmAuthenticatedEncryptor encryptor = new GcmAuthenticatedEncryptor(kdk, CachedAlgorithmHandles.AES_GCM, symmetricAlgorithmKeySizeInBytes: 256 / 8);
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
+ ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
+
+ // Act
+ byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
+ byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
+
+ // Assert
+ Assert.Equal(plaintext, decipheredtext);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_Decrypt_Tampering_Fails()
+ {
+ // Arrange
+ Secret kdk = new Secret(new byte[512 / 8]);
+ GcmAuthenticatedEncryptor encryptor = new GcmAuthenticatedEncryptor(kdk, CachedAlgorithmHandles.AES_GCM, symmetricAlgorithmKeySizeInBytes: 256 / 8);
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
+ ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
+ byte[] validCiphertext = encryptor.Encrypt(plaintext, aad);
+
+ // Act & assert - 1
+ // Ciphertext is too short to be a valid payload
+ byte[] invalidCiphertext_tooShort = new byte[10];
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_tooShort), aad);
+ });
+
+ // Act & assert - 2
+ // Ciphertext has been manipulated
+ byte[] invalidCiphertext_manipulated = (byte[])validCiphertext.Clone();
+ invalidCiphertext_manipulated[0] ^= 0x01;
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_manipulated), aad);
+ });
+
+ // Act & assert - 3
+ // Ciphertext is too long
+ byte[] invalidCiphertext_tooLong = validCiphertext.Concat(new byte[] { 0 }).ToArray();
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_tooLong), aad);
+ });
+
+ // Act & assert - 4
+ // AAD is incorrect
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(validCiphertext), new ArraySegment<byte>(Encoding.UTF8.GetBytes("different aad")));
+ });
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_KnownKey()
+ {
+ // Arrange
+ Secret kdk = new Secret(Encoding.UTF8.GetBytes("master key"));
+ GcmAuthenticatedEncryptor encryptor = new GcmAuthenticatedEncryptor(kdk, CachedAlgorithmHandles.AES_GCM, symmetricAlgorithmKeySizeInBytes: 128 / 8, genRandom: new SequentialGenRandom());
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, 2, 3);
+ ArraySegment<byte> aad = new ArraySegment<byte>(new byte[] { 7, 6, 5, 4, 3, 2, 1, 0 }, 1, 4);
+
+ // Act
+ byte[] retVal = encryptor.Encrypt(
+ plaintext: plaintext,
+ additionalAuthenticatedData: aad,
+ preBufferSize: 3,
+ postBufferSize: 4);
+
+ // Assert
+
+ // retVal := 00 00 00 (preBuffer)
+ // | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (keyModifier)
+ // | 10 11 12 13 14 15 16 17 18 19 1A 1B (nonce)
+ // | 43 B6 91 (encryptedData)
+ // | 8D 0D 66 D9 A1 D9 44 2D 5D 8E 41 DA 39 60 9C E8 (authTag)
+ // | 00 00 00 00 (postBuffer)
+
+ string retValAsString = Convert.ToBase64String(retVal);
+ Assert.Equal("AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaG0O2kY0NZtmh2UQtXY5B2jlgnOgAAAAA", retValAsString);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DataProtectionUtilityExtensionsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DataProtectionUtilityExtensionsTests.cs
new file mode 100644
index 0000000000..5af33b1b25
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DataProtectionUtilityExtensionsTests.cs
@@ -0,0 +1,87 @@
+// 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 Microsoft.AspNetCore.DataProtection.Infrastructure;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class DataProtectionUtilityExtensionsTests
+ {
+ [Theory]
+ [InlineData("app-path", "app-path")]
+ [InlineData("app-path ", "app-path")] // normalized trim
+ [InlineData(" ", null)] // normalized whitespace -> null
+ [InlineData(null, null)] // nothing provided at all
+ public void GetApplicationUniqueIdentifierFromHosting(string contentRootPath, string expected)
+ {
+ // Arrange
+ var mockEnvironment = new Mock<IHostingEnvironment>();
+ mockEnvironment.Setup(o => o.ContentRootPath).Returns(contentRootPath);
+
+ var services = new ServiceCollection()
+ .AddSingleton(mockEnvironment.Object)
+ .AddDataProtection()
+ .Services
+ .BuildServiceProvider();
+
+ // Act
+ var actual = services.GetApplicationUniqueIdentifier();
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(" discriminator ", "discriminator")]
+ [InlineData(" discriminator", "discriminator")] // normalized trim
+ [InlineData(" ", null)] // normalized whitespace -> null
+ [InlineData(null, null)] // nothing provided at all
+ public void GetApplicationIdentifierFromApplicationDiscriminator(string discriminator, string expected)
+ {
+ // Arrange
+ var mockAppDiscriminator = new Mock<IApplicationDiscriminator>();
+ mockAppDiscriminator.Setup(o => o.Discriminator).Returns(discriminator);
+
+ var mockEnvironment = new Mock<IHostingEnvironment>();
+ mockEnvironment.SetupGet(o => o.ContentRootPath).Throws(new InvalidOperationException("Hosting environment should not be checked"));
+
+ var services = new ServiceCollection()
+ .AddSingleton(mockEnvironment.Object)
+ .AddSingleton(mockAppDiscriminator.Object)
+ .AddDataProtection()
+ .Services
+ .BuildServiceProvider();
+
+ // Act
+ var actual = services.GetApplicationUniqueIdentifier();
+
+ // Assert
+ Assert.Equal(expected, actual);
+ mockAppDiscriminator.VerifyAll();
+ }
+
+ [Fact]
+ public void GetApplicationUniqueIdentifier_NoServiceProvider_ReturnsNull()
+ {
+ Assert.Null(((IServiceProvider)null).GetApplicationUniqueIdentifier());
+ }
+
+ [Fact]
+ public void GetApplicationUniqueIdentifier_NoHostingEnvironment_ReturnsNull()
+ {
+ // arrange
+ var services = new ServiceCollection()
+ .AddDataProtection()
+ .Services
+ .BuildServiceProvider();
+
+ // act & assert
+ Assert.Null(services.GetApplicationUniqueIdentifier());
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DockerUtilsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DockerUtilsTests.cs
new file mode 100644
index 0000000000..9ede10426b
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/DockerUtilsTests.cs
@@ -0,0 +1,56 @@
+// 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;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Test
+{
+ public class DockerUtilsTests
+ {
+ // example of content from /proc/self/mounts
+ private static readonly string[] fstab = new []
+ {
+ "none / aufs rw,relatime,si=f9bfcf896de3f6c2,dio,dirperm1 0 0",
+ "# comments",
+ "",
+ "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0",
+ "tmpfs /dev tmpfs rw,nosuid,mode=755 0 0",
+ "devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0",
+ "/dev/vda2 /etc/resolv.conf ext4 rw,relatime,data=ordered 0 0",
+ "/dev/vda2 /etc/hostname ext4 rw,relatime,data=ordered 0 0",
+ "/dev/vda2 /etc/hosts ext4 rw,relatime,data=ordered 0 0",
+ "shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k 0 0",
+ // the mounted directory
+ "osxfs /app fuse.osxfs rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other,max_read=1048576 0 0",
+ };
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData("/")]
+ [InlineData("/home")]
+ [InlineData("/home/")]
+ [InlineData("/home/root")]
+ [InlineData("./dir")]
+ [InlineData("../dir")]
+ public void DeterminesFolderIsNotMounted(string directory)
+ {
+ Assert.False(DockerUtils.IsDirectoryMounted(new DirectoryInfo(directory), fstab));
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData("/app")]
+ [InlineData("/app/")]
+ [InlineData("/app/subdir")]
+ [InlineData("/app/subdir/")]
+ [InlineData("/app/subdir/two")]
+ [InlineData("/app/subdir/two/")]
+ public void DeterminesFolderIsMounted(string directory)
+ {
+ Assert.True(DockerUtils.IsDirectoryMounted(new DirectoryInfo(directory), fstab));
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/EphemeralDataProtectionProviderTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
new file mode 100644
index 0000000000..d42fe2113c
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
@@ -0,0 +1,65 @@
+// 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.Security.Cryptography;
+using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class EphemeralDataProtectionProviderTests
+ {
+ [Fact]
+ public void DifferentProvider_SamePurpose_DoesNotRoundTripData()
+ {
+ // Arrange
+ var dataProtector1 = new EphemeralDataProtectionProvider().CreateProtector("purpose");
+ var dataProtector2 = new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("purpose");
+ byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
+
+ // Act & assert
+ // Each instance of the EphemeralDataProtectionProvider has its own unique KDK, so payloads can't be shared.
+ byte[] protectedBytes = dataProtector1.Protect(bytes);
+ Assert.ThrowsAny<CryptographicException>(() =>
+ {
+ byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
+ });
+ }
+
+ [Fact]
+ public void SingleProvider_DifferentPurpose_DoesNotRoundTripData()
+ {
+ // Arrange
+ var dataProtectionProvider = new EphemeralDataProtectionProvider(NullLoggerFactory.Instance);
+ var dataProtector1 = dataProtectionProvider.CreateProtector("purpose");
+ var dataProtector2 = dataProtectionProvider.CreateProtector("different purpose");
+ byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
+
+ // Act & assert
+ byte[] protectedBytes = dataProtector1.Protect(bytes);
+ Assert.ThrowsAny<CryptographicException>(() =>
+ {
+ byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
+ });
+ }
+
+ [Fact]
+ public void SingleProvider_SamePurpose_RoundTripsData()
+ {
+ // Arrange
+ var dataProtectionProvider = new EphemeralDataProtectionProvider(NullLoggerFactory.Instance);
+ var dataProtector1 = dataProtectionProvider.CreateProtector("purpose");
+ var dataProtector2 = dataProtectionProvider.CreateProtector("purpose"); // should be equivalent to the previous instance
+ byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
+
+ // Act
+ byte[] protectedBytes = dataProtector1.Protect(bytes);
+ byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
+
+ // Assert
+ Assert.Equal(bytes, unprotectedBytes);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/HostingTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/HostingTests.cs
new file mode 100644
index 0000000000..cd43effe37
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/HostingTests.cs
@@ -0,0 +1,104 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Test
+{
+ public class HostingTests
+ {
+ [Fact]
+ public async Task LoadsKeyRingBeforeServerStarts()
+ {
+ var tcs = new TaskCompletionSource<object>();
+ var mockKeyRing = new Mock<IKeyRingProvider>();
+ mockKeyRing.Setup(m => m.GetCurrentKeyRing())
+ .Returns(Mock.Of<IKeyRing>())
+ .Callback(() => tcs.TrySetResult(null));
+
+ var builder = new WebHostBuilder()
+ .UseStartup<TestStartup>()
+ .ConfigureServices(s =>
+ s.AddDataProtection()
+ .Services
+ .Replace(ServiceDescriptor.Singleton(mockKeyRing.Object))
+ .AddSingleton<IServer>(
+ new FakeServer(onStart: () => tcs.TrySetException(new InvalidOperationException("Server was started before key ring was initialized")))));
+
+ using (var host = builder.Build())
+ {
+ await host.StartAsync();
+ }
+
+ await tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
+ mockKeyRing.VerifyAll();
+ }
+
+ [Fact]
+ public async Task StartupContinuesOnFailureToLoadKey()
+ {
+ var mockKeyRing = new Mock<IKeyRingProvider>();
+ mockKeyRing.Setup(m => m.GetCurrentKeyRing())
+ .Throws(new NotSupportedException("This mock doesn't actually work, but shouldn't kill the server"))
+ .Verifiable();
+
+ var builder = new WebHostBuilder()
+ .UseStartup<TestStartup>()
+ .ConfigureServices(s =>
+ s.AddDataProtection()
+ .Services
+ .Replace(ServiceDescriptor.Singleton(mockKeyRing.Object))
+ .AddSingleton(Mock.Of<IServer>()));
+
+ using (var host = builder.Build())
+ {
+ await host.StartAsync();
+ }
+
+ mockKeyRing.VerifyAll();
+ }
+
+ private class TestStartup
+ {
+ public void Configure(IApplicationBuilder app)
+ {
+ }
+ }
+
+ public class FakeServer : IServer
+ {
+ private readonly Action _onStart;
+
+ public FakeServer(Action onStart)
+ {
+ _onStart = onStart;
+ }
+
+ public IFeatureCollection Features => new FeatureCollection();
+
+ public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
+ {
+ _onStart();
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Internal/KeyManagementOptionsSetupTest.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Internal/KeyManagementOptionsSetupTest.cs
new file mode 100644
index 0000000000..ae49c7edab
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Internal/KeyManagementOptionsSetupTest.cs
@@ -0,0 +1,154 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Win32;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Internal
+{
+ public class KeyManagementOptionsSetupTest
+ {
+ [Fact]
+ public void Configure_SetsExpectedValues()
+ {
+ // Arrange
+ var setup = new KeyManagementOptionsSetup(NullLoggerFactory.Instance);
+ var options = new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = null
+ };
+
+ // Act
+ setup.Configure(options);
+
+ // Assert
+ Assert.Empty(options.KeyEscrowSinks);
+ Assert.NotNull(options.AuthenticatedEncryptorConfiguration);
+ Assert.IsType<AuthenticatedEncryptorConfiguration>(options.AuthenticatedEncryptorConfiguration);
+ Assert.Collection(
+ options.AuthenticatedEncryptorFactories,
+ f => Assert.IsType<CngGcmAuthenticatedEncryptorFactory>(f),
+ f => Assert.IsType<CngCbcAuthenticatedEncryptorFactory>(f),
+ f => Assert.IsType<ManagedAuthenticatedEncryptorFactory>(f),
+ f => Assert.IsType<AuthenticatedEncryptorFactory>(f));
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void Configure_WithRegistryPolicyResolver_SetsValuesFromResolver()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["KeyEscrowSinks"] = String.Join(" ;; ; ", new Type[] { typeof(MyKeyEscrowSink1), typeof(MyKeyEscrowSink2) }.Select(t => t.AssemblyQualifiedName)),
+ ["EncryptionType"] = "managed",
+ ["DefaultKeyLifetime"] = 1024 // days
+ };
+ var options = new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = null
+ };
+
+ // Act
+ RunTest(registryEntries, options);
+
+ // Assert
+ Assert.Collection(
+ options.KeyEscrowSinks,
+ k => Assert.IsType<MyKeyEscrowSink1>(k),
+ k => Assert.IsType<MyKeyEscrowSink2>(k));
+ Assert.Equal(TimeSpan.FromDays(1024), options.NewKeyLifetime);
+ Assert.NotNull(options.AuthenticatedEncryptorConfiguration);
+ Assert.IsType<ManagedAuthenticatedEncryptorConfiguration>(options.AuthenticatedEncryptorConfiguration);
+ Assert.Collection(
+ options.AuthenticatedEncryptorFactories,
+ f => Assert.IsType<CngGcmAuthenticatedEncryptorFactory>(f),
+ f => Assert.IsType<CngCbcAuthenticatedEncryptorFactory>(f),
+ f => Assert.IsType<ManagedAuthenticatedEncryptorFactory>(f),
+ f => Assert.IsType<AuthenticatedEncryptorFactory>(f));
+ }
+
+ private static void RunTest(Dictionary<string, object> regValues, KeyManagementOptions options)
+ {
+ WithUniqueTempRegKey(registryKey =>
+ {
+ foreach (var entry in regValues)
+ {
+ registryKey.SetValue(entry.Key, entry.Value);
+ }
+
+ var policyResolver = new RegistryPolicyResolver(
+ registryKey,
+ activator: SimpleActivator.DefaultWithoutServices);
+
+ var setup = new KeyManagementOptionsSetup(NullLoggerFactory.Instance, policyResolver);
+
+ setup.Configure(options);
+ });
+ }
+
+ /// <summary>
+ /// Runs a test and cleans up the registry key afterward.
+ /// </summary>
+ private static void WithUniqueTempRegKey(Action<RegistryKey> testCode)
+ {
+ string uniqueName = Guid.NewGuid().ToString();
+ var uniqueSubkey = LazyHkcuTempKey.Value.CreateSubKey(uniqueName);
+ try
+ {
+ testCode(uniqueSubkey);
+ }
+ finally
+ {
+ // clean up when test is done
+ LazyHkcuTempKey.Value.DeleteSubKeyTree(uniqueName, throwOnMissingSubKey: false);
+ }
+ }
+
+ private static readonly Lazy<RegistryKey> LazyHkcuTempKey = new Lazy<RegistryKey>(() =>
+ {
+ try
+ {
+ return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\ASP.NET\temp");
+ }
+ catch
+ {
+ // swallow all failures
+ return null;
+ }
+ });
+
+ private class ConditionalRunTestOnlyIfHkcuRegistryAvailable : Attribute, ITestCondition
+ {
+ public bool IsMet => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && LazyHkcuTempKey.Value != null);
+
+ public string SkipReason { get; } = "HKCU registry couldn't be opened.";
+ }
+
+ private class MyKeyEscrowSink1 : IKeyEscrowSink
+ {
+ public void Store(Guid keyId, XElement element)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyKeyEscrowSink2 : IKeyEscrowSink
+ {
+ public void Store(Guid keyId, XElement element)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs
new file mode 100644
index 0000000000..27eaa3bf31
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs
@@ -0,0 +1,61 @@
+// 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.Threading;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class CacheableKeyRingTests
+ {
+ [Fact]
+ public void IsValid_NullKeyRing_ReturnsFalse()
+ {
+ Assert.False(CacheableKeyRing.IsValid(null, DateTime.UtcNow));
+ }
+
+ [Fact]
+ public void IsValid_CancellationTokenTriggered_ReturnsFalse()
+ {
+ // Arrange
+ var keyRing = new Mock<IKeyRing>().Object;
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+ var cts = new CancellationTokenSource();
+ var cacheableKeyRing = new CacheableKeyRing(cts.Token, now.AddHours(1), keyRing);
+
+ // Act & assert
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now.UtcDateTime));
+ cts.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now.UtcDateTime));
+ }
+
+ [Fact]
+ public void IsValid_Expired_ReturnsFalse()
+ {
+ // Arrange
+ var keyRing = new Mock<IKeyRing>().Object;
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+ var cts = new CancellationTokenSource();
+ var cacheableKeyRing = new CacheableKeyRing(cts.Token, now.AddHours(1), keyRing);
+
+ // Act & assert
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now.UtcDateTime));
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now.AddHours(1).UtcDateTime));
+ }
+
+
+ [Fact]
+ public void KeyRing_Prop()
+ {
+ // Arrange
+ var keyRing = new Mock<IKeyRing>().Object;
+ var cacheableKeyRing = new CacheableKeyRing(CancellationToken.None, DateTimeOffset.Now, keyRing);
+
+ // Act & assert
+ Assert.Same(keyRing, cacheableKeyRing.KeyRing);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs
new file mode 100644
index 0000000000..46e9b5f993
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs
@@ -0,0 +1,277 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class DefaultKeyResolverTests
+ {
+ [Fact]
+ public void ResolveDefaultKeyPolicy_EmptyKeyRing_ReturnsNullDefaultKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy(DateTimeOffset.Now, new IKey[0]);
+
+ // Assert
+ Assert.Null(resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_ReturnsExistingKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-20 23:59:00Z", key1, key2);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_AllowsForClockSkew_KeysStraddleSkewLine_ReturnsExistingKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:59:00Z", key1, key2);
+
+ // Assert
+ Assert.Same(key2, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_AllowsForClockSkew_AllKeysInFuture_ReturnsExistingKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:59:00Z", key1);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_NoSuccessor_ReturnsExistingKey_SignalsGenerateNewKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:59:00Z", key1);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_NoLegitimateSuccessor_ReturnsExistingKey_SignalsGenerateNewKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z", isRevoked: true);
+ var key3 = CreateKey("2016-03-01 00:00:00Z", "2016-03-02 00:00:00Z"); // key expires too soon
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2016-02-29 23:50:00Z", key1, key2, key3);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_BecauseOfRevocation_ReturnsNull()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2015-03-02 00:00:00Z", "2016-03-01 00:00:00Z", isRevoked: true);
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-04-01 00:00:00Z", key1, key2);
+
+ // Assert
+ Assert.Null(resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_BecauseOfFailureToDecipher_ReturnsNull()
+ {
+ // Arrange
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2015-03-02 00:00:00Z", "2016-03-01 00:00:00Z", createEncryptorThrows: true);
+ var resolver = CreateDefaultKeyResolver();
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-04-01 00:00:00Z", key1, key2);
+
+ // Assert
+ Assert.Null(resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_FutureKeyIsValidAndWithinClockSkew_ReturnsFutureKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-02-28 23:55:00Z", key1);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_FutureKeyIsValidButNotWithinClockSkew_ReturnsNull()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-02-28 23:00:00Z", key1);
+
+ // Assert
+ Assert.Null(resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_IgnoresExpiredOrRevokedFutureKeys()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2014-03-01 00:00:00Z"); // expiration before activation should never occur
+ var key2 = CreateKey("2015-03-01 00:01:00Z", "2015-04-01 00:00:00Z", isRevoked: true);
+ var key3 = CreateKey("2015-03-01 00:02:00Z", "2015-04-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-02-28 23:59:00Z", key1, key2, key3);
+
+ // Assert
+ Assert.Same(key3, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow_IgnoresRevokedKeys()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-01 00:00:00Z");
+ var key2 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-02 00:00:00Z");
+ var key3 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-03 00:00:00Z", isRevoked: true);
+ var key4 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-04 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2000-01-05 00:00:00Z", key1, key2, key3, key4);
+
+ // Assert
+ Assert.Same(key2, resolution.FallbackKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow_IgnoresFailures()
+ {
+ // Arrange
+ var key1 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-01 00:00:00Z");
+ var key2 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-02 00:00:00Z");
+ var key3 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-03 00:00:00Z", createEncryptorThrows: true);
+ var key4 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-04 00:00:00Z");
+ var resolver = CreateDefaultKeyResolver();
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2000-01-05 00:00:00Z", key1, key2, key3, key4);
+
+ // Assert
+ Assert.Same(key2, resolution.FallbackKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropagationWindow_SelectsEarliestNonRevokedKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-03 00:00:00Z", isRevoked: true);
+ var key2 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-04 00:00:00Z");
+ var key3 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-05 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2000-01-05 00:00:00Z", key1, key2, key3);
+
+ // Assert
+ Assert.Same(key2, resolution.FallbackKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ private static IDefaultKeyResolver CreateDefaultKeyResolver()
+ {
+ var options = Options.Create(new KeyManagementOptions());
+ return new DefaultKeyResolver(options, NullLoggerFactory.Instance);
+ }
+
+ private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false, bool createEncryptorThrows = false)
+ {
+ var mockKey = new Mock<IKey>();
+ mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid());
+ mockKey.Setup(o => o.CreationDate).Returns((creationDate != null) ? DateTimeOffset.ParseExact(creationDate, "u", CultureInfo.InvariantCulture) : DateTimeOffset.MinValue);
+ mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture));
+ mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture));
+ mockKey.Setup(o => o.IsRevoked).Returns(isRevoked);
+ if (createEncryptorThrows)
+ {
+ mockKey.Setup(o => o.CreateEncryptor()).Throws(new Exception("This method fails."));
+ }
+ else
+ {
+ mockKey.Setup(o => o.CreateEncryptor()).Returns(Mock.Of<IAuthenticatedEncryptor>());
+ }
+
+ return mockKey.Object;
+ }
+ }
+
+ internal static class DefaultKeyResolverExtensions
+ {
+ public static DefaultKeyResolution ResolveDefaultKeyPolicy(this IDefaultKeyResolver resolver, string now, params IKey[] allKeys)
+ {
+ return resolver.ResolveDefaultKeyPolicy(DateTimeOffset.ParseExact(now, "u", CultureInfo.InvariantCulture), (IEnumerable<IKey>)allKeys);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DeferredKeyTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DeferredKeyTests.cs
new file mode 100644
index 0000000000..2a166564d0
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/DeferredKeyTests.cs
@@ -0,0 +1,84 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class DeferredKeyTests
+ {
+ [Fact]
+ public void Ctor_Properties()
+ {
+ // Arrange
+ var keyId = Guid.NewGuid();
+ var creationDate = DateTimeOffset.Now;
+ var activationDate = creationDate.AddDays(2);
+ var expirationDate = creationDate.AddDays(90);
+ var mockDescriptor = Mock.Of<IAuthenticatedEncryptorDescriptor>();
+ var mockInternalKeyManager = new Mock<IInternalXmlKeyManager>();
+ mockInternalKeyManager.Setup(o => o.DeserializeDescriptorFromKeyElement(It.IsAny<XElement>()))
+ .Returns<XElement>(element =>
+ {
+ XmlAssert.Equal(@"<node />", element);
+ return mockDescriptor;
+ });
+ var encryptorFactory = Mock.Of<IAuthenticatedEncryptorFactory>();
+
+ // Act
+ var key = new DeferredKey(keyId, creationDate, activationDate, expirationDate, mockInternalKeyManager.Object, XElement.Parse(@"<node />"), new[] { encryptorFactory });
+
+ // Assert
+ Assert.Equal(keyId, key.KeyId);
+ Assert.Equal(creationDate, key.CreationDate);
+ Assert.Equal(activationDate, key.ActivationDate);
+ Assert.Equal(expirationDate, key.ExpirationDate);
+ Assert.Same(mockDescriptor, key.Descriptor);
+ }
+
+ [Fact]
+ public void SetRevoked_Respected()
+ {
+ // Arrange
+ var now = DateTimeOffset.UtcNow;
+ var encryptorFactory = Mock.Of<IAuthenticatedEncryptorFactory>();
+ var key = new DeferredKey(Guid.Empty, now, now, now, new Mock<IInternalXmlKeyManager>().Object, XElement.Parse(@"<node />"), new[] { encryptorFactory });
+
+ // Act & assert
+ Assert.False(key.IsRevoked);
+ key.SetRevoked();
+ Assert.True(key.IsRevoked);
+ }
+
+ [Fact]
+ public void Get_Descriptor_CachesFailures()
+ {
+ // Arrange
+ int numTimesCalled = 0;
+ var mockKeyManager = new Mock<IInternalXmlKeyManager>();
+ mockKeyManager.Setup(o => o.DeserializeDescriptorFromKeyElement(It.IsAny<XElement>()))
+ .Returns<XElement>(element =>
+ {
+ numTimesCalled++;
+ throw new Exception("How exceptional.");
+ });
+
+ var now = DateTimeOffset.UtcNow;
+ var encryptorFactory = Mock.Of<IAuthenticatedEncryptorFactory>();
+ var key = new DeferredKey(Guid.Empty, now, now, now, mockKeyManager.Object, XElement.Parse(@"<node />"), new[] { encryptorFactory });
+
+ // Act & assert
+ ExceptionAssert.Throws<Exception>(() => key.Descriptor, "How exceptional.");
+ ExceptionAssert.Throws<Exception>(() => key.Descriptor, "How exceptional.");
+ Assert.Equal(1, numTimesCalled);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs
new file mode 100644
index 0000000000..8db64657db
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs
@@ -0,0 +1,90 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Xml.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class KeyEscrowServiceProviderExtensionsTests
+ {
+ [Fact]
+ public void GetKeyEscrowSink_NullServiceProvider_ReturnsNull()
+ {
+ Assert.Null(((IServiceProvider)null).GetKeyEscrowSink());
+ }
+
+ [Fact]
+ public void GetKeyEscrowSink_EmptyServiceProvider_ReturnsNull()
+ {
+ // Arrange
+ var services = new ServiceCollection().BuildServiceProvider();
+
+ // Act & assert
+ Assert.Null(services.GetKeyEscrowSink());
+ }
+
+ [Fact]
+ public void GetKeyEscrowSink_SingleKeyEscrowRegistration_ReturnsAggregateOverSingleSink()
+ {
+ // Arrange
+ List<string> output = new List<string>();
+
+ var mockKeyEscrowSink = new Mock<IKeyEscrowSink>();
+ mockKeyEscrowSink.Setup(o => o.Store(It.IsAny<Guid>(), It.IsAny<XElement>()))
+ .Callback<Guid, XElement>((keyId, element) =>
+ {
+ output.Add(string.Format(CultureInfo.InvariantCulture, "{0:D}: {1}", keyId, element.Name.LocalName));
+ });
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton<IKeyEscrowSink>(mockKeyEscrowSink.Object);
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Act
+ var sink = services.GetKeyEscrowSink();
+ sink.Store(new Guid("39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b"), XElement.Parse("<theElement />"));
+
+ // Assert
+ Assert.Equal(new[] { "39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b: theElement" }, output);
+ }
+
+ [Fact]
+ public void GetKeyEscrowSink_MultipleKeyEscrowRegistration_ReturnsAggregate()
+ {
+ // Arrange
+ List<string> output = new List<string>();
+
+ var mockKeyEscrowSink1 = new Mock<IKeyEscrowSink>();
+ mockKeyEscrowSink1.Setup(o => o.Store(It.IsAny<Guid>(), It.IsAny<XElement>()))
+ .Callback<Guid, XElement>((keyId, element) =>
+ {
+ output.Add(string.Format(CultureInfo.InvariantCulture, "[sink1] {0:D}: {1}", keyId, element.Name.LocalName));
+ });
+
+ var mockKeyEscrowSink2 = new Mock<IKeyEscrowSink>();
+ mockKeyEscrowSink2.Setup(o => o.Store(It.IsAny<Guid>(), It.IsAny<XElement>()))
+ .Callback<Guid, XElement>((keyId, element) =>
+ {
+ output.Add(string.Format(CultureInfo.InvariantCulture, "[sink2] {0:D}: {1}", keyId, element.Name.LocalName));
+ });
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton<IKeyEscrowSink>(mockKeyEscrowSink1.Object);
+ serviceCollection.AddSingleton<IKeyEscrowSink>(mockKeyEscrowSink2.Object);
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Act
+ var sink = services.GetKeyEscrowSink();
+ sink.Store(new Guid("39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b"), XElement.Parse("<theElement />"));
+
+ // Assert
+ Assert.Equal(new[] { "[sink1] 39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b: theElement", "[sink2] 39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b: theElement" }, output);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs
new file mode 100644
index 0000000000..d28ea7ff84
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs
@@ -0,0 +1,500 @@
+// 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.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class KeyRingBasedDataProtectorTests
+ {
+ [Fact]
+ public void Protect_NullPlaintext_Throws()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock<IKeyRingProvider>().Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgumentNull(() => protector.Protect(plaintext: null), "plaintext");
+ }
+
+ [Fact]
+ public void Protect_EncryptsToDefaultProtector_MultiplePurposes()
+ {
+ // Arrange
+ Guid defaultKey = new Guid("ba73c9ce-d322-4e45-af90-341307e11c38");
+ byte[] expectedPlaintext = new byte[] { 0x03, 0x05, 0x07, 0x11, 0x13, 0x17, 0x19 };
+ byte[] expectedAad = BuildAadFromPurposeStrings(defaultKey, "purpose1", "purpose2", "yet another purpose");
+ byte[] expectedProtectedData = BuildProtectedDataFromCiphertext(defaultKey, new byte[] { 0x23, 0x29, 0x31, 0x37 });
+
+ var mockEncryptor = new Mock<IAuthenticatedEncryptor>();
+ mockEncryptor
+ .Setup(o => o.Encrypt(It.IsAny<ArraySegment<byte>>(), It.IsAny<ArraySegment<byte>>()))
+ .Returns<ArraySegment<byte>, ArraySegment<byte>>((actualPlaintext, actualAad) =>
+ {
+ Assert.Equal(expectedPlaintext, actualPlaintext);
+ Assert.Equal(expectedAad, actualAad);
+ return new byte[] { 0x23, 0x29, 0x31, 0x37 }; // ciphertext + tag
+ });
+
+ var mockKeyRing = new Mock<IKeyRing>(MockBehavior.Strict);
+ mockKeyRing.Setup(o => o.DefaultKeyId).Returns(defaultKey);
+ mockKeyRing.Setup(o => o.DefaultAuthenticatedEncryptor).Returns(mockEncryptor.Object);
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(mockKeyRing.Object);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: new[] { "purpose1", "purpose2" },
+ newPurpose: "yet another purpose");
+
+ // Act
+ byte[] retVal = protector.Protect(expectedPlaintext);
+
+ // Assert
+ Assert.Equal(expectedProtectedData, retVal);
+ }
+
+ [Fact]
+ public void Protect_EncryptsToDefaultProtector_SinglePurpose()
+ {
+ // Arrange
+ Guid defaultKey = new Guid("ba73c9ce-d322-4e45-af90-341307e11c38");
+ byte[] expectedPlaintext = new byte[] { 0x03, 0x05, 0x07, 0x11, 0x13, 0x17, 0x19 };
+ byte[] expectedAad = BuildAadFromPurposeStrings(defaultKey, "single purpose");
+ byte[] expectedProtectedData = BuildProtectedDataFromCiphertext(defaultKey, new byte[] { 0x23, 0x29, 0x31, 0x37 });
+
+ var mockEncryptor = new Mock<IAuthenticatedEncryptor>();
+ mockEncryptor
+ .Setup(o => o.Encrypt(It.IsAny<ArraySegment<byte>>(), It.IsAny<ArraySegment<byte>>()))
+ .Returns<ArraySegment<byte>, ArraySegment<byte>>((actualPlaintext, actualAad) =>
+ {
+ Assert.Equal(expectedPlaintext, actualPlaintext);
+ Assert.Equal(expectedAad, actualAad);
+ return new byte[] { 0x23, 0x29, 0x31, 0x37 }; // ciphertext + tag
+ });
+
+ var mockKeyRing = new Mock<IKeyRing>(MockBehavior.Strict);
+ mockKeyRing.Setup(o => o.DefaultKeyId).Returns(defaultKey);
+ mockKeyRing.Setup(o => o.DefaultAuthenticatedEncryptor).Returns(mockEncryptor.Object);
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(mockKeyRing.Object);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: new string[0],
+ newPurpose: "single purpose");
+
+ // Act
+ byte[] retVal = protector.Protect(expectedPlaintext);
+
+ // Assert
+ Assert.Equal(expectedProtectedData, retVal);
+ }
+
+ [Fact]
+ public void Protect_HomogenizesExceptionsToCryptographicException()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock<IKeyRingProvider>(MockBehavior.Strict).Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Protect(new byte[0]));
+ Assert.IsAssignableFrom<MockException>(ex.InnerException);
+ }
+
+ [Fact]
+ public void Unprotect_NullProtectedData_Throws()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock<IKeyRingProvider>().Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgumentNull(() => protector.Unprotect(protectedData: null), "protectedData");
+ }
+
+ [Fact]
+ public void Unprotect_PayloadTooShort_ThrowsBadMagicHeader()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock<IKeyRingProvider>().Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ byte[] badProtectedPayload = BuildProtectedDataFromCiphertext(Guid.NewGuid(), new byte[0]);
+ badProtectedPayload = badProtectedPayload.Take(badProtectedPayload.Length - 1).ToArray(); // chop off the last byte
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(badProtectedPayload));
+ Assert.Equal(Resources.ProtectionProvider_BadMagicHeader, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_PayloadHasBadMagicHeader_ThrowsBadMagicHeader()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock<IKeyRingProvider>().Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ byte[] badProtectedPayload = BuildProtectedDataFromCiphertext(Guid.NewGuid(), new byte[0]);
+ badProtectedPayload[0]++; // corrupt the magic header
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(badProtectedPayload));
+ Assert.Equal(Resources.ProtectionProvider_BadMagicHeader, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_PayloadHasIncorrectVersionMarker_ThrowsNewerVersion()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock<IKeyRingProvider>().Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ byte[] badProtectedPayload = BuildProtectedDataFromCiphertext(Guid.NewGuid(), new byte[0]);
+ badProtectedPayload[3]++; // bump the version payload
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(badProtectedPayload));
+ Assert.Equal(Resources.ProtectionProvider_BadVersion, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_KeyNotFound_ThrowsKeyNotFound()
+ {
+ // Arrange
+ Guid notFoundKeyId = new Guid("654057ab-2491-4471-a72a-b3b114afda38");
+ byte[] protectedData = BuildProtectedDataFromCiphertext(
+ keyId: notFoundKeyId,
+ ciphertext: new byte[0]);
+
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(o => o.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(new Mock<IAuthenticatedEncryptor>().Object);
+ var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+
+ // the keyring has only one key
+ Key key = new Key(Guid.Empty, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object, new[] { mockEncryptorFactory.Object });
+ var keyRing = new KeyRing(key, new[] { key });
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(protectedData));
+ Assert.Equal(Error.Common_KeyNotFound(notFoundKeyId).Message, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_KeyRevoked_RevocationDisallowed_ThrowsKeyRevoked()
+ {
+ // Arrange
+ Guid keyId = new Guid("654057ab-2491-4471-a72a-b3b114afda38");
+ byte[] protectedData = BuildProtectedDataFromCiphertext(
+ keyId: keyId,
+ ciphertext: new byte[0]);
+
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(o => o.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(new Mock<IAuthenticatedEncryptor>().Object);
+
+ // the keyring has only one key
+ Key key = new Key(keyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object, new[] { mockEncryptorFactory.Object });
+ key.SetRevoked();
+ var keyRing = new KeyRing(key, new[] { key });
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Unprotect(protectedData));
+ Assert.Equal(Error.Common_KeyRevoked(keyId).Message, ex.Message);
+ }
+
+ [Fact]
+ public void Unprotect_KeyRevoked_RevocationAllowed_ReturnsOriginalData_SetsRevokedAndMigrationFlags()
+ {
+ // Arrange
+ Guid defaultKeyId = new Guid("ba73c9ce-d322-4e45-af90-341307e11c38");
+ byte[] expectedCiphertext = new byte[] { 0x03, 0x05, 0x07, 0x11, 0x13, 0x17, 0x19 };
+ byte[] protectedData = BuildProtectedDataFromCiphertext(defaultKeyId, expectedCiphertext);
+ byte[] expectedAad = BuildAadFromPurposeStrings(defaultKeyId, "purpose");
+ byte[] expectedPlaintext = new byte[] { 0x23, 0x29, 0x31, 0x37 };
+
+ var mockEncryptor = new Mock<IAuthenticatedEncryptor>();
+ mockEncryptor
+ .Setup(o => o.Decrypt(It.IsAny<ArraySegment<byte>>(), It.IsAny<ArraySegment<byte>>()))
+ .Returns<ArraySegment<byte>, ArraySegment<byte>>((actualCiphertext, actualAad) =>
+ {
+ Assert.Equal(expectedCiphertext, actualCiphertext);
+ Assert.Equal(expectedAad, actualAad);
+ return expectedPlaintext;
+ });
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(o => o.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(mockEncryptor.Object);
+
+ Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object, new[] { mockEncryptorFactory.Object });
+ defaultKey.SetRevoked();
+ var keyRing = new KeyRing(defaultKey, new[] { defaultKey });
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act
+ byte[] retVal = ((IPersistedDataProtector)protector).DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: true,
+ requiresMigration: out var requiresMigration,
+ wasRevoked: out var wasRevoked);
+
+ // Assert
+ Assert.Equal(expectedPlaintext, retVal);
+ Assert.True(requiresMigration);
+ Assert.True(wasRevoked);
+ }
+
+ [Fact]
+ public void Unprotect_IsAlsoDefaultKey_Success_NoMigrationRequired()
+ {
+ // Arrange
+ Guid defaultKeyId = new Guid("ba73c9ce-d322-4e45-af90-341307e11c38");
+ byte[] expectedCiphertext = new byte[] { 0x03, 0x05, 0x07, 0x11, 0x13, 0x17, 0x19 };
+ byte[] protectedData = BuildProtectedDataFromCiphertext(defaultKeyId, expectedCiphertext);
+ byte[] expectedAad = BuildAadFromPurposeStrings(defaultKeyId, "purpose");
+ byte[] expectedPlaintext = new byte[] { 0x23, 0x29, 0x31, 0x37 };
+
+ var mockEncryptor = new Mock<IAuthenticatedEncryptor>();
+ mockEncryptor
+ .Setup(o => o.Decrypt(It.IsAny<ArraySegment<byte>>(), It.IsAny<ArraySegment<byte>>()))
+ .Returns<ArraySegment<byte>, ArraySegment<byte>>((actualCiphertext, actualAad) =>
+ {
+ Assert.Equal(expectedCiphertext, actualCiphertext);
+ Assert.Equal(expectedAad, actualAad);
+ return expectedPlaintext;
+ });
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(o => o.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(mockEncryptor.Object);
+
+ Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object, new[] { mockEncryptorFactory.Object });
+ var keyRing = new KeyRing(defaultKey, new[] { defaultKey });
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert - IDataProtector
+ byte[] retVal = protector.Unprotect(protectedData);
+ Assert.Equal(expectedPlaintext, retVal);
+
+ // Act & assert - IPersistedDataProtector
+ retVal = ((IPersistedDataProtector)protector).DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: false,
+ requiresMigration: out var requiresMigration,
+ wasRevoked: out var wasRevoked);
+ Assert.Equal(expectedPlaintext, retVal);
+ Assert.False(requiresMigration);
+ Assert.False(wasRevoked);
+ }
+
+ [Fact]
+ public void Unprotect_IsNotDefaultKey_Success_RequiresMigration()
+ {
+ // Arrange
+ Guid defaultKeyId = new Guid("ba73c9ce-d322-4e45-af90-341307e11c38");
+ Guid embeddedKeyId = new Guid("9b5d2db3-299f-4eac-89e9-e9067a5c1853");
+ byte[] expectedCiphertext = new byte[] { 0x03, 0x05, 0x07, 0x11, 0x13, 0x17, 0x19 };
+ byte[] protectedData = BuildProtectedDataFromCiphertext(embeddedKeyId, expectedCiphertext);
+ byte[] expectedAad = BuildAadFromPurposeStrings(embeddedKeyId, "purpose");
+ byte[] expectedPlaintext = new byte[] { 0x23, 0x29, 0x31, 0x37 };
+
+ var mockEncryptor = new Mock<IAuthenticatedEncryptor>();
+ mockEncryptor
+ .Setup(o => o.Decrypt(It.IsAny<ArraySegment<byte>>(), It.IsAny<ArraySegment<byte>>()))
+ .Returns<ArraySegment<byte>, ArraySegment<byte>>((actualCiphertext, actualAad) =>
+ {
+ Assert.Equal(expectedCiphertext, actualCiphertext);
+ Assert.Equal(expectedAad, actualAad);
+ return expectedPlaintext;
+ });
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(o => o.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(mockEncryptor.Object);
+
+ Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new Mock<IAuthenticatedEncryptorDescriptor>().Object, new[] { mockEncryptorFactory.Object });
+ Key embeddedKey = new Key(embeddedKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object, new[] { mockEncryptorFactory.Object });
+ var keyRing = new KeyRing(defaultKey, new[] { defaultKey, embeddedKey });
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert - IDataProtector
+ byte[] retVal = protector.Unprotect(protectedData);
+ Assert.Equal(expectedPlaintext, retVal);
+
+ // Act & assert - IPersistedDataProtector
+ retVal = ((IPersistedDataProtector)protector).DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: false,
+ requiresMigration: out var requiresMigration,
+ wasRevoked: out var wasRevoked);
+ Assert.Equal(expectedPlaintext, retVal);
+ Assert.True(requiresMigration);
+ Assert.False(wasRevoked);
+ }
+
+ [Fact]
+ public void Protect_Unprotect_RoundTripsProperly()
+ {
+ // Arrange
+ byte[] plaintext = new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50 };
+ var encryptorFactory = new AuthenticatedEncryptorFactory(NullLoggerFactory.Instance);
+ Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration().CreateNewDescriptor(), new[] { encryptorFactory });
+ var keyRing = new KeyRing(key, new[] { key });
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ var protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act - protect
+ byte[] protectedData = protector.Protect(plaintext);
+ Assert.NotNull(protectedData);
+ Assert.NotEqual(plaintext, protectedData);
+
+ // Act - unprotect
+ byte[] roundTrippedPlaintext = protector.Unprotect(protectedData);
+ Assert.Equal(plaintext, roundTrippedPlaintext);
+ }
+
+ [Fact]
+ public void CreateProtector_ChainsPurposes()
+ {
+ // Arrange
+ Guid defaultKey = new Guid("ba73c9ce-d322-4e45-af90-341307e11c38");
+ byte[] expectedPlaintext = new byte[] { 0x03, 0x05, 0x07, 0x11, 0x13, 0x17, 0x19 };
+ byte[] expectedAad = BuildAadFromPurposeStrings(defaultKey, "purpose1", "purpose2");
+ byte[] expectedProtectedData = BuildProtectedDataFromCiphertext(defaultKey, new byte[] { 0x23, 0x29, 0x31, 0x37 });
+
+ var mockEncryptor = new Mock<IAuthenticatedEncryptor>();
+ mockEncryptor
+ .Setup(o => o.Encrypt(It.IsAny<ArraySegment<byte>>(), It.IsAny<ArraySegment<byte>>()))
+ .Returns<ArraySegment<byte>, ArraySegment<byte>>((actualPlaintext, actualAad) =>
+ {
+ Assert.Equal(expectedPlaintext, actualPlaintext);
+ Assert.Equal(expectedAad, actualAad);
+ return new byte[] { 0x23, 0x29, 0x31, 0x37 }; // ciphertext + tag
+ });
+
+ var mockKeyRing = new Mock<IKeyRing>(MockBehavior.Strict);
+ mockKeyRing.Setup(o => o.DefaultKeyId).Returns(defaultKey);
+ mockKeyRing.Setup(o => o.DefaultAuthenticatedEncryptor).Returns(mockEncryptor.Object);
+ var mockKeyRingProvider = new Mock<IKeyRingProvider>();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(mockKeyRing.Object);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: GetLogger(),
+ originalPurposes: null,
+ newPurpose: "purpose1").CreateProtector("purpose2");
+
+ // Act
+ byte[] retVal = protector.Protect(expectedPlaintext);
+
+ // Assert
+ Assert.Equal(expectedProtectedData, retVal);
+ }
+
+ private static byte[] BuildAadFromPurposeStrings(Guid keyId, params string[] purposes)
+ {
+ var expectedAad = new byte[] { 0x09, 0xF0, 0xC9, 0xF0 } // magic header
+ .Concat(keyId.ToByteArray()) // key id
+ .Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(purposes.Length))); // purposeCount
+
+ foreach (string purpose in purposes)
+ {
+ var memStream = new MemoryStream();
+ var writer = new BinaryWriter(memStream, encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true);
+ writer.Write(purpose); // also writes 7-bit encoded int length
+ writer.Dispose();
+ expectedAad = expectedAad.Concat(memStream.ToArray());
+ }
+
+ return expectedAad.ToArray();
+ }
+
+ private static byte[] BuildProtectedDataFromCiphertext(Guid keyId, byte[] ciphertext)
+ {
+ return new byte[] { 0x09, 0xF0, 0xC9, 0xF0 } // magic header
+ .Concat(keyId.ToByteArray()) // key id
+ .Concat(ciphertext).ToArray();
+
+ }
+
+ private static ILogger GetLogger()
+ {
+ var loggerFactory = NullLoggerFactory.Instance;
+ return loggerFactory.CreateLogger(typeof(KeyRingBasedDataProtector));
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs
new file mode 100644
index 0000000000..8582ed8359
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs
@@ -0,0 +1,652 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class KeyRingProviderTests
+ {
+ [Fact]
+ public void CreateCacheableKeyRing_NoGenerationRequired_DefaultKeyExpiresAfterRefreshPeriod()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts = new CancellationTokenSource();
+
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+ var allKeys = new[] { key1, key2 };
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts.Token },
+ getAllKeysReturnValues: new[] { allKeys },
+ createNewKeyCallbacks: null,
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ DefaultKey = key1,
+ ShouldGenerateNewKey = false
+ })
+ });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(key1.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_NoGenerationRequired_DefaultKeyExpiresBeforeRefreshPeriod()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts = new CancellationTokenSource();
+
+ var now = StringToDateTime("2016-02-29 20:00:00Z");
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+ var allKeys = new[] { key1, key2 };
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts.Token },
+ getAllKeysReturnValues: new[] { allKeys },
+ createNewKeyCallbacks: null,
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ DefaultKey = key1,
+ ShouldGenerateNewKey = false
+ })
+ });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(key1.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ Assert.Equal(StringToDateTime("2016-03-01 00:00:00Z"), cacheableKeyRing.ExpirationTimeUtc);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_CreatesNewKeyWithImmediateActivation()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts1 = new CancellationTokenSource();
+ var expirationCts2 = new CancellationTokenSource();
+
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var allKeys1 = new IKey[0];
+
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+ var allKeys2 = new[] { key1, key2 };
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token },
+ getAllKeysReturnValues: new[] { allKeys1, allKeys2 },
+ createNewKeyCallbacks: new[] {
+ Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey())
+ },
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys1, new DefaultKeyResolution()
+ {
+ DefaultKey = null,
+ ShouldGenerateNewKey = true
+ }),
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys2, new DefaultKeyResolution()
+ {
+ DefaultKey = key1,
+ ShouldGenerateNewKey = false
+ })
+ });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(key1.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts1.Cancel();
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts2.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_CreatesNewKeyWithImmediateActivation_StillNoDefaultKey_ReturnsNewlyCreatedKey()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts1 = new CancellationTokenSource();
+ var expirationCts2 = new CancellationTokenSource();
+
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var allKeys = new IKey[0];
+
+ var newlyCreatedKey = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token },
+ getAllKeysReturnValues: new[] { allKeys, allKeys },
+ createNewKeyCallbacks: new[] {
+ Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), newlyCreatedKey)
+ },
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ DefaultKey = null,
+ ShouldGenerateNewKey = true
+ }),
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ DefaultKey = null,
+ ShouldGenerateNewKey = true
+ })
+ });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(newlyCreatedKey.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts1.Cancel();
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts2.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_KeyGenerationDisabled_Fails()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var allKeys = new IKey[0];
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { CancellationToken.None },
+ getAllKeysReturnValues: new[] { allKeys },
+ createNewKeyCallbacks: new[] {
+ Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey())
+ },
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ DefaultKey = null,
+ ShouldGenerateNewKey = true
+ })
+ },
+ keyManagementOptions: new KeyManagementOptions() { AutoGenerateKeys = false });
+
+ // Act
+ var exception = Assert.Throws<InvalidOperationException>(() => keyRingProvider.GetCacheableKeyRing(now));
+
+ // Assert
+ Assert.Equal(Resources.KeyRingProvider_NoDefaultKey_AutoGenerateDisabled, exception.Message);
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_GenerationRequired_WithDefaultKey_CreatesNewKeyWithDeferredActivationAndExpirationBasedOnCreationTime()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts1 = new CancellationTokenSource();
+ var expirationCts2 = new CancellationTokenSource();
+
+ var now = StringToDateTime("2016-02-01 00:00:00Z");
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var allKeys1 = new[] { key1 };
+
+ var key2 = CreateKey("2016-03-01 00:00:00Z", "2017-03-01 00:00:00Z");
+ var allKeys2 = new[] { key1, key2 };
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token },
+ getAllKeysReturnValues: new[] { allKeys1, allKeys2 },
+ createNewKeyCallbacks: new[] {
+ Tuple.Create(key1.ExpirationDate, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey())
+ },
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys1, new DefaultKeyResolution()
+ {
+ DefaultKey = key1,
+ ShouldGenerateNewKey = true
+ }),
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys2, new DefaultKeyResolution()
+ {
+ DefaultKey = key2,
+ ShouldGenerateNewKey = false
+ })
+ });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(key2.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts1.Cancel();
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts2.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_GenerationRequired_WithDefaultKey_KeyGenerationDisabled_DoesNotCreateDefaultKey()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts = new CancellationTokenSource();
+
+ var now = StringToDateTime("2016-02-01 00:00:00Z");
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z");
+ var allKeys = new[] { key1 };
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts.Token },
+ getAllKeysReturnValues: new[] { allKeys },
+ createNewKeyCallbacks: null, // empty
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ DefaultKey = key1,
+ ShouldGenerateNewKey = true
+ })
+ },
+ keyManagementOptions: new KeyManagementOptions() { AutoGenerateKeys = false });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(key1.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void CreateCacheableKeyRing_GenerationRequired_WithFallbackKey_KeyGenerationDisabled_DoesNotCreateDefaultKey()
+ {
+ // Arrange
+ var callSequence = new List<string>();
+ var expirationCts = new CancellationTokenSource();
+
+ var now = StringToDateTime("2016-02-01 00:00:00Z");
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-03-01 00:00:00Z");
+ var allKeys = new[] { key1 };
+
+ var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ callSequence: callSequence,
+ getCacheExpirationTokenReturnValues: new[] { expirationCts.Token },
+ getAllKeysReturnValues: new[] { allKeys },
+ createNewKeyCallbacks: null, // empty
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable<IKey>)allKeys, new DefaultKeyResolution()
+ {
+ FallbackKey = key1,
+ ShouldGenerateNewKey = true
+ })
+ },
+ keyManagementOptions: new KeyManagementOptions() { AutoGenerateKeys = false });
+
+ // Act
+ var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now);
+
+ // Assert
+ Assert.Equal(key1.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId);
+ AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now);
+ Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ expirationCts.Cancel();
+ Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now));
+ Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence);
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_NoKeyRingCached_CachesAndReturns()
+ {
+ // Arrange
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var expectedKeyRing = new Mock<IKeyRing>().Object;
+ var mockCacheableKeyRingProvider = new Mock<ICacheableKeyRingProvider>();
+ mockCacheableKeyRingProvider
+ .Setup(o => o.GetCacheableKeyRing(now))
+ .Returns(new CacheableKeyRing(
+ expirationToken: CancellationToken.None,
+ expirationTime: StringToDateTime("2015-03-02 00:00:00Z"),
+ keyRing: expectedKeyRing));
+
+ var keyRingProvider = CreateKeyRingProvider(mockCacheableKeyRingProvider.Object);
+
+ // Act
+ var retVal1 = keyRingProvider.GetCurrentKeyRingCore(now);
+ var retVal2 = keyRingProvider.GetCurrentKeyRingCore(now + TimeSpan.FromHours(1));
+
+ // Assert - underlying provider only should have been called once
+ Assert.Same(expectedKeyRing, retVal1);
+ Assert.Same(expectedKeyRing, retVal2);
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(It.IsAny<DateTimeOffset>()), Times.Once);
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_KeyRingCached_AfterExpiration_ClearsCache()
+ {
+ // Arrange
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var expectedKeyRing1 = new Mock<IKeyRing>().Object;
+ var expectedKeyRing2 = new Mock<IKeyRing>().Object;
+ var mockCacheableKeyRingProvider = new Mock<ICacheableKeyRingProvider>();
+ mockCacheableKeyRingProvider
+ .Setup(o => o.GetCacheableKeyRing(now))
+ .Returns(new CacheableKeyRing(
+ expirationToken: CancellationToken.None,
+ expirationTime: StringToDateTime("2015-03-01 00:30:00Z"), // expire in half an hour
+ keyRing: expectedKeyRing1));
+ mockCacheableKeyRingProvider
+ .Setup(o => o.GetCacheableKeyRing(now + TimeSpan.FromHours(1)))
+ .Returns(new CacheableKeyRing(
+ expirationToken: CancellationToken.None,
+ expirationTime: StringToDateTime("2015-03-02 00:00:00Z"),
+ keyRing: expectedKeyRing2));
+
+ var keyRingProvider = CreateKeyRingProvider(mockCacheableKeyRingProvider.Object);
+
+ // Act
+ var retVal1 = keyRingProvider.GetCurrentKeyRingCore(now);
+ var retVal2 = keyRingProvider.GetCurrentKeyRingCore(now + TimeSpan.FromHours(1));
+
+ // Assert - underlying provider only should have been called once
+ Assert.Same(expectedKeyRing1, retVal1);
+ Assert.Same(expectedKeyRing2, retVal2);
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(It.IsAny<DateTimeOffset>()), Times.Exactly(2));
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_NoExistingKeyRing_HoldsAllThreadsUntilKeyRingCreated()
+ {
+ // Arrange
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var expectedKeyRing = new Mock<IKeyRing>().Object;
+ var mockCacheableKeyRingProvider = new Mock<ICacheableKeyRingProvider>();
+ var keyRingProvider = CreateKeyRingProvider(mockCacheableKeyRingProvider.Object);
+
+ // This test spawns a background thread which calls GetCurrentKeyRing then waits
+ // for the foreground thread to call GetCurrentKeyRing. When the foreground thread
+ // blocks (inside the lock), the background thread will return the cached keyring
+ // object, and the foreground thread should consume that same object instance.
+
+ TimeSpan testTimeout = TimeSpan.FromSeconds(10);
+
+ Thread foregroundThread = Thread.CurrentThread;
+ ManualResetEventSlim mreBackgroundThreadHasCalledGetCurrentKeyRing = new ManualResetEventSlim();
+ ManualResetEventSlim mreForegroundThreadIsCallingGetCurrentKeyRing = new ManualResetEventSlim();
+ var backgroundGetKeyRingTask = Task.Run(() =>
+ {
+ mockCacheableKeyRingProvider
+ .Setup(o => o.GetCacheableKeyRing(now))
+ .Returns(() =>
+ {
+ mreBackgroundThreadHasCalledGetCurrentKeyRing.Set();
+ Assert.True(mreForegroundThreadIsCallingGetCurrentKeyRing.Wait(testTimeout), "Test timed out.");
+ SpinWait.SpinUntil(() => (foregroundThread.ThreadState & ThreadState.WaitSleepJoin) != 0, testTimeout);
+ return new CacheableKeyRing(
+ expirationToken: CancellationToken.None,
+ expirationTime: StringToDateTime("2015-03-02 00:00:00Z"),
+ keyRing: expectedKeyRing);
+ });
+
+ return keyRingProvider.GetCurrentKeyRingCore(now);
+ });
+
+ Assert.True(mreBackgroundThreadHasCalledGetCurrentKeyRing.Wait(testTimeout), "Test timed out.");
+ mreForegroundThreadIsCallingGetCurrentKeyRing.Set();
+ var foregroundRetVal = keyRingProvider.GetCurrentKeyRingCore(now);
+ backgroundGetKeyRingTask.Wait(testTimeout);
+ var backgroundRetVal = backgroundGetKeyRingTask.GetAwaiter().GetResult();
+
+ // Assert - underlying provider only should have been called once
+ Assert.Same(expectedKeyRing, foregroundRetVal);
+ Assert.Same(expectedKeyRing, backgroundRetVal);
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(It.IsAny<DateTimeOffset>()), Times.Once);
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_WithExpiredExistingKeyRing_AllowsOneThreadToUpdate_ReturnsExistingKeyRingToOtherCallersWithoutBlocking()
+ {
+ // Arrange
+ var originalKeyRing = new Mock<IKeyRing>().Object;
+ var originalKeyRingTime = StringToDateTime("2015-03-01 00:00:00Z");
+ var updatedKeyRing = new Mock<IKeyRing>().Object;
+ var updatedKeyRingTime = StringToDateTime("2015-03-02 00:00:00Z");
+ var mockCacheableKeyRingProvider = new Mock<ICacheableKeyRingProvider>();
+ var keyRingProvider = CreateKeyRingProvider(mockCacheableKeyRingProvider.Object);
+
+ // In this test, the foreground thread acquires the critial section in GetCurrentKeyRing,
+ // and the background thread returns the original key ring rather than blocking while
+ // waiting for the foreground thread to update the key ring.
+
+ TimeSpan testTimeout = TimeSpan.FromSeconds(10);
+ IKeyRing keyRingReturnedToBackgroundThread = null;
+
+ mockCacheableKeyRingProvider.Setup(o => o.GetCacheableKeyRing(originalKeyRingTime))
+ .Returns(new CacheableKeyRing(CancellationToken.None, StringToDateTime("2015-03-02 00:00:00Z"), originalKeyRing));
+ mockCacheableKeyRingProvider.Setup(o => o.GetCacheableKeyRing(updatedKeyRingTime))
+ .Returns<DateTimeOffset>(dto =>
+ {
+ // at this point we're inside the critical section - spawn the background thread now
+ var backgroundGetKeyRingTask = Task.Run(() =>
+ {
+ keyRingReturnedToBackgroundThread = keyRingProvider.GetCurrentKeyRingCore(updatedKeyRingTime);
+ });
+ Assert.True(backgroundGetKeyRingTask.Wait(testTimeout), "Test timed out.");
+
+ return new CacheableKeyRing(CancellationToken.None, StringToDateTime("2015-03-03 00:00:00Z"), updatedKeyRing);
+ });
+
+ // Assert - underlying provider only should have been called once with the updated time (by the foreground thread)
+ Assert.Same(originalKeyRing, keyRingProvider.GetCurrentKeyRingCore(originalKeyRingTime));
+ Assert.Same(updatedKeyRing, keyRingProvider.GetCurrentKeyRingCore(updatedKeyRingTime));
+ Assert.Same(originalKeyRing, keyRingReturnedToBackgroundThread);
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(updatedKeyRingTime), Times.Once);
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_WithExpiredExistingKeyRing_UpdateFails_ThrowsButCachesOldKeyRing()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+ var mockCacheableKeyRingProvider = new Mock<ICacheableKeyRingProvider>();
+ var originalKeyRing = new Mock<IKeyRing>().Object;
+ var originalKeyRingTime = StringToDateTime("2015-03-01 00:00:00Z");
+ mockCacheableKeyRingProvider.Setup(o => o.GetCacheableKeyRing(originalKeyRingTime))
+ .Returns(new CacheableKeyRing(cts.Token, StringToDateTime("2015-03-02 00:00:00Z"), originalKeyRing));
+ var throwKeyRingTime = StringToDateTime("2015-03-01 12:00:00Z");
+ mockCacheableKeyRingProvider.Setup(o => o.GetCacheableKeyRing(throwKeyRingTime)).Throws(new Exception("How exceptional."));
+ var updatedKeyRing = new Mock<IKeyRing>().Object;
+ var updatedKeyRingTime = StringToDateTime("2015-03-01 12:02:00Z");
+ mockCacheableKeyRingProvider.Setup(o => o.GetCacheableKeyRing(updatedKeyRingTime))
+ .Returns(new CacheableKeyRing(CancellationToken.None, StringToDateTime("2015-03-02 00:00:00Z"), updatedKeyRing));
+ var keyRingProvider = CreateKeyRingProvider(mockCacheableKeyRingProvider.Object);
+
+ // Act & assert
+ Assert.Same(originalKeyRing, keyRingProvider.GetCurrentKeyRingCore(originalKeyRingTime));
+ cts.Cancel(); // invalidate the key ring
+ ExceptionAssert.Throws<Exception>(() => keyRingProvider.GetCurrentKeyRingCore(throwKeyRingTime), "How exceptional.");
+ Assert.Same(originalKeyRing, keyRingProvider.GetCurrentKeyRingCore(throwKeyRingTime));
+ Assert.Same(updatedKeyRing, keyRingProvider.GetCurrentKeyRingCore(updatedKeyRingTime));
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(originalKeyRingTime), Times.Once);
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(throwKeyRingTime), Times.Once);
+ mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(updatedKeyRingTime), Times.Once);
+ }
+
+ private static ICacheableKeyRingProvider SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ IList<string> callSequence,
+ IEnumerable<CancellationToken> getCacheExpirationTokenReturnValues,
+ IEnumerable<IReadOnlyCollection<IKey>> getAllKeysReturnValues,
+ IEnumerable<Tuple<DateTimeOffset, DateTimeOffset, IKey>> createNewKeyCallbacks,
+ IEnumerable<Tuple<DateTimeOffset, IEnumerable<IKey>, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues,
+ KeyManagementOptions keyManagementOptions = null)
+ {
+ var getCacheExpirationTokenReturnValuesEnumerator = getCacheExpirationTokenReturnValues.GetEnumerator();
+ var mockKeyManager = new Mock<IKeyManager>(MockBehavior.Strict);
+ mockKeyManager.Setup(o => o.GetCacheExpirationToken())
+ .Returns(() =>
+ {
+ callSequence.Add("GetCacheExpirationToken");
+ getCacheExpirationTokenReturnValuesEnumerator.MoveNext();
+ return getCacheExpirationTokenReturnValuesEnumerator.Current;
+ });
+
+ var getAllKeysReturnValuesEnumerator = getAllKeysReturnValues.GetEnumerator();
+ mockKeyManager.Setup(o => o.GetAllKeys())
+ .Returns(() =>
+ {
+ callSequence.Add("GetAllKeys");
+ getAllKeysReturnValuesEnumerator.MoveNext();
+ return getAllKeysReturnValuesEnumerator.Current;
+ });
+
+ if (createNewKeyCallbacks != null)
+ {
+ var createNewKeyCallbacksEnumerator = createNewKeyCallbacks.GetEnumerator();
+ mockKeyManager.Setup(o => o.CreateNewKey(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>()))
+ .Returns<DateTimeOffset, DateTimeOffset>((activationDate, expirationDate) =>
+ {
+ callSequence.Add("CreateNewKey");
+ createNewKeyCallbacksEnumerator.MoveNext();
+ Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item1, activationDate);
+ Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item2, expirationDate);
+ return createNewKeyCallbacksEnumerator.Current.Item3;
+ });
+ }
+
+ var resolveDefaultKeyPolicyReturnValuesEnumerator = resolveDefaultKeyPolicyReturnValues.GetEnumerator();
+ var mockDefaultKeyResolver = new Mock<IDefaultKeyResolver>(MockBehavior.Strict);
+ mockDefaultKeyResolver.Setup(o => o.ResolveDefaultKeyPolicy(It.IsAny<DateTimeOffset>(), It.IsAny<IEnumerable<IKey>>()))
+ .Returns<DateTimeOffset, IEnumerable<IKey>>((now, allKeys) =>
+ {
+ callSequence.Add("ResolveDefaultKeyPolicy");
+ resolveDefaultKeyPolicyReturnValuesEnumerator.MoveNext();
+ Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item1, now);
+ Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item2, allKeys);
+ return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3;
+ });
+
+ return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object, keyManagementOptions);
+ }
+
+ private static KeyRingProvider CreateKeyRingProvider(ICacheableKeyRingProvider cacheableKeyRingProvider)
+ {
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(m => m.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(new Mock<IAuthenticatedEncryptor>().Object);
+ var options = new KeyManagementOptions();
+ options.AuthenticatedEncryptorFactories.Add(mockEncryptorFactory.Object);
+
+ return new KeyRingProvider(
+ keyManager: null,
+ keyManagementOptions: Options.Create(options),
+ defaultKeyResolver: null,
+ loggerFactory: NullLoggerFactory.Instance)
+ {
+ CacheableKeyRingProvider = cacheableKeyRingProvider
+ };
+ }
+
+ private static ICacheableKeyRingProvider CreateKeyRingProvider(IKeyManager keyManager, IDefaultKeyResolver defaultKeyResolver, KeyManagementOptions keyManagementOptions= null)
+ {
+ var mockEncryptorFactory = new Mock<IAuthenticatedEncryptorFactory>();
+ mockEncryptorFactory.Setup(m => m.CreateEncryptorInstance(It.IsAny<IKey>())).Returns(new Mock<IAuthenticatedEncryptor>().Object);
+ keyManagementOptions = keyManagementOptions ?? new KeyManagementOptions();
+ keyManagementOptions.AuthenticatedEncryptorFactories.Add(mockEncryptorFactory.Object);
+
+ return new KeyRingProvider(
+ keyManager: keyManager,
+ keyManagementOptions: Options.Create(keyManagementOptions),
+ defaultKeyResolver: defaultKeyResolver,
+ loggerFactory: NullLoggerFactory.Instance);
+ }
+
+ private static void AssertWithinJitterRange(DateTimeOffset actual, DateTimeOffset now)
+ {
+ // The jitter can cause the actual value to fall in the range [now + 80% of refresh period, now + 100% of refresh period)
+ Assert.InRange(actual, now + TimeSpan.FromHours(24 * 0.8), now + TimeSpan.FromHours(24));
+ }
+
+ private static DateTime StringToDateTime(string input)
+ {
+ return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime;
+ }
+
+ private static IKey CreateKey()
+ {
+ var now = DateTimeOffset.Now;
+ return CreateKey(
+ string.Format(CultureInfo.InvariantCulture, "{0:u}", now),
+ string.Format(CultureInfo.InvariantCulture, "{0:u}", now.AddDays(90)));
+ }
+
+ private static IKey CreateKey(string activationDate, string expirationDate, bool isRevoked = false)
+ {
+ var mockKey = new Mock<IKey>();
+ mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid());
+ mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture));
+ mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture));
+ mockKey.Setup(o => o.IsRevoked).Returns(isRevoked);
+ mockKey.Setup(o => o.Descriptor).Returns(new Mock<IAuthenticatedEncryptorDescriptor>().Object);
+ mockKey.Setup(o => o.CreateEncryptor()).Returns(new Mock<IAuthenticatedEncryptor>().Object);
+ return mockKey.Object;
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingTests.cs
new file mode 100644
index 0000000000..177c7c5d63
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyRingTests.cs
@@ -0,0 +1,126 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class KeyRingTests
+ {
+ [Fact]
+ public void DefaultAuthenticatedEncryptor_Prop_InstantiationIsDeferred()
+ {
+ // Arrange
+ var expectedEncryptorInstance = new Mock<IAuthenticatedEncryptor>().Object;
+
+ var key1 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance);
+ var key2 = new MyKey();
+
+ // Act
+ var keyRing = new KeyRing(key1, new[] { key1, key2 });
+
+ // Assert
+ Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance, keyRing.DefaultAuthenticatedEncryptor);
+ Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance, keyRing.DefaultAuthenticatedEncryptor);
+ Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled); // should've been cached
+ }
+
+ [Fact]
+ public void DefaultKeyId_Prop()
+ {
+ // Arrange
+ var key1 = new MyKey();
+ var key2 = new MyKey();
+
+ // Act
+ var keyRing = new KeyRing(key2, new[] { key1, key2 });
+
+ // Assert
+ Assert.Equal(key2.KeyId, keyRing.DefaultKeyId);
+ }
+
+ [Fact]
+ public void DefaultKeyIdAndEncryptor_IfDefaultKeyNotPresentInAllKeys()
+ {
+ // Arrange
+ var key1 = new MyKey();
+ var key2 = new MyKey();
+ var key3 = new MyKey(expectedEncryptorInstance: new Mock<IAuthenticatedEncryptor>().Object);
+
+ // Act
+ var keyRing = new KeyRing(key3, new[] { key1, key2 });
+
+ // Assert
+ Assert.Equal(key3.KeyId, keyRing.DefaultKeyId);
+ Assert.Equal(key3.CreateEncryptor(), keyRing.GetAuthenticatedEncryptorByKeyId(key3.KeyId, out var _));
+ }
+
+ [Fact]
+ public void GetAuthenticatedEncryptorByKeyId_DefersInstantiation_AndReturnsRevocationInfo()
+ {
+ // Arrange
+ var expectedEncryptorInstance1 = new Mock<IAuthenticatedEncryptor>().Object;
+ var expectedEncryptorInstance2 = new Mock<IAuthenticatedEncryptor>().Object;
+
+ var key1 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance1, isRevoked: true);
+ var key2 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance2);
+
+
+ // Act
+ var keyRing = new KeyRing(key2, new[] { key1, key2 });
+
+ // Assert
+ Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance1, keyRing.GetAuthenticatedEncryptorByKeyId(key1.KeyId, out var isRevoked));
+ Assert.True(isRevoked);
+ Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance1, keyRing.GetAuthenticatedEncryptorByKeyId(key1.KeyId, out isRevoked));
+ Assert.True(isRevoked);
+ Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Equal(0, key2.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance2, keyRing.GetAuthenticatedEncryptorByKeyId(key2.KeyId, out isRevoked));
+ Assert.False(isRevoked);
+ Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance2, keyRing.GetAuthenticatedEncryptorByKeyId(key2.KeyId, out isRevoked));
+ Assert.False(isRevoked);
+ Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled);
+ Assert.Same(expectedEncryptorInstance2, keyRing.DefaultAuthenticatedEncryptor);
+ Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled);
+ }
+
+ private sealed class MyKey : IKey
+ {
+ public int NumTimesCreateEncryptorInstanceCalled;
+ private readonly Func<IAuthenticatedEncryptor> _encryptorFactory;
+
+ public MyKey(bool isRevoked = false, IAuthenticatedEncryptor expectedEncryptorInstance = null)
+ {
+ CreationDate = DateTimeOffset.Now;
+ ActivationDate = CreationDate + TimeSpan.FromHours(1);
+ ExpirationDate = CreationDate + TimeSpan.FromDays(30);
+ IsRevoked = isRevoked;
+ KeyId = Guid.NewGuid();
+ _encryptorFactory = () => expectedEncryptorInstance ?? new Mock<IAuthenticatedEncryptor>().Object;
+ }
+
+ public DateTimeOffset ActivationDate { get; }
+ public DateTimeOffset CreationDate { get; }
+ public DateTimeOffset ExpirationDate { get; }
+ public bool IsRevoked { get; }
+ public Guid KeyId { get; }
+ public IAuthenticatedEncryptorDescriptor Descriptor => throw new NotImplementedException();
+
+ public IAuthenticatedEncryptor CreateEncryptor()
+ {
+ NumTimesCreateEncryptorInstanceCalled++;
+ return _encryptorFactory();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyTests.cs
new file mode 100644
index 0000000000..6aa691723d
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/KeyTests.cs
@@ -0,0 +1,50 @@
+// 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 Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Moq;
+using Xunit;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class KeyTests
+ {
+ [Fact]
+ public void Ctor_Properties()
+ {
+ // Arrange
+ var keyId = Guid.NewGuid();
+ var creationDate = DateTimeOffset.Now;
+ var activationDate = creationDate.AddDays(2);
+ var expirationDate = creationDate.AddDays(90);
+ var descriptor = Mock.Of<IAuthenticatedEncryptorDescriptor>();
+ var encryptorFactory = Mock.Of<IAuthenticatedEncryptorFactory>();
+
+ // Act
+ var key = new Key(keyId, creationDate, activationDate, expirationDate, descriptor, new[] { encryptorFactory });
+
+ // Assert
+ Assert.Equal(keyId, key.KeyId);
+ Assert.Equal(creationDate, key.CreationDate);
+ Assert.Equal(activationDate, key.ActivationDate);
+ Assert.Equal(expirationDate, key.ExpirationDate);
+ Assert.Same(descriptor, key.Descriptor);
+ }
+
+ [Fact]
+ public void SetRevoked_Respected()
+ {
+ // Arrange
+ var now = DateTimeOffset.UtcNow;
+ var encryptorFactory = Mock.Of<IAuthenticatedEncryptorFactory>();
+ var key = new Key(Guid.Empty, now, now, now, new Mock<IAuthenticatedEncryptorDescriptor>().Object, new[] { encryptorFactory });
+
+ // Act & assert
+ Assert.False(key.IsRevoked);
+ key.SetRevoked();
+ Assert.True(key.IsRevoked);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs
new file mode 100644
index 0000000000..c6a2e068a3
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs
@@ -0,0 +1,770 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.KeyManagement
+{
+ public class XmlKeyManagerTests
+ {
+ private static readonly XElement serializedDescriptor = XElement.Parse(@"
+ <theElement>
+ <secret enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <![CDATA[This is a secret value.]]>
+ </secret>
+ </theElement>");
+
+ [Fact]
+ public void Ctor_WithoutEncryptorOrRepository_UsesFallback()
+ {
+ // Arrange
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = null,
+ XmlEncryptor = null
+ });
+
+ // Act
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
+
+ // Assert
+ Assert.NotNull(keyManager.KeyRepository);
+
+ if (OSVersionUtil.IsWindows())
+ {
+ Assert.NotNull(keyManager.KeyEncryptor);
+ }
+ }
+
+ [Fact]
+ public void Ctor_WithEncryptorButNoRepository_IgnoresFallback_FailsWithServiceNotFound()
+ {
+ // Arrange
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = null,
+ XmlEncryptor = new Mock<IXmlEncryptor>().Object
+ });
+
+ // Act & assert - we don't care about exception type, only exception message
+ Exception ex = Assert.ThrowsAny<Exception>(
+ () => new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance));
+ Assert.Contains("IXmlRepository", ex.Message);
+ }
+
+ [Fact]
+ public void CreateNewKey_Internal_NoEscrowOrEncryption()
+ {
+ // Constants
+ var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
+ var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero);
+ var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero);
+ var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093");
+
+ // Arrange
+ XElement elementStoredInRepository = null;
+ string friendlyNameStoredInRepository = null;
+ var expectedAuthenticatedEncryptor = new Mock<IAuthenticatedEncryptor>().Object;
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer)));
+ var expectedDescriptor = mockDescriptor.Object;
+ var testEncryptorFactory = new TestEncryptorFactory(expectedDescriptor, expectedAuthenticatedEncryptor);
+ var mockConfiguration = new Mock<AlgorithmConfiguration>();
+ mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(expectedDescriptor);
+ var mockXmlRepository = new Mock<IXmlRepository>();
+ mockXmlRepository
+ .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
+ .Callback<XElement, string>((el, friendlyName) =>
+ {
+ elementStoredInRepository = el;
+ friendlyNameStoredInRepository = friendlyName;
+ });
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = mockConfiguration.Object,
+ XmlRepository = mockXmlRepository.Object,
+ XmlEncryptor = null
+ });
+ options.Value.AuthenticatedEncryptorFactories.Add(testEncryptorFactory);
+
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
+
+ // Act & assert
+
+ // The cancellation token should not already be fired
+ var firstCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.False(firstCancellationToken.IsCancellationRequested);
+
+ // After the call to CreateNewKey, the first CT should be fired,
+ // and we should've gotten a new CT.
+ var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey(
+ keyId: keyId,
+ creationDate: creationDate,
+ activationDate: activationDate,
+ expirationDate: expirationDate);
+ var secondCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.True(firstCancellationToken.IsCancellationRequested);
+ Assert.False(secondCancellationToken.IsCancellationRequested);
+
+ // Does the IKey have the properties we requested?
+ Assert.Equal(keyId, newKey.KeyId);
+ Assert.Equal(creationDate, newKey.CreationDate);
+ Assert.Equal(activationDate, newKey.ActivationDate);
+ Assert.Equal(expirationDate, newKey.ExpirationDate);
+ Assert.Same(expectedDescriptor, newKey.Descriptor);
+ Assert.False(newKey.IsRevoked);
+ Assert.Same(expectedAuthenticatedEncryptor, testEncryptorFactory.CreateEncryptorInstance(newKey));
+
+ // Finally, was the correct element stored in the repository?
+ string expectedXml = string.Format(@"
+ <key id='3d6d01fd-c0e7-44ae-82dd-013b996b4093' version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ {1}
+ {2}
+ {3}
+ <descriptor deserializerType='{0}'>
+ <theElement>
+ <secret enc:requiresEncryption='true'>
+ <![CDATA[This is a secret value.]]>
+ </secret>
+ </theElement>
+ </descriptor>
+ </key>",
+ typeof(MyDeserializer).AssemblyQualifiedName,
+ new XElement("creationDate", creationDate),
+ new XElement("activationDate", activationDate),
+ new XElement("expirationDate", expirationDate));
+ XmlAssert.Equal(expectedXml, elementStoredInRepository);
+ Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository);
+ }
+
+ [Fact]
+ public void CreateNewKey_Internal_WithEscrowAndEncryption()
+ {
+ // Constants
+ var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
+ var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero);
+ var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero);
+ var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093");
+
+ // Arrange
+ XElement elementStoredInEscrow = null;
+ Guid? keyIdStoredInEscrow = null;
+ XElement elementStoredInRepository = null;
+ string friendlyNameStoredInRepository = null;
+ var expectedAuthenticatedEncryptor = new Mock<IAuthenticatedEncryptor>().Object;
+ var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
+ mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer)));
+ var expectedDescriptor = mockDescriptor.Object;
+ var testEncryptorFactory = new TestEncryptorFactory(expectedDescriptor, expectedAuthenticatedEncryptor);
+ var mockConfiguration = new Mock<AlgorithmConfiguration>();
+ mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(expectedDescriptor);
+ var mockXmlRepository = new Mock<IXmlRepository>();
+ mockXmlRepository
+ .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
+ .Callback<XElement, string>((el, friendlyName) =>
+ {
+ elementStoredInRepository = el;
+ friendlyNameStoredInRepository = friendlyName;
+ });
+ var mockKeyEscrow = new Mock<IKeyEscrowSink>();
+ mockKeyEscrow
+ .Setup(o => o.Store(It.IsAny<Guid>(), It.IsAny<XElement>()))
+ .Callback<Guid, XElement>((innerKeyId, el) =>
+ {
+ keyIdStoredInEscrow = innerKeyId;
+ elementStoredInEscrow = el;
+ });
+
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = mockConfiguration.Object,
+ XmlRepository = mockXmlRepository.Object,
+ XmlEncryptor = new NullXmlEncryptor()
+ });
+ options.Value.AuthenticatedEncryptorFactories.Add(testEncryptorFactory);
+ options.Value.KeyEscrowSinks.Add(mockKeyEscrow.Object);
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
+
+ // Act & assert
+
+ // The cancellation token should not already be fired
+ var firstCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.False(firstCancellationToken.IsCancellationRequested);
+
+ // After the call to CreateNewKey, the first CT should be fired,
+ // and we should've gotten a new CT.
+ var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey(
+ keyId: keyId,
+ creationDate: creationDate,
+ activationDate: activationDate,
+ expirationDate: expirationDate);
+ var secondCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.True(firstCancellationToken.IsCancellationRequested);
+ Assert.False(secondCancellationToken.IsCancellationRequested);
+
+ // Does the IKey have the properties we requested?
+ Assert.Equal(keyId, newKey.KeyId);
+ Assert.Equal(creationDate, newKey.CreationDate);
+ Assert.Equal(activationDate, newKey.ActivationDate);
+ Assert.Equal(expirationDate, newKey.ExpirationDate);
+ Assert.Same(expectedDescriptor, newKey.Descriptor);
+ Assert.False(newKey.IsRevoked);
+ Assert.Same(expectedAuthenticatedEncryptor, testEncryptorFactory.CreateEncryptorInstance(newKey));
+
+ // Was the correct element stored in escrow?
+ // This should not have gone through the encryptor.
+ string expectedEscrowXml = string.Format(@"
+ <key id='3d6d01fd-c0e7-44ae-82dd-013b996b4093' version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ {1}
+ {2}
+ {3}
+ <descriptor deserializerType='{0}'>
+ <theElement>
+ <secret enc:requiresEncryption='true'>
+ <![CDATA[This is a secret value.]]>
+ </secret>
+ </theElement>
+ </descriptor>
+ </key>",
+ typeof(MyDeserializer).AssemblyQualifiedName,
+ new XElement("creationDate", creationDate),
+ new XElement("activationDate", activationDate),
+ new XElement("expirationDate", expirationDate));
+ XmlAssert.Equal(expectedEscrowXml, elementStoredInEscrow);
+ Assert.Equal(keyId, keyIdStoredInEscrow.Value);
+
+ // Finally, was the correct element stored in the repository?
+ // This should have gone through the encryptor (which we set to be the null encryptor in this test)
+ string expectedRepositoryXml = String.Format(@"
+ <key id='3d6d01fd-c0e7-44ae-82dd-013b996b4093' version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ {2}
+ {3}
+ {4}
+ <descriptor deserializerType='{0}'>
+ <theElement>
+ <enc:encryptedSecret decryptorType='{1}'>
+ <unencryptedKey>
+ <secret enc:requiresEncryption='true'>
+ <![CDATA[This is a secret value.]]>
+ </secret>
+ </unencryptedKey>
+ </enc:encryptedSecret>
+ </theElement>
+ </descriptor>
+ </key>",
+ typeof(MyDeserializer).AssemblyQualifiedName,
+ typeof(NullXmlDecryptor).AssemblyQualifiedName,
+ new XElement("creationDate", creationDate),
+ new XElement("activationDate", activationDate),
+ new XElement("expirationDate", expirationDate));
+ XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
+ Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository);
+ }
+
+ [Fact]
+ public void CreateNewKey_CallsInternalManager()
+ {
+ // Arrange
+ DateTimeOffset minCreationDate = DateTimeOffset.UtcNow;
+ DateTimeOffset? actualCreationDate = null;
+ DateTimeOffset activationDate = minCreationDate + TimeSpan.FromDays(7);
+ DateTimeOffset expirationDate = activationDate.AddMonths(1);
+ var mockInternalKeyManager = new Mock<IInternalXmlKeyManager>();
+ mockInternalKeyManager
+ .Setup(o => o.CreateNewKey(It.IsAny<Guid>(), It.IsAny<DateTimeOffset>(), activationDate, expirationDate))
+ .Callback<Guid, DateTimeOffset, DateTimeOffset, DateTimeOffset>((innerKeyId, innerCreationDate, innerActivationDate, innerExpirationDate) =>
+ {
+ actualCreationDate = innerCreationDate;
+ });
+
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = new Mock<IXmlRepository>().Object,
+ XmlEncryptor = null
+ });
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance, mockInternalKeyManager.Object);
+
+ // Act
+ keyManager.CreateNewKey(activationDate, expirationDate);
+
+ // Assert
+ Assert.InRange(actualCreationDate.Value, minCreationDate, DateTimeOffset.UtcNow);
+ }
+
+ [Fact]
+ public void GetAllKeys_Empty()
+ {
+ // Arrange
+ const string xml = @"<root />";
+ var activator = new Mock<IActivator>().Object;
+
+ // Act
+ var keys = RunGetAllKeysCore(xml, activator);
+
+ // Assert
+ Assert.Equal(0, keys.Count);
+ }
+
+ [Fact]
+ public void GetAllKeys_IgnoresUnknownElements()
+ {
+ // Arrange
+ const string xml = @"
+ <root>
+ <key id='62a72ad9-42d7-4e97-b3fa-05bad5d53d33' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>2015-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='deserializer-A'>
+ <elementA />
+ </descriptor>
+ </key>
+ <unknown>
+ <![CDATA[Unknown elements are ignored.]]>
+ </unknown>
+ <key id='041be4c0-52d7-48b4-8d32-f8c0ff315459' version='1'>
+ <creationDate>2015-04-01T00:00:00Z</creationDate>
+ <activationDate>2015-05-01T00:00:00Z</activationDate>
+ <expirationDate>2015-06-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='deserializer-B'>
+ <elementB />
+ </descriptor>
+ </key>
+ </root>";
+
+ var descriptorA = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
+ var descriptorB = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
+ var mockActivator = new Mock<IActivator>();
+ mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("deserializer-A", "<elementA />", descriptorA);
+ mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("deserializer-B", "<elementB />", descriptorB);
+
+ // Act
+ var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
+
+ // Assert
+ Assert.Equal(2, keys.Length);
+ Assert.Equal(new Guid("62a72ad9-42d7-4e97-b3fa-05bad5d53d33"), keys[0].KeyId);
+ Assert.Equal(XmlConvert.ToDateTimeOffset("2015-01-01T00:00:00Z"), keys[0].CreationDate);
+ Assert.Equal(XmlConvert.ToDateTimeOffset("2015-02-01T00:00:00Z"), keys[0].ActivationDate);
+ Assert.Equal(XmlConvert.ToDateTimeOffset("2015-03-01T00:00:00Z"), keys[0].ExpirationDate);
+ Assert.False(keys[0].IsRevoked);
+ Assert.Same(descriptorA, keys[0].Descriptor);
+ Assert.Equal(new Guid("041be4c0-52d7-48b4-8d32-f8c0ff315459"), keys[1].KeyId);
+ Assert.Equal(XmlConvert.ToDateTimeOffset("2015-04-01T00:00:00Z"), keys[1].CreationDate);
+ Assert.Equal(XmlConvert.ToDateTimeOffset("2015-05-01T00:00:00Z"), keys[1].ActivationDate);
+ Assert.Equal(XmlConvert.ToDateTimeOffset("2015-06-01T00:00:00Z"), keys[1].ExpirationDate);
+ Assert.False(keys[1].IsRevoked);
+ Assert.Same(descriptorB, keys[1].Descriptor);
+ }
+
+ [Fact]
+ public void GetAllKeys_UnderstandsRevocations()
+ {
+ // Arrange
+ const string xml = @"
+ <root>
+ <key id='67f9cdea-83ba-41ed-b160-2b1d0ea30251' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>2015-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='theDeserializer'>
+ <node />
+ </descriptor>
+ </key>
+ <key id='0cf83742-d175-42a8-94b5-1ec049b354c3' version='1'>
+ <creationDate>2016-01-01T00:00:00Z</creationDate>
+ <activationDate>2016-02-01T00:00:00Z</activationDate>
+ <expirationDate>2016-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='theDeserializer'>
+ <node />
+ </descriptor>
+ </key>
+ <key id='21580ac4-c83a-493c-bde6-29a1cc97ca0f' version='1'>
+ <creationDate>2017-01-01T00:00:00Z</creationDate>
+ <activationDate>2017-02-01T00:00:00Z</activationDate>
+ <expirationDate>2017-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='theDeserializer'>
+ <node />
+ </descriptor>
+ </key>
+ <key id='6bd14f12-0bb8-4822-91d7-04b360de0497' version='1'>
+ <creationDate>2018-01-01T00:00:00Z</creationDate>
+ <activationDate>2018-02-01T00:00:00Z</activationDate>
+ <expirationDate>2018-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='theDeserializer'>
+ <node />
+ </descriptor>
+ </key>
+ <revocation version='1'>
+ <!-- The below will revoke no keys. -->
+ <revocationDate>2014-01-01T00:00:00Z</revocationDate>
+ <key id='*' />
+ </revocation>
+ <revocation version='1'>
+ <!-- The below will revoke the first two keys. -->
+ <revocationDate>2017-01-01T00:00:00Z</revocationDate>
+ <key id='*' />
+ </revocation>
+ <revocation version='1'>
+ <!-- The below will revoke only the last key. -->
+ <revocationDate>2020-01-01T00:00:00Z</revocationDate>
+ <key id='6bd14f12-0bb8-4822-91d7-04b360de0497' />
+ </revocation>
+ </root>";
+
+ var mockActivator = new Mock<IActivator>();
+ mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("theDeserializer", "<node />", new Mock<IAuthenticatedEncryptorDescriptor>().Object);
+
+ // Act
+ var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
+
+ // Assert
+ Assert.Equal(4, keys.Length);
+ Assert.Equal(new Guid("67f9cdea-83ba-41ed-b160-2b1d0ea30251"), keys[0].KeyId);
+ Assert.True(keys[0].IsRevoked);
+ Assert.Equal(new Guid("0cf83742-d175-42a8-94b5-1ec049b354c3"), keys[1].KeyId);
+ Assert.True(keys[1].IsRevoked);
+ Assert.Equal(new Guid("21580ac4-c83a-493c-bde6-29a1cc97ca0f"), keys[2].KeyId);
+ Assert.False(keys[2].IsRevoked);
+ Assert.Equal(new Guid("6bd14f12-0bb8-4822-91d7-04b360de0497"), keys[3].KeyId);
+ Assert.True(keys[3].IsRevoked);
+ }
+
+ [Fact]
+ public void GetAllKeys_PerformsDecryption()
+ {
+ // Arrange
+ const string xml = @"
+ <root xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
+ <key id='09712588-ba68-438a-a5ee-fe842b3453b2' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>2015-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='theDeserializer'>
+ <enc:encryptedSecret decryptorType='theDecryptor'>
+ <node xmlns='private' />
+ </enc:encryptedSecret>
+ </descriptor>
+ </key>
+ </root>";
+
+ var expectedDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
+ var mockActivator = new Mock<IActivator>();
+ mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput("theDecryptor", "<node xmlns='private' />", "<decryptedNode />");
+ mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("theDeserializer", "<decryptedNode />", expectedDescriptor);
+
+ // Act
+ var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
+
+ // Assert
+ Assert.Single(keys);
+ Assert.Equal(new Guid("09712588-ba68-438a-a5ee-fe842b3453b2"), keys[0].KeyId);
+ Assert.Same(expectedDescriptor, keys[0].Descriptor);
+ }
+
+ [Fact]
+ public void GetAllKeys_SwallowsKeyDeserializationErrors()
+ {
+ // Arrange
+ const string xml = @"
+ <root>
+ <!-- The below key will throw an exception when deserializing. -->
+ <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>NOT A VALID DATE</expirationDate>
+ <descriptor deserializerType='badDeserializer'>
+ <node />
+ </descriptor>
+ </key>
+ <!-- The below key will deserialize properly. -->
+ <key id='49c0cda9-0232-4d8c-a541-de20cc5a73d6' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>2015-03-01T00:00:00Z</expirationDate>
+ <descriptor deserializerType='goodDeserializer'>
+ <node xmlns='private' />
+ </descriptor>
+ </key>
+ </root>";
+
+ var expectedDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
+ var mockActivator = new Mock<IActivator>();
+ mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("goodDeserializer", "<node xmlns='private' />", expectedDescriptor);
+
+ // Act
+ var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
+
+ // Assert
+ Assert.Single(keys);
+ Assert.Equal(new Guid("49c0cda9-0232-4d8c-a541-de20cc5a73d6"), keys[0].KeyId);
+ Assert.Same(expectedDescriptor, keys[0].Descriptor);
+ }
+
+ [Fact]
+ public void GetAllKeys_WithKeyDeserializationError_LogLevelDebug_DoesNotWriteSensitiveInformation()
+ {
+ // Arrange
+ const string xml = @"
+ <root>
+ <!-- The below key will throw an exception when deserializing. -->
+ <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>NOT A VALID DATE</expirationDate>
+ <!-- Secret information: 1A2B3C4D -->
+ </key>
+ </root>";
+
+ var loggerFactory = new StringLoggerFactory(LogLevel.Debug);
+
+ // Act
+ RunGetAllKeysCore(xml, new Mock<IActivator>().Object, loggerFactory).ToArray();
+
+ // Assert
+ Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged.");
+ }
+
+ [Fact]
+ public void GetAllKeys_WithKeyDeserializationError_LogLevelTrace_WritesSensitiveInformation()
+ {
+ // Arrange
+ const string xml = @"
+ <root>
+ <!-- The below key will throw an exception when deserializing. -->
+ <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
+ <creationDate>2015-01-01T00:00:00Z</creationDate>
+ <activationDate>2015-02-01T00:00:00Z</activationDate>
+ <expirationDate>NOT A VALID DATE</expirationDate>
+ <!-- Secret information: 1A2B3C4D -->
+ </key>
+ </root>";
+
+ var loggerFactory = new StringLoggerFactory(LogLevel.Trace);
+
+ // Act
+ RunGetAllKeysCore(xml, new Mock<IActivator>().Object, loggerFactory).ToArray();
+
+ // Assert
+ Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged.");
+ }
+
+ [Fact]
+ public void GetAllKeys_SurfacesRevocationDeserializationErrors()
+ {
+ // Arrange
+ const string xml = @"
+ <root>
+ <revocation version='1'>
+ <revocationDate>2015-01-01T00:00:00Z</revocationDate>
+ <key id='{invalid}' />
+ </revocation>
+ </root>";
+
+ // Act & assert
+ // Bad GUID will lead to FormatException
+ Assert.Throws<FormatException>(() => RunGetAllKeysCore(xml, new Mock<IActivator>().Object));
+ }
+
+ private static IReadOnlyCollection<IKey> RunGetAllKeysCore(string xml, IActivator activator, ILoggerFactory loggerFactory = null)
+ {
+ // Arrange
+ var mockXmlRepository = new Mock<IXmlRepository>();
+ mockXmlRepository.Setup(o => o.GetAllElements()).Returns(XElement.Parse(xml).Elements().ToArray());
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = mockXmlRepository.Object,
+ XmlEncryptor = null
+ });
+ var keyManager = new XmlKeyManager(options, activator, loggerFactory ?? NullLoggerFactory.Instance);
+
+ // Act
+ return keyManager.GetAllKeys();
+ }
+
+ [Fact]
+ public void RevokeAllKeys()
+ {
+ // Arrange
+ XElement elementStoredInRepository = null;
+ string friendlyNameStoredInRepository = null;
+ var mockXmlRepository = new Mock<IXmlRepository>();
+ mockXmlRepository
+ .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
+ .Callback<XElement, string>((el, friendlyName) =>
+ {
+ elementStoredInRepository = el;
+ friendlyNameStoredInRepository = friendlyName;
+ });
+
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = mockXmlRepository.Object,
+ XmlEncryptor = null
+ });
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
+
+ var revocationDate = XmlConvert.ToDateTimeOffset("2015-03-01T19:13:19.7573854-08:00");
+
+ // Act & assert
+
+ // The cancellation token should not already be fired
+ var firstCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.False(firstCancellationToken.IsCancellationRequested);
+
+ // After the call to RevokeAllKeys, the first CT should be fired,
+ // and we should've gotten a new CT.
+ keyManager.RevokeAllKeys(revocationDate, "Here's some reason text.");
+ var secondCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.True(firstCancellationToken.IsCancellationRequested);
+ Assert.False(secondCancellationToken.IsCancellationRequested);
+
+ // Was the correct element stored in the repository?
+ const string expectedRepositoryXml = @"
+ <revocation version='1'>
+ <revocationDate>2015-03-01T19:13:19.7573854-08:00</revocationDate>
+ <!--All keys created before the revocation date are revoked.-->
+ <key id='*' />
+ <reason>Here's some reason text.</reason>
+ </revocation>";
+ XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
+ Assert.Equal("revocation-20150302T0313197573854Z", friendlyNameStoredInRepository);
+ }
+
+ [Fact]
+ public void RevokeSingleKey_Internal()
+ {
+ // Arrange - mocks
+ XElement elementStoredInRepository = null;
+ string friendlyNameStoredInRepository = null;
+ var mockXmlRepository = new Mock<IXmlRepository>();
+ mockXmlRepository
+ .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
+ .Callback<XElement, string>((el, friendlyName) =>
+ {
+ elementStoredInRepository = el;
+ friendlyNameStoredInRepository = friendlyName;
+ });
+
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = mockXmlRepository.Object,
+ XmlEncryptor = null
+ });
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
+
+ var revocationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
+
+ // Act & assert
+
+ // The cancellation token should not already be fired
+ var firstCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.False(firstCancellationToken.IsCancellationRequested);
+
+ // After the call to RevokeKey, the first CT should be fired,
+ // and we should've gotten a new CT.
+ ((IInternalXmlKeyManager)keyManager).RevokeSingleKey(
+ keyId: new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932"),
+ revocationDate: revocationDate,
+ reason: "Here's some reason text.");
+ var secondCancellationToken = keyManager.GetCacheExpirationToken();
+ Assert.True(firstCancellationToken.IsCancellationRequested);
+ Assert.False(secondCancellationToken.IsCancellationRequested);
+
+ // Was the correct element stored in the repository?
+ var expectedRepositoryXml = string.Format(@"
+ <revocation version='1'>
+ {0}
+ <key id='a11f35fc-1fed-4bd4-b727-056a63b70932' />
+ <reason>Here's some reason text.</reason>
+ </revocation>",
+ new XElement("revocationDate", revocationDate));
+ XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
+ Assert.Equal("revocation-a11f35fc-1fed-4bd4-b727-056a63b70932", friendlyNameStoredInRepository);
+ }
+
+ [Fact]
+ public void RevokeKey_CallsInternalManager()
+ {
+ // Arrange
+ var keyToRevoke = new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932");
+ DateTimeOffset minRevocationDate = DateTimeOffset.UtcNow;
+ DateTimeOffset? actualRevocationDate = null;
+ var mockInternalKeyManager = new Mock<IInternalXmlKeyManager>();
+ mockInternalKeyManager
+ .Setup(o => o.RevokeSingleKey(keyToRevoke, It.IsAny<DateTimeOffset>(), "Here's some reason text."))
+ .Callback<Guid, DateTimeOffset, string>((innerKeyId, innerRevocationDate, innerReason) =>
+ {
+ actualRevocationDate = innerRevocationDate;
+ });
+
+ var options = Options.Create(new KeyManagementOptions()
+ {
+ AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
+ XmlRepository = new Mock<IXmlRepository>().Object,
+ XmlEncryptor = null
+ });
+ var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance, mockInternalKeyManager.Object);
+
+ // Act
+ keyManager.RevokeKey(keyToRevoke, "Here's some reason text.");
+
+ // Assert
+ Assert.InRange(actualRevocationDate.Value, minRevocationDate, DateTimeOffset.UtcNow);
+ }
+
+ private class MyDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class TestEncryptorFactory : IAuthenticatedEncryptorFactory
+ {
+ private IAuthenticatedEncryptorDescriptor _associatedDescriptor;
+ private IAuthenticatedEncryptor _expectedEncryptor;
+
+ public TestEncryptorFactory(IAuthenticatedEncryptorDescriptor associatedDescriptor = null, IAuthenticatedEncryptor expectedEncryptor = null)
+ {
+ _associatedDescriptor = associatedDescriptor;
+ _expectedEncryptor = expectedEncryptor;
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance(IKey key)
+ {
+ if (_associatedDescriptor != null && _associatedDescriptor != key.Descriptor)
+ {
+ return null;
+ }
+
+ return _expectedEncryptor ?? new Mock<IAuthenticatedEncryptor>().Object;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs
new file mode 100644
index 0000000000..d279f73cf6
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.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.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Managed
+{
+ public class ManagedAuthenticatedEncryptorTests
+ {
+ [Fact]
+ public void Encrypt_Decrypt_RoundTrips()
+ {
+ // Arrange
+ Secret kdk = new Secret(new byte[512 / 8]);
+ ManagedAuthenticatedEncryptor encryptor = new ManagedAuthenticatedEncryptor(kdk,
+ symmetricAlgorithmFactory: Aes.Create,
+ symmetricAlgorithmKeySizeInBytes: 256 / 8,
+ validationAlgorithmFactory: () => new HMACSHA256());
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
+ ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
+
+ // Act
+ byte[] ciphertext = encryptor.Encrypt(plaintext, aad);
+ byte[] decipheredtext = encryptor.Decrypt(new ArraySegment<byte>(ciphertext), aad);
+
+ // Assert
+ Assert.Equal(plaintext, decipheredtext);
+ }
+
+ [Fact]
+ public void Encrypt_Decrypt_Tampering_Fails()
+ {
+ // Arrange
+ Secret kdk = new Secret(new byte[512 / 8]);
+ ManagedAuthenticatedEncryptor encryptor = new ManagedAuthenticatedEncryptor(kdk,
+ symmetricAlgorithmFactory: Aes.Create,
+ symmetricAlgorithmKeySizeInBytes: 256 / 8,
+ validationAlgorithmFactory: () => new HMACSHA256());
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
+ ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("aad"));
+ byte[] validCiphertext = encryptor.Encrypt(plaintext, aad);
+
+ // Act & assert - 1
+ // Ciphertext is too short to be a valid payload
+ byte[] invalidCiphertext_tooShort = new byte[10];
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_tooShort), aad);
+ });
+
+ // Act & assert - 2
+ // Ciphertext has been manipulated
+ byte[] invalidCiphertext_manipulated = (byte[])validCiphertext.Clone();
+ invalidCiphertext_manipulated[0] ^= 0x01;
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_manipulated), aad);
+ });
+
+ // Act & assert - 3
+ // Ciphertext is too long
+ byte[] invalidCiphertext_tooLong = validCiphertext.Concat(new byte[] { 0 }).ToArray();
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(invalidCiphertext_tooLong), aad);
+ });
+
+ // Act & assert - 4
+ // AAD is incorrect
+ Assert.Throws<CryptographicException>(() =>
+ {
+ encryptor.Decrypt(new ArraySegment<byte>(validCiphertext), new ArraySegment<byte>(Encoding.UTF8.GetBytes("different aad")));
+ });
+ }
+
+ [Fact]
+ public void Encrypt_KnownKey()
+ {
+ // Arrange
+ Secret kdk = new Secret(Encoding.UTF8.GetBytes("master key"));
+ ManagedAuthenticatedEncryptor encryptor = new ManagedAuthenticatedEncryptor(kdk,
+ symmetricAlgorithmFactory: Aes.Create,
+ symmetricAlgorithmKeySizeInBytes: 256 / 8,
+ validationAlgorithmFactory: () => new HMACSHA256(),
+ genRandom: new SequentialGenRandom());
+ ArraySegment<byte> plaintext = new ArraySegment<byte>(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, 2, 3);
+ ArraySegment<byte> aad = new ArraySegment<byte>(new byte[] { 7, 6, 5, 4, 3, 2, 1, 0 }, 1, 4);
+
+ // Act
+ byte[] retVal = encryptor.Encrypt(
+ plaintext: plaintext,
+ additionalAuthenticatedData: aad);
+
+ // Assert
+
+ // retVal := 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (keyModifier)
+ // | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F (IV)
+ // | B7 EA 3E 32 58 93 A3 06 03 89 C6 66 03 63 08 4B (encryptedData)
+ // | 9D 8A 85 C7 0F BD 98 D8 7F 72 E7 72 3E B5 A6 26 (HMAC)
+ // | 6C 38 77 F7 66 19 A2 C9 2C BB AD DA E7 62 00 00
+
+ string retValAsString = Convert.ToBase64String(retVal);
+ Assert.Equal("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh+36j4yWJOjBgOJxmYDYwhLnYqFxw+9mNh/cudyPrWmJmw4d/dmGaLJLLut2udiAAA=", retValAsString);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj
new file mode 100644
index 0000000000..bf45498fbf
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\shared\*.cs" />
+ <Content Include="TestFiles\**" CopyToOutputDirectory="PreserveNewest" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/MockExtensions.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/MockExtensions.cs
new file mode 100644
index 0000000000..76f5dc94e6
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/MockExtensions.cs
@@ -0,0 +1,62 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Moq;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal static class MockExtensions
+ {
+ /// <summary>
+ /// Sets up a mock such that given the name of a deserializer class and the XML node that class's
+ /// Import method should expect returns a descriptor which produces the given authenticator.
+ /// </summary>
+ public static void ReturnDescriptorGivenDeserializerTypeNameAndInput(this Mock<IActivator> mockActivator, string typeName, string xml, IAuthenticatedEncryptorDescriptor descriptor)
+ {
+ mockActivator
+ .Setup(o => o.CreateInstance(typeof(IAuthenticatedEncryptorDescriptorDeserializer), typeName))
+ .Returns(() =>
+ {
+ var mockDeserializer = new Mock<IAuthenticatedEncryptorDescriptorDeserializer>();
+ mockDeserializer
+ .Setup(o => o.ImportFromXml(It.IsAny<XElement>()))
+ .Returns<XElement>(el =>
+ {
+ // Only return the descriptor if the XML matches
+ XmlAssert.Equal(xml, el);
+ return descriptor;
+ });
+ return mockDeserializer.Object;
+ });
+ }
+
+ /// <summary>
+ /// Sets up a mock such that given the name of a decryptor class and the XML node that class's
+ /// Decrypt method should expect returns the specified XML elmeent.
+ /// </summary>
+ public static void ReturnDecryptedElementGivenDecryptorTypeNameAndInput(this Mock<IActivator> mockActivator, string typeName, string expectedInputXml, string outputXml)
+ {
+ mockActivator
+ .Setup(o => o.CreateInstance(typeof(IXmlDecryptor), typeName))
+ .Returns(() =>
+ {
+ var mockDecryptor = new Mock<IXmlDecryptor>();
+ mockDecryptor
+ .Setup(o => o.Decrypt(It.IsAny<XElement>()))
+ .Returns<XElement>(el =>
+ {
+ // Only return the descriptor if the XML matches
+ XmlAssert.Equal(expectedInputXml, el);
+ return XElement.Parse(outputXml);
+ });
+ return mockDecryptor.Object;
+ });
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Properties/AssemblyInfo.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3adbc7af4e
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// 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.Runtime.CompilerServices;
+
+// for unit testing
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/RegistryPolicyResolverTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/RegistryPolicyResolverTests.cs
new file mode 100644
index 0000000000..d10fd872cd
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/RegistryPolicyResolverTests.cs
@@ -0,0 +1,314 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Win32;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class RegistryPolicyResolverTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_NoEntries_ResultsInNoPolicies()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>();
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ Assert.Null(context.EncryptorConfiguration);
+ Assert.Null(context.DefaultKeyLifetime);
+ Assert.Empty(context.KeyEscrowSinks);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_KeyEscrowSinks()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["KeyEscrowSinks"] = String.Join(" ;; ; ", new Type[] { typeof(MyKeyEscrowSink1), typeof(MyKeyEscrowSink2) }.Select(t => t.AssemblyQualifiedName))
+ };
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualKeyEscrowSinks = context.KeyEscrowSinks.ToArray();
+ Assert.Equal(2, actualKeyEscrowSinks.Length);
+ Assert.IsType<MyKeyEscrowSink1>(actualKeyEscrowSinks[0]);
+ Assert.IsType<MyKeyEscrowSink2>(actualKeyEscrowSinks[1]);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_DefaultKeyLifetime()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["DefaultKeyLifetime"] = 1024 // days
+ };
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ Assert.Equal(1024, context.DefaultKeyLifetime);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_CngCbcEncryption_WithoutExplicitSettings()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["EncryptionType"] = "cng-cbc"
+ };
+ var expectedConfiguration = new CngCbcAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualConfiguration = (CngCbcAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;
+
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
+ Assert.Equal(expectedConfiguration.HashAlgorithm, actualConfiguration.HashAlgorithm);
+ Assert.Equal(expectedConfiguration.HashAlgorithmProvider, actualConfiguration.HashAlgorithmProvider);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_CngCbcEncryption_WithExplicitSettings()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["EncryptionType"] = "cng-cbc",
+ ["EncryptionAlgorithm"] = "enc-alg",
+ ["EncryptionAlgorithmKeySize"] = 2048,
+ ["EncryptionAlgorithmProvider"] = "my-enc-alg-provider",
+ ["HashAlgorithm"] = "hash-alg",
+ ["HashAlgorithmProvider"] = "my-hash-alg-provider"
+ };
+ var expectedConfiguration = new CngCbcAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048,
+ EncryptionAlgorithmProvider = "my-enc-alg-provider",
+ HashAlgorithm = "hash-alg",
+ HashAlgorithmProvider = "my-hash-alg-provider"
+ };
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualConfiguration = (CngCbcAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;
+
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
+ Assert.Equal(expectedConfiguration.HashAlgorithm, actualConfiguration.HashAlgorithm);
+ Assert.Equal(expectedConfiguration.HashAlgorithmProvider, actualConfiguration.HashAlgorithmProvider);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_CngGcmEncryption_WithoutExplicitSettings()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["EncryptionType"] = "cng-gcm"
+ };
+ var expectedConfiguration = new CngGcmAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualConfiguration = (CngGcmAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;
+
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_CngGcmEncryption_WithExplicitSettings()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["EncryptionType"] = "cng-gcm",
+ ["EncryptionAlgorithm"] = "enc-alg",
+ ["EncryptionAlgorithmKeySize"] = 2048,
+ ["EncryptionAlgorithmProvider"] = "my-enc-alg-provider"
+ };
+ var expectedConfiguration = new CngGcmAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048,
+ EncryptionAlgorithmProvider = "my-enc-alg-provider"
+ };
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualConfiguration = (CngGcmAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;
+
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithm, actualConfiguration.EncryptionAlgorithm);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmProvider, actualConfiguration.EncryptionAlgorithmProvider);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_ManagedEncryption_WithoutExplicitSettings()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["EncryptionType"] = "managed"
+ };
+ var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration();
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualConfiguration = (ManagedAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;
+
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmType, actualConfiguration.EncryptionAlgorithmType);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
+ Assert.Equal(expectedConfiguration.ValidationAlgorithmType, actualConfiguration.ValidationAlgorithmType);
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void ResolvePolicy_ManagedEncryption_WithExplicitSettings()
+ {
+ // Arrange
+ var registryEntries = new Dictionary<string, object>()
+ {
+ ["EncryptionType"] = "managed",
+ ["EncryptionAlgorithmType"] = typeof(TripleDES).AssemblyQualifiedName,
+ ["EncryptionAlgorithmKeySize"] = 2048,
+ ["ValidationAlgorithmType"] = typeof(HMACSHA1).AssemblyQualifiedName
+ };
+ var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration()
+ {
+ EncryptionAlgorithmType = typeof(TripleDES),
+ EncryptionAlgorithmKeySize = 2048,
+ ValidationAlgorithmType = typeof(HMACSHA1)
+ };
+
+ // Act
+ var context = RunTestWithRegValues(registryEntries);
+
+ // Assert
+ var actualConfiguration = (ManagedAuthenticatedEncryptorConfiguration)context.EncryptorConfiguration;
+
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmType, actualConfiguration.EncryptionAlgorithmType);
+ Assert.Equal(expectedConfiguration.EncryptionAlgorithmKeySize, actualConfiguration.EncryptionAlgorithmKeySize);
+ Assert.Equal(expectedConfiguration.ValidationAlgorithmType, actualConfiguration.ValidationAlgorithmType);
+ }
+
+ private static RegistryPolicy RunTestWithRegValues(Dictionary<string, object> regValues)
+ {
+ return WithUniqueTempRegKey(registryKey =>
+ {
+ foreach (var entry in regValues)
+ {
+ registryKey.SetValue(entry.Key, entry.Value);
+ }
+
+ var policyResolver = new RegistryPolicyResolver(
+ registryKey,
+ activator: SimpleActivator.DefaultWithoutServices);
+
+ return policyResolver.ResolvePolicy();
+ });
+ }
+
+ /// <summary>
+ /// Runs a test and cleans up the registry key afterward.
+ /// </summary>
+ private static RegistryPolicy WithUniqueTempRegKey(Func<RegistryKey, RegistryPolicy> testCode)
+ {
+ string uniqueName = Guid.NewGuid().ToString();
+ var uniqueSubkey = LazyHkcuTempKey.Value.CreateSubKey(uniqueName);
+ try
+ {
+ return testCode(uniqueSubkey);
+ }
+ finally
+ {
+ // clean up when test is done
+ LazyHkcuTempKey.Value.DeleteSubKeyTree(uniqueName, throwOnMissingSubKey: false);
+ }
+ }
+
+ private static readonly Lazy<RegistryKey> LazyHkcuTempKey = new Lazy<RegistryKey>(() =>
+ {
+ try
+ {
+ return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\ASP.NET\temp");
+ }
+ catch
+ {
+ // swallow all failures
+ return null;
+ }
+ });
+
+ private class ConditionalRunTestOnlyIfHkcuRegistryAvailable : Attribute, ITestCondition
+ {
+ public bool IsMet => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && LazyHkcuTempKey.Value != null);
+
+ public string SkipReason { get; } = "HKCU registry couldn't be opened.";
+ }
+
+ private class MyKeyEscrowSink1 : IKeyEscrowSink
+ {
+ public void Store(Guid keyId, XElement element)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyKeyEscrowSink2 : IKeyEscrowSink
+ {
+ public void Store(Guid keyId, XElement element)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.cs
new file mode 100644
index 0000000000..b903267415
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.cs
@@ -0,0 +1,40 @@
+// 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.Xml.Linq;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ public class EphemeralXmlRepositoryTests
+ {
+ [Fact]
+ public void GetAllElements_Empty()
+ {
+ // Arrange
+ var repository = new EphemeralXmlRepository(NullLoggerFactory.Instance);
+
+ // Act & assert
+ Assert.Empty(repository.GetAllElements());
+ }
+
+ [Fact]
+ public void Store_Then_Get()
+ {
+ // Arrange
+ var element1 = XElement.Parse(@"<element1 />");
+ var element2 = XElement.Parse(@"<element1 />");
+ var element3 = XElement.Parse(@"<element1 />");
+ var repository = new EphemeralXmlRepository(NullLoggerFactory.Instance);
+
+ // Act & assert
+ repository.StoreElement(element1, "Invalid friendly name."); // nobody should care about the friendly name
+ repository.StoreElement(element2, "abcdefg");
+ Assert.Equal(new[] { element1, element2 }, repository.GetAllElements(), XmlAssert.EqualityComparer);
+ repository.StoreElement(element3, null);
+ Assert.Equal(new[] { element1, element2, element3 }, repository.GetAllElements(), XmlAssert.EqualityComparer);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs
new file mode 100644
index 0000000000..4bc2e10171
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs
@@ -0,0 +1,182 @@
+// 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.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ public class FileSystemXmlRepositoryTests
+ {
+ [Fact]
+ public void DefaultKeyStorageDirectory_Property()
+ {
+ var baseDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ASP.NET")
+ : Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".aspnet");
+ var expectedDir = new DirectoryInfo(Path.Combine(baseDir, "DataProtection-Keys")).FullName;
+
+ // Act
+ var defaultDirInfo = FileSystemXmlRepository.DefaultKeyStorageDirectory;
+
+ // Assert
+ Assert.Equal(expectedDir, defaultDirInfo.FullName);
+ }
+
+ [Fact]
+ public void Directory_Property()
+ {
+ WithUniqueTempDirectory(dirInfo =>
+ {
+ // Arrange
+ var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance);
+
+ // Act
+ var retVal = repository.Directory;
+
+ // Assert
+ Assert.Equal(dirInfo, retVal);
+ });
+ }
+
+ [Fact]
+ public void GetAllElements_EmptyOrNonexistentDirectory_ReturnsEmptyCollection()
+ {
+ WithUniqueTempDirectory(dirInfo =>
+ {
+ // Arrange
+ var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance);
+
+ // Act
+ var allElements = repository.GetAllElements();
+
+ // Assert
+ Assert.Equal(0, allElements.Count);
+ });
+ }
+
+ [Fact]
+ public void StoreElement_WithValidFriendlyName_UsesFriendlyName()
+ {
+ WithUniqueTempDirectory(dirInfo =>
+ {
+ // Arrange
+ var element = XElement.Parse("<element1 />");
+ var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance);
+
+ // Act
+ repository.StoreElement(element, "valid-friendly-name");
+
+ // Assert
+ var fileInfos = dirInfo.GetFiles();
+ var fileInfo = fileInfos.Single(); // only one file should've been created
+
+ // filename should be "valid-friendly-name.xml"
+ Assert.Equal("valid-friendly-name.xml", fileInfo.Name, StringComparer.OrdinalIgnoreCase);
+
+ // file contents should be "<element1 />"
+ var parsedElement = XElement.Parse(File.ReadAllText(fileInfo.FullName));
+ XmlAssert.Equal("<element1 />", parsedElement);
+ });
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("..")]
+ [InlineData("not*friendly")]
+ public void StoreElement_WithInvalidFriendlyName_CreatesNewGuidAsName(string friendlyName)
+ {
+ WithUniqueTempDirectory(dirInfo =>
+ {
+ // Arrange
+ var element = XElement.Parse("<element1 />");
+ var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance);
+
+ // Act
+ repository.StoreElement(element, friendlyName);
+
+ // Assert
+ var fileInfos = dirInfo.GetFiles();
+ var fileInfo = fileInfos.Single(); // only one file should've been created
+
+ // filename should be "{GUID}.xml"
+ var filename = fileInfo.Name;
+ Assert.EndsWith(".xml", filename, StringComparison.OrdinalIgnoreCase);
+ var filenameNoSuffix = filename.Substring(0, filename.Length - ".xml".Length);
+ Guid parsedGuid = Guid.Parse(filenameNoSuffix);
+ Assert.NotEqual(Guid.Empty, parsedGuid);
+
+ // file contents should be "<element1 />"
+ var parsedElement = XElement.Parse(File.ReadAllText(fileInfo.FullName));
+ XmlAssert.Equal("<element1 />", parsedElement);
+ });
+ }
+
+ [Fact]
+ public void StoreElements_ThenRetrieve_SeesAllElements()
+ {
+ WithUniqueTempDirectory(dirInfo =>
+ {
+ // Arrange
+ var repository = new FileSystemXmlRepository(dirInfo, NullLoggerFactory.Instance);
+
+ // Act
+ repository.StoreElement(new XElement("element1"), friendlyName: null);
+ repository.StoreElement(new XElement("element2"), friendlyName: null);
+ repository.StoreElement(new XElement("element3"), friendlyName: null);
+ var allElements = repository.GetAllElements();
+
+ // Assert
+ var orderedNames = allElements.Select(el => el.Name.LocalName).OrderBy(name => name);
+ Assert.Equal(new[] { "element1", "element2", "element3" }, orderedNames);
+ });
+ }
+
+ [ConditionalFact]
+ [DockerOnly]
+ [Trait("Docker", "true")]
+ public void Logs_DockerEphemeralFolders()
+ {
+ // Arrange
+ var loggerFactory = new StringLoggerFactory(LogLevel.Warning);
+ WithUniqueTempDirectory(dirInfo =>
+ {
+ // Act
+ var repo = new FileSystemXmlRepository(dirInfo, loggerFactory);
+
+ // Assert
+ Assert.Contains(Resources.FormatFileSystem_EphemeralKeysLocationInContainer(dirInfo.FullName), loggerFactory.ToString());
+ });
+ }
+
+ /// <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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs
new file mode 100644
index 0000000000..11f0060ca3
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs
@@ -0,0 +1,168 @@
+// 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.Linq;
+using System.Runtime.InteropServices;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Win32;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Repositories
+{
+ public class RegistryXmlRepositoryTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void RegistryKey_Property()
+ {
+ WithUniqueTempRegKey(regKey =>
+ {
+ // Arrange
+ var repository = new RegistryXmlRepository(regKey, NullLoggerFactory.Instance);
+
+ // Act
+ var retVal = repository.RegistryKey;
+
+ // Assert
+ Assert.Equal(regKey, retVal);
+ });
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void GetAllElements_EmptyOrNonexistentDirectory_ReturnsEmptyCollection()
+ {
+ WithUniqueTempRegKey(regKey =>
+ {
+ // Arrange
+ var repository = new RegistryXmlRepository(regKey, NullLoggerFactory.Instance);
+
+ // Act
+ var allElements = repository.GetAllElements();
+
+ // Assert
+ Assert.Equal(0, allElements.Count);
+ });
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void StoreElement_WithValidFriendlyName_UsesFriendlyName()
+ {
+ WithUniqueTempRegKey(regKey =>
+ {
+ // Arrange
+ var element = XElement.Parse("<element1 />");
+ var repository = new RegistryXmlRepository(regKey, NullLoggerFactory.Instance);
+
+ // Act
+ repository.StoreElement(element, "valid-friendly-name");
+
+ // Assert
+ var valueNames = regKey.GetValueNames();
+ var valueName = valueNames.Single(); // only one value should've been created
+
+ // value name should be "valid-friendly-name"
+ Assert.Equal("valid-friendly-name", valueName, StringComparer.OrdinalIgnoreCase);
+
+ // value contents should be "<element1 />"
+ var parsedElement = XElement.Parse(regKey.GetValue(valueName) as string);
+ XmlAssert.Equal("<element1 />", parsedElement);
+ });
+ }
+
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("..")]
+ [InlineData("not*friendly")]
+ public void StoreElement_WithInvalidFriendlyName_CreatesNewGuidAsName(string friendlyName)
+ {
+ WithUniqueTempRegKey(regKey =>
+ {
+ // Arrange
+ var element = XElement.Parse("<element1 />");
+ var repository = new RegistryXmlRepository(regKey, NullLoggerFactory.Instance);
+
+ // Act
+ repository.StoreElement(element, friendlyName);
+
+ // Assert
+ var valueNames = regKey.GetValueNames();
+ var valueName = valueNames.Single(); // only one value should've been created
+
+ // value name should be "{GUID}"
+ Guid parsedGuid = Guid.Parse(valueName as string);
+ Assert.NotEqual(Guid.Empty, parsedGuid);
+
+ // value contents should be "<element1 />"
+ var parsedElement = XElement.Parse(regKey.GetValue(valueName) as string);
+ XmlAssert.Equal("<element1 />", parsedElement);
+ });
+ }
+
+ [ConditionalFact]
+ [ConditionalRunTestOnlyIfHkcuRegistryAvailable]
+ public void StoreElements_ThenRetrieve_SeesAllElements()
+ {
+ WithUniqueTempRegKey(regKey =>
+ {
+ // Arrange
+ var repository = new RegistryXmlRepository(regKey, NullLoggerFactory.Instance);
+
+ // Act
+ repository.StoreElement(new XElement("element1"), friendlyName: null);
+ repository.StoreElement(new XElement("element2"), friendlyName: null);
+ repository.StoreElement(new XElement("element3"), friendlyName: null);
+ var allElements = repository.GetAllElements();
+
+ // Assert
+ var orderedNames = allElements.Select(el => el.Name.LocalName).OrderBy(name => name);
+ Assert.Equal(new[] { "element1", "element2", "element3" }, orderedNames);
+ });
+ }
+
+ /// <summary>
+ /// Runs a test and cleans up the registry key afterward.
+ /// </summary>
+ private static void WithUniqueTempRegKey(Action<RegistryKey> testCode)
+ {
+ string uniqueName = Guid.NewGuid().ToString();
+ var uniqueSubkey = LazyHkcuTempKey.Value.CreateSubKey(uniqueName);
+ try
+ {
+ testCode(uniqueSubkey);
+ }
+ finally
+ {
+ // clean up when test is done
+ LazyHkcuTempKey.Value.DeleteSubKeyTree(uniqueName, throwOnMissingSubKey: false);
+ }
+ }
+
+ private static readonly Lazy<RegistryKey> LazyHkcuTempKey = new Lazy<RegistryKey>(() =>
+ {
+ try
+ {
+ return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\ASP.NET\temp");
+ }
+ catch
+ {
+ // swallow all failures
+ return null;
+ }
+ });
+
+ private class ConditionalRunTestOnlyIfHkcuRegistryAvailable : Attribute, ITestCondition
+ {
+ public bool IsMet => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && LazyHkcuTempKey.Value != null);
+
+ public string SkipReason { get; } = "HKCU registry couldn't be opened.";
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SP800_108/SP800_108Tests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SP800_108/SP800_108Tests.cs
new file mode 100644
index 0000000000..871ca83f5b
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SP800_108/SP800_108Tests.cs
@@ -0,0 +1,173 @@
+// 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.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.SP800_108
+{
+ public unsafe class SP800_108Tests
+ {
+ private delegate ISP800_108_CTR_HMACSHA512Provider ProviderFactory(byte* pbKdk, uint cbKdk);
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
+ [Theory]
+ [InlineData(512 / 8 - 1, "V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")]
+ [InlineData(512 / 8 + 0, "mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")]
+ [InlineData(512 / 8 + 1, "GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")]
+ public void DeriveKeyWithContextHeader_Normal_Managed(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ // Arrange
+ byte[] kdk = Encoding.UTF8.GetBytes("kdk");
+ byte[] label = Encoding.UTF8.GetBytes("label");
+ byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
+ byte[] context = Encoding.UTF8.GetBytes("context");
+
+ // Act & assert
+ TestManagedKeyDerivation(kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
+ [InlineData(512 / 8 - 1, "V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")]
+ [InlineData(512 / 8 + 0, "mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")]
+ [InlineData(512 / 8 + 1, "GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")]
+ public void DeriveKeyWithContextHeader_Normal_Win7(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ // Arrange
+ byte[] kdk = Encoding.UTF8.GetBytes("kdk");
+ byte[] label = Encoding.UTF8.GetBytes("label");
+ byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
+ byte[] context = Encoding.UTF8.GetBytes("context");
+
+ // Act & assert
+ TestCngKeyDerivation((pbKdk, cbKdk) => new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
+ [InlineData(512 / 8 - 1, "V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")]
+ [InlineData(512 / 8 + 0, "mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")]
+ [InlineData(512 / 8 + 1, "GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")]
+ public void DeriveKeyWithContextHeader_Normal_Win8(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ // Arrange
+ byte[] kdk = Encoding.UTF8.GetBytes("kdk");
+ byte[] label = Encoding.UTF8.GetBytes("label");
+ byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
+ byte[] context = Encoding.UTF8.GetBytes("context");
+
+ // Act & assert
+ TestCngKeyDerivation((pbKdk, cbKdk) => new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
+ [Theory]
+ [InlineData(512 / 8 - 1, "rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")]
+ [InlineData(512 / 8 + 0, "RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")]
+ [InlineData(512 / 8 + 1, "KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")]
+ public void DeriveKeyWithContextHeader_LongKey_Managed(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ // Arrange
+ byte[] kdk = new byte[50000]; // CNG can't normally handle a 50,000 byte KDK, but we coerce it into working :)
+ for (int i = 0; i < kdk.Length; i++)
+ {
+ kdk[i] = (byte)i;
+ }
+
+ byte[] label = Encoding.UTF8.GetBytes("label");
+ byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
+ byte[] context = Encoding.UTF8.GetBytes("context");
+
+ // Act & assert
+ TestManagedKeyDerivation(kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
+ [InlineData(512 / 8 - 1, "rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")]
+ [InlineData(512 / 8 + 0, "RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")]
+ [InlineData(512 / 8 + 1, "KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")]
+ public void DeriveKeyWithContextHeader_LongKey_Win7(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ // Arrange
+ byte[] kdk = new byte[50000]; // CNG can't normally handle a 50,000 byte KDK, but we coerce it into working :)
+ for (int i = 0; i < kdk.Length; i++)
+ {
+ kdk[i] = (byte)i;
+ }
+
+ byte[] label = Encoding.UTF8.GetBytes("label");
+ byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
+ byte[] context = Encoding.UTF8.GetBytes("context");
+
+ // Act & assert
+ TestCngKeyDerivation((pbKdk, cbKdk) => new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
+ }
+
+ // The 'numBytesRequested' parameters below are chosen to exercise code paths where
+ // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
+ [InlineData(512 / 8 - 1, "rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")]
+ [InlineData(512 / 8 + 0, "RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")]
+ [InlineData(512 / 8 + 1, "KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")]
+ public void DeriveKeyWithContextHeader_LongKey_Win8(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ // Arrange
+ byte[] kdk = new byte[50000]; // CNG can't normally handle a 50,000 byte KDK, but we coerce it into working :)
+ for (int i = 0; i < kdk.Length; i++)
+ {
+ kdk[i] = (byte)i;
+ }
+
+ byte[] label = Encoding.UTF8.GetBytes("label");
+ byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
+ byte[] context = Encoding.UTF8.GetBytes("context");
+
+ // Act & assert
+ TestCngKeyDerivation((pbKdk, cbKdk) => new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
+ }
+
+ private static void TestCngKeyDerivation(ProviderFactory factory, byte[] kdk, byte[] label, byte[] contextHeader, byte[] context, int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ byte[] derivedSubkey = new byte[numDerivedBytes];
+
+ fixed (byte* pbKdk = kdk)
+ fixed (byte* pbLabel = label)
+ fixed (byte* pbContext = context)
+ fixed (byte* pbDerivedSubkey = derivedSubkey)
+ {
+ ISP800_108_CTR_HMACSHA512Provider provider = factory(pbKdk, (uint)kdk.Length);
+ provider.DeriveKeyWithContextHeader(pbLabel, (uint)label.Length, contextHeader, pbContext, (uint)context.Length, pbDerivedSubkey, (uint)derivedSubkey.Length);
+ }
+
+ Assert.Equal(expectedDerivedSubkeyAsBase64, Convert.ToBase64String(derivedSubkey));
+ }
+
+ private static void TestManagedKeyDerivation(byte[] kdk, byte[] label, byte[] contextHeader, byte[] context, int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
+ {
+ var labelSegment = new ArraySegment<byte>(new byte[label.Length + 10], 3, label.Length);
+ Buffer.BlockCopy(label, 0, labelSegment.Array, labelSegment.Offset, labelSegment.Count);
+ var contextSegment = new ArraySegment<byte>(new byte[context.Length + 10], 5, context.Length);
+ Buffer.BlockCopy(context, 0, contextSegment.Array, contextSegment.Offset, contextSegment.Count);
+ var derivedSubkeySegment = new ArraySegment<byte>(new byte[numDerivedBytes + 10], 4, numDerivedBytes);
+
+ ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(kdk, labelSegment, contextHeader, contextSegment,
+ bytes => new HMACSHA512(bytes), derivedSubkeySegment);
+ Assert.Equal(expectedDerivedSubkeyAsBase64, Convert.ToBase64String(derivedSubkeySegment.AsStandaloneArray()));
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretAssert.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretAssert.cs
new file mode 100644
index 0000000000..d3fb1cbc70
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretAssert.cs
@@ -0,0 +1,45 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Helpful ISecret-based assertions.
+ /// </summary>
+ public static class SecretAssert
+ {
+ /// <summary>
+ /// Asserts that two <see cref="ISecret"/> instances contain the same material.
+ /// </summary>
+ public static void Equal(ISecret secret1, ISecret secret2)
+ {
+ Assert.Equal(SecretToBase64String(secret1), SecretToBase64String(secret2));
+ }
+
+ /// <summary>
+ /// Asserts that <paramref name="secret"/> has the length specified by <paramref name="expectedLengthInBits"/>.
+ /// </summary>
+ public static void LengthIs(int expectedLengthInBits, ISecret secret)
+ {
+ Assert.Equal(expectedLengthInBits, checked(secret.Length * 8));
+ }
+
+ /// <summary>
+ /// Asserts that two <see cref="ISecret"/> instances do not contain the same material.
+ /// </summary>
+ public static void NotEqual(ISecret secret1, ISecret secret2)
+ {
+ Assert.NotEqual(SecretToBase64String(secret1), SecretToBase64String(secret2));
+ }
+
+ private static string SecretToBase64String(ISecret secret)
+ {
+ byte[] secretBytes = new byte[secret.Length];
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(secretBytes));
+ return Convert.ToBase64String(secretBytes);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretTests.cs
new file mode 100644
index 0000000000..b9342ad765
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SecretTests.cs
@@ -0,0 +1,269 @@
+// 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 Microsoft.AspNetCore.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public unsafe class SecretTests
+ {
+ [Fact]
+ public void Ctor_ArraySegment_Default_Throws()
+ {
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => new Secret(default(ArraySegment<byte>)),
+ paramName: "array",
+ exceptionMessage: null);
+ }
+
+ [Fact]
+ public void Ctor_ArraySegment_Success()
+ {
+ // Arrange
+ var input = new ArraySegment<byte>(new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60 }, 1, 3);
+
+ // Act
+ var secret = new Secret(input);
+ input.Array[2] = 0xFF; // mutate original array - secret shouldn't be modified
+
+ // Assert - length
+ Assert.Equal(3, secret.Length);
+
+ // Assert - managed buffer
+ var outputSegment = new ArraySegment<byte>(new byte[7], 2, 3);
+ secret.WriteSecretIntoBuffer(outputSegment);
+ Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputSegment.AsStandaloneArray());
+
+ // Assert - unmanaged buffer
+ var outputBuffer = new byte[3];
+ fixed (byte* pOutputBuffer = outputBuffer)
+ {
+ secret.WriteSecretIntoBuffer(pOutputBuffer, 3);
+ }
+ Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputBuffer);
+ }
+
+ [Fact]
+ public void Ctor_Buffer_Success()
+ {
+ // Arrange
+ var input = new byte[] { 0x20, 0x30, 0x40 };
+
+ // Act
+ var secret = new Secret(input);
+ input[1] = 0xFF; // mutate original array - secret shouldn't be modified
+
+ // Assert - length
+ Assert.Equal(3, secret.Length);
+
+ // Assert - managed buffer
+ var outputSegment = new ArraySegment<byte>(new byte[7], 2, 3);
+ secret.WriteSecretIntoBuffer(outputSegment);
+ Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputSegment.AsStandaloneArray());
+
+ // Assert - unmanaged buffer
+ var outputBuffer = new byte[3];
+ fixed (byte* pOutputBuffer = outputBuffer)
+ {
+ secret.WriteSecretIntoBuffer(pOutputBuffer, 3);
+ }
+ Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputBuffer);
+ }
+
+ [Fact]
+ public void Ctor_Buffer_ZeroLength_Success()
+ {
+ // Act
+ var secret = new Secret(new byte[0]);
+
+ // Assert - none of these methods should throw
+ Assert.Equal(0, secret.Length);
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(new byte[0]));
+ byte dummy;
+ secret.WriteSecretIntoBuffer(&dummy, 0);
+ }
+
+ [Fact]
+ public void Ctor_Pointer_WithNullPointer_ThrowsArgumentNull()
+ {
+ // Act & assert
+ ExceptionAssert.ThrowsArgumentNull(
+ testCode: () => new Secret(null, 0),
+ paramName: "secret");
+ }
+
+ [Fact]
+ public void Ctor_Pointer_WithNegativeLength_ThrowsArgumentOutOfRange()
+ {
+ // Act & assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ testCode: () =>
+ {
+ byte dummy;
+ new Secret(&dummy, -1);
+ },
+ paramName: "secretLength",
+ exceptionMessage: Resources.Common_ValueMustBeNonNegative);
+ }
+
+ [Fact]
+ public void Ctor_Pointer_ZeroLength_Success()
+ {
+ // Arrange
+ byte input;
+
+ // Act
+ var secret = new Secret(&input, 0);
+
+ // Assert - none of these methods should throw
+ Assert.Equal(0, secret.Length);
+ secret.WriteSecretIntoBuffer(new ArraySegment<byte>(new byte[0]));
+ byte dummy;
+ secret.WriteSecretIntoBuffer(&dummy, 0);
+ }
+
+ [Fact]
+ public void Ctor_Pointer_Success()
+ {
+ // Arrange
+ byte* input = stackalloc byte[3];
+ input[0] = 0x20;
+ input[1] = 0x30;
+ input[2] = 0x40;
+
+ // Act
+ var secret = new Secret(input, 3);
+ input[1] = 0xFF; // mutate original buffer - secret shouldn't be modified
+
+ // Assert - length
+ Assert.Equal(3, secret.Length);
+
+ // Assert - managed buffer
+ var outputSegment = new ArraySegment<byte>(new byte[7], 2, 3);
+ secret.WriteSecretIntoBuffer(outputSegment);
+ Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputSegment.AsStandaloneArray());
+
+ // Assert - unmanaged buffer
+ var outputBuffer = new byte[3];
+ fixed (byte* pOutputBuffer = outputBuffer)
+ {
+ secret.WriteSecretIntoBuffer(pOutputBuffer, 3);
+ }
+ Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputBuffer);
+ }
+
+ [Fact]
+ public void Random_ZeroLength_Success()
+ {
+ // Act
+ var secret = Secret.Random(0);
+
+ // Assert
+ Assert.Equal(0, secret.Length);
+ }
+
+ [Fact]
+ public void Random_LengthIsMultipleOf16_Success()
+ {
+ // Act
+ var secret = Secret.Random(32);
+
+ // Assert
+ Assert.Equal(32, secret.Length);
+ Guid* pGuids = stackalloc Guid[2];
+ secret.WriteSecretIntoBuffer((byte*)pGuids, 32);
+ Assert.NotEqual(Guid.Empty, pGuids[0]);
+ Assert.NotEqual(Guid.Empty, pGuids[1]);
+ Assert.NotEqual(pGuids[0], pGuids[1]);
+ }
+
+ [Fact]
+ public void Random_LengthIsNotMultipleOf16_Success()
+ {
+ // Act
+ var secret = Secret.Random(31);
+
+ // Assert
+ Assert.Equal(31, secret.Length);
+ Guid* pGuids = stackalloc Guid[2];
+ secret.WriteSecretIntoBuffer((byte*)pGuids, 31);
+ Assert.NotEqual(Guid.Empty, pGuids[0]);
+ Assert.NotEqual(Guid.Empty, pGuids[1]);
+ Assert.NotEqual(pGuids[0], pGuids[1]);
+ Assert.Equal(0, ((byte*)pGuids)[31]); // last byte shouldn't have been overwritten
+ }
+
+ [Fact]
+ public void WriteSecretIntoBuffer_ArraySegment_IncorrectlySizedBuffer_Throws()
+ {
+ // Arrange
+ var secret = Secret.Random(16);
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => secret.WriteSecretIntoBuffer(new ArraySegment<byte>(new byte[100])),
+ paramName: "buffer",
+ exceptionMessage: Resources.FormatCommon_BufferIncorrectlySized(100, 16));
+ }
+
+ [Fact]
+ public void WriteSecretIntoBuffer_ArraySegment_Disposed_Throws()
+ {
+ // Arrange
+ var secret = Secret.Random(16);
+ secret.Dispose();
+
+ // Act & assert
+ Assert.Throws<ObjectDisposedException>(
+ testCode: () => secret.WriteSecretIntoBuffer(new ArraySegment<byte>(new byte[16])));
+ }
+
+ [Fact]
+ public void WriteSecretIntoBuffer_Pointer_NullBuffer_Throws()
+ {
+ // Arrange
+ var secret = Secret.Random(16);
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgumentNull(
+ testCode: () => secret.WriteSecretIntoBuffer(null, 100),
+ paramName: "buffer");
+ }
+
+ [Fact]
+ public void WriteSecretIntoBuffer_Pointer_IncorrectlySizedBuffer_Throws()
+ {
+ // Arrange
+ var secret = Secret.Random(16);
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () =>
+ {
+ byte* pBuffer = stackalloc byte[100];
+ secret.WriteSecretIntoBuffer(pBuffer, 100);
+ },
+ paramName: "bufferLength",
+ exceptionMessage: Resources.FormatCommon_BufferIncorrectlySized(100, 16));
+ }
+
+ [Fact]
+ public void WriteSecretIntoBuffer_Pointer_Disposed_Throws()
+ {
+ // Arrange
+ var secret = Secret.Random(16);
+ secret.Dispose();
+
+ // Act & assert
+ Assert.Throws<ObjectDisposedException>(
+ testCode: () =>
+ {
+ byte* pBuffer = stackalloc byte[16];
+ secret.WriteSecretIntoBuffer(pBuffer, 16);
+ });
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SequentialGenRandom.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SequentialGenRandom.cs
new file mode 100644
index 0000000000..c37462ef97
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/SequentialGenRandom.cs
@@ -0,0 +1,32 @@
+// 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 Microsoft.AspNetCore.DataProtection.Cng;
+using Microsoft.AspNetCore.DataProtection.Managed;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal unsafe class SequentialGenRandom : IBCryptGenRandom, IManagedGenRandom
+ {
+ private byte _value;
+
+ public byte[] GenRandom(int numBytes)
+ {
+ byte[] bytes = new byte[numBytes];
+ for (int i = 0; i < bytes.Length; i++)
+ {
+ bytes[i] = _value++;
+ }
+ return bytes;
+ }
+
+ public void GenRandom(byte* pbBuffer, uint cbBuffer)
+ {
+ for (uint i = 0; i < cbBuffer; i++)
+ {
+ pbBuffer[i] = _value++;
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ServiceCollectionTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ServiceCollectionTests.cs
new file mode 100644
index 0000000000..ad05973c0b
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/ServiceCollectionTests.cs
@@ -0,0 +1,67 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class ServiceCollectionTests
+ {
+ [Fact]
+ public void AddsOptions()
+ {
+ var services = new ServiceCollection()
+ .AddDataProtection()
+ .Services
+ .BuildServiceProvider();
+
+ Assert.NotNull(services.GetService<IOptions<DataProtectionOptions>>());
+ }
+
+ [Fact]
+ public void DoesNotOverrideLogging()
+ {
+ var services1 = new ServiceCollection()
+ .AddLogging()
+ .AddDataProtection()
+ .Services
+ .BuildServiceProvider();
+
+ var services2 = new ServiceCollection()
+ .AddDataProtection()
+ .Services
+ .AddLogging()
+ .BuildServiceProvider();
+
+ Assert.Equal(
+ services1.GetRequiredService<ILoggerFactory>().GetType(),
+ services2.GetRequiredService<ILoggerFactory>().GetType());
+ }
+
+ [Fact]
+ public void CanResolveAllRegisteredServices()
+ {
+ var serviceCollection = new ServiceCollection()
+ .AddDataProtection()
+ .Services;
+ var services = serviceCollection.BuildServiceProvider(validateScopes: true);
+
+ Assert.Null(services.GetService<ILoggerFactory>());
+
+ foreach (var descriptor in serviceCollection)
+ {
+ if (descriptor.ServiceType.Assembly.GetName().Name == "Microsoft.Extensions.Options")
+ {
+ // ignore any descriptors added by the call to .AddOptions()
+ continue;
+ }
+
+ Assert.NotNull(services.GetService(descriptor.ServiceType));
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/StringLoggerFactory.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/StringLoggerFactory.cs
new file mode 100644
index 0000000000..8d2b146b27
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/StringLoggerFactory.cs
@@ -0,0 +1,82 @@
+// 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.Globalization;
+using System.Text;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ internal sealed class StringLoggerFactory : ILoggerFactory
+ {
+ private readonly StringBuilder _log = new StringBuilder();
+
+ public StringLoggerFactory(LogLevel logLevel)
+ {
+ MinimumLevel = logLevel;
+ }
+
+ public LogLevel MinimumLevel { get; set; }
+
+ public void AddProvider(ILoggerProvider provider)
+ {
+ // no-op
+ }
+
+ public ILogger CreateLogger(string name)
+ {
+ return new StringLogger(name, this);
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public override string ToString()
+ {
+ return _log.ToString();
+ }
+
+ private sealed class StringLogger : ILogger
+ {
+ private readonly StringLoggerFactory _factory;
+ private readonly string _name;
+
+ public StringLogger(string name, StringLoggerFactory factory)
+ {
+ _name = name;
+ _factory = factory;
+ }
+
+ public IDisposable BeginScope<TState>(TState state)
+ {
+ return new DummyDisposable();
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return (logLevel >= _factory.MinimumLevel);
+ }
+
+ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+ {
+ string message = string.Format(CultureInfo.InvariantCulture,
+ "Provider: {0}" + Environment.NewLine +
+ "Log level: {1}" + Environment.NewLine +
+ "Event id: {2}" + Environment.NewLine +
+ "Exception: {3}" + Environment.NewLine +
+ "Message: {4}", _name, logLevel, eventId, exception?.ToString(), formatter(state, exception));
+ _factory._log.AppendLine(message);
+ }
+
+ private sealed class DummyDisposable : IDisposable
+ {
+ public void Dispose()
+ {
+ // no-op
+ }
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer
new file mode 100644
index 0000000000..329c90a83b
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx
new file mode 100644
index 0000000000..8bf695f1d6
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx
new file mode 100644
index 0000000000..a54c93ba34
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx
Binary files differ
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs
new file mode 100644
index 0000000000..c9a68ff746
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs
@@ -0,0 +1,172 @@
+// 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.Collections.Generic;
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class TypeForwardingActivatorTests : MarshalByRefObject
+ {
+ [Fact]
+ public void CreateInstance_ForwardsToNewNamespaceIfExists()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddDataProtection();
+ var services = serviceCollection.BuildServiceProvider();
+ var activator = services.GetActivator();
+
+ // Act
+ var name = "Microsoft.AspNet.DataProtection.TypeForwardingActivatorTests+ClassWithParameterlessCtor, Microsoft.AspNet.DataProtection.Test, Version=1.0.0.0";
+ var instance = activator.CreateInstance<object>(name);
+
+ // Assert
+ Assert.IsType<ClassWithParameterlessCtor>(instance);
+ }
+
+ [Fact]
+ public void CreateInstance_DoesNotForwardIfClassDoesNotExist()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddDataProtection();
+ var services = serviceCollection.BuildServiceProvider();
+ var activator = services.GetActivator();
+
+ // Act & Assert
+ var name = "Microsoft.AspNet.DataProtection.TypeForwardingActivatorTests+NonExistentClassWithParameterlessCtor, Microsoft.AspNet.DataProtection.Test";
+ var exception = Assert.ThrowsAny<Exception>(() => activator.CreateInstance<object>(name));
+
+ Assert.Contains("Microsoft.AspNet.DataProtection.Test", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(typeof(GenericType<GenericType<ClassWithParameterlessCtor>>))]
+ [InlineData(typeof(GenericType<ClassWithParameterlessCtor>))]
+ [InlineData(typeof(GenericType<GenericType<string>>))]
+ [InlineData(typeof(GenericType<GenericType<string, string>>))]
+ [InlineData(typeof(GenericType<string>))]
+ [InlineData(typeof(GenericType<int>))]
+ [InlineData(typeof(List<ClassWithParameterlessCtor>))]
+ public void CreateInstance_Generics(Type type)
+ {
+ // Arrange
+ var activator = new TypeForwardingActivator(null);
+ var name = type.AssemblyQualifiedName;
+
+ // Act & Assert
+ Assert.IsType(type, activator.CreateInstance<object>(name));
+ }
+
+ [Theory]
+ [InlineData(typeof(GenericType<>))]
+ [InlineData(typeof(GenericType<,>))]
+ public void CreateInstance_ThrowsForOpenGenerics(Type type)
+ {
+ // Arrange
+ var activator = new TypeForwardingActivator(null);
+ var name = type.AssemblyQualifiedName;
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(() => activator.CreateInstance<object>(name));
+ }
+
+ [Theory]
+ [InlineData(
+ "System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Version=1.0.0.0, Culture=neutral]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
+ "System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Culture=neutral]], mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
+ [InlineData(
+ "Some.Type`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Microsoft.AspNetCore.DataProtection, Version=1.0.0.0, Culture=neutral",
+ "Some.Type`1[[System.Int32, mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Microsoft.AspNetCore.DataProtection, Culture=neutral")]
+ [InlineData(
+ "System.Tuple`1[[System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Version=1.0.0.0, Culture=neutral]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
+ "System.Tuple`1[[System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Culture=neutral]], mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
+ public void ParsesFullyQualifiedTypeName(string typeName, string expected)
+ {
+ Assert.Equal(expected, new MockTypeForwardingActivator().Parse(typeName));
+ }
+
+ [Theory]
+ [InlineData(typeof(List<string>))]
+ [InlineData(typeof(FactAttribute))]
+ public void CreateInstance_DoesNotForwardingTypesExternalTypes(Type type)
+ {
+ new TypeForwardingActivator(null).CreateInstance(typeof(object), type.AssemblyQualifiedName, out var forwarded);
+ Assert.False(forwarded, "Should not have forwarded types that are not in Microsoft.AspNetCore.DataProjection");
+ }
+
+ [Theory]
+ [MemberData(nameof(AssemblyVersions))]
+ public void CreateInstance_ForwardsAcrossVersionChanges(Version version)
+ {
+#if NET461
+ // run this test in an appdomain without testhost's custom assembly resolution hooks
+ var setupInfo = new AppDomainSetup
+ {
+ ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
+ ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
+ };
+ var domain = AppDomain.CreateDomain("TestDomain", null, setupInfo);
+ var wrappedTestClass = (TypeForwardingActivatorTests)domain.CreateInstanceAndUnwrap(GetType().Assembly.FullName, typeof(TypeForwardingActivatorTests).FullName);
+ wrappedTestClass.CreateInstance_ForwardsAcrossVersionChangesImpl(version);
+#elif NETCOREAPP3_0
+ CreateInstance_ForwardsAcrossVersionChangesImpl(version);
+#else
+#error Target framework should be updated
+#endif
+ }
+
+ private void CreateInstance_ForwardsAcrossVersionChangesImpl(Version newVersion)
+ {
+ var activator = new TypeForwardingActivator(null);
+
+ var typeInfo = typeof(ClassWithParameterlessCtor).GetTypeInfo();
+ var typeName = typeInfo.FullName;
+ var assemblyName = typeInfo.Assembly.GetName();
+
+ assemblyName.Version = newVersion;
+ var newName = $"{typeName}, {assemblyName}";
+
+ Assert.NotEqual(typeInfo.AssemblyQualifiedName, newName);
+ Assert.IsType<ClassWithParameterlessCtor>(activator.CreateInstance(typeof(object), newName, out var forwarded));
+ Assert.True(forwarded, "Should have forwarded this type to new version or namespace");
+ }
+
+ public static TheoryData<Version> AssemblyVersions
+ {
+ get
+ {
+ var current = typeof(ActivatorTests).Assembly.GetName().Version;
+ return new TheoryData<Version>
+ {
+ new Version(Math.Max(0, current.Major - 1), 0, 0, 0),
+ new Version(current.Major + 1, 0, 0, 0),
+ new Version(current.Major, current.Minor + 1, 0, 0),
+ new Version(current.Major, current.Minor, current.Build + 1, 0),
+ };
+ }
+ }
+
+ private class MockTypeForwardingActivator : TypeForwardingActivator
+ {
+ public MockTypeForwardingActivator() : base(null) { }
+ public string Parse(string typeName) => RemoveVersionFromAssemblyName(typeName);
+ }
+
+ private class ClassWithParameterlessCtor
+ {
+ }
+
+ private class GenericType<T>
+ {
+ }
+
+ private class GenericType<T1, T2>
+ {
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlAssert.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlAssert.cs
new file mode 100644
index 0000000000..3bd5ccdc16
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlAssert.cs
@@ -0,0 +1,151 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ /// <summary>
+ /// Helpful XML-based assertions.
+ /// </summary>
+ public static class XmlAssert
+ {
+ public static readonly IEqualityComparer<XNode> EqualityComparer = new CallbackBasedEqualityComparer<XNode>(Core.AreEqual);
+
+ /// <summary>
+ /// Asserts that a <see cref="string"/> and an <see cref="XElement"/> are semantically equivalent.
+ /// </summary>
+ public static void Equal(string expected, XElement actual)
+ {
+ Assert.NotNull(expected);
+ Assert.NotNull(actual);
+ Equal(XElement.Parse(expected), actual);
+ }
+
+ /// <summary>
+ /// Asserts that two <see cref="XElement"/> instances are semantically equivalent.
+ /// </summary>
+ public static void Equal(XElement expected, XElement actual)
+ {
+ Assert.NotNull(expected);
+ Assert.NotNull(actual);
+
+ if (!Core.AreEqual(expected, actual))
+ {
+ Assert.True(false,
+ "Expected element:" + Environment.NewLine
+ + expected.ToString() + Environment.NewLine
+ + "Actual element:" + Environment.NewLine
+ + actual.ToString());
+ }
+ }
+
+ private static class Core
+ {
+ private static readonly IEqualityComparer<XAttribute> AttributeEqualityComparer = new CallbackBasedEqualityComparer<XAttribute>(AreEqual);
+
+ private static bool AreEqual(XElement expected, XElement actual)
+ {
+ return expected.Name == actual.Name
+ && AreEqual(expected.Attributes(), actual.Attributes())
+ && AreEqual(expected.Nodes(), actual.Nodes());
+ }
+
+ private static bool AreEqual(IEnumerable<XNode> expected, IEnumerable<XNode> actual)
+ {
+ List<XNode> filteredExpected = expected.Where(ShouldIncludeNodeDuringComparison).ToList();
+ List<XNode> filteredActual = actual.Where(ShouldIncludeNodeDuringComparison).ToList();
+ return filteredExpected.SequenceEqual(filteredActual, EqualityComparer);
+ }
+
+ internal static bool AreEqual(XNode expected, XNode actual)
+ {
+ if (expected is XText && actual is XText)
+ {
+ return AreEqual((XText)expected, (XText)actual);
+ }
+ else if (expected is XElement && actual is XElement)
+ {
+ return AreEqual((XElement)expected, (XElement)actual);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private static bool AreEqual(XText expected, XText actual)
+ {
+ return expected.Value == actual.Value;
+ }
+
+ private static bool AreEqual(IEnumerable<XAttribute> expected, IEnumerable<XAttribute> actual)
+ {
+ List<XAttribute> orderedExpected = expected
+ .Where(ShouldIncludeAttributeDuringComparison)
+ .OrderBy(attr => attr.Name.ToString())
+ .ToList();
+
+ List<XAttribute> orderedActual = actual
+ .Where(ShouldIncludeAttributeDuringComparison)
+ .OrderBy(attr => attr.Name.ToString())
+ .ToList();
+
+ return orderedExpected.SequenceEqual(orderedActual, AttributeEqualityComparer);
+ }
+
+ private static bool AreEqual(XAttribute expected, XAttribute actual)
+ {
+ return expected.Name == actual.Name
+ && expected.Value == actual.Value;
+ }
+
+ private static bool ShouldIncludeAttributeDuringComparison(XAttribute attribute)
+ {
+ // exclude 'xmlns' attributes since they're already considered in the
+ // actual element and attribute names
+ return attribute.Name != (XName)"xmlns"
+ && attribute.Name.Namespace != XNamespace.Xmlns;
+ }
+
+ private static bool ShouldIncludeNodeDuringComparison(XNode node)
+ {
+ if (node is XComment)
+ {
+ return false; // not contextually relevant
+ }
+
+ if (node is XText /* includes XCData */ || node is XElement)
+ {
+ return true; // relevant
+ }
+
+ throw new NotSupportedException(string.Format("Node of type '{0}' is not supported.", node.GetType().Name));
+ }
+ }
+
+ private sealed class CallbackBasedEqualityComparer<T> : IEqualityComparer<T>
+ {
+ private readonly Func<T, T, bool> _equalityCheck;
+
+ public CallbackBasedEqualityComparer(Func<T, T, bool> equalityCheck)
+ {
+ _equalityCheck = equalityCheck;
+ }
+
+ public bool Equals(T x, T y)
+ {
+ return _equalityCheck(x, y);
+ }
+
+ public int GetHashCode(T obj)
+ {
+ return obj.ToString().GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.cs
new file mode 100644
index 0000000000..425e15f51c
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.cs
@@ -0,0 +1,61 @@
+// 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.Security.Cryptography;
+using System.Security.Cryptography.Xml;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ public class CertificateXmlEncryptorTests
+ {
+ [Fact]
+ public void Encrypt_Decrypt_RoundTrips()
+ {
+ // Arrange
+ var symmetricAlgorithm = new TripleDESCryptoServiceProvider();
+ symmetricAlgorithm.GenerateKey();
+
+ var mockInternalEncryptor = new Mock<IInternalCertificateXmlEncryptor>();
+ mockInternalEncryptor.Setup(o => o.PerformEncryption(It.IsAny<EncryptedXml>(), It.IsAny<XmlElement>()))
+ .Returns<EncryptedXml, XmlElement>((encryptedXml, element) =>
+ {
+ encryptedXml.AddKeyNameMapping("theKey", symmetricAlgorithm); // use symmetric encryption
+ return encryptedXml.Encrypt(element, "theKey");
+ });
+
+ var mockInternalDecryptor = new Mock<IInternalEncryptedXmlDecryptor>();
+ mockInternalDecryptor.Setup(o => o.PerformPreDecryptionSetup(It.IsAny<EncryptedXml>()))
+ .Callback<EncryptedXml>(encryptedXml =>
+ {
+ encryptedXml.AddKeyNameMapping("theKey", symmetricAlgorithm); // use symmetric encryption
+ });
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton<IInternalEncryptedXmlDecryptor>(mockInternalDecryptor.Object);
+
+ var services = serviceCollection.BuildServiceProvider();
+ var encryptor = new CertificateXmlEncryptor(NullLoggerFactory.Instance, mockInternalEncryptor.Object);
+ var decryptor = new EncryptedXmlDecryptor(services);
+
+ var originalXml = XElement.Parse(@"<mySecret value='265ee4ea-ade2-43b1-b706-09b259e58b6b' />");
+
+ // Act & assert - run through encryptor and make sure we get back <EncryptedData> element
+ var encryptedXmlInfo = encryptor.Encrypt(originalXml);
+ Assert.Equal(typeof(EncryptedXmlDecryptor), encryptedXmlInfo.DecryptorType);
+ Assert.Equal(XName.Get("EncryptedData", "http://www.w3.org/2001/04/xmlenc#"), encryptedXmlInfo.EncryptedElement.Name);
+ Assert.Equal("http://www.w3.org/2001/04/xmlenc#Element", (string)encryptedXmlInfo.EncryptedElement.Attribute("Type"));
+ Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ // Act & assert - run through decryptor and make sure we get back the original value
+ var roundTrippedElement = decryptor.Decrypt(encryptedXmlInfo.EncryptedElement);
+ XmlAssert.Equal(originalXml, roundTrippedElement);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.cs
new file mode 100644
index 0000000000..6b16c638a8
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.cs
@@ -0,0 +1,34 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ public class DpapiNGXmlEncryptionTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
+ public void Encrypt_Decrypt_RoundTrips()
+ {
+ // Arrange
+ var originalXml = XElement.Parse(@"<mySecret value='265ee4ea-ade2-43b1-b706-09b259e58b6b' />");
+ var encryptor = new DpapiNGXmlEncryptor("LOCAL=user", DpapiNGProtectionDescriptorFlags.None, NullLoggerFactory.Instance);
+ var decryptor = new DpapiNGXmlDecryptor();
+
+ // Act & assert - run through encryptor and make sure we get back an obfuscated element
+ var encryptedXmlInfo = encryptor.Encrypt(originalXml);
+ Assert.Equal(typeof(DpapiNGXmlDecryptor), encryptedXmlInfo.DecryptorType);
+ Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ // Act & assert - run through decryptor and make sure we get back the original value
+ var roundTrippedElement = decryptor.Decrypt(encryptedXmlInfo.EncryptedElement);
+ XmlAssert.Equal(originalXml, roundTrippedElement);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.cs
new file mode 100644
index 0000000000..7274d846ad
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.cs
@@ -0,0 +1,61 @@
+// 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.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Test.Shared;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ public class DpapiXmlEncryptionTests
+ {
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Encrypt_CurrentUserOrLocalMachine_Decrypt_RoundTrips(bool protectToLocalMachine)
+ {
+ // Arrange
+ var originalXml = XElement.Parse(@"<mySecret value='265ee4ea-ade2-43b1-b706-09b259e58b6b' />");
+ var encryptor = new DpapiXmlEncryptor(protectToLocalMachine, NullLoggerFactory.Instance);
+ var decryptor = new DpapiXmlDecryptor();
+
+ // Act & assert - run through encryptor and make sure we get back an obfuscated element
+ var encryptedXmlInfo = encryptor.Encrypt(originalXml);
+ Assert.Equal(typeof(DpapiXmlDecryptor), encryptedXmlInfo.DecryptorType);
+ Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ // Act & assert - run through decryptor and make sure we get back the original value
+ var roundTrippedElement = decryptor.Decrypt(encryptedXmlInfo.EncryptedElement);
+ XmlAssert.Equal(originalXml, roundTrippedElement);
+ }
+
+#if NET461
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void Encrypt_CurrentUser_Decrypt_ImpersonatedAsAnonymous_Fails()
+ {
+ // Arrange
+ var originalXml = XElement.Parse(@"<mySecret value='265ee4ea-ade2-43b1-b706-09b259e58b6b' />");
+ var encryptor = new DpapiXmlEncryptor(protectToLocalMachine: false, loggerFactory: NullLoggerFactory.Instance);
+ var decryptor = new DpapiXmlDecryptor();
+
+ // Act & assert - run through encryptor and make sure we get back an obfuscated element
+ var encryptedXmlInfo = encryptor.Encrypt(originalXml);
+ Assert.Equal(typeof(DpapiXmlDecryptor), encryptedXmlInfo.DecryptorType);
+ Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ // Act & assert - run through decryptor (while impersonated as anonymous) and verify failure
+ ExceptionAssert2.ThrowsCryptographicException(() =>
+ AnonymousImpersonation.Run(() => decryptor.Decrypt(encryptedXmlInfo.EncryptedElement)));
+ }
+#elif NETCOREAPP3_0
+#else
+#error Target framework needs to be updated
+#endif
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/EncryptedXmlDecryptorTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/EncryptedXmlDecryptorTests.cs
new file mode 100644
index 0000000000..5d3bb6943a
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/EncryptedXmlDecryptorTests.cs
@@ -0,0 +1,91 @@
+// 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.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.XmlEncryption;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Test.XmlEncryption
+{
+ public class EncryptedXmlDecryptorTests
+ {
+ [Fact]
+ public void ThrowsIfCannotDecrypt()
+ {
+ var testCert1 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password");
+ var encryptor = new CertificateXmlEncryptor(testCert1, NullLoggerFactory.Instance);
+ var data = new XElement("SampleData", "Lorem ipsum");
+ var encryptedXml = encryptor.Encrypt(data);
+ var decryptor = new EncryptedXmlDecryptor();
+
+ var ex = Assert.Throws<CryptographicException>(() =>
+ decryptor.Decrypt(encryptedXml.EncryptedElement));
+ Assert.Equal("Unable to retrieve the decryption key.", ex.Message);
+ }
+
+ [Fact]
+ public void ThrowsIfProvidedCertificateDoesNotMatch()
+ {
+ var testCert1 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password");
+ var testCert2 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert2.pfx"), "password");
+ var services = new ServiceCollection()
+ .Configure<XmlKeyDecryptionOptions>(o => o.AddKeyDecryptionCertificate(testCert2))
+ .BuildServiceProvider();
+ var encryptor = new CertificateXmlEncryptor(testCert1, NullLoggerFactory.Instance);
+ var data = new XElement("SampleData", "Lorem ipsum");
+ var encryptedXml = encryptor.Encrypt(data);
+ var decryptor = new EncryptedXmlDecryptor(services);
+
+ var ex = Assert.Throws<CryptographicException>(() =>
+ decryptor.Decrypt(encryptedXml.EncryptedElement));
+ Assert.Equal("Unable to retrieve the decryption key.", ex.Message);
+ }
+
+ [Fact]
+ public void ThrowsIfProvidedCertificateDoesHavePrivateKey()
+ {
+ var fullCert = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password");
+ var publicKeyOnly = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.PublicKeyOnly.cer"), "");
+ var services = new ServiceCollection()
+ .Configure<XmlKeyDecryptionOptions>(o => o.AddKeyDecryptionCertificate(publicKeyOnly))
+ .BuildServiceProvider();
+ var encryptor = new CertificateXmlEncryptor(fullCert, NullLoggerFactory.Instance);
+ var data = new XElement("SampleData", "Lorem ipsum");
+ var encryptedXml = encryptor.Encrypt(data);
+ var decryptor = new EncryptedXmlDecryptor(services);
+
+ var ex = Assert.Throws<CryptographicException>(() =>
+ decryptor.Decrypt(encryptedXml.EncryptedElement));
+ Assert.Equal("Unable to retrieve the decryption key.", ex.Message);
+ }
+
+ [Fact]
+ public void XmlCanRoundTrip()
+ {
+ var testCert1 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert1.pfx"), "password");
+ var testCert2 = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "TestFiles", "TestCert2.pfx"), "password");
+ var services = new ServiceCollection()
+ .Configure<XmlKeyDecryptionOptions>(o =>
+ {
+ o.AddKeyDecryptionCertificate(testCert1);
+ o.AddKeyDecryptionCertificate(testCert2);
+ })
+ .BuildServiceProvider();
+ var encryptor = new CertificateXmlEncryptor(testCert1, NullLoggerFactory.Instance);
+ var data = new XElement("SampleData", "Lorem ipsum");
+ var encryptedXml = encryptor.Encrypt(data);
+ var decryptor = new EncryptedXmlDecryptor(services);
+
+ var decrypted = decryptor.Decrypt(encryptedXml.EncryptedElement);
+
+ Assert.Equal("SampleData", decrypted.Name);
+ Assert.Equal("Lorem ipsum", decrypted.Value);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.cs
new file mode 100644
index 0000000000..8f4433d78c
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.cs
@@ -0,0 +1,39 @@
+// 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.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ public class NullXmlEncryptionTests
+ {
+ [Fact]
+ public void NullDecryptor_ReturnsOriginalElement()
+ {
+ // Arrange
+ var decryptor = new NullXmlDecryptor();
+
+ // Act
+ var retVal = decryptor.Decrypt(XElement.Parse("<unencryptedKey><theElement /></unencryptedKey>"));
+
+ // Assert
+ XmlAssert.Equal("<theElement />", retVal);
+ }
+
+ [Fact]
+ public void NullEncryptor_ReturnsOriginalElement()
+ {
+ // Arrange
+ var encryptor = new NullXmlEncryptor();
+
+ // Act
+ var retVal = encryptor.Encrypt(XElement.Parse("<theElement />"));
+
+ // Assert
+ Assert.Equal(typeof(NullXmlDecryptor), retVal.DecryptorType);
+ XmlAssert.Equal("<unencryptedKey><theElement /></unencryptedKey>", retVal.EncryptedElement);
+ }
+ }
+}
diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs
new file mode 100644
index 0000000000..bf3c455b5a
--- /dev/null
+++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs
@@ -0,0 +1,235 @@
+// 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.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.XmlEncryption
+{
+ public class XmlEncryptionExtensionsTests
+ {
+ [Fact]
+ public void DecryptElement_NothingToDecrypt_ReturnsOriginalElement()
+ {
+ // Arrange
+ var original = XElement.Parse(@"<element />");
+
+ // Act
+ var retVal = original.DecryptElement(activator: null);
+
+ // Assert
+ Assert.Same(original, retVal);
+ XmlAssert.Equal("<element />", original); // unmutated
+ }
+
+ [Fact]
+ public void DecryptElement_RootNodeRequiresDecryption_Success()
+ {
+ // Arrange
+ var original = XElement.Parse(@"
+ <x:encryptedSecret decryptorType='theDecryptor' xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <node />
+ </x:encryptedSecret>");
+
+ var mockActivator = new Mock<IActivator>();
+ mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput("theDecryptor", "<node />", "<newNode />");
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton<IActivator>(mockActivator.Object);
+ var services = serviceCollection.BuildServiceProvider();
+ var activator = services.GetActivator();
+
+ // Act
+ var retVal = original.DecryptElement(activator);
+
+ // Assert
+ XmlAssert.Equal("<newNode />", retVal);
+ }
+
+ [Fact]
+ public void DecryptElement_MultipleNodesRequireDecryption_AvoidsRecursion_Success()
+ {
+ // Arrange
+ var original = XElement.Parse(@"
+ <rootNode xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <x:encryptedSecret decryptorType='myDecryptor'>
+ <node1 />
+ </x:encryptedSecret>
+ <node2 x:requiresEncryption='false'>
+ <![CDATA[This data should stick around.]]>
+ <x:encryptedSecret decryptorType='myDecryptor'>
+ <node3 />
+ </x:encryptedSecret>
+ </node2>
+ </rootNode>");
+
+ var expected = @"
+ <rootNode xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <node1_decrypted>
+ <x:encryptedSecret>nested</x:encryptedSecret>
+ </node1_decrypted>
+ <node2 x:requiresEncryption='false'>
+ <![CDATA[This data should stick around.]]>
+ <node3_decrypted>
+ <x:encryptedSecret>nested</x:encryptedSecret>
+ </node3_decrypted>
+ </node2>
+ </rootNode>";
+
+ var mockDecryptor = new Mock<IXmlDecryptor>();
+ mockDecryptor
+ .Setup(o => o.Decrypt(It.IsAny<XElement>()))
+ .Returns<XElement>(el => new XElement(el.Name.LocalName + "_decrypted", new XElement(XmlConstants.EncryptedSecretElementName, "nested")));
+
+ var mockActivator = new Mock<IActivator>();
+ mockActivator.Setup(o => o.CreateInstance(typeof(IXmlDecryptor), "myDecryptor")).Returns(mockDecryptor.Object);
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton<IActivator>(mockActivator.Object);
+ var services = serviceCollection.BuildServiceProvider();
+ var activator = services.GetActivator();
+
+ // Act
+ var retVal = original.DecryptElement(activator);
+
+ // Assert
+ XmlAssert.Equal(expected, retVal);
+ }
+
+ [Fact]
+ public void EncryptIfNecessary_NothingToEncrypt_ReturnsNull()
+ {
+ // Arrange
+ var original = XElement.Parse(@"<element />");
+ var xmlEncryptor = new Mock<IXmlEncryptor>(MockBehavior.Strict).Object;
+
+ // Act
+ var retVal = xmlEncryptor.EncryptIfNecessary(original);
+
+ // Assert
+ Assert.Null(retVal);
+ XmlAssert.Equal("<element />", original); // unmutated
+ }
+
+ [Fact]
+ public void EncryptIfNecessary_RootNodeRequiresEncryption_Success()
+ {
+ // Arrange
+ var original = XElement.Parse(@"<rootNode x:requiresEncryption='true' xmlns:x='http://schemas.asp.net/2015/03/dataProtection' />");
+ var mockXmlEncryptor = new Mock<IXmlEncryptor>();
+ mockXmlEncryptor.Setup(o => o.Encrypt(It.IsAny<XElement>())).Returns(new EncryptedXmlInfo(new XElement("theElement"), typeof(MyXmlDecryptor)));
+
+ // Act
+ var retVal = mockXmlEncryptor.Object.EncryptIfNecessary(original);
+
+ // Assert
+ XmlAssert.Equal(@"<rootNode x:requiresEncryption='true' xmlns:x='http://schemas.asp.net/2015/03/dataProtection' />", original); // unmutated
+ Assert.Equal(XmlConstants.EncryptedSecretElementName, retVal.Name);
+ Assert.Equal(typeof(MyXmlDecryptor).AssemblyQualifiedName, (string)retVal.Attribute(XmlConstants.DecryptorTypeAttributeName));
+ XmlAssert.Equal("<theElement />", retVal.Descendants().Single());
+ }
+
+ [Fact]
+ public void EncryptIfNecessary_MultipleNodesRequireEncryption_Success()
+ {
+ // Arrange
+ var original = XElement.Parse(@"
+ <rootNode xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <node1 x:requiresEncryption='true'>
+ <![CDATA[This data should be encrypted.]]>
+ </node1>
+ <node2 x:requiresEncryption='false'>
+ <![CDATA[This data should stick around.]]>
+ <node3 x:requiresEncryption='true'>
+ <node4 x:requiresEncryption='true' />
+ </node3>
+ </node2>
+ </rootNode>");
+
+ var expected = string.Format(@"
+ <rootNode xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <x:encryptedSecret decryptorType='{0}'>
+ <node1_encrypted />
+ </x:encryptedSecret>
+ <node2 x:requiresEncryption='false'>
+ <![CDATA[This data should stick around.]]>
+ <x:encryptedSecret decryptorType='{0}'>
+ <node3_encrypted />
+ </x:encryptedSecret>
+ </node2>
+ </rootNode>",
+ typeof(MyXmlDecryptor).AssemblyQualifiedName);
+
+ var mockXmlEncryptor = new Mock<IXmlEncryptor>();
+ mockXmlEncryptor
+ .Setup(o => o.Encrypt(It.IsAny<XElement>()))
+ .Returns<XElement>(element => new EncryptedXmlInfo(new XElement(element.Name.LocalName + "_encrypted"), typeof(MyXmlDecryptor)));
+
+ // Act
+ var retVal = mockXmlEncryptor.Object.EncryptIfNecessary(original);
+
+ // Assert
+ XmlAssert.Equal(expected, retVal);
+ }
+
+ [Fact]
+ public void EncryptIfNecessary_NullEncryptorWithRecursion_NoStackDive_Success()
+ {
+ // Arrange
+ var original = XElement.Parse(@"
+ <rootNode xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <node1 x:requiresEncryption='true'>
+ <![CDATA[This data should be encrypted.]]>
+ </node1>
+ <node2 x:requiresEncryption='false'>
+ <![CDATA[This data should stick around.]]>
+ <node3 x:requiresEncryption='true'>
+ <node4 x:requiresEncryption='true' />
+ </node3>
+ </node2>
+ </rootNode>");
+
+ var expected = string.Format(@"
+ <rootNode xmlns:x='http://schemas.asp.net/2015/03/dataProtection'>
+ <x:encryptedSecret decryptorType='{0}'>
+ <node1 x:requiresEncryption='true'>
+ <![CDATA[This data should be encrypted.]]>
+ </node1>
+ </x:encryptedSecret>
+ <node2 x:requiresEncryption='false'>
+ <![CDATA[This data should stick around.]]>
+ <x:encryptedSecret decryptorType='{0}'>
+ <node3 x:requiresEncryption='true'>
+ <node4 x:requiresEncryption='true' />
+ </node3>
+ </x:encryptedSecret>
+ </node2>
+ </rootNode>",
+ typeof(MyXmlDecryptor).AssemblyQualifiedName);
+
+ var mockXmlEncryptor = new Mock<IXmlEncryptor>();
+ mockXmlEncryptor
+ .Setup(o => o.Encrypt(It.IsAny<XElement>()))
+ .Returns<XElement>(element => new EncryptedXmlInfo(new XElement(element), typeof(MyXmlDecryptor)));
+
+ // Act
+ var retVal = mockXmlEncryptor.Object.EncryptIfNecessary(original);
+
+ // Assert
+ XmlAssert.Equal(expected, retVal);
+ }
+
+ private sealed class MyXmlDecryptor : IXmlDecryptor
+ {
+ public XElement Decrypt(XElement encryptedElement)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/DataProtection/test/shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs b/src/DataProtection/test/shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs
new file mode 100644
index 0000000000..d5ef4730f6
--- /dev/null
+++ b/src/DataProtection/test/shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs
@@ -0,0 +1,16 @@
+// 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 Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Testing.xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Test.Shared
+{
+ public class ConditionalRunTestOnlyOnWindows8OrLaterAttribute : Attribute, ITestCondition
+ {
+ public bool IsMet => OSVersionUtil.IsWindows8OrLater();
+
+ public string SkipReason { get; } = "Test requires Windows 8 / Windows Server 2012 or higher.";
+ }
+}
diff --git a/src/DataProtection/test/shared/ConditionalRunTestOnlyWindowsAttribute.cs b/src/DataProtection/test/shared/ConditionalRunTestOnlyWindowsAttribute.cs
new file mode 100644
index 0000000000..5033b3e38e
--- /dev/null
+++ b/src/DataProtection/test/shared/ConditionalRunTestOnlyWindowsAttribute.cs
@@ -0,0 +1,16 @@
+// 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 Microsoft.AspNetCore.Cryptography.Cng;
+using Microsoft.AspNetCore.Testing.xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.Test.Shared
+{
+ public class ConditionalRunTestOnlyOnWindowsAttribute : Attribute, ITestCondition
+ {
+ public bool IsMet => OSVersionUtil.IsWindows();
+
+ public string SkipReason { get; } = "Test requires Windows 7 / Windows Server 2008 R2 or higher.";
+ }
+}
diff --git a/src/DataProtection/test/shared/ExceptionAssert2.cs b/src/DataProtection/test/shared/ExceptionAssert2.cs
new file mode 100644
index 0000000000..ccc596b48c
--- /dev/null
+++ b/src/DataProtection/test/shared/ExceptionAssert2.cs
@@ -0,0 +1,23 @@
+// 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.Security.Cryptography;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Testing
+{
+ internal static class ExceptionAssert2
+ {
+ /// <summary>
+ /// Verifies that the code throws a <see cref="CryptographicException"/>.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <returns>The <see cref="CryptographicException"/> that was thrown, when successful</returns>
+ /// <exception>Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static CryptographicException ThrowsCryptographicException(Action testCode)
+ {
+ return Assert.Throws<CryptographicException>(testCode);
+ }
+ }
+}
diff --git a/src/DataProtection/version.props b/src/DataProtection/version.props
new file mode 100644
index 0000000000..71a78cddd8
--- /dev/null
+++ b/src/DataProtection/version.props
@@ -0,0 +1,12 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>3.0.0</VersionPrefix>
+ <VersionSuffix>alpha1</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/test/Cli.FunctionalTests/Templates/RazorBootstrapJQueryTemplate.cs b/test/Cli.FunctionalTests/Templates/RazorBootstrapJQueryTemplate.cs
index 0ddc5a4539..b4752c4e26 100644
--- a/test/Cli.FunctionalTests/Templates/RazorBootstrapJQueryTemplate.cs
+++ b/test/Cli.FunctionalTests/Templates/RazorBootstrapJQueryTemplate.cs
@@ -22,12 +22,10 @@ namespace Cli.FunctionalTests.Templates
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "css", "bootstrap.min.css.map"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.js"),
Path.Combine("wwwroot", "lib", "bootstrap", "dist", "js", "bootstrap.min.js"),
- Path.Combine("wwwroot", "lib", "jquery", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery", "LICENSE.txt"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.js"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.min.js"),
Path.Combine("wwwroot", "lib", "jquery", "dist", "jquery.min.map"),
- Path.Combine("wwwroot", "lib", "jquery-validation", ".bower.json"),
Path.Combine("wwwroot", "lib", "jquery-validation", "LICENSE.md"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "additional-methods.js"),
Path.Combine("wwwroot", "lib", "jquery-validation", "dist", "additional-methods.min.js"),
diff --git a/version.props b/version.props
index 203d44e279..96b67d47f8 100644
--- a/version.props
+++ b/version.props
@@ -18,7 +18,7 @@
<!-- The 'human friendly' version to display in installers. In pre-release builds, this might be "2.0.7 Preview 2 Build 12356". In final builds, it should be "2.0.7" -->
<PackageBrandingVersion>$(VersionPrefix)</PackageBrandingVersion>
- <PackageBrandingVersion Condition=" '$(IncludePreReleaseLabelInPackageVersion)' == 'true' ">$(PackageBrandingVersion) $(BrandingVersionSuffix)</PackageBrandingVersion>
+ <PackageBrandingVersion Condition=" '$(IncludePreReleaseLabelInPackageVersion)' == 'true' ">$(PackageBrandingVersion) $(BrandingVersionSuffix.Trim())</PackageBrandingVersion>
<!-- The version in files -->
<PackageVersion>$(VersionPrefix)</PackageVersion>