diff options
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 @@ -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' } @@ -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 <machineKey> 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 <masterKey> 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 Binary files differnew file mode 100644 index 0000000000..266754e8ee --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert.pfx 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 Binary files differnew file mode 100644 index 0000000000..4ed9bbe394 --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert2.pfx 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 Binary files differnew file mode 100644 index 0000000000..364251ba09 --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3.pfx 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 Binary files differnew file mode 100644 index 0000000000..9776e9006d --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCert3WithoutPrivateKey.pfx 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 Binary files differnew file mode 100644 index 0000000000..812374c50c --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Extensions.Test/TestFiles/TestCertWithoutPrivateKey.pfx 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 Binary files differnew file mode 100644 index 0000000000..329c90a83b --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.PublicKeyOnly.cer diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx Binary files differnew file mode 100644 index 0000000000..8bf695f1d6 --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert1.pfx diff --git a/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx Binary files differnew file mode 100644 index 0000000000..a54c93ba34 --- /dev/null +++ b/src/DataProtection/test/Microsoft.AspNetCore.DataProtection.Test/TestFiles/TestCert2.pfx 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> |