diff --git a/DataProtection.sln b/DataProtection.sln
index f632e6dc89..2151e23bba 100644
--- a/DataProtection.sln
+++ b/DataProtection.sln
@@ -9,8 +9,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtec
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Azure", "src\Microsoft.AspNet.DataProtection.Azure\Microsoft.AspNet.DataProtection.Azure.kproj", "{DF3671D7-A9B1-45F1-A195-0AD596001735}"
EndProject
-Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Compatibility", "src\Microsoft.AspNet.DataProtection.Compatibility\Microsoft.AspNet.DataProtection.Compatibility.kproj", "{C2FD9D02-AA0E-45FA-8561-EE357A94B73D}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{60336AB3-948D-4D15-A5FB-F32A2B91E814}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Test", "test\Microsoft.AspNet.DataProtection.Test\Microsoft.AspNet.DataProtection.Test.kproj", "{7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}"
@@ -23,6 +21,16 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Cryptograp
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Cryptography.Internal.Test", "test\Microsoft.AspNet.Cryptography.Internal.Test\Microsoft.AspNet.Cryptography.Internal.Test.kproj", "{37053D5F-5B61-47CE-8B72-298CE007FFB0}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Interfaces", "src\Microsoft.AspNet.DataProtection.Interfaces\Microsoft.AspNet.DataProtection.Interfaces.kproj", "{4B115BDE-B253-46A6-97BF-A8B37B344FF2}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Interfaces.Test", "test\Microsoft.AspNet.DataProtection.Interfaces.Test\Microsoft.AspNet.DataProtection.Interfaces.Test.kproj", "{FF650A69-DEE4-4B36-9E30-264EE7CFB478}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Test.Shared", "test\Microsoft.AspNet.DataProtection.Test.Shared\Microsoft.AspNet.DataProtection.Test.Shared.kproj", "{4F14BA2A-4F04-4676-8586-EC380977EE2E}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.Shared", "src\Microsoft.AspNet.DataProtection.Shared\Microsoft.AspNet.DataProtection.Shared.kproj", "{3277BB22-033F-4010-8131-A515B910CAAD}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.DataProtection.SystemWeb", "src\Microsoft.AspNet.DataProtection.SystemWeb\Microsoft.AspNet.DataProtection.SystemWeb.kproj", "{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -43,12 +51,6 @@ Global
{DF3671D7-A9B1-45F1-A195-0AD596001735}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF3671D7-A9B1-45F1-A195-0AD596001735}.Release|Any CPU.Build.0 = Release|Any CPU
{DF3671D7-A9B1-45F1-A195-0AD596001735}.Release|x86.ActiveCfg = Release|Any CPU
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Release|Any CPU.Build.0 = Release|Any CPU
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.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
@@ -87,6 +89,46 @@ Global
{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
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Debug|x86.Build.0 = Debug|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Release|x86.ActiveCfg = Release|Any CPU
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E}.Release|x86.Build.0 = Release|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Debug|x86.Build.0 = Debug|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.Release|x86.ActiveCfg = Release|Any CPU
+ {3277BB22-033F-4010-8131-A515B910CAAD}.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -94,11 +136,15 @@ Global
GlobalSection(NestedProjects) = preSolution
{1E570CD4-6F12-44F4-961E-005EE2002BC2} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
{DF3671D7-A9B1-45F1-A195-0AD596001735} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
- {C2FD9D02-AA0E-45FA-8561-EE357A94B73D} = {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}
+ {4F14BA2A-4F04-4676-8586-EC380977EE2E} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {3277BB22-033F-4010-8131-A515B910CAAD} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {E3552DEB-4173-43AE-BF69-3C10DFF3BAB6} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
EndGlobalSection
EndGlobal
diff --git a/makefile.shade b/makefile.shade
index 562494d144..bc16f4545f 100644
--- a/makefile.shade
+++ b/makefile.shade
@@ -1,3 +1,5 @@
+use assembly='WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
+use namespace='System.IO.Packaging'
var VERSION='0.1'
var FULL_VERSION='0.1'
@@ -5,3 +7,39 @@ var AUTHORS='Microsoft Open Technologies, Inc.'
use-standard-lifecycle
k-standard-goals
+
+#nupkg-patch target='compile'
+ @{
+ var packagePaths = Files.Include("artifacts/build/**/Microsoft.AspNet.DataProtection.SystemWeb.*.nupkg")
+ .Exclude("**/*.symbols.nupkg");
+ foreach (var packagePath in packagePaths)
+ {
+ using (var package = Package.Open(packagePath, FileMode.Open, FileAccess.ReadWrite))
+ {
+ CreatePartFromFile(
+ package,
+ @"src\Microsoft.AspNet.DataProtection.SystemWeb\web.config.transform",
+ @"content\web.config.transform");
+ }
+ }
+ }
+
+functions
+ @{
+ PackagePart CreatePartFromFile(
+ Package destination,
+ string sourceFileName,
+ string partUriString)
+ {
+ var partUri = PackUriHelper.CreatePartUri(new Uri(partUriString, UriKind.Relative));
+ var packagePart = destination.CreatePart(partUri, "application/octet", CompressionOption.Maximum);
+
+ using (var sourceStream = File.OpenRead(sourceFileName))
+ using (var stream = packagePart.GetStream())
+ {
+ sourceStream.CopyTo(stream);
+ }
+
+ return packagePart;
+ }
+ }
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs b/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs
index 31d7d468fc..ec2bbd8cc1 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs
@@ -2,7 +2,6 @@
// 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.Runtime.InteropServices;
using Microsoft.AspNet.Cryptography.Internal;
@@ -14,16 +13,16 @@ namespace Microsoft.AspNet.Cryptography.Cng
{
// MSDN says these fields represent the key length in bytes.
// It's wrong: these key lengths are all actually in bits.
- private uint dwMinLength;
- private uint dwMaxLength;
- private uint dwIncrement;
+ internal uint dwMinLength;
+ internal uint dwMaxLength;
+ internal uint dwIncrement;
public void EnsureValidKeyLength(uint keyLengthInBits)
{
if (!IsValidKeyLength(keyLengthInBits))
{
- string message = String.Format(CultureInfo.CurrentCulture, Resources.BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength, keyLengthInBits, dwMinLength, dwMaxLength, dwIncrement);
- throw new ArgumentException(message, "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");
}
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCryptUtil.cs b/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCryptUtil.cs
index 3256965416..aeca87fbe5 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCryptUtil.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Cng/BCryptUtil.cs
@@ -5,9 +5,14 @@ using System;
namespace Microsoft.AspNet.Cryptography.Cng
{
+ ///
+ /// Wraps utility BCRYPT APIs that don't work directly with handles.
+ ///
internal unsafe static class BCryptUtil
{
- // helper function that's similar to RNGCryptoServiceProvider, but works directly with pointers
+ ///
+ /// Fills a buffer with cryptographically secure random data.
+ ///
public static void GenRandom(byte* pbBuffer, uint cbBuffer)
{
if (cbBuffer != 0)
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs b/src/Microsoft.AspNet.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs
index 78a6bef2f5..f1231ffa6f 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Cng/CachedAlgorithmHandles.cs
@@ -23,86 +23,27 @@ namespace Microsoft.AspNet.Cryptography.Cng
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
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesCbc);
- }
- }
+ public static BCryptAlgorithmHandle AES_CBC => CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesCbc);
- public static BCryptAlgorithmHandle AES_GCM
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesGcm);
- }
- }
+ public static BCryptAlgorithmHandle AES_GCM => CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesGcm);
- public static BCryptAlgorithmHandle HMAC_SHA1
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha1);
- }
- }
+ public static BCryptAlgorithmHandle HMAC_SHA1 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha1);
- public static BCryptAlgorithmHandle HMAC_SHA256
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha256);
- }
- }
+ public static BCryptAlgorithmHandle HMAC_SHA256 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha256);
- public static BCryptAlgorithmHandle HMAC_SHA512
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha512);
- }
- }
+ public static BCryptAlgorithmHandle HMAC_SHA512 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha512);
// Only available on Win8+.
- public static BCryptAlgorithmHandle PBKDF2
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _pbkdf2);
- }
- }
+ public static BCryptAlgorithmHandle PBKDF2 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _pbkdf2);
- public static BCryptAlgorithmHandle SHA1
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha1);
- }
- }
+ public static BCryptAlgorithmHandle SHA1 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha1);
- public static BCryptAlgorithmHandle SHA256
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha256);
- }
- }
+ public static BCryptAlgorithmHandle SHA256 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha256);
- public static BCryptAlgorithmHandle SHA512
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha512);
- }
- }
+ public static BCryptAlgorithmHandle SHA512 => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha512);
- public static BCryptAlgorithmHandle SP800_108_CTR_HMAC
- {
- get
- {
- return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sp800_108_ctr_hmac);
- }
- }
+ // Only available on Win8+.
+ public static BCryptAlgorithmHandle SP800_108_CTR_HMAC => CachedAlgorithmInfo.GetAlgorithmHandle(ref _sp800_108_ctr_hmac);
private static BCryptAlgorithmHandle GetAesAlgorithm(string chainingMode)
{
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Cng/OSVersionUtil.cs b/src/Microsoft.AspNet.Cryptography.Internal/Cng/OSVersionUtil.cs
index aace9f7b33..541302a0c9 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Cng/OSVersionUtil.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Cng/OSVersionUtil.cs
@@ -46,12 +46,12 @@ namespace Microsoft.AspNet.Cryptography.Cng
}
}
- public static bool IsBCryptOnWin7OrLaterAvailable()
+ public static bool IsWindows()
{
return (_osVersion >= OSVersion.Win7OrLater);
}
- public static bool IsBCryptOnWin8OrLaterAvailable()
+ public static bool IsWindows8OrLater()
{
return (_osVersion >= OSVersion.Win8OrLater);
}
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/CryptoUtil.cs b/src/Microsoft.AspNet.Cryptography.Internal/CryptoUtil.cs
index 14e047c0c2..1b402a834e 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/CryptoUtil.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/CryptoUtil.cs
@@ -6,6 +6,8 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.Cryptography.Internal;
#if !DNXCORE50
using System.Runtime.ConstrainedExecution;
@@ -32,6 +34,24 @@ namespace Microsoft.AspNet.Cryptography
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
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Cryptography.Internal/Properties/AssemblyInfo.cs
index b903a20b6f..51cf267319 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Properties/AssemblyInfo.cs
@@ -12,4 +12,5 @@ using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("Microsoft.AspNet.Cryptography.KeyDerivation")]
[assembly: InternalsVisibleTo("Microsoft.AspNet.Cryptography.KeyDerivation.Test")]
[assembly: InternalsVisibleTo("Microsoft.AspNet.DataProtection")]
+[assembly: InternalsVisibleTo("Microsoft.AspNet.DataProtection.Interfaces.Test")]
[assembly: InternalsVisibleTo("Microsoft.AspNet.DataProtection.Test")]
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Cryptography.Internal/Properties/Resources.Designer.cs
index a33deb5f8a..3732eae0dc 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Properties/Resources.Designer.cs
@@ -42,6 +42,38 @@ namespace Microsoft.AspNet.Cryptography.Internal
return string.Format(CultureInfo.CurrentCulture, GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"), p0, p1, p2, p3);
}
+ ///
+ /// This operation requires Windows 7 / Windows Server 2008 R2 or later.
+ ///
+ internal static string Platform_Windows7Required
+ {
+ get { return GetString("Platform_Windows7Required"); }
+ }
+
+ ///
+ /// This operation requires Windows 7 / Windows Server 2008 R2 or later.
+ ///
+ internal static string FormatPlatform_Windows7Required()
+ {
+ return GetString("Platform_Windows7Required");
+ }
+
+ ///
+ /// This operation requires Windows 8 / Windows Server 2012 or later.
+ ///
+ internal static string Platform_Windows8Required
+ {
+ get { return GetString("Platform_Windows8Required"); }
+ }
+
+ ///
+ /// This operation requires Windows 8 / Windows Server 2012 or later.
+ ///
+ internal static string FormatPlatform_Windows8Required()
+ {
+ return GetString("Platform_Windows8Required");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/Resources.resx b/src/Microsoft.AspNet.Cryptography.Internal/Resources.resx
index 351535df12..125f619abb 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/Resources.resx
+++ b/src/Microsoft.AspNet.Cryptography.Internal/Resources.resx
@@ -123,4 +123,10 @@
The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}).
+
+ This operation requires Windows 7 / Windows Server 2008 R2 or later.
+
+
+ This operation requires Windows 8 / Windows Server 2012 or later.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs
index 8f89eba6bb..76cd840558 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/BCryptAlgorithmHandle.cs
@@ -10,6 +10,9 @@ using Microsoft.AspNet.Cryptography.Internal;
namespace Microsoft.AspNet.Cryptography.SafeHandles
{
+ ///
+ /// Represents a handle to a BCrypt algorithm provider from which keys and hashes can be created.
+ ///
internal unsafe sealed class BCryptAlgorithmHandle : BCryptHandle
{
// Called by P/Invoke when returning SafeHandles
@@ -20,10 +23,10 @@ namespace Microsoft.AspNet.Cryptography.SafeHandles
///
public BCryptHashHandle CreateHash()
{
- return CreateHashImpl(null, 0);
+ return CreateHashCore(null, 0);
}
- private BCryptHashHandle CreateHashImpl(byte* pbKey, uint cbKey)
+ private BCryptHashHandle CreateHashCore(byte* pbKey, uint cbKey)
{
BCryptHashHandle retVal;
int ntstatus = UnsafeNativeMethods.BCryptCreateHash(this, out retVal, IntPtr.Zero, 0, pbKey, cbKey, dwFlags: 0);
@@ -40,7 +43,7 @@ namespace Microsoft.AspNet.Cryptography.SafeHandles
public BCryptHashHandle CreateHmac(byte* pbKey, uint cbKey)
{
Debug.Assert(pbKey != null);
- return CreateHashImpl(pbKey, cbKey);
+ return CreateHashCore(pbKey, cbKey);
}
///
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs
index f2782aa2fa..f5d227cc1d 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/NCryptDescriptorHandle.cs
@@ -6,13 +6,33 @@ using Microsoft.Win32.SafeHandles;
namespace Microsoft.AspNet.Cryptography.SafeHandles
{
- internal sealed class NCryptDescriptorHandle : SafeHandleZeroOrMinusOneIsInvalid
+ 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()
{
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SafeCertContextHandle.cs b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SafeCertContextHandle.cs
deleted file mode 100644
index dbfc561884..0000000000
--- a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SafeCertContextHandle.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Runtime.CompilerServices;
-using Microsoft.Win32.SafeHandles;
-
-namespace Microsoft.AspNet.Cryptography.SafeHandles
-{
- internal sealed class SafeCertContextHandle : SafeHandleZeroOrMinusOneIsInvalid
- {
- private SafeCertContextHandle()
- : base(ownsHandle: true)
- {
- }
-
- public static SafeCertContextHandle CreateDuplicateFrom(IntPtr existingHandle)
- {
- SafeCertContextHandle newHandle = UnsafeNativeMethods.CertDuplicateCertificateContext(existingHandle);
- CryptoUtil.AssertSafeHandleIsValid(newHandle);
- return newHandle;
- }
-
- // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
- protected override bool ReleaseHandle()
- {
- return UnsafeNativeMethods.CertFreeCertificateContext(handle);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SafeNCryptKeyHandle.cs b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SafeNCryptKeyHandle.cs
deleted file mode 100644
index 8898809059..0000000000
--- a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SafeNCryptKeyHandle.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Runtime.InteropServices;
-using Microsoft.Win32.SafeHandles;
-
-#if DNXCORE50
-namespace Microsoft.AspNet.Cryptography.SafeHandles
-{
- ///
- /// Represents a managed view over an NCRYPT_KEY_HANDLE.
- ///
- internal class SafeNCryptKeyHandle : SafeHandleZeroOrMinusOneIsInvalid
- {
- // Called by P/Invoke when returning SafeHandles
- protected SafeNCryptKeyHandle()
- : base(ownsHandle: true) { }
-
- // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
- protected override bool ReleaseHandle()
- {
- // TODO: Replace me with a real implementation on CoreClr.
- throw new NotImplementedException();
- }
- }
-}
-#endif
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs
index 67d2072815..f2316b6d37 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/SafeHandles/SecureLocalAllocHandle.cs
@@ -2,9 +2,7 @@
// 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;
-using System.Security;
#if !DNXCORE50
using System.Runtime.ConstrainedExecution;
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/UnsafeBufferUtil.cs b/src/Microsoft.AspNet.Cryptography.Internal/UnsafeBufferUtil.cs
index 2949371fb9..629f4caa19 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/UnsafeBufferUtil.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/UnsafeBufferUtil.cs
@@ -14,8 +14,6 @@ namespace Microsoft.AspNet.Cryptography
{
internal unsafe static class UnsafeBufferUtil
{
- private static readonly byte[] _emptyArray = new byte[0];
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if !DNXCORE50
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
@@ -33,7 +31,7 @@ namespace Microsoft.AspNet.Cryptography
{
if (byteCount != 0)
{
- BlockCopyImpl((byte*)from, (byte*)to, byteCount);
+ BlockCopyCore((byte*)from, (byte*)to, byteCount);
}
}
@@ -60,7 +58,7 @@ namespace Microsoft.AspNet.Cryptography
#if !DNXCORE50
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
#endif
- public static void BlockCopy(byte* from, LocalAllocHandle to, uint byteCount)
+ public static void BlockCopy(void* from, LocalAllocHandle to, uint byteCount)
{
bool refAdded = false;
try
@@ -95,10 +93,11 @@ namespace Microsoft.AspNet.Cryptography
to.DangerousAddRef(ref toRefAdded);
if (sizeof(IntPtr) == 4)
{
- BlockCopyImpl(from: (byte*)from.DangerousGetHandle(), to: (byte*)to.DangerousGetHandle(), byteCount: (uint)length.ToInt32());
- } else
+ BlockCopyCore(from: (byte*)from.DangerousGetHandle(), to: (byte*)to.DangerousGetHandle(), byteCount: (uint)length.ToInt32());
+ }
+ else
{
- BlockCopyImpl(from: (byte*)from.DangerousGetHandle(), to: (byte*)to.DangerousGetHandle(), byteCount: (ulong)length.ToInt64());
+ BlockCopyCore(from: (byte*)from.DangerousGetHandle(), to: (byte*)to.DangerousGetHandle(), byteCount: (ulong)length.ToInt64());
}
}
finally
@@ -115,24 +114,26 @@ namespace Microsoft.AspNet.Cryptography
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void BlockCopyImpl(byte* from, byte* to, uint byteCount)
+ private static void BlockCopyCore(byte* from, byte* to, uint byteCount)
{
#if DNXCORE50
Buffer.MemoryCopy(from, to, (ulong)byteCount, (ulong)byteCount);
#else
- while (byteCount-- != 0) {
+ while (byteCount-- != 0)
+ {
to[byteCount] = from[byteCount];
}
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void BlockCopyImpl(byte* from, byte* to, ulong byteCount)
+ private static void BlockCopyCore(byte* from, byte* to, ulong byteCount)
{
#if DNXCORE50
Buffer.MemoryCopy(from, to, byteCount, byteCount);
#else
- while (byteCount-- != 0) {
+ while (byteCount-- != 0)
+ {
to[byteCount] = from[byteCount];
}
#endif
@@ -209,33 +210,5 @@ namespace Microsoft.AspNet.Cryptography
SecureZeroMemory(buffer, (ulong)length.ToInt64());
}
}
-
- ///
- /// Creates a new managed byte[] from unmanaged memory.
- ///
- public static byte[] ToManagedByteArray(byte* ptr, int byteCount)
- {
- return ToManagedByteArray(ptr, checked((uint)byteCount));
- }
-
- ///
- /// Creates a new managed byte[] from unmanaged memory.
- ///
- public static byte[] ToManagedByteArray(byte* ptr, uint byteCount)
- {
- if (byteCount == 0)
- {
- return _emptyArray; // degenerate case
- }
- else
- {
- byte[] bytes = new byte[byteCount];
- fixed (byte* pBytes = bytes)
- {
- BlockCopy(from: ptr, to: pBytes, byteCount: byteCount);
- }
- return bytes;
- }
- }
}
}
diff --git a/src/Microsoft.AspNet.Cryptography.Internal/UnsafeNativeMethods.cs b/src/Microsoft.AspNet.Cryptography.Internal/UnsafeNativeMethods.cs
index 07769f8dd4..80c9111d46 100644
--- a/src/Microsoft.AspNet.Cryptography.Internal/UnsafeNativeMethods.cs
+++ b/src/Microsoft.AspNet.Cryptography.Internal/UnsafeNativeMethods.cs
@@ -200,44 +200,7 @@ namespace Microsoft.AspNet.Cryptography
/*
* CRYPT32.DLL
*/
-
- [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi)]
-#if !DNXCORE50
- [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
-#endif
- // http://msdn.microsoft.com/en-us/library/windows/desktop/aa376045(v=vs.85).aspx
- internal static extern SafeCertContextHandle CertDuplicateCertificateContext(
- [In] IntPtr pCertContext);
-
- [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi)]
-#if !DNXCORE50
- [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
-#endif
- // http://msdn.microsoft.com/en-us/library/windows/desktop/aa376075(v=vs.85).aspx
- internal static extern bool CertFreeCertificateContext(
- [In] IntPtr pCertContext);
-
- [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
- // http://msdn.microsoft.com/en-us/library/windows/desktop/aa376079(v=vs.85).aspx
- internal static extern bool CertGetCertificateContextProperty(
- [In] SafeCertContextHandle pCertContext,
- [In] uint dwPropId,
- [In] void* pvData,
- [In, Out] ref uint pcbData);
-
- [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
- // http://msdn.microsoft.com/en-us/library/windows/desktop/aa379885(v=vs.85).aspx
-#if !DNXCORE50
- [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
-#endif
- internal static extern bool CryptAcquireCertificatePrivateKey(
- [In] SafeCertContextHandle pCert,
- [In] uint dwFlags,
- [In] void* pvParameters,
- [Out] out SafeNCryptKeyHandle phCryptProvOrNCryptKey,
- [Out] out uint pdwKeySpec,
- [Out] out bool pfCallerFreeProvOrNCryptKey);
-
+
[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(
@@ -301,16 +264,12 @@ namespace Microsoft.AspNet.Cryptography
[Out] out NCryptDescriptorHandle phDescriptor);
[DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)]
- // http://msdn.microsoft.com/en-us/library/windows/desktop/aa376249(v=vs.85).aspx
- internal static extern int NCryptDecrypt(
- [In] SafeNCryptKeyHandle hKey,
- [In] byte* pbInput,
- [In] uint cbInput,
- [In] void* pPaddingInfo,
- [In] byte* pbOutput,
- [In] uint cbOutput,
- [Out] out uint pcbResult,
- [In] NCryptEncryptFlags dwFlags);
+ // 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
@@ -336,6 +295,18 @@ namespace Microsoft.AspNet.Cryptography
[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
*/
diff --git a/src/Microsoft.AspNet.Cryptography.KeyDerivation/KeyDerivation.cs b/src/Microsoft.AspNet.Cryptography.KeyDerivation/KeyDerivation.cs
index 8e2a4db593..3bb818b433 100644
--- a/src/Microsoft.AspNet.Cryptography.KeyDerivation/KeyDerivation.cs
+++ b/src/Microsoft.AspNet.Cryptography.KeyDerivation/KeyDerivation.cs
@@ -6,8 +6,24 @@ using Microsoft.AspNet.Cryptography.KeyDerivation.PBKDF2;
namespace Microsoft.AspNet.Cryptography.KeyDerivation
{
+ ///
+ /// Provides algorithms for performing key derivation.
+ ///
public static class KeyDerivation
{
+ ///
+ /// Performs key derivation using the PBKDF2 algorithm.
+ ///
+ /// The password from which to derive the key.
+ /// The salt to be used during the key derivation process.
+ /// The pseudo-random function to be used in the key derivation process.
+ /// The number of iterations of the pseudo-random function to apply
+ /// during the key derivation process.
+ /// The desired length (in bytes) of the derived key.
+ /// The derived key.
+ ///
+ /// The PBKDF2 algorithm is specified in RFC 2898.
+ ///
public static byte[] Pbkdf2(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
{
// parameter checking
diff --git a/src/Microsoft.AspNet.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs b/src/Microsoft.AspNet.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs
index 3e0d1a0c3a..26ce118b15 100644
--- a/src/Microsoft.AspNet.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs
+++ b/src/Microsoft.AspNet.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs
@@ -16,11 +16,11 @@ namespace Microsoft.AspNet.Cryptography.KeyDerivation.PBKDF2
private static IPbkdf2Provider GetPbkdf2Provider()
{
// In priority order, our three implementations are Win8, Win7, and "other".
- if (OSVersionUtil.IsBCryptOnWin8OrLaterAvailable())
+ if (OSVersionUtil.IsWindows8OrLater())
{
// fastest implementation
return new Win8Pbkdf2Provider();
- } else if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
+ } else if (OSVersionUtil.IsWindows())
{
// acceptable implementation
return new Win7Pbkdf2Provider();
diff --git a/src/Microsoft.AspNet.DataProtection.Azure/BlobStorageXmlRepository.cs b/src/Microsoft.AspNet.DataProtection.Azure/BlobStorageXmlRepository.cs
index df31596d09..777a9654ea 100644
--- a/src/Microsoft.AspNet.DataProtection.Azure/BlobStorageXmlRepository.cs
+++ b/src/Microsoft.AspNet.DataProtection.Azure/BlobStorageXmlRepository.cs
@@ -9,6 +9,7 @@ using System.Net;
using System.Runtime.ExceptionServices;
using System.Xml.Linq;
using Microsoft.AspNet.DataProtection.Repositories;
+using Microsoft.Framework.Internal;
using Microsoft.Framework.OptionsModel;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
@@ -49,7 +50,7 @@ namespace Microsoft.AspNet.DataProtection.Azure
{
var blobRef = GetKeyRingBlockBlobReference();
XDocument document = ReadDocumentFromStorage(blobRef);
- return document?.Root.Elements().ToArray() ?? new XElement[0];
+ return (IReadOnlyCollection)document?.Root.Elements().ToList().AsReadOnly() ?? new XElement[0];
}
private XDocument ReadDocumentFromStorage(CloudBlockBlob blobRef)
diff --git a/src/Microsoft.AspNet.DataProtection.Azure/NotNullAttribute.cs b/src/Microsoft.AspNet.DataProtection.Azure/NotNullAttribute.cs
deleted file mode 100644
index 05b991841e..0000000000
--- a/src/Microsoft.AspNet.DataProtection.Azure/NotNullAttribute.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNet.DataProtection.Azure
-{
- [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
- internal sealed class NotNullAttribute : Attribute
- {
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection.Azure/project.json b/src/Microsoft.AspNet.DataProtection.Azure/project.json
index 159333396b..38fe54a39b 100644
--- a/src/Microsoft.AspNet.DataProtection.Azure/project.json
+++ b/src/Microsoft.AspNet.DataProtection.Azure/project.json
@@ -3,6 +3,7 @@
"description": "ASP.NET 5 blob storage repository for DataProtection.",
"dependencies": {
"Microsoft.AspNet.DataProtection": "1.0.0-*",
+ "Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" },
"WindowsAzure.Storage": "4.3.0"
},
"frameworks": {
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtectionProviderHelper.cs b/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtectionProviderHelper.cs
deleted file mode 100644
index 0237a782a0..0000000000
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtectionProviderHelper.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-namespace Microsoft.AspNet.DataProtection.Compatibility
-{
- internal sealed class DataProtectionProviderHelper
- {
- private IDataProtectionProvider _dataProtectionProvider;
-
- private DataProtectionProviderHelper() { } // can only be instantaited by self
-
- public static IDataProtectionProvider GetDataProtectionProvider(ref DataProtectionProviderHelper helperRef, IFactorySupportFunctions supportFunctions)
- {
- // First, make sure that only one thread ever initializes the helper instance.
- var helper = Volatile.Read(ref helperRef);
- if (helper == null)
- {
- var newHelper = new DataProtectionProviderHelper();
- helper = Interlocked.CompareExchange(ref helperRef, newHelper, null) ?? newHelper;
- }
-
- // Has the provider already been created?
- var provider = Volatile.Read(ref helper._dataProtectionProvider);
- if (provider == null)
- {
- // Since the helper is accessed by reference, all threads should agree on the one true helper
- // instance, so this lock is global given a particular reference. This is an implementation
- // of the double-check lock pattern.
- lock (helper)
- {
- provider = Volatile.Read(ref helper._dataProtectionProvider);
- if (provider == null)
- {
- provider = supportFunctions.CreateDataProtectionProvider();
- Volatile.Write(ref helper._dataProtectionProvider, provider);
- }
- }
- }
-
- // And we're done!
- Debug.Assert(provider != null);
- return provider;
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtector.cs b/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtector.cs
deleted file mode 100644
index b05407d92f..0000000000
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtector.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Diagnostics;
-using System.Security.Cryptography;
-
-namespace Microsoft.AspNet.DataProtection.Compatibility
-{
- public sealed class DataProtector : DataProtector, IFactorySupportFunctions
- where T : class, IDataProtectionProviderFactory, new()
- {
- private static DataProtectionProviderHelper _staticHelper;
- private DataProtectorHelper _helper;
-
- public DataProtector(string applicationName, string primaryPurpose, string[] specificPurposes)
- : base(applicationName, primaryPurpose, specificPurposes)
- {
- }
-
- protected override bool PrependHashedPurposeToPlaintext
- {
- get
- {
- return false;
- }
- }
-
- private IDataProtector GetCachedDataProtector()
- {
- var dataProtectionProvider = DataProtectionProviderHelper.GetDataProtectionProvider(ref _staticHelper, this);
- return DataProtectorHelper.GetDataProtector(ref _helper, dataProtectionProvider, this);
- }
-
- public override bool IsReprotectRequired(byte[] encryptedData)
- {
- return false;
- }
-
- protected override byte[] ProviderProtect(byte[] userData)
- {
- return GetCachedDataProtector().Protect(userData);
- }
-
- protected override byte[] ProviderUnprotect(byte[] encryptedData)
- {
- return GetCachedDataProtector().Unprotect(encryptedData);
- }
-
- IDataProtectionProvider IFactorySupportFunctions.CreateDataProtectionProvider()
- {
- IDataProtectionProviderFactory factory = Activator.CreateInstance();
- IDataProtectionProvider dataProtectionProvider = factory.CreateDataProtectionProvider();
- Debug.Assert(dataProtectionProvider != null);
- return dataProtectionProvider;
- }
-
- IDataProtector IFactorySupportFunctions.CreateDataProtector(IDataProtectionProvider dataProtectionProvider)
- {
- Debug.Assert(dataProtectionProvider != null);
-
- IDataProtector dataProtector = dataProtectionProvider.CreateProtector(ApplicationName).CreateProtector(PrimaryPurpose);
- foreach (string specificPurpose in SpecificPurposes)
- {
- dataProtector = dataProtector.CreateProtector(specificPurpose);
- }
-
- Debug.Assert(dataProtector != null);
- return dataProtector;
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtectorHelper.cs b/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtectorHelper.cs
deleted file mode 100644
index 62e756a442..0000000000
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/DataProtectorHelper.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-namespace Microsoft.AspNet.DataProtection.Compatibility
-{
- internal sealed class DataProtectorHelper
- {
- private IDataProtector _dataProtector;
-
- private DataProtectorHelper() { } // can only be instantaited by self
-
- public static IDataProtector GetDataProtector(ref DataProtectorHelper helperRef, IDataProtectionProvider protectionProvider, IFactorySupportFunctions supportFunctions)
- {
- // First, make sure that only one thread ever initializes the helper instance.
- var helper = Volatile.Read(ref helperRef);
- if (helper == null)
- {
- var newHelper = new DataProtectorHelper();
- helper = Interlocked.CompareExchange(ref helperRef, newHelper, null) ?? newHelper;
- }
-
- // Has the protector already been created?
- var protector = Volatile.Read(ref helper._dataProtector);
- if (protector == null)
- {
- // Since the helper is accessed by reference, all threads should agree on the one true helper
- // instance, so this lock is global given a particular reference. This is an implementation
- // of the double-check lock pattern.
- lock (helper)
- {
- protector = Volatile.Read(ref helper._dataProtector);
- if (protector == null)
- {
- protector = supportFunctions.CreateDataProtector(protectionProvider);
- Volatile.Write(ref helper._dataProtector, protector);
- }
- }
- }
-
- // And we're done!
- Debug.Assert(protector != null);
- return protector;
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/IFactorySupportFunctions.cs b/src/Microsoft.AspNet.DataProtection.Compatibility/IFactorySupportFunctions.cs
deleted file mode 100644
index 1adc41e58f..0000000000
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/IFactorySupportFunctions.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNet.DataProtection.Compatibility
-{
- internal interface IFactorySupportFunctions
- {
- IDataProtectionProvider CreateDataProtectionProvider();
-
- IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/CryptoUtil.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/CryptoUtil.cs
new file mode 100644
index 0000000000..a6c7fc2d9f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/CryptoUtil.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNet.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(message);" as a convenience to guard
+ // against a method returning null unexpectedly.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Fail(string message) where T : class
+ {
+ throw Fail(message);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs
new file mode 100644
index 0000000000..291ab59633
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/DataProtectionExtensions.cs
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.AspNet.DataProtection.Interfaces;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Helpful extension methods for data protection APIs.
+ ///
+ public static class DataProtectionExtensions
+ {
+ ///
+ /// Creates an given a list of purposes.
+ ///
+ /// The from which to generate the purpose chain.
+ /// 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.
+ /// An tied to the provided purpose chain.
+ ///
+ /// This is a convenience method which chains together several calls to
+ /// . See that method's
+ /// documentation for more information.
+ ///
+ public static IDataProtector CreateProtector([NotNull] this IDataProtectionProvider provider, [NotNull] IEnumerable 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("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;
+ }
+
+ ///
+ /// Creates an given a list of purposes.
+ ///
+ /// The from which to generate the purpose chain.
+ /// The primary purpose used to create the .
+ /// An optional list of secondary purposes which contribute to the purpose chain.
+ /// If this list is provided it cannot contain null elements.
+ /// An tied to the provided purpose chain.
+ ///
+ /// This is a convenience method which chains together several calls to
+ /// . See that method's
+ /// documentation for more information.
+ ///
+ public static IDataProtector CreateProtector([NotNull] this IDataProtectionProvider provider, [NotNull] string purpose, params string[] subPurposes)
+ {
+ // 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)subPurposes);
+ }
+ return protector ?? CryptoUtil.Fail("CreateProtector returned null.");
+ }
+
+ ///
+ /// Cryptographically protects a piece of plaintext data.
+ ///
+ /// The data protector to use for this operation.
+ /// The plaintext data to protect.
+ /// The protected form of the plaintext data.
+ public static string Protect([NotNull] this IDataProtector protector, [NotNull] string 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);
+ }
+ }
+
+ ///
+ /// Cryptographically unprotects a piece of protected data.
+ ///
+ /// The data protector to use for this operation.
+ /// The protected data to unprotect.
+ /// The plaintext form of the protected data.
+ ///
+ /// This method will throw CryptographicException if the input is invalid or malformed.
+ ///
+ public static string Unprotect([NotNull] this IDataProtector protector, [NotNull] string 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/Microsoft.AspNet.DataProtection.Interfaces/Error.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/Error.cs
new file mode 100644
index 0000000000..e479a1b833
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Error.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.AspNet.DataProtection.Interfaces;
+
+namespace Microsoft.AspNet.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/Microsoft.AspNet.DataProtection.Interfaces/IApplicationDiscriminator.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/IApplicationDiscriminator.cs
new file mode 100644
index 0000000000..232780a311
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/IApplicationDiscriminator.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Provides information used to discriminate applications.
+ ///
+ public interface IApplicationDiscriminator
+ {
+ ///
+ /// An identifier that uniquely discriminates this application from all other
+ /// applications on the machine.
+ ///
+ string Discriminator { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/IDataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/IDataProtectionProvider.cs
new file mode 100644
index 0000000000..cc06dbadf0
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/IDataProtectionProvider.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// An interface that can be used to create instances.
+ ///
+ public interface IDataProtectionProvider
+ {
+ ///
+ /// Creates an given a purpose.
+ ///
+ ///
+ /// The purpose to be assigned to the newly-created .
+ ///
+ /// An IDataProtector tied to the provided purpose.
+ ///
+ /// The parameter must be unique for the intended use case; two
+ /// different instances created with two different
+ /// values will not be able to decipher each other's payloads. The parameter
+ /// value is not intended to be kept secret.
+ ///
+ IDataProtector CreateProtector([NotNull] string purpose);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/IDataProtector.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/IDataProtector.cs
similarity index 68%
rename from src/Microsoft.AspNet.DataProtection/IDataProtector.cs
rename to src/Microsoft.AspNet.DataProtection.Interfaces/IDataProtector.cs
index 28a0d571b3..89dd31d759 100644
--- a/src/Microsoft.AspNet.DataProtection/IDataProtector.cs
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/IDataProtector.cs
@@ -2,6 +2,8 @@
// 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.Framework.Internal;
namespace Microsoft.AspNet.DataProtection
{
@@ -13,19 +15,18 @@ namespace Microsoft.AspNet.DataProtection
///
/// Cryptographically protects a piece of plaintext data.
///
- /// The plaintext data to protect.
+ /// The plaintext data to protect.
/// The protected form of the plaintext data.
- byte[] Protect(byte[] unprotectedData);
+ byte[] Protect([NotNull] byte[] plaintext);
///
/// Cryptographically unprotects a piece of protected data.
///
/// The protected data to unprotect.
/// The plaintext form of the protected data.
- ///
- /// Implementations should throw CryptographicException if the protected data is
- /// invalid or malformed.
- ///
- byte[] Unprotect(byte[] protectedData);
+ ///
+ /// Thrown if the protected data is invalid or malformed.
+ ///
+ byte[] Unprotect([NotNull] byte[] protectedData);
}
}
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/Microsoft.AspNet.DataProtection.Compatibility.kproj b/src/Microsoft.AspNet.DataProtection.Interfaces/Microsoft.AspNet.DataProtection.Interfaces.kproj
similarity index 94%
rename from src/Microsoft.AspNet.DataProtection.Compatibility/Microsoft.AspNet.DataProtection.Compatibility.kproj
rename to src/Microsoft.AspNet.DataProtection.Interfaces/Microsoft.AspNet.DataProtection.Interfaces.kproj
index 24ce7cf3b8..2937e9a8f7 100644
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/Microsoft.AspNet.DataProtection.Compatibility.kproj
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Microsoft.AspNet.DataProtection.Interfaces.kproj
@@ -6,7 +6,7 @@
- C2FD9D02-AA0E-45FA-8561-EE357A94B73D
+ 4b115bde-b253-46a6-97bf-a8b37b344ff2
..\..\artifacts\obj\$(MSBuildProjectName)
..\..\artifacts\bin\$(MSBuildProjectName)\
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/IDataProtectionProviderFactory.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/AssemblyInfo.cs
similarity index 51%
rename from src/Microsoft.AspNet.DataProtection.Compatibility/IDataProtectionProviderFactory.cs
rename to src/Microsoft.AspNet.DataProtection.Interfaces/Properties/AssemblyInfo.cs
index f470d7827a..57b7412919 100644
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/IDataProtectionProviderFactory.cs
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/AssemblyInfo.cs
@@ -2,11 +2,7 @@
// 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.AspNet.DataProtection.Compatibility
-{
- public interface IDataProtectionProviderFactory
- {
- IDataProtectionProvider CreateDataProtectionProvider();
- }
-}
+// for unit testing
+[assembly: InternalsVisibleTo("Microsoft.AspNet.DataProtection.Interfaces.Test")]
diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..9c0eed3510
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Properties/Resources.Designer.cs
@@ -0,0 +1,78 @@
+//
+namespace Microsoft.AspNet.DataProtection.Interfaces
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNet.DataProtection.Interfaces.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// The payload was invalid.
+ ///
+ internal static string CryptCommon_PayloadInvalid
+ {
+ get { return GetString("CryptCommon_PayloadInvalid"); }
+ }
+
+ ///
+ /// The payload was invalid.
+ ///
+ internal static string FormatCryptCommon_PayloadInvalid()
+ {
+ return GetString("CryptCommon_PayloadInvalid");
+ }
+
+ ///
+ /// The purposes collection cannot be null or empty and cannot contain null elements.
+ ///
+ internal static string DataProtectionExtensions_NullPurposesCollection
+ {
+ get { return GetString("DataProtectionExtensions_NullPurposesCollection"); }
+ }
+
+ ///
+ /// The purposes collection cannot be null or empty and cannot contain null elements.
+ ///
+ internal static string FormatDataProtectionExtensions_NullPurposesCollection()
+ {
+ return GetString("DataProtectionExtensions_NullPurposesCollection");
+ }
+
+ ///
+ /// An error occurred during a cryptographic operation.
+ ///
+ internal static string CryptCommon_GenericError
+ {
+ get { return GetString("CryptCommon_GenericError"); }
+ }
+
+ ///
+ /// An error occurred during a cryptographic operation.
+ ///
+ internal static string FormatCryptCommon_GenericError()
+ {
+ return GetString("CryptCommon_GenericError");
+ }
+
+ 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/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx b/src/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx
new file mode 100644
index 0000000000..84fa596602
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/Resources.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ The payload was invalid.
+
+
+ The purposes collection cannot be null or empty and cannot contain null elements.
+
+
+ An error occurred during a cryptographic operation.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.DataProtection/WebEncoders.cs b/src/Microsoft.AspNet.DataProtection.Interfaces/WebEncoders.cs
similarity index 95%
rename from src/Microsoft.AspNet.DataProtection/WebEncoders.cs
rename to src/Microsoft.AspNet.DataProtection.Interfaces/WebEncoders.cs
index c963b0c4b4..17d225f9d1 100644
--- a/src/Microsoft.AspNet.DataProtection/WebEncoders.cs
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/WebEncoders.cs
@@ -18,7 +18,7 @@ namespace Microsoft.AspNet.DataProtection
/// The input must not contain any whitespace or padding characters.
/// Throws FormatException if the input is malformed.
///
- public static byte[] Base64UrlDecode([NotNull] string input)
+ public static byte[] Base64UrlDecode(string input)
{
// Assumption: input is base64url encoded without padding and contains no whitespace.
@@ -56,7 +56,7 @@ namespace Microsoft.AspNet.DataProtection
///
/// The binary input to encode.
/// The base64url-encoded form of the input.
- public static string Base64UrlEncode([NotNull] byte[] input)
+ public static string Base64UrlEncode(byte[] input)
{
// Special-case empty input
if (input.Length == 0)
@@ -126,7 +126,7 @@ namespace Microsoft.AspNet.DataProtection
case 3:
return 1;
default:
- throw new FormatException("TODO: Malformed input.");
+ throw Error.CryptCommon_PayloadInvalid(); // not valid base64
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection.Interfaces/project.json b/src/Microsoft.AspNet.DataProtection.Interfaces/project.json
new file mode 100644
index 0000000000..f8543204e6
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Interfaces/project.json
@@ -0,0 +1,25 @@
+{
+ "version": "1.0.0-*",
+ "description": "Contains the core IDataProtector and IDataProtectionProvider interfaces for ASP.NET 5 Data Protection.",
+ "dependencies": {
+ "Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" },
+ "Microsoft.AspNet.DataProtection.Shared": { "type": "build", "version": "" }
+ },
+ "frameworks": {
+ "net451": { },
+ "dnx451": { },
+ "dnxcore50": {
+ "dependencies": {
+ "System.Diagnostics.Debug": "4.0.10-beta-*",
+ "System.Reflection": "4.0.10-beta-*",
+ "System.Resources.ResourceManager": "4.0.0-beta-*",
+ "System.Runtime.Extensions": "4.0.10-beta-*",
+ "System.Security.Cryptography.Encryption": "4.0.0-beta-*",
+ "System.Text.Encoding.Extensions": "4.0.10-beta-*"
+ }
+ }
+ },
+ "compilationOptions": {
+ "warningsAsErrors": true
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/EncodingUtil.cs b/src/Microsoft.AspNet.DataProtection.Shared/EncodingUtil.cs
similarity index 91%
rename from src/Microsoft.AspNet.DataProtection/EncodingUtil.cs
rename to src/Microsoft.AspNet.DataProtection.Shared/EncodingUtil.cs
index 0966289874..46571e69ab 100644
--- a/src/Microsoft.AspNet.DataProtection/EncodingUtil.cs
+++ b/src/Microsoft.AspNet.DataProtection.Shared/EncodingUtil.cs
@@ -6,7 +6,7 @@ using System.Text;
namespace Microsoft.AspNet.DataProtection
{
- internal unsafe static class EncodingUtil
+ 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/Microsoft.AspNet.DataProtection/ExceptionExtensions.cs b/src/Microsoft.AspNet.DataProtection.Shared/ExceptionExtensions.cs
similarity index 100%
rename from src/Microsoft.AspNet.DataProtection/ExceptionExtensions.cs
rename to src/Microsoft.AspNet.DataProtection.Shared/ExceptionExtensions.cs
diff --git a/src/Microsoft.AspNet.DataProtection.Shared/Microsoft.AspNet.DataProtection.Shared.kproj b/src/Microsoft.AspNet.DataProtection.Shared/Microsoft.AspNet.DataProtection.Shared.kproj
new file mode 100644
index 0000000000..081f013085
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Shared/Microsoft.AspNet.DataProtection.Shared.kproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 3277bb22-033f-4010-8131-a515b910caad
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+
+
+
diff --git a/src/Microsoft.AspNet.DataProtection.Shared/project.json b/src/Microsoft.AspNet.DataProtection.Shared/project.json
new file mode 100644
index 0000000000..96df0952d9
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.Shared/project.json
@@ -0,0 +1,20 @@
+{
+ "version": "1.0.0-*",
+ "description": "ASP.NET 5 Data Protection shared code.",
+ "dependencies": {
+ },
+ "frameworks": {
+ "net451": { },
+ "dnx451": { },
+ "dnxcore50": {
+ "dependencies": {
+ "System.Security.Cryptography.Encryption": "4.0.0-beta-*",
+ "System.Text.Encoding.Extensions": "4.0.10-beta-*"
+ }
+ }
+ },
+ "shared": "**\\*.cs",
+ "compilationOptions": {
+ "warningsAsErrors": true
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs b/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs
new file mode 100644
index 0000000000..5bf5b5b6d4
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/CompatibilityDataProtector.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.ComponentModel;
+using System.Configuration;
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNet.DataProtection.SystemWeb
+{
+ ///
+ /// A that can be used by ASP.NET 4.x to interact with ASP.NET 5's
+ /// DataProtection stack. This type is for internal use only and shouldn't be directly used by
+ /// developers.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public sealed class CompatibilityDataProtector : DataProtector
+ {
+ private static readonly Lazy _lazyProtectionProvider = new Lazy(CreateProtectionProvider);
+
+ private readonly Lazy _lazyProtector;
+
+ 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(() => _lazyProtectionProvider.Value.CreateProtector(primaryPurpose, specificPurposes));
+ }
+
+ // We take care of flowing purposes ourselves.
+ protected override bool PrependHashedPurposeToPlaintext { get; } = false;
+
+ private static IDataProtectionProvider CreateProtectionProvider()
+ {
+ // Read from 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 _lazyProtector.Value.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 _lazyProtector.Value.Unprotect(encryptedData);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs b/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs
new file mode 100644
index 0000000000..b6792c9882
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/DataProtectionStartup.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Configuration;
+using System.Web;
+using System.Web.Configuration;
+using Microsoft.Framework.DependencyInjection;
+
+namespace Microsoft.AspNet.DataProtection.SystemWeb
+{
+ ///
+ /// Allows controlling the configuration of the ASP.NET 5 Data Protection system.
+ ///
+ ///
+ /// Developers should not call these APIs directly. Instead, developers should subclass
+ /// this type and override the
+ /// method or methods
+ /// as appropriate.
+ ///
+ public class DataProtectionStartup
+ {
+ ///
+ /// Configures services used by the Data Protection system.
+ ///
+ /// A mutable collection of services.
+ ///
+ /// Developers may override this method to change the default behaviors of
+ /// the Data Protection system.
+ ///
+ 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.
+ }
+
+ ///
+ /// Creates a new instance of an .
+ ///
+ /// A collection of services from which to create the .
+ /// An .
+ ///
+ /// Developers should generally override the
+ /// method instead of this method.
+ ///
+ public virtual IDataProtectionProvider CreateDataProtectionProvider(IServiceProvider services)
+ {
+ return services.GetRequiredService();
+ }
+
+ ///
+ /// Provides a default implementation of required services, calls the developer's
+ /// configuration overrides, then creates an .
+ ///
+ internal IDataProtectionProvider InternalConfigureServicesAndCreateProtectionProvider()
+ {
+ var services = new ServiceCollection();
+ services.AddDataProtection();
+ services.Configure(options =>
+ {
+ // Try reading the discriminator from defined
+ // at the web app root. If the value was set explicitly (even if the value is empty),
+ // honor it as the discriminator. Otherwise, fall back to the metabase config path.
+ var machineKeySection = (MachineKeySection)WebConfigurationManager.GetWebApplicationSection("system.web/machineKey");
+ if (machineKeySection.ElementInformation.Properties["applicationName"].ValueOrigin != PropertyValueOrigin.Default)
+ {
+ options.ApplicationDiscriminator = machineKeySection.ApplicationName;
+ }
+ else
+ {
+ options.ApplicationDiscriminator = HttpRuntime.AppDomainAppId;
+ }
+
+ if (String.IsNullOrEmpty(options.ApplicationDiscriminator))
+ {
+ options.ApplicationDiscriminator = null; // homogenize to null
+ }
+ });
+
+ // Run 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/Microsoft.AspNet.DataProtection.SystemWeb.kproj b/src/Microsoft.AspNet.DataProtection.SystemWeb/Microsoft.AspNet.DataProtection.SystemWeb.kproj
new file mode 100644
index 0000000000..07283ae05e
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/Microsoft.AspNet.DataProtection.SystemWeb.kproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ e3552deb-4173-43ae-bf69-3c10dff3bab6
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+
+
+
diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection.SystemWeb/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..2a33533a17
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/Properties/Resources.Designer.cs
@@ -0,0 +1,62 @@
+//
+namespace Microsoft.AspNet.DataProtection.SystemWeb
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNet.DataProtection.SystemWeb.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.
+ ///
+ internal static string DataProtector_ProtectFailed
+ {
+ get { return GetString("DataProtector_ProtectFailed"); }
+ }
+
+ ///
+ /// A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.
+ ///
+ internal static string FormatDataProtector_ProtectFailed()
+ {
+ return GetString("DataProtector_ProtectFailed");
+ }
+
+ ///
+ /// The CreateDataProtectionProvider method returned null.
+ ///
+ internal static string Startup_CreateProviderReturnedNull
+ {
+ get { return GetString("Startup_CreateProviderReturnedNull"); }
+ }
+
+ ///
+ /// The CreateDataProtectionProvider method returned null.
+ ///
+ internal static string FormatStartup_CreateProviderReturnedNull()
+ {
+ return 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/Microsoft.AspNet.DataProtection.SystemWeb/Resources.resx b/src/Microsoft.AspNet.DataProtection.SystemWeb/Resources.resx
new file mode 100644
index 0000000000..0923e71d3c
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/Resources.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ A call to Protect failed. This most likely means that the data protection system is misconfigured. See the inner exception for more information.
+
+
+ The CreateDataProtectionProvider method returned null.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.DataProtection.Compatibility/project.json b/src/Microsoft.AspNet.DataProtection.SystemWeb/project.json
similarity index 58%
rename from src/Microsoft.AspNet.DataProtection.Compatibility/project.json
rename to src/Microsoft.AspNet.DataProtection.SystemWeb/project.json
index 519529f83f..620beafd64 100644
--- a/src/Microsoft.AspNet.DataProtection.Compatibility/project.json
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/project.json
@@ -4,10 +4,13 @@
"frameworks": {
"net451": {
"dependencies": {
- "Microsoft.AspNet.DataProtection": "1.0.0-*"
+ "Microsoft.AspNet.DataProtection": "1.0.0-*",
+ "Microsoft.Framework.DependencyInjection": "1.0.0-*"
},
"frameworkAssemblies": {
- "System.Security": "4.0.0.0"
+ "System.Configuration": "4.0.0.0",
+ "System.Security": "4.0.0.0",
+ "System.Web": "4.0.0.0"
}
}
},
diff --git a/src/Microsoft.AspNet.DataProtection.SystemWeb/web.config.transform b/src/Microsoft.AspNet.DataProtection.SystemWeb/web.config.transform
new file mode 100644
index 0000000000..470f2ca79c
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection.SystemWeb/web.config.transform
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNet.DataProtection/ActivatorExtensions.cs b/src/Microsoft.AspNet.DataProtection/ActivatorExtensions.cs
new file mode 100644
index 0000000000..5801287b72
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/ActivatorExtensions.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Extension methods for working with .
+ ///
+ internal static class ActivatorExtensions
+ {
+ ///
+ /// Creates an instance of and ensures
+ /// that it is assignable to .
+ ///
+ public static T CreateInstance(this IActivator activator, [NotNull] string implementationTypeName)
+ where T : class
+ {
+ return activator.CreateInstance(typeof(T), implementationTypeName) as T
+ ?? CryptoUtil.Fail("CreateInstance returned null.");
+ }
+
+ ///
+ /// Returns a given an .
+ /// Guaranteed to return non-null, even if is null.
+ ///
+ public static IActivator GetActivator(this IServiceProvider serviceProvider)
+ {
+ return (serviceProvider != null)
+ ? (serviceProvider.GetService() ?? new SimpleActivator(serviceProvider))
+ : SimpleActivator.DefaultWithoutServices;
+ }
+
+ ///
+ /// A simplified default implementation of that understands
+ /// how to call ctors which take .
+ ///
+ private sealed class SimpleActivator : IActivator
+ {
+ ///
+ /// A default whose wrapped is null.
+ ///
+ internal static readonly SimpleActivator DefaultWithoutServices = new SimpleActivator(null);
+
+ private readonly IServiceProvider _services;
+
+ public SimpleActivator(IServiceProvider services)
+ {
+ _services = services;
+ }
+
+ public 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);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/ApplyPolicyAttribute.cs b/src/Microsoft.AspNet.DataProtection/ApplyPolicyAttribute.cs
new file mode 100644
index 0000000000..43db6a0021
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/ApplyPolicyAttribute.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Signifies that the should bind this property from the registry.
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+ internal sealed class ApplyPolicyAttribute : Attribute { }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs
new file mode 100644
index 0000000000..2687a34a8f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AlgorithmAssert.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Cryptography;
+
+namespace Microsoft.AspNet.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/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptionOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptionOptions.cs
new file mode 100644
index 0000000000..da90f3b5b5
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptionOptions.cs
@@ -0,0 +1,200 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
+{
+ ///
+ /// Options for configuring authenticated encryption algorithms.
+ ///
+ public sealed class AuthenticatedEncryptionOptions : IInternalAuthenticatedEncryptionOptions
+ {
+ ///
+ /// The algorithm to use for symmetric encryption (confidentiality).
+ ///
+ ///
+ /// The default value is .
+ ///
+ public EncryptionAlgorithm EncryptionAlgorithm { get; set; } = EncryptionAlgorithm.AES_256_CBC;
+
+ ///
+ /// The algorithm to use for message authentication (tamper-proofing).
+ ///
+ ///
+ /// The default value is .
+ /// This property is ignored if specifies a 'GCM' algorithm.
+ ///
+ public ValidationAlgorithm ValidationAlgorithm { get; set; } = ValidationAlgorithm.HMACSHA256;
+
+ ///
+ /// Validates that this 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.
+ ///
+ public void Validate()
+ {
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ var encryptor = CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8));
+ try
+ {
+ encryptor.PerformSelfTest();
+ }
+ finally
+ {
+ (encryptor as IDisposable)?.Dispose();
+ }
+ }
+
+ /*
+ * HELPER ROUTINES
+ */
+
+ internal IAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(ISecret secret)
+ {
+ return CreateImplementationOptions()
+ .ToConfiguration()
+ .CreateDescriptorFromSecret(secret)
+ .CreateEncryptorInstance();
+ }
+
+ internal IInternalAuthenticatedEncryptionOptions CreateImplementationOptions()
+ {
+ if (IsGcmAlgorithm(EncryptionAlgorithm))
+ {
+ // GCM requires CNG, and CNG is only supported on Windows.
+ if (!OSVersionUtil.IsWindows())
+ {
+ throw new PlatformNotSupportedException(Resources.Platform_WindowsRequiredForGcm);
+ }
+ return new CngGcmAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = GetBCryptAlgorithmName(EncryptionAlgorithm),
+ EncryptionAlgorithmKeySize = GetAlgorithmKeySizeInBits(EncryptionAlgorithm)
+ };
+ }
+ else
+ {
+ if (OSVersionUtil.IsWindows())
+ {
+ // CNG preferred over managed implementations if running on Windows
+ return new CngCbcAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = GetBCryptAlgorithmName(EncryptionAlgorithm),
+ EncryptionAlgorithmKeySize = GetAlgorithmKeySizeInBits(EncryptionAlgorithm),
+ HashAlgorithm = GetBCryptAlgorithmName(ValidationAlgorithm)
+ };
+ }
+ else
+ {
+ // Use managed implementations as a fallback
+ return new ManagedAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithmType = GetManagedTypeForAlgorithm(EncryptionAlgorithm),
+ EncryptionAlgorithmKeySize = GetAlgorithmKeySizeInBits(EncryptionAlgorithm),
+ ValidationAlgorithmType = GetManagedTypeForAlgorithm(ValidationAlgorithm)
+ };
+ }
+ }
+ }
+
+ 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(algorithm));
+ }
+ }
+
+ private static string GetBCryptAlgorithmName(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(algorithm));
+ }
+ }
+
+ private static string GetBCryptAlgorithmName(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(algorithm));
+ }
+ }
+
+ private static Type GetManagedTypeForAlgorithm(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(algorithm));
+ }
+ }
+
+ private static Type GetManagedTypeForAlgorithm(ValidationAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case ValidationAlgorithm.HMACSHA256:
+ return typeof(HMACSHA256);
+
+ case ValidationAlgorithm.HMACSHA512:
+ return typeof(HMACSHA512);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(algorithm));
+ }
+ }
+
+ internal static bool IsGcmAlgorithm(EncryptionAlgorithm algorithm)
+ {
+ return (EncryptionAlgorithm.AES_128_GCM <= algorithm && algorithm <= EncryptionAlgorithm.AES_256_GCM);
+ }
+
+ IInternalAuthenticatedEncryptorConfiguration IInternalAuthenticatedEncryptionOptions.ToConfiguration()
+ {
+ return new AuthenticatedEncryptorConfiguration(this);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs
index 3941cb2e5f..56261ad27d 100644
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs
@@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.AspNet.Cryptography;
namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
{
@@ -31,5 +31,25 @@ namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
return retVal;
}
}
+
+ ///
+ /// Performs a self-test of this encryptor by running a sample payload through an
+ /// encrypt-then-decrypt operation. Throws if the operation fails.
+ ///
+ public static void PerformSelfTest(this IAuthenticatedEncryptor encryptor)
+ {
+ // Arrange
+ Guid plaintextAsGuid = Guid.NewGuid();
+ byte[] plaintextAsBytes = plaintextAsGuid.ToByteArray();
+ byte[] aad = Guid.NewGuid().ToByteArray();
+
+ // Act
+ byte[] protectedData = encryptor.Encrypt(new ArraySegment(plaintextAsBytes), new ArraySegment(aad));
+ byte[] roundTrippedData = encryptor.Decrypt(new ArraySegment(protectedData), new ArraySegment(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/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptionOptions.cs
similarity index 53%
rename from src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs
rename to src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptionOptions.cs
index b403c37203..feacc7996b 100644
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptionOptions.cs
@@ -5,15 +5,16 @@ using System;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.Cryptography.Cng;
using Microsoft.AspNet.Cryptography.SafeHandles;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNet.DataProtection.Cng;
namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
{
///
/// Options for configuring an authenticated encryption mechanism which uses
- /// Windows CNG algorithms in CBC encryption + HMAC validation modes.
+ /// Windows CNG algorithms in CBC encryption + HMAC authentication modes.
///
- public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
+ public sealed class CngCbcAuthenticatedEncryptionOptions : IInternalAuthenticatedEncryptionOptions
{
///
/// The name of the algorithm to use for symmetric encryption.
@@ -21,9 +22,11 @@ namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
/// This property is required to have a value.
///
///
- /// The algorithm must support CBC-style encryption and must have a block size of 64 bits or greater.
+ /// The algorithm must support CBC-style encryption and must have a block size of 64 bits
+ /// or greater.
/// The default value is 'AES'.
///
+ [ApplyPolicy]
public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
///
@@ -34,6 +37,7 @@ namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
///
/// The default value is null.
///
+ [ApplyPolicy]
public string EncryptionAlgorithmProvider { get; set; } = null;
///
@@ -44,6 +48,7 @@ namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
/// The key length must be 128 bits or greater.
/// The default value is 256.
///
+ [ApplyPolicy]
public int EncryptionAlgorithmKeySize { get; set; } = 256;
///
@@ -56,6 +61,7 @@ namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
/// of 128 bits or greater.
/// The default value is 'SHA256'.
///
+ [ApplyPolicy]
public string HashAlgorithm { get; set; } = Constants.BCRYPT_SHA256_ALGORITHM;
///
@@ -66,124 +72,109 @@ namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
///
/// The default value is null.
///
+ [ApplyPolicy]
public string HashAlgorithmProvider { get; set; } = null;
///
- /// Makes a duplicate of this object, which allows the original object to remain mutable.
+ /// Validates that this 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.
///
- internal CngCbcAuthenticatedEncryptorConfigurationOptions Clone()
+ public void Validate()
{
- return new CngCbcAuthenticatedEncryptorConfigurationOptions()
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ using (var encryptor = CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8)))
{
- EncryptionAlgorithm = this.EncryptionAlgorithm,
- EncryptionAlgorithmKeySize = this.EncryptionAlgorithmKeySize,
- EncryptionAlgorithmProvider = this.EncryptionAlgorithmProvider,
- HashAlgorithm = this.HashAlgorithm,
- HashAlgorithmProvider = this.HashAlgorithmProvider
- };
+ encryptor.PerformSelfTest();
+ }
}
- internal IAuthenticatedEncryptor CreateAuthenticatedEncryptor([NotNull] ISecret secret)
+ /*
+ * HELPER ROUTINES
+ */
+
+ internal CbcAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(ISecret secret)
{
- // Create the encryption object
- string encryptionAlgorithm = GetPropertyValueNotNullOrEmpty(EncryptionAlgorithm, nameof(EncryptionAlgorithm));
- string encryptionAlgorithmProvider = GetPropertyValueNormalizeToNull(EncryptionAlgorithmProvider);
- uint encryptionAlgorithmKeySizeInBits = GetKeySizeInBits(EncryptionAlgorithmKeySize);
- BCryptAlgorithmHandle encryptionAlgorithmHandle = GetEncryptionAlgorithmHandleAndCheckKeySize(encryptionAlgorithm, encryptionAlgorithmProvider, encryptionAlgorithmKeySizeInBits);
-
- // Create the validation object
- string hashAlgorithm = GetPropertyValueNotNullOrEmpty(HashAlgorithm, nameof(HashAlgorithm));
- string hashAlgorithmProvider = GetPropertyValueNormalizeToNull(HashAlgorithmProvider);
- BCryptAlgorithmHandle hashAlgorithmHandle = GetHashAlgorithmHandle(hashAlgorithm, hashAlgorithmProvider);
-
- // and we're good to go!
return new CbcAuthenticatedEncryptor(
keyDerivationKey: new Secret(secret),
- symmetricAlgorithmHandle: encryptionAlgorithmHandle,
- symmetricAlgorithmKeySizeInBytes: encryptionAlgorithmKeySizeInBits / 8,
- hmacAlgorithmHandle: hashAlgorithmHandle);
+ symmetricAlgorithmHandle: GetSymmetricBlockCipherAlgorithmHandle(),
+ symmetricAlgorithmKeySizeInBytes: (uint)(EncryptionAlgorithmKeySize / 8),
+ hmacAlgorithmHandle: GetHmacAlgorithmHandle());
}
- private static BCryptAlgorithmHandle GetEncryptionAlgorithmHandleAndCheckKeySize(string encryptionAlgorithm, string encryptionAlgorithmProvider, uint keyLengthInBits)
+ private BCryptAlgorithmHandle GetHmacAlgorithmHandle()
{
+ // basic argument checking
+ if (String.IsNullOrEmpty(HashAlgorithm))
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(HashAlgorithm));
+ }
+
BCryptAlgorithmHandle algorithmHandle = null;
// Special-case cached providers
- if (encryptionAlgorithmProvider == null)
+ if (HashAlgorithmProvider == null)
{
- if (encryptionAlgorithm == Constants.BCRYPT_AES_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.AES_CBC; }
+ if (HashAlgorithm == Constants.BCRYPT_SHA1_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA1; }
+ else if (HashAlgorithm == Constants.BCRYPT_SHA256_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA256; }
+ else if (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(encryptionAlgorithm, encryptionAlgorithmProvider);
- algorithmHandle.SetChainingMode(Constants.BCRYPT_CHAIN_MODE_CBC);
- }
-
- // make sure we're using a block cipher with an appropriate block size
- uint cipherBlockSizeInBytes = algorithmHandle.GetCipherBlockLength();
- CryptoUtil.Assert(cipherBlockSizeInBytes >= CbcAuthenticatedEncryptor.SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES,
- "cipherBlockSizeInBytes >= CbcAuthenticatedEncryptor.SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES");
-
- // make sure the provided key length is valid
- algorithmHandle.GetSupportedKeyLengths().EnsureValidKeyLength(keyLengthInBits);
-
- // all good!
- return algorithmHandle;
- }
-
- private static BCryptAlgorithmHandle GetHashAlgorithmHandle(string hashAlgorithm, string hashAlgorithmProvider)
- {
- BCryptAlgorithmHandle algorithmHandle = null;
-
- // Special-case cached providers
- if (hashAlgorithmProvider == null)
- {
- if (hashAlgorithm == Constants.BCRYPT_SHA1_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA1; }
- else if (hashAlgorithm == Constants.BCRYPT_SHA256_ALGORITHM) { algorithmHandle = CachedAlgorithmHandles.HMAC_SHA256; }
- else if (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(hashAlgorithm, hashAlgorithmProvider, hmac: true);
+ algorithmHandle = BCryptAlgorithmHandle.OpenAlgorithmHandle(HashAlgorithm, HashAlgorithmProvider, hmac: true);
}
// Make sure we're using a hash algorithm. We require a minimum 128-bit digest.
uint digestSize = algorithmHandle.GetHashDigestLength();
- CryptoUtil.Assert(digestSize >= CbcAuthenticatedEncryptor.HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES,
- "digestSize >= CbcAuthenticatedEncryptor.HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES");
+ AlgorithmAssert.IsAllowableValidationAlgorithmDigestSize(checked(digestSize * 8));
// all good!
return algorithmHandle;
}
- private static uint GetKeySizeInBits(int value)
+ private BCryptAlgorithmHandle GetSymmetricBlockCipherAlgorithmHandle()
{
- CryptoUtil.Assert(value >= 0, "value >= 0");
- CryptoUtil.Assert(value % 8 == 0, "value % 8 == 0");
- return (uint)value;
- }
-
- private static string GetPropertyValueNormalizeToNull(string value)
- {
- return (String.IsNullOrEmpty(value)) ? null : value;
- }
-
- private static string GetPropertyValueNotNullOrEmpty(string value, string propertyName)
- {
- if (String.IsNullOrEmpty(value))
+ // basic argument checking
+ if (String.IsNullOrEmpty(EncryptionAlgorithm))
{
- throw Error.Common_PropertyCannotBeNullOrEmpty(propertyName);
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(EncryptionAlgorithm));
}
- return value;
+ if (EncryptionAlgorithmKeySize < 0)
+ {
+ throw Error.Common_PropertyMustBeNonNegative(nameof(EncryptionAlgorithmKeySize));
+ }
+
+ BCryptAlgorithmHandle algorithmHandle = null;
+
+ // Special-case cached providers
+ if (EncryptionAlgorithmProvider == null)
+ {
+ if (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(EncryptionAlgorithm, 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)EncryptionAlgorithmKeySize));
+
+ // make sure the provided key length is valid
+ algorithmHandle.GetSupportedKeyLengths().EnsureValidKeyLength((uint)EncryptionAlgorithmKeySize);
+
+ // all good!
+ return algorithmHandle;
}
- IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
+ IInternalAuthenticatedEncryptorConfiguration IInternalAuthenticatedEncryptionOptions.ToConfiguration()
{
- return CreateAuthenticatedEncryptor(secret);
+ return new CngCbcAuthenticatedEncryptorConfiguration(this);
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfiguration.cs
deleted file mode 100644
index dc4b3b7a89..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfiguration.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal sealed class CngCbcAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration
- {
- internal static readonly XNamespace XmlNamespace = XNamespace.Get("http://www.asp.net/2014/dataProtection/cng");
- internal static readonly XName CbcEncryptorElementName = XmlNamespace.GetName("cbcEncryptor");
- internal static readonly XName EncryptionElementName = XmlNamespace.GetName("encryption");
- internal static readonly XName SecretElementName = XmlNamespace.GetName("secret");
- internal static readonly XName ValidationElementName = XmlNamespace.GetName("validation");
-
- private readonly CngCbcAuthenticatedEncryptorConfigurationOptions _options;
- private readonly ISecret _secret;
-
- public CngCbcAuthenticatedEncryptorConfiguration(CngCbcAuthenticatedEncryptorConfigurationOptions options, ISecret secret)
- {
- _options = options;
- _secret = secret;
- }
-
- public IAuthenticatedEncryptor CreateEncryptorInstance()
- {
- return _options.CreateAuthenticatedEncryptor(_secret);
- }
-
- private XElement EncryptSecret(IXmlEncryptor encryptor)
- {
- // First, create the inner element.
- XElement secretElement;
- byte[] plaintextSecret = new byte[_secret.Length];
- try
- {
- _secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret));
- secretElement = new XElement(SecretElementName, Convert.ToBase64String(plaintextSecret));
- }
- finally
- {
- Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
- }
-
- // Then encrypt it and wrap it in another element.
- var encryptedSecretElement = encryptor.Encrypt(secretElement);
- CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptedSecretElement.Attribute("decryptor")),
- @"TODO: encryption was invalid.");
-
- return new XElement(SecretElementName, encryptedSecretElement);
- }
-
- public XElement ToXml([NotNull] IXmlEncryptor xmlEncryptor)
- {
- //
- //
- //
- // ...
- //
-
- return new XElement(CbcEncryptorElementName,
- new XAttribute("reader", typeof(CngCbcAuthenticatedEncryptorConfigurationXmlReader).AssemblyQualifiedName),
- new XElement(EncryptionElementName,
- new XAttribute("algorithm", _options.EncryptionAlgorithm),
- new XAttribute("provider", _options.EncryptionAlgorithmProvider ?? String.Empty),
- new XAttribute("keyLength", _options.EncryptionAlgorithmKeySize)),
- new XElement(ValidationElementName,
- new XAttribute("algorithm", _options.HashAlgorithm),
- new XAttribute("provider", _options.HashAlgorithmProvider ?? String.Empty)),
- EncryptSecret(xmlEncryptor));
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationFactory.cs
deleted file mode 100644
index a82760350f..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationFactory.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.Framework.OptionsModel;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// A factory that is able to create a CNG-based IAuthenticatedEncryptor
- /// using CBC encryption + HMAC validation.
- ///
- public unsafe sealed class CngCbcAuthenticatedEncryptorConfigurationFactory : IAuthenticatedEncryptorConfigurationFactory
- {
- private readonly CngCbcAuthenticatedEncryptorConfigurationOptions _options;
-
- public CngCbcAuthenticatedEncryptorConfigurationFactory([NotNull] IOptions optionsAccessor)
- {
- _options = optionsAccessor.Options.Clone();
- }
-
- public IAuthenticatedEncryptorConfiguration CreateNewConfiguration()
- {
- // generate a 512-bit secret randomly
- const int KDK_SIZE_IN_BYTES = 512 / 8;
- var secret = Secret.Random(KDK_SIZE_IN_BYTES);
- return new CngCbcAuthenticatedEncryptorConfiguration(_options, secret);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationXmlReader.cs
deleted file mode 100644
index c799c3823c..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationXmlReader.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Linq;
-using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-using Microsoft.Framework.DependencyInjection;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal sealed class CngCbcAuthenticatedEncryptorConfigurationXmlReader : IAuthenticatedEncryptorConfigurationXmlReader
- {
- private readonly IServiceProvider _serviceProvider;
-
- public CngCbcAuthenticatedEncryptorConfigurationXmlReader(
- [NotNull] IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
- public IAuthenticatedEncryptorConfiguration FromXml([NotNull] XElement element)
- {
- //
- //
- //
- // ...
- //
-
- CryptoUtil.Assert(element.Name == CngCbcAuthenticatedEncryptorConfiguration.CbcEncryptorElementName,
- @"TODO: Bad element.");
-
- var options = new CngCbcAuthenticatedEncryptorConfigurationOptions();
-
- // read element
- var encryptionElement = element.Element(CngCbcAuthenticatedEncryptorConfiguration.EncryptionElementName);
- options.EncryptionAlgorithm = (string)encryptionElement.Attribute("algorithm");
- options.EncryptionAlgorithmProvider = (string)encryptionElement.Attribute("provider");
- options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
-
- // read element
- var validationElement = element.Element(CngCbcAuthenticatedEncryptorConfiguration.ValidationElementName);
- options.HashAlgorithm = (string)validationElement.Attribute("algorithm");
- options.HashAlgorithmProvider = (string)validationElement.Attribute("provider");
-
- // read the child of the element, then decrypt it
- var encryptedSecretElement = element.Element(CngCbcAuthenticatedEncryptorConfiguration.SecretElementName).Elements().Single();
- var secretElementDecryptorTypeName = (string)encryptedSecretElement.Attribute("decryptor");
- var secretElementDecryptorType = Type.GetType(secretElementDecryptorTypeName, throwOnError: true);
- var secretElementDecryptor = (IXmlDecryptor)ActivatorUtilities.CreateInstance(_serviceProvider, secretElementDecryptorType);
- var decryptedSecretElement = secretElementDecryptor.Decrypt(encryptedSecretElement);
- CryptoUtil.Assert(decryptedSecretElement.Name == CngCbcAuthenticatedEncryptorConfiguration.SecretElementName,
- @"TODO: Bad element.");
-
- byte[] decryptedSecretBytes = Convert.FromBase64String((string)decryptedSecretElement);
- try
- {
- var secret = new Secret(decryptedSecretBytes);
- return new CngCbcAuthenticatedEncryptorConfiguration(options, secret);
- }
- finally
- {
- Array.Clear(decryptedSecretBytes, 0, decryptedSecretBytes.Length);
- }
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptionOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptionOptions.cs
new file mode 100644
index 0000000000..c9b1f38b84
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptionOptions.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.Cryptography.SafeHandles;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNet.DataProtection.Cng;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
+{
+ ///
+ /// Options for configuring an authenticated encryption mechanism which uses
+ /// Windows CNG algorithms in GCM encryption + authentication modes.
+ ///
+ public sealed class CngGcmAuthenticatedEncryptionOptions : IInternalAuthenticatedEncryptionOptions
+ {
+ ///
+ /// 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.
+ ///
+ ///
+ /// The algorithm must support CBC-style encryption and must have a block size exactly
+ /// 128 bits.
+ /// The default value is 'AES'.
+ ///
+ [ApplyPolicy]
+ public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// The default value is null.
+ ///
+ [ApplyPolicy]
+ public string EncryptionAlgorithmProvider { get; set; } = null;
+
+ ///
+ /// The length (in bits) of the key that will be used for symmetric encryption.
+ /// This property is required to have a value.
+ ///
+ ///
+ /// The key length must be 128 bits or greater.
+ /// The default value is 256.
+ ///
+ [ApplyPolicy]
+ public int EncryptionAlgorithmKeySize { get; set; } = 256;
+
+ ///
+ /// Validates that this is well-formed, i.e.,
+ /// that the specified algorithm actually exists and can be instantiated properly.
+ /// An exception will be thrown if validation fails.
+ ///
+ public void Validate()
+ {
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ using (var encryptor = CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8)))
+ {
+ encryptor.PerformSelfTest();
+ }
+ }
+
+ /*
+ * HELPER ROUTINES
+ */
+
+ internal GcmAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(ISecret secret)
+ {
+ return new GcmAuthenticatedEncryptor(
+ keyDerivationKey: new Secret(secret),
+ symmetricAlgorithmHandle: GetSymmetricBlockCipherAlgorithmHandle(),
+ symmetricAlgorithmKeySizeInBytes: (uint)(EncryptionAlgorithmKeySize / 8));
+ }
+
+ private BCryptAlgorithmHandle GetSymmetricBlockCipherAlgorithmHandle()
+ {
+ // basic argument checking
+ if (String.IsNullOrEmpty(EncryptionAlgorithm))
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(EncryptionAlgorithm));
+ }
+ if (EncryptionAlgorithmKeySize < 0)
+ {
+ throw Error.Common_PropertyMustBeNonNegative(nameof(EncryptionAlgorithmKeySize));
+ }
+
+ BCryptAlgorithmHandle algorithmHandle = null;
+
+ // Special-case cached providers
+ if (EncryptionAlgorithmProvider == null)
+ {
+ if (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(EncryptionAlgorithm, 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)EncryptionAlgorithmKeySize));
+
+ // make sure the provided key length is valid
+ algorithmHandle.GetSupportedKeyLengths().EnsureValidKeyLength((uint)EncryptionAlgorithmKeySize);
+
+ // all good!
+ return algorithmHandle;
+ }
+
+ IInternalAuthenticatedEncryptorConfiguration IInternalAuthenticatedEncryptionOptions.ToConfiguration()
+ {
+ return new CngGcmAuthenticatedEncryptorConfiguration(this);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfiguration.cs
deleted file mode 100644
index 2224bfa71d..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfiguration.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal sealed class CngGcmAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration
- {
- internal static readonly XNamespace XmlNamespace = XNamespace.Get("http://www.asp.net/2014/dataProtection/cng");
- internal static readonly XName EncryptionElementName = XmlNamespace.GetName("encryption");
- internal static readonly XName GcmEncryptorElementName = XmlNamespace.GetName("gcmEncryptor");
- internal static readonly XName SecretElementName = XmlNamespace.GetName("secret");
-
- private readonly CngGcmAuthenticatedEncryptorConfigurationOptions _options;
- private readonly ISecret _secret;
-
- public CngGcmAuthenticatedEncryptorConfiguration(CngGcmAuthenticatedEncryptorConfigurationOptions options, ISecret secret)
- {
- _options = options;
- _secret = secret;
- }
-
- public IAuthenticatedEncryptor CreateEncryptorInstance()
- {
- return _options.CreateAuthenticatedEncryptor(_secret);
- }
-
- private XElement EncryptSecret(IXmlEncryptor encryptor)
- {
- // First, create the inner element.
- XElement secretElement;
- byte[] plaintextSecret = new byte[_secret.Length];
- try
- {
- _secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret));
- secretElement = new XElement(SecretElementName, Convert.ToBase64String(plaintextSecret));
- }
- finally
- {
- Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
- }
-
- // Then encrypt it and wrap it in another element.
- var encryptedSecretElement = encryptor.Encrypt(secretElement);
- CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptedSecretElement.Attribute("decryptor")),
- @"TODO: encryption was invalid.");
-
- return new XElement(SecretElementName, encryptedSecretElement);
- }
-
- public XElement ToXml([NotNull] IXmlEncryptor xmlEncryptor)
- {
- //
- //
- // ...
- //
-
- return new XElement(GcmEncryptorElementName,
- new XAttribute("reader", typeof(CngGcmAuthenticatedEncryptorConfigurationXmlReader).AssemblyQualifiedName),
- new XElement(EncryptionElementName,
- new XAttribute("algorithm", _options.EncryptionAlgorithm),
- new XAttribute("provider", _options.EncryptionAlgorithmProvider ?? String.Empty),
- new XAttribute("keyLength", _options.EncryptionAlgorithmKeySize)),
- EncryptSecret(xmlEncryptor));
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationFactory.cs
deleted file mode 100644
index 6c87153d04..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationFactory.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.Framework.OptionsModel;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// A factory that is able to create a CNG-based IAuthenticatedEncryptor
- /// using CBC encryption + HMAC validation.
- ///
- public unsafe sealed class CngGcmAuthenticatedEncryptorConfigurationFactory : IAuthenticatedEncryptorConfigurationFactory
- {
- private readonly CngGcmAuthenticatedEncryptorConfigurationOptions _options;
-
- public CngGcmAuthenticatedEncryptorConfigurationFactory([NotNull] IOptions optionsAccessor)
- {
- _options = optionsAccessor.Options.Clone();
- }
-
- public IAuthenticatedEncryptorConfiguration CreateNewConfiguration()
- {
- // generate a 512-bit secret randomly
- const int KDK_SIZE_IN_BYTES = 512 / 8;
- var secret = Secret.Random(KDK_SIZE_IN_BYTES);
- return new CngGcmAuthenticatedEncryptorConfiguration(_options, secret);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs
deleted file mode 100644
index bd455d36c9..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.Cryptography.Cng;
-using Microsoft.AspNet.Cryptography.SafeHandles;
-using Microsoft.AspNet.DataProtection.Cng;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// Options for configuring an authenticated encryption mechanism which uses
- /// Windows CNG encryption algorithms in Galois/Counter Mode.
- ///
- public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
- {
- ///
- /// 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.
- ///
- ///
- /// The algorithm must support GCM-style encryption and must have a block size of exactly 128 bits.
- /// The default value is 'AES'.
- ///
- public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
-
- ///
- /// 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.
- ///
- ///
- /// The default value is null.
- ///
- public string EncryptionAlgorithmProvider { get; set; } = null;
-
- ///
- /// The length (in bits) of the key that will be used for symmetric encryption.
- /// This property is required to have a value.
- ///
- ///
- /// The key length must be 128 bits or greater.
- /// The default value is 256.
- ///
- public int EncryptionAlgorithmKeySize { get; set; } = 256;
-
- ///
- /// Makes a duplicate of this object, which allows the original object to remain mutable.
- ///
- internal CngGcmAuthenticatedEncryptorConfigurationOptions Clone()
- {
- return new CngGcmAuthenticatedEncryptorConfigurationOptions()
- {
- EncryptionAlgorithm = this.EncryptionAlgorithm,
- EncryptionAlgorithmKeySize = this.EncryptionAlgorithmKeySize,
- EncryptionAlgorithmProvider = this.EncryptionAlgorithmProvider
- };
- }
-
- internal IAuthenticatedEncryptor CreateAuthenticatedEncryptor([NotNull] ISecret secret)
- {
- // Create the encryption object
- string encryptionAlgorithm = GetPropertyValueNotNullOrEmpty(EncryptionAlgorithm, nameof(EncryptionAlgorithm));
- string encryptionAlgorithmProvider = GetPropertyValueNormalizeToNull(EncryptionAlgorithmProvider);
- uint encryptionAlgorithmKeySizeInBits = GetKeySizeInBits(EncryptionAlgorithmKeySize);
- BCryptAlgorithmHandle encryptionAlgorithmHandle = GetEncryptionAlgorithmHandleAndCheckKeySize(encryptionAlgorithm, encryptionAlgorithmProvider, encryptionAlgorithmKeySizeInBits);
-
- // and we're good to go!
- return new GcmAuthenticatedEncryptor(
- keyDerivationKey: new Secret(secret),
- symmetricAlgorithmHandle: encryptionAlgorithmHandle,
- symmetricAlgorithmKeySizeInBytes: encryptionAlgorithmKeySizeInBits / 8);
- }
-
- private static BCryptAlgorithmHandle GetEncryptionAlgorithmHandleAndCheckKeySize(string encryptionAlgorithm, string encryptionAlgorithmProvider, uint keyLengthInBits)
- {
- BCryptAlgorithmHandle algorithmHandle = null;
-
- // Special-case cached providers
- if (encryptionAlgorithmProvider == null)
- {
- if (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(encryptionAlgorithm, encryptionAlgorithmProvider);
- algorithmHandle.SetChainingMode(Constants.BCRYPT_CHAIN_MODE_GCM);
- }
-
- // make sure we're using a block cipher with an appropriate block size
- uint cipherBlockSizeInBytes = algorithmHandle.GetCipherBlockLength();
- CryptoUtil.Assert(cipherBlockSizeInBytes == 128 / 8, "cipherBlockSizeInBytes == 128 / 8");
-
- // make sure the provided key length is valid
- algorithmHandle.GetSupportedKeyLengths().EnsureValidKeyLength(keyLengthInBits);
-
- // all good!
- return algorithmHandle;
- }
-
- private static uint GetKeySizeInBits(int value)
- {
- CryptoUtil.Assert(value >= 0, "value >= 0");
- CryptoUtil.Assert(value % 8 == 0, "value % 8 == 0");
- return (uint)value;
- }
-
- private static string GetPropertyValueNormalizeToNull(string value)
- {
- return (String.IsNullOrEmpty(value)) ? null : value;
- }
-
- private static string GetPropertyValueNotNullOrEmpty(string value, string propertyName)
- {
- if (String.IsNullOrEmpty(value))
- {
- throw Error.Common_PropertyCannotBeNullOrEmpty(propertyName);
- }
- return value;
- }
-
- IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
- {
- return CreateAuthenticatedEncryptor(secret);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationXmlReader.cs
deleted file mode 100644
index de6a1bc707..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationXmlReader.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Linq;
-using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-using Microsoft.Framework.DependencyInjection;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal sealed class CngGcmAuthenticatedEncryptorConfigurationXmlReader : IAuthenticatedEncryptorConfigurationXmlReader
- {
- private readonly IServiceProvider _serviceProvider;
-
- public CngGcmAuthenticatedEncryptorConfigurationXmlReader(
- [NotNull] IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
- public IAuthenticatedEncryptorConfiguration FromXml([NotNull] XElement element)
- {
- //
- //
- // ...
- //
-
- CryptoUtil.Assert(element.Name == CngGcmAuthenticatedEncryptorConfiguration.GcmEncryptorElementName,
- @"TODO: Bad element.");
-
- var options = new CngGcmAuthenticatedEncryptorConfigurationOptions();
-
- // read element
- var encryptionElement = element.Element(CngGcmAuthenticatedEncryptorConfiguration.EncryptionElementName);
- options.EncryptionAlgorithm = (string)encryptionElement.Attribute("algorithm");
- options.EncryptionAlgorithmProvider = (string)encryptionElement.Attribute("provider");
- options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
-
- // read the child of the element, then decrypt it
- var encryptedSecretElement = element.Element(CngGcmAuthenticatedEncryptorConfiguration.SecretElementName).Elements().Single();
- var secretElementDecryptorTypeName = (string)encryptedSecretElement.Attribute("decryptor");
- var secretElementDecryptorType = Type.GetType(secretElementDecryptorTypeName, throwOnError: true);
- var secretElementDecryptor = (IXmlDecryptor)ActivatorUtilities.CreateInstance(_serviceProvider, secretElementDecryptorType);
- var decryptedSecretElement = secretElementDecryptor.Decrypt(encryptedSecretElement);
- CryptoUtil.Assert(decryptedSecretElement.Name == CngGcmAuthenticatedEncryptorConfiguration.SecretElementName,
- @"TODO: Bad element.");
-
- byte[] decryptedSecretBytes = Convert.FromBase64String((string)decryptedSecretElement);
- try
- {
- var secret = new Secret(decryptedSecretBytes);
- return new CngGcmAuthenticatedEncryptorConfiguration(options, secret);
- }
- finally
- {
- Array.Clear(decryptedSecretBytes, 0, decryptedSecretBytes.Length);
- }
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..7b39b10715
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// Represents a generalized authenticated encryption mechanism.
+ ///
+ public unsafe sealed class AuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration, IInternalAuthenticatedEncryptorConfiguration
+ {
+ public AuthenticatedEncryptorConfiguration([NotNull] AuthenticatedEncryptionOptions options)
+ {
+ Options = options;
+ }
+
+ public AuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ // generate a 512-bit secret randomly
+ const int KDK_SIZE_IN_BYTES = 512 / 8;
+ var secret = Secret.Random(KDK_SIZE_IN_BYTES);
+ return ((IInternalAuthenticatedEncryptorConfiguration)this).CreateDescriptorFromSecret(secret);
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAuthenticatedEncryptorConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new AuthenticatedEncryptorDescriptor(Options, secret);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..c5ca78573d
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an object.
+ ///
+ public sealed class AuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ private readonly ISecret _masterKey;
+ private readonly AuthenticatedEncryptionOptions _options;
+
+ public AuthenticatedEncryptorDescriptor([NotNull] AuthenticatedEncryptionOptions options, [NotNull] ISecret masterKey)
+ {
+ _options = options;
+ _masterKey = masterKey;
+ }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance()
+ {
+ return _options.CreateAuthenticatedEncryptorInstance(_masterKey);
+ }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ //
+ //
+ //
+ // ...
+ //
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", _options.EncryptionAlgorithm));
+
+ var validationElement = (AuthenticatedEncryptionOptions.IsGcmAlgorithm(_options.EncryptionAlgorithm))
+ ? (object)new XComment(" AES-GCM includes a 128-bit authentication tag, no extra validation algorithm required. ")
+ : (object)new XElement("validation",
+ new XAttribute("algorithm", _options.ValidationAlgorithm));
+
+ var outerElement = new XElement("descriptor",
+ encryptionElement,
+ validationElement,
+ _masterKey.ToMasterKeyElement());
+
+ return new XmlSerializedDescriptorInfo(outerElement, typeof(AuthenticatedEncryptorDescriptorDeserializer));
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..7908b98748
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A class that can deserialize an that represents the serialized version
+ /// of an .
+ ///
+ public sealed class AuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ ///
+ /// Imports the from serialized XML.
+ ///
+ public IAuthenticatedEncryptorDescriptor ImportFromXml([NotNull] XElement element)
+ {
+ //
+ //
+ //
+ // ...
+ //
+
+ var options = new AuthenticatedEncryptionOptions();
+
+ var encryptionElement = element.Element("encryption");
+ options.EncryptionAlgorithm = (EncryptionAlgorithm)Enum.Parse(typeof(EncryptionAlgorithm), (string)encryptionElement.Attribute("algorithm"));
+
+ // only read if not GCM
+ if (!AuthenticatedEncryptionOptions.IsGcmAlgorithm(options.EncryptionAlgorithm))
+ {
+ var validationElement = element.Element("validation");
+ options.ValidationAlgorithm = (ValidationAlgorithm)Enum.Parse(typeof(ValidationAlgorithm), (string)validationElement.Attribute("algorithm"));
+ }
+
+ Secret masterKey = ((string)element.Elements("masterKey").Single()).ToSecret();
+ return new AuthenticatedEncryptorDescriptor(options, masterKey);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..b5dd186849
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// Represents a configured authenticated encryption mechanism which uses
+ /// Windows CNG algorithms in CBC encryption + HMAC authentication modes.
+ ///
+ public unsafe sealed class CngCbcAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration, IInternalAuthenticatedEncryptorConfiguration
+ {
+ public CngCbcAuthenticatedEncryptorConfiguration([NotNull] CngCbcAuthenticatedEncryptionOptions options)
+ {
+ Options = options;
+ }
+
+ public CngCbcAuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ // generate a 512-bit secret randomly
+ const int KDK_SIZE_IN_BYTES = 512 / 8;
+ var secret = Secret.Random(KDK_SIZE_IN_BYTES);
+ return ((IInternalAuthenticatedEncryptorConfiguration)this).CreateDescriptorFromSecret(secret);
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAuthenticatedEncryptorConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new CngCbcAuthenticatedEncryptorDescriptor(Options, secret);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..536dd573b4
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an object.
+ ///
+ public sealed class CngCbcAuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public CngCbcAuthenticatedEncryptorDescriptor([NotNull] CngCbcAuthenticatedEncryptionOptions options, [NotNull] ISecret masterKey)
+ {
+ Options = options;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal CngCbcAuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance()
+ {
+ return Options.CreateAuthenticatedEncryptorInstance(MasterKey);
+ }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ //
+ //
+ //
+ //
+ // ...
+ //
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", Options.EncryptionAlgorithm),
+ new XAttribute("keyLength", Options.EncryptionAlgorithmKeySize));
+ if (Options.EncryptionAlgorithmProvider != null)
+ {
+ encryptionElement.SetAttributeValue("provider", Options.EncryptionAlgorithmProvider);
+ }
+
+ var hashElement = new XElement("hash",
+ new XAttribute("algorithm", Options.HashAlgorithm));
+ if (Options.HashAlgorithmProvider != null)
+ {
+ hashElement.SetAttributeValue("provider", Options.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/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..86f5c5a162
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A class that can deserialize an that represents the serialized version
+ /// of an .
+ ///
+ public sealed class CngCbcAuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ ///
+ /// Imports the from serialized XML.
+ ///
+ public IAuthenticatedEncryptorDescriptor ImportFromXml([NotNull] XElement element)
+ {
+ //
+ //
+ //
+ //
+ // ...
+ //
+
+ var options = new CngCbcAuthenticatedEncryptionOptions();
+
+ var encryptionElement = element.Element("encryption");
+ options.EncryptionAlgorithm = (string)encryptionElement.Attribute("algorithm");
+ options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
+ options.EncryptionAlgorithmProvider = (string)encryptionElement.Attribute("provider"); // could be null
+
+ var hashElement = element.Element("hash");
+ options.HashAlgorithm = (string)hashElement.Attribute("algorithm");
+ options.HashAlgorithmProvider = (string)hashElement.Attribute("provider"); // could be null
+
+ Secret masterKey = ((string)element.Element("masterKey")).ToSecret();
+
+ return new CngCbcAuthenticatedEncryptorDescriptor(options, masterKey);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..4dc914bb70
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// Represents a configured authenticated encryption mechanism which uses
+ /// Windows CNG algorithms in GCM encryption + authentication modes.
+ ///
+ public unsafe sealed class CngGcmAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration, IInternalAuthenticatedEncryptorConfiguration
+ {
+ public CngGcmAuthenticatedEncryptorConfiguration([NotNull] CngGcmAuthenticatedEncryptionOptions options)
+ {
+ Options = options;
+ }
+
+ public CngGcmAuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ // generate a 512-bit secret randomly
+ const int KDK_SIZE_IN_BYTES = 512 / 8;
+ var secret = Secret.Random(KDK_SIZE_IN_BYTES);
+ return ((IInternalAuthenticatedEncryptorConfiguration)this).CreateDescriptorFromSecret(secret);
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAuthenticatedEncryptorConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new CngGcmAuthenticatedEncryptorDescriptor(Options, secret);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..82bb7217a6
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an object.
+ ///
+ public sealed class CngGcmAuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public CngGcmAuthenticatedEncryptorDescriptor([NotNull] CngGcmAuthenticatedEncryptionOptions options, [NotNull] ISecret masterKey)
+ {
+ Options = options;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal CngGcmAuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance()
+ {
+ return Options.CreateAuthenticatedEncryptorInstance(MasterKey);
+ }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ //
+ //
+ //
+ // ...
+ //
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", Options.EncryptionAlgorithm),
+ new XAttribute("keyLength", Options.EncryptionAlgorithmKeySize));
+ if (Options.EncryptionAlgorithmProvider != null)
+ {
+ encryptionElement.SetAttributeValue("provider", Options.EncryptionAlgorithmProvider);
+ }
+
+ var rootElement = new XElement("descriptor",
+ new XComment(" Algorithms provided by Windows CNG, using GCM mode encryption and validation "),
+ encryptionElement,
+ MasterKey.ToMasterKeyElement());
+
+ return new XmlSerializedDescriptorInfo(rootElement, typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer));
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..6da12b3b23
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A class that can deserialize an that represents the serialized version
+ /// of an .
+ ///
+ public sealed class CngGcmAuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ ///
+ /// Imports the from serialized XML.
+ ///
+ public IAuthenticatedEncryptorDescriptor ImportFromXml([NotNull] XElement element)
+ {
+ //
+ //
+ //
+ // ...
+ //
+
+ var options = new CngGcmAuthenticatedEncryptionOptions();
+
+ var encryptionElement = element.Element("encryption");
+ options.EncryptionAlgorithm = (string)encryptionElement.Attribute("algorithm");
+ options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
+ options.EncryptionAlgorithmProvider = (string)encryptionElement.Attribute("provider"); // could be null
+
+ Secret masterKey = ((string)element.Element("masterKey")).ToSecret();
+
+ return new CngGcmAuthenticatedEncryptorDescriptor(options, masterKey);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..40817c3b3a
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// The basic configuration that serves as a factory for types related to authenticated encryption.
+ ///
+ public interface IAuthenticatedEncryptorConfiguration
+ {
+ ///
+ /// Creates a new instance based on this
+ /// configuration. The newly-created instance contains unique key material and is distinct
+ /// from all other descriptors created by the method.
+ ///
+ /// A unique .
+ IAuthenticatedEncryptorDescriptor CreateNewDescriptor();
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..09d4334ce7
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A self-contained descriptor that wraps all information (including secret key
+ /// material) necessary to create an instance of an .
+ ///
+ public interface IAuthenticatedEncryptorDescriptor
+ {
+ ///
+ /// Creates an instance based on the current descriptor.
+ ///
+ /// An instance.
+ ///
+ /// For a given descriptor, any two instances returned by this method should
+ /// be considered equivalent, e.g., the payload returned by one's
+ /// method should be consumable by the other's method.
+ ///
+ IAuthenticatedEncryptor CreateEncryptorInstance();
+
+ ///
+ /// Exports the current descriptor to XML.
+ ///
+ ///
+ /// An wrapping the which represents the serialized
+ /// current descriptor object. The deserializer type must be assignable to .
+ ///
+ ///
+ /// If an element contains sensitive information (such as key material), the
+ /// element should be marked via the
+ /// extension method, and the caller should encrypt the element before persisting
+ /// the XML to storage.
+ ///
+ XmlSerializedDescriptorInfo ExportToXml();
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..805ded53b4
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// The basic interface for deserializing an XML element into an .
+ ///
+ public interface IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ ///
+ /// Deserializes the specified XML element.
+ ///
+ /// The element to deserialize.
+ /// The represented by .
+ IAuthenticatedEncryptorDescriptor ImportFromXml([NotNull] XElement element);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..f05c33fb4f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/IInternalAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ // 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.
+
+ ///
+ /// A type that knows how to create instances of an
+ /// given specific secret key material.
+ ///
+ internal interface IInternalAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration
+ {
+ ///
+ /// Creates a new instance from this
+ /// configuration given specific secret key material.
+ ///
+ ///
+ IAuthenticatedEncryptorDescriptor CreateDescriptorFromSecret(ISecret secret);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs
new file mode 100644
index 0000000000..3bdc2e2f96
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.Internal;
+using System.Security.Cryptography;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// Represents a configured authenticated encryption mechanism which uses
+ /// managed and types.
+ ///
+ public sealed class ManagedAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration, IInternalAuthenticatedEncryptorConfiguration
+ {
+ public ManagedAuthenticatedEncryptorConfiguration([NotNull] ManagedAuthenticatedEncryptionOptions options)
+ {
+ Options = options;
+ }
+
+ public ManagedAuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptorDescriptor CreateNewDescriptor()
+ {
+ // generate a 512-bit secret randomly
+ const int KDK_SIZE_IN_BYTES = 512 / 8;
+ var secret = Secret.Random(KDK_SIZE_IN_BYTES);
+ return ((IInternalAuthenticatedEncryptorConfiguration)this).CreateDescriptorFromSecret(secret);
+ }
+
+ IAuthenticatedEncryptorDescriptor IInternalAuthenticatedEncryptorConfiguration.CreateDescriptorFromSecret(ISecret secret)
+ {
+ return new ManagedAuthenticatedEncryptorDescriptor(Options, secret);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs
new file mode 100644
index 0000000000..0d0642b1f1
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A descriptor which can create an authenticated encryption system based upon the
+ /// configuration provided by an object.
+ ///
+ public sealed class ManagedAuthenticatedEncryptorDescriptor : IAuthenticatedEncryptorDescriptor
+ {
+ public ManagedAuthenticatedEncryptorDescriptor([NotNull] ManagedAuthenticatedEncryptionOptions options, [NotNull] ISecret masterKey)
+ {
+ Options = options;
+ MasterKey = masterKey;
+ }
+
+ internal ISecret MasterKey { get; }
+
+ internal ManagedAuthenticatedEncryptionOptions Options { get; }
+
+ public IAuthenticatedEncryptor CreateEncryptorInstance()
+ {
+ return Options.CreateAuthenticatedEncryptorInstance(MasterKey);
+ }
+
+ public XmlSerializedDescriptorInfo ExportToXml()
+ {
+ //
+ //
+ //
+ //
+ // ...
+ //
+
+ var encryptionElement = new XElement("encryption",
+ new XAttribute("algorithm", TypeToFriendlyName(Options.EncryptionAlgorithmType)),
+ new XAttribute("keyLength", Options.EncryptionAlgorithmKeySize));
+
+ var validationElement = new XElement("validation",
+ new XAttribute("algorithm", TypeToFriendlyName(Options.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/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs
new file mode 100644
index 0000000000..59878538f3
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// A class that can deserialize an that represents the serialized version
+ /// of an .
+ ///
+ public sealed class ManagedAuthenticatedEncryptorDescriptorDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
+ {
+ ///
+ /// Imports the from serialized XML.
+ ///
+ public IAuthenticatedEncryptorDescriptor ImportFromXml([NotNull] XElement element)
+ {
+ //
+ //
+ //
+ //
+ // ...
+ //
+
+ var options = new ManagedAuthenticatedEncryptionOptions();
+
+ var encryptionElement = element.Element("encryption");
+ options.EncryptionAlgorithmType = FriendlyNameToType((string)encryptionElement.Attribute("algorithm"));
+ options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
+
+ var validationElement = element.Element("validation");
+ options.ValidationAlgorithmType = FriendlyNameToType((string)validationElement.Attribute("algorithm"));
+
+ Secret masterKey = ((string)element.Element("masterKey")).ToSecret();
+
+ return new ManagedAuthenticatedEncryptorDescriptor(options, 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/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs
new file mode 100644
index 0000000000..de3b2cb607
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/SecretExtensions.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ internal unsafe static class SecretExtensions
+ {
+ ///
+ /// Converts an to an <masterKey> element which is marked
+ /// as requiring encryption.
+ ///
+ ///
+ 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(unprotectedSecretRawBytes));
+ unprotectedSecretAsBase64String = Convert.ToBase64String(unprotectedSecretRawBytes);
+ }
+ finally
+ {
+ Array.Clear(unprotectedSecretRawBytes, 0, unprotectedSecretRawBytes.Length);
+ }
+ }
+
+ XElement masterKeyElement = new XElement("masterKey",
+ new XComment(" Warning: the key below is in an unencrypted form. "),
+ new XElement("value", unprotectedSecretAsBase64String));
+ masterKeyElement.MarkAsRequiresEncryption();
+ return masterKeyElement;
+ }
+
+ ///
+ /// Converts a base64-encoded string into an .
+ ///
+ ///
+ 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/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs
new file mode 100644
index 0000000000..d6914c83d3
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlExtensions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public static class XmlExtensions
+ {
+ internal static bool IsMarkedAsRequiringEncryption(this XElement element)
+ {
+ return ((bool?)element.Attribute(XmlConstants.RequiresEncryptionAttributeName)).GetValueOrDefault();
+ }
+
+ ///
+ /// Marks the provided as requiring encryption before being persisted
+ /// to storage. Use when implementing .
+ ///
+ public static void MarkAsRequiresEncryption([NotNull] this XElement element)
+ {
+ element.SetAttributeValue(XmlConstants.RequiresEncryptionAttributeName, true);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs
new file mode 100644
index 0000000000..0f0b695b9f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ConfigurationModel/XmlSerializedDescriptorInfo.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ ///
+ /// Wraps an that contains the XML-serialized representation of an
+ /// along with the type that can be used
+ /// to deserialize it.
+ ///
+ public sealed class XmlSerializedDescriptorInfo
+ {
+ ///
+ /// Creates an instance of an .
+ ///
+ /// The XML-serialized form of the .
+ /// The class whose
+ /// method can be used to deserialize .
+ public XmlSerializedDescriptorInfo([NotNull] XElement serializedDescriptorElement, [NotNull] Type deserializerType)
+ {
+ if (!typeof(IAuthenticatedEncryptorDescriptorDeserializer).IsAssignableFrom(deserializerType))
+ {
+ throw new ArgumentException(
+ Resources.FormatTypeExtensions_BadCast(deserializerType.FullName, typeof(IAuthenticatedEncryptorDescriptorDeserializer).FullName),
+ nameof(deserializerType));
+ }
+
+ SerializedDescriptorElement = serializedDescriptorElement;
+ DeserializerType = deserializerType;
+ }
+
+ ///
+ /// The class whose
+ /// method can be used to deserialize the value stored in .
+ ///
+ public Type DeserializerType { get; }
+
+ ///
+ /// An XML-serialized representation of an .
+ ///
+ public XElement SerializedDescriptorElement { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs
new file mode 100644
index 0000000000..26b6e38fe4
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/EncryptionAlgorithm.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
+{
+ ///
+ /// Specifies a symmetric encryption algorithm to use for providing confidentiality
+ /// to protected payloads.
+ ///
+ public enum EncryptionAlgorithm
+ {
+ ///
+ /// The AES algorithm (FIPS 197) with a 128-bit key running in Cipher Block Chaining mode.
+ ///
+ AES_128_CBC,
+
+ ///
+ /// The AES algorithm (FIPS 197) with a 192-bit key running in Cipher Block Chaining mode.
+ ///
+ AES_192_CBC,
+
+ ///
+ /// The AES algorithm (FIPS 197) with a 256-bit key running in Cipher Block Chaining mode.
+ ///
+ AES_256_CBC,
+
+ ///
+ /// The AES algorithm (FIPS 197) with a 128-bit key running in Galois/Counter Mode (FIPS SP 800-38D).
+ ///
+ ///
+ /// This cipher mode produces a 128-bit authentication tag. This algorithm is currently only
+ /// supported on Windows.
+ ///
+ AES_128_GCM,
+
+ ///
+ /// The AES algorithm (FIPS 197) with a 192-bit key running in Galois/Counter Mode (FIPS SP 800-38D).
+ ///
+ ///
+ /// This cipher mode produces a 128-bit authentication tag.
+ ///
+ AES_192_GCM,
+
+ ///
+ /// The AES algorithm (FIPS 197) with a 256-bit key running in Galois/Counter Mode (FIPS SP 800-38D).
+ ///
+ ///
+ /// This cipher mode produces a 128-bit authentication tag.
+ ///
+ AES_256_GCM,
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfiguration.cs
deleted file mode 100644
index 6d4b3f518a..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfiguration.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Xml.Linq;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// Represents a type that contains configuration information about an IAuthenticatedEncryptor
- /// instance, including how to serialize it to XML.
- ///
- public interface IAuthenticatedEncryptorConfiguration
- {
- ///
- /// Creates a new IAuthenticatedEncryptor instance based on the current configuration.
- ///
- /// An IAuthenticatedEncryptor instance.
- IAuthenticatedEncryptor CreateEncryptorInstance();
-
- ///
- /// Exports the current configuration to XML, optionally encrypting secret key material.
- ///
- /// The XML encryptor used to encrypt secret material.
- /// An XElement representing the current configuration object.
- XElement ToXml(IXmlEncryptor xmlEncryptor);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationFactory.cs
deleted file mode 100644
index e25bacbcc8..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationFactory.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// Represents a type that can create new authenticated encryption configuration objects.
- ///
- public interface IAuthenticatedEncryptorConfigurationFactory
- {
- ///
- /// Creates a new configuration object with fresh secret key material.
- ///
- ///
- /// An IAuthenticatedEncryptorConfiguration instance.
- ///
- IAuthenticatedEncryptorConfiguration CreateNewConfiguration();
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationXmlReader.cs
deleted file mode 100644
index 7a211fc8cd..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationXmlReader.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Xml.Linq;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// Represents a type that can deserialize an XML-serialized IAuthenticatedEncryptorConfiguration.
- ///
- public interface IAuthenticatedEncryptorConfigurationXmlReader
- {
- ///
- /// Deserializes an XML-serialized IAuthenticatedEncryptorConfiguration.
- ///
- /// The XML element to deserialize.
- /// The deserialized IAuthenticatedEncryptorConfiguration.
- IAuthenticatedEncryptorConfiguration FromXml(XElement element);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IInternalAuthenticatedEncryptionOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IInternalAuthenticatedEncryptionOptions.cs
new file mode 100644
index 0000000000..444990a3ba
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IInternalAuthenticatedEncryptionOptions.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
+{
+ ///
+ /// Implemented by our options classes to generalize creating configuration objects.
+ ///
+ internal interface IInternalAuthenticatedEncryptionOptions
+ {
+ ///
+ /// Creates a object
+ /// from the given options.
+ ///
+ IInternalAuthenticatedEncryptorConfiguration ToConfiguration();
+
+ ///
+ /// Performs a self-test of the algorithm specified by the options object.
+ ///
+ void Validate();
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IInternalConfigurationOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IInternalConfigurationOptions.cs
deleted file mode 100644
index 6ae9384f03..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/IInternalConfigurationOptions.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal interface IInternalConfigurationOptions
- {
- IAuthenticatedEncryptor CreateAuthenticatedEncryptor(ISecret secret);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptionOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptionOptions.cs
new file mode 100644
index 0000000000..cb71ca58bc
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptionOptions.cs
@@ -0,0 +1,162 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNet.DataProtection.Managed;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
+{
+ ///
+ /// Options for configuring an authenticated encryption mechanism which uses
+ /// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
+ ///
+ public sealed class ManagedAuthenticatedEncryptionOptions : IInternalAuthenticatedEncryptionOptions
+ {
+ ///
+ /// The type of the algorithm to use for symmetric encryption.
+ /// The type must subclass .
+ /// This property is required to have a value.
+ ///
+ ///
+ /// 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.
+ ///
+ [ApplyPolicy]
+ public Type EncryptionAlgorithmType { get; set; } = typeof(Aes);
+
+ ///
+ /// The length (in bits) of the key that will be used for symmetric encryption.
+ /// This property is required to have a value.
+ ///
+ ///
+ /// The key length must be 128 bits or greater.
+ /// The default value is 256.
+ ///
+ [ApplyPolicy]
+ public int EncryptionAlgorithmKeySize { get; set; } = 256;
+
+ ///
+ /// The type of the algorithm to use for validation.
+ /// Type type must subclass .
+ /// This property is required to have a value.
+ ///
+ ///
+ /// The algorithm must have a digest length of 128 bits or greater.
+ /// The default algorithm is HMACSHA256.
+ ///
+ [ApplyPolicy]
+ public Type ValidationAlgorithmType { get; set; } = typeof(HMACSHA256);
+
+ ///
+ /// Validates that this is well-formed, i.e.,
+ /// that the specified algorithms actually exist and can be instantiated properly.
+ /// An exception will be thrown if validation fails.
+ ///
+ public void Validate()
+ {
+ // Run a sample payload through an encrypt -> decrypt operation to make sure data round-trips properly.
+ using (var encryptor = CreateAuthenticatedEncryptorInstance(Secret.Random(512 / 8)))
+ {
+ encryptor.PerformSelfTest();
+ }
+ }
+
+ /*
+ * HELPER ROUTINES
+ */
+
+ internal ManagedAuthenticatedEncryptor CreateAuthenticatedEncryptorInstance(ISecret secret)
+ {
+ return new ManagedAuthenticatedEncryptor(
+ keyDerivationKey: new Secret(secret),
+ symmetricAlgorithmFactory: GetSymmetricBlockCipherAlgorithmFactory(),
+ symmetricAlgorithmKeySizeInBytes: EncryptionAlgorithmKeySize / 8,
+ validationAlgorithmFactory: GetKeyedHashAlgorithmFactory());
+ }
+
+ private Func GetKeyedHashAlgorithmFactory()
+ {
+ // basic argument checking
+ if (ValidationAlgorithmType == null)
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(ValidationAlgorithmType));
+ }
+
+ if (ValidationAlgorithmType == typeof(HMACSHA256))
+ {
+ return () => new HMACSHA256();
+ }
+ else if (ValidationAlgorithmType == typeof(HMACSHA512))
+ {
+ return () => new HMACSHA512();
+ }
+ else
+ {
+ return AlgorithmActivator.CreateFactory(ValidationAlgorithmType);
+ }
+ }
+
+ private Func GetSymmetricBlockCipherAlgorithmFactory()
+ {
+ // basic argument checking
+ if (EncryptionAlgorithmType == null)
+ {
+ throw Error.Common_PropertyCannotBeNullOrEmpty(nameof(EncryptionAlgorithmType));
+ }
+ typeof(SymmetricAlgorithm).AssertIsAssignableFrom(EncryptionAlgorithmType);
+ if (EncryptionAlgorithmKeySize < 0)
+ {
+ throw Error.Common_PropertyMustBeNonNegative(nameof(EncryptionAlgorithmKeySize));
+ }
+
+ if (EncryptionAlgorithmType == typeof(Aes))
+ {
+ Func factory = null;
+#if !DNXCORE50
+ if (OSVersionUtil.IsWindows())
+ {
+ // If we're on desktop CLR and running on Windows, use the FIPS-compliant implementation.
+ factory = () => new AesCryptoServiceProvider();
+ }
+#endif
+ return factory ?? Aes.Create;
+ }
+ else
+ {
+ return AlgorithmActivator.CreateFactory(EncryptionAlgorithmType);
+ }
+ }
+
+ IInternalAuthenticatedEncryptorConfiguration IInternalAuthenticatedEncryptionOptions.ToConfiguration()
+ {
+ return new ManagedAuthenticatedEncryptorConfiguration(this);
+ }
+
+ ///
+ /// Contains helper methods for generating cryptographic algorithm factories.
+ ///
+ private static class AlgorithmActivator
+ {
+ ///
+ /// Creates a factory that wraps a call to .
+ ///
+ public static Func CreateFactory(Type implementation)
+ {
+ return ((IActivator)Activator.CreateInstance(typeof(AlgorithmActivatorCore<>).MakeGenericType(implementation))).Creator;
+ }
+
+ private interface IActivator
+ {
+ Func Creator { get; }
+ }
+
+ private class AlgorithmActivatorCore : IActivator where T : new()
+ {
+ public Func Creator { get; } = Activator.CreateInstance;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfiguration.cs
deleted file mode 100644
index 8e0295711b..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfiguration.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal sealed class ManagedAuthenticatedEncryptorConfiguration : IAuthenticatedEncryptorConfiguration
- {
- internal static readonly XNamespace XmlNamespace = XNamespace.Get("http://www.asp.net/2014/dataProtection/managed");
- internal static readonly XName ManagedEncryptorElementName = XmlNamespace.GetName("managedEncryptor");
- internal static readonly XName EncryptionElementName = XmlNamespace.GetName("encryption");
- internal static readonly XName SecretElementName = XmlNamespace.GetName("secret");
- internal static readonly XName ValidationElementName = XmlNamespace.GetName("validation");
-
- private readonly ManagedAuthenticatedEncryptorConfigurationOptions _options;
- private readonly ISecret _secret;
-
- public ManagedAuthenticatedEncryptorConfiguration(ManagedAuthenticatedEncryptorConfigurationOptions options, ISecret secret)
- {
- _options = options;
- _secret = secret;
- }
-
- public IAuthenticatedEncryptor CreateEncryptorInstance()
- {
- return _options.CreateAuthenticatedEncryptor(_secret);
- }
-
- private XElement EncryptSecret(IXmlEncryptor encryptor)
- {
- // First, create the inner element.
- XElement secretElement;
- byte[] plaintextSecret = new byte[_secret.Length];
- try
- {
- _secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret));
- secretElement = new XElement(SecretElementName, Convert.ToBase64String(plaintextSecret));
- }
- finally
- {
- Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
- }
-
- // Then encrypt it and wrap it in another element.
- var encryptedSecretElement = encryptor.Encrypt(secretElement);
- CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptedSecretElement.Attribute("decryptor")),
- @"TODO: encryption was invalid.");
-
- return new XElement(SecretElementName, encryptedSecretElement);
- }
-
- public XElement ToXml([NotNull] IXmlEncryptor xmlEncryptor)
- {
- //
- //
- //
- // ...
- //
-
- return new XElement(ManagedEncryptorElementName,
- new XAttribute("reader", typeof(ManagedAuthenticatedEncryptorConfigurationXmlReader).AssemblyQualifiedName),
- new XElement(EncryptionElementName,
- new XAttribute("type", _options.EncryptionAlgorithmType),
- new XAttribute("keyLength", _options.EncryptionAlgorithmKeySize)),
- new XElement(ValidationElementName,
- new XAttribute("type", _options.ValidationAlgorithmType)),
- EncryptSecret(xmlEncryptor));
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationFactory.cs
deleted file mode 100644
index e977694d1d..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationFactory.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.AspNet.DataProtection.Managed;
-using Microsoft.Framework.OptionsModel;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- public sealed class ManagedAuthenticatedEncryptorConfigurationFactory : IAuthenticatedEncryptorConfigurationFactory
- {
- private readonly ManagedAuthenticatedEncryptorConfigurationOptions _options;
-
- public ManagedAuthenticatedEncryptorConfigurationFactory([NotNull] IOptions optionsAccessor)
- {
- _options = optionsAccessor.Options.Clone();
- }
-
- public IAuthenticatedEncryptorConfiguration CreateNewConfiguration()
- {
- // generate a 512-bit secret randomly
- const int KDK_SIZE_IN_BYTES = 512 / 8;
- byte[] kdk = ManagedGenRandomImpl.Instance.GenRandom(KDK_SIZE_IN_BYTES);
- Secret secret;
- try
- {
- secret = new Secret(kdk);
- }
- finally
- {
- Array.Clear(kdk, 0, kdk.Length);
- }
-
- return new ManagedAuthenticatedEncryptorConfiguration(_options, secret);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs
deleted file mode 100644
index 4495f0ec94..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Reflection;
-using System.Security.Cryptography;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.Managed;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- ///
- /// Options for configuring an authenticated encryption mechanism which uses
- /// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
- ///
- public sealed class ManagedAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
- {
- ///
- /// The type of the algorithm to use for symmetric encryption.
- /// This property is required to have a value.
- ///
- ///
- /// 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.
- ///
- public Type EncryptionAlgorithmType { get; set; } = typeof(Aes);
-
- ///
- /// The length (in bits) of the key that will be used for symmetric encryption.
- /// This property is required to have a value.
- ///
- ///
- /// The key length must be 128 bits or greater.
- /// The default value is 256.
- ///
- public int EncryptionAlgorithmKeySize { get; set; } = 256;
-
- ///
- /// A factory for the algorithm to use for validation.
- /// This property is required to have a value.
- ///
- ///
- /// The algorithm must have a digest length of 128 bits or greater.
- /// The default algorithm is HMACSHA256.
- ///
- public Type ValidationAlgorithmType { get; set; } = typeof(HMACSHA256);
-
- ///
- /// Makes a duplicate of this object, which allows the original object to remain mutable.
- ///
- internal ManagedAuthenticatedEncryptorConfigurationOptions Clone()
- {
- return new ManagedAuthenticatedEncryptorConfigurationOptions()
- {
- EncryptionAlgorithmType = this.EncryptionAlgorithmType,
- EncryptionAlgorithmKeySize = this.EncryptionAlgorithmKeySize,
- ValidationAlgorithmType = this.ValidationAlgorithmType
- };
- }
-
- internal IAuthenticatedEncryptor CreateAuthenticatedEncryptor([NotNull] ISecret secret)
- {
- // Create the encryption and validation object
- Func encryptorFactory = GetEncryptionAlgorithmFactory();
- Func validatorFactory = GetValidationAlgorithmFactory();
-
- // Check key size here
- int keySizeInBits = EncryptionAlgorithmKeySize;
- CryptoUtil.Assert(keySizeInBits % 8 == 0, "keySizeInBits % 8 == 0");
- int keySizeInBytes = keySizeInBits / 8;
-
- // We're good to go!
- return new ManagedAuthenticatedEncryptor(
- keyDerivationKey: new Secret(secret),
- symmetricAlgorithmFactory: encryptorFactory,
- symmetricAlgorithmKeySizeInBytes: keySizeInBytes,
- validationAlgorithmFactory: validatorFactory);
- }
-
- private Func GetEncryptionAlgorithmFactory()
- {
- CryptoUtil.Assert(EncryptionAlgorithmType != null, "EncryptionAlgorithmType != null");
- CryptoUtil.Assert(typeof(SymmetricAlgorithm).IsAssignableFrom(EncryptionAlgorithmType), "typeof(SymmetricAlgorithm).IsAssignableFrom(EncryptionAlgorithmType)");
-
- if (EncryptionAlgorithmType == typeof(Aes))
- {
- // On Core CLR, there's no public concrete implementation of AES, so we'll special-case it here
- return Aes.Create;
- }
- else
- {
- // Otherwise the algorithm must have a default ctor
- return ((IActivator)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(EncryptionAlgorithmType))).Creator;
- }
- }
-
- private Func GetValidationAlgorithmFactory()
- {
- CryptoUtil.Assert(ValidationAlgorithmType != null, "ValidationAlgorithmType != null");
- CryptoUtil.Assert(typeof(KeyedHashAlgorithm).IsAssignableFrom(ValidationAlgorithmType), "typeof(KeyedHashAlgorithm).IsAssignableFrom(ValidationAlgorithmType)");
-
- // The algorithm must have a default ctor
- return ((IActivator)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator;
- }
-
- IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
- {
- return CreateAuthenticatedEncryptor(secret);
- }
-
- private interface IActivator
- {
- Func Creator { get; }
- }
-
- private class AlgorithmActivator : IActivator where T : new()
- {
- public Func Creator { get; } = Activator.CreateInstance;
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationXmlReader.cs
deleted file mode 100644
index b9b8821a40..0000000000
--- a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationXmlReader.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Linq;
-using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
-using Microsoft.Framework.DependencyInjection;
-
-namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
-{
- internal sealed class ManagedAuthenticatedEncryptorConfigurationXmlReader : IAuthenticatedEncryptorConfigurationXmlReader
- {
- private readonly IServiceProvider _serviceProvider;
-
- public ManagedAuthenticatedEncryptorConfigurationXmlReader(
- [NotNull] IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
- public IAuthenticatedEncryptorConfiguration FromXml([NotNull] XElement element)
- {
- //
- //
- //
- // ...
- //
-
- CryptoUtil.Assert(element.Name == ManagedAuthenticatedEncryptorConfiguration.EncryptionElementName,
- @"TODO: Bad element.");
-
- var options = new ManagedAuthenticatedEncryptorConfigurationOptions();
-
- // read element
- var encryptionElement = element.Element(ManagedAuthenticatedEncryptorConfiguration.EncryptionElementName);
- options.EncryptionAlgorithmType = Type.GetType((string)encryptionElement.Attribute("type"), throwOnError: true);
- options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
-
- // read element
- var validationElement = element.Element(ManagedAuthenticatedEncryptorConfiguration.ValidationElementName);
- options.ValidationAlgorithmType = Type.GetType((string)validationElement.Attribute("type"), throwOnError: true);
-
- // read the child of the element, then decrypt it
- var encryptedSecretElement = element.Element(ManagedAuthenticatedEncryptorConfiguration.SecretElementName).Elements().Single();
- var secretElementDecryptorTypeName = (string)encryptedSecretElement.Attribute("decryptor");
- var secretElementDecryptorType = Type.GetType(secretElementDecryptorTypeName, throwOnError: true);
- var secretElementDecryptor = (IXmlDecryptor)ActivatorUtilities.CreateInstance(_serviceProvider, secretElementDecryptorType);
- var decryptedSecretElement = secretElementDecryptor.Decrypt(encryptedSecretElement);
- CryptoUtil.Assert(decryptedSecretElement.Name == ManagedAuthenticatedEncryptorConfiguration.SecretElementName,
- @"TODO: Bad element.");
-
- byte[] decryptedSecretBytes = Convert.FromBase64String((string)decryptedSecretElement);
- try
- {
- var secret = new Secret(decryptedSecretBytes);
- return new ManagedAuthenticatedEncryptorConfiguration(options, secret);
- }
- finally
- {
- Array.Clear(decryptedSecretBytes, 0, decryptedSecretBytes.Length);
- }
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs
new file mode 100644
index 0000000000..93d96fdd97
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/AuthenticatedEncryption/ValidationAlgorithm.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption
+{
+ ///
+ /// Specifies a message authentication algorithm to use for providing tamper-proofing
+ /// to protected payloads.
+ ///
+ public enum ValidationAlgorithm
+ {
+ ///
+ /// The HMAC algorithm (RFC 2104) using the SHA-256 hash function (FIPS 180-4).
+ ///
+ HMACSHA256,
+
+ ///
+ /// The HMAC algorithm (RFC 2104) using the SHA-512 hash function (FIPS 180-4).
+ ///
+ HMACSHA512,
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/Cng/CbcAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.DataProtection/Cng/CbcAuthenticatedEncryptor.cs
index 2a27d5633b..f88c224a68 100644
--- a/src/Microsoft.AspNet.DataProtection/Cng/CbcAuthenticatedEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/Cng/CbcAuthenticatedEncryptor.cs
@@ -5,6 +5,7 @@ using System;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.Cryptography.Cng;
using Microsoft.AspNet.Cryptography.SafeHandles;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNet.DataProtection.SP800_108;
namespace Microsoft.AspNet.DataProtection.Cng
@@ -25,13 +26,6 @@ namespace Microsoft.AspNet.DataProtection.Cng
// probability of collision, and this is acceptable for the expected KDK lifetime.
private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
- // Our analysis re: IV collision resistance only holds if we're working with block ciphers
- // with a block length of 64 bits or greater.
- internal const uint SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES = 64 / 8;
-
- // Min security bar: authentication tag must have at least 128 bits of output.
- internal const uint HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES = 128 / 8;
-
private readonly byte[] _contextHeader;
private readonly IBCryptGenRandom _genRandom;
private readonly BCryptAlgorithmHandle _hmacAlgorithmHandle;
@@ -44,9 +38,6 @@ namespace Microsoft.AspNet.DataProtection.Cng
public CbcAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, BCryptAlgorithmHandle hmacAlgorithmHandle, IBCryptGenRandom genRandom = null)
{
- CryptoUtil.Assert(KEY_MODIFIER_SIZE_IN_BYTES <= symmetricAlgorithmKeySizeInBytes && symmetricAlgorithmKeySizeInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "KEY_MODIFIER_SIZE_IN_BYTES <= symmetricAlgorithmKeySizeInBytes && symmetricAlgorithmKeySizeInBytes <= Constants.MAX_STACKALLOC_BYTES");
-
_genRandom = genRandom ?? BCryptGenRandomImpl.Instance;
_sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey);
_symmetricAlgorithmHandle = symmetricAlgorithmHandle;
@@ -56,14 +47,10 @@ namespace Microsoft.AspNet.DataProtection.Cng
_hmacAlgorithmDigestLengthInBytes = hmacAlgorithmHandle.GetHashDigestLength();
_hmacAlgorithmSubkeyLengthInBytes = _hmacAlgorithmDigestLengthInBytes; // for simplicity we'll generate HMAC subkeys with a length equal to the digest length
- CryptoUtil.Assert(SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES <= _symmetricAlgorithmBlockSizeInBytes && _symmetricAlgorithmBlockSizeInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES <= _symmetricAlgorithmBlockSizeInBytes && _symmetricAlgorithmBlockSizeInBytes <= Constants.MAX_STACKALLOC_BYTES");
-
- CryptoUtil.Assert(HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES <= _hmacAlgorithmDigestLengthInBytes,
- "HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES <= _hmacAlgorithmDigestLengthInBytes");
-
- CryptoUtil.Assert(KEY_MODIFIER_SIZE_IN_BYTES <= _hmacAlgorithmSubkeyLengthInBytes && _hmacAlgorithmSubkeyLengthInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "KEY_MODIFIER_SIZE_IN_BYTES <= _hmacAlgorithmSubkeyLengthInBytes && _hmacAlgorithmSubkeyLengthInBytes <= Constants.MAX_STACKALLOC_BYTES");
+ // 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();
}
diff --git a/src/Microsoft.AspNet.DataProtection/Cng/DpapiSecretSerializerHelper.cs b/src/Microsoft.AspNet.DataProtection/Cng/DpapiSecretSerializerHelper.cs
index 13b583c4bf..791f6a5915 100644
--- a/src/Microsoft.AspNet.DataProtection/Cng/DpapiSecretSerializerHelper.cs
+++ b/src/Microsoft.AspNet.DataProtection/Cng/DpapiSecretSerializerHelper.cs
@@ -23,6 +23,22 @@ namespace Microsoft.AspNet.DataProtection.Cng
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);
@@ -35,7 +51,7 @@ namespace Microsoft.AspNet.DataProtection.Cng
secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret));
fixed (byte* pbPurpose = _purpose)
{
- return ProtectWithDpapiImpl(pbPlaintextSecret, (uint)plaintextSecret.Length, pbPurpose, (uint)_purpose.Length, fLocalMachine: protectToLocalMachine);
+ return ProtectWithDpapiCore(pbPlaintextSecret, (uint)plaintextSecret.Length, pbPurpose, (uint)_purpose.Length, fLocalMachine: protectToLocalMachine);
}
}
finally
@@ -46,7 +62,7 @@ namespace Microsoft.AspNet.DataProtection.Cng
}
}
- internal static byte[] ProtectWithDpapiImpl(byte* pbSecret, uint cbSecret, byte* pbOptionalEntropy, uint cbOptionalEntropy, bool fLocalMachine = false)
+ 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
@@ -110,7 +126,7 @@ namespace Microsoft.AspNet.DataProtection.Cng
secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret));
byte dummy; // used to provide a valid memory address if secret is zero-length
- return ProtectWithDpapiNGImpl(
+ return ProtectWithDpapiNGCore(
protectionDescriptorHandle: protectionDescriptorHandle,
pbData: (pbPlaintextSecret != null) ? pbPlaintextSecret : &dummy,
cbData: (uint)plaintextSecret.Length);
@@ -123,7 +139,7 @@ namespace Microsoft.AspNet.DataProtection.Cng
}
}
- private static byte[] ProtectWithDpapiNGImpl(NCryptDescriptorHandle protectionDescriptorHandle, byte* pbData, uint cbData)
+ private static byte[] ProtectWithDpapiNGCore(NCryptDescriptorHandle protectionDescriptorHandle, byte* pbData, uint cbData)
{
Debug.Assert(protectionDescriptorHandle != null);
Debug.Assert(pbData != null);
@@ -141,7 +157,7 @@ namespace Microsoft.AspNet.DataProtection.Cng
ppbProtectedBlob: out protectedData,
pcbProtectedBlob: out cbProtectedData);
UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
- CryptoUtil.Assert(protectedData != null && !protectedData.IsInvalid, "protectedData != null && !protectedData.IsInvalid");
+ CryptoUtil.AssertSafeHandleIsValid(protectedData);
// Copy the data from LocalAlloc-allocated memory into a managed memory buffer.
using (protectedData)
@@ -181,12 +197,12 @@ namespace Microsoft.AspNet.DataProtection.Cng
{
fixed (byte* pbPurpose = _purpose)
{
- return UnprotectWithDpapiImpl(pbProtectedSecret, (uint)protectedSecret.Length, pbPurpose, (uint)_purpose.Length);
+ return UnprotectWithDpapiCore(pbProtectedSecret, (uint)protectedSecret.Length, pbPurpose, (uint)_purpose.Length);
}
}
}
- internal static Secret UnprotectWithDpapiImpl(byte* pbProtectedData, uint cbProtectedData, byte* pbOptionalEntropy, uint cbOptionalEntropy)
+ 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
@@ -242,13 +258,13 @@ namespace Microsoft.AspNet.DataProtection.Cng
fixed (byte* pbProtectedData = protectedData)
{
byte dummy; // used to provide a valid memory address if protected data is zero-length
- return UnprotectWithDpapiNGImpl(
+ return UnprotectWithDpapiNGCore(
pbData: (pbProtectedData != null) ? pbProtectedData : &dummy,
cbData: (uint)protectedData.Length);
}
}
- private static Secret UnprotectWithDpapiNGImpl(byte* pbData, uint cbData)
+ private static Secret UnprotectWithDpapiNGCore(byte* pbData, uint cbData)
{
Debug.Assert(pbData != null);
@@ -265,7 +281,7 @@ namespace Microsoft.AspNet.DataProtection.Cng
ppbData: out unencryptedPayloadHandle,
pcbData: out cbUnencryptedPayload);
UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
- CryptoUtil.Assert(unencryptedPayloadHandle != null && !unencryptedPayloadHandle.IsInvalid, "unencryptedPayloadHandle != null && !unencryptedPayloadHandle.IsInvalid");
+ 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
@@ -293,5 +309,50 @@ namespace Microsoft.AspNet.DataProtection.Cng
}
}
}
+
+ 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;
+ int 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/Microsoft.AspNet.DataProtection/Cng/GcmAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.DataProtection/Cng/GcmAuthenticatedEncryptor.cs
index a7998c0885..5176da5fc6 100644
--- a/src/Microsoft.AspNet.DataProtection/Cng/GcmAuthenticatedEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/Cng/GcmAuthenticatedEncryptor.cs
@@ -5,6 +5,7 @@ using System;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.Cryptography.Cng;
using Microsoft.AspNet.Cryptography.SafeHandles;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNet.DataProtection.SP800_108;
namespace Microsoft.AspNet.DataProtection.Cng
@@ -38,9 +39,10 @@ namespace Microsoft.AspNet.DataProtection.Cng
public GcmAuthenticatedEncryptor(Secret keyDerivationKey, BCryptAlgorithmHandle symmetricAlgorithmHandle, uint symmetricAlgorithmKeySizeInBytes, IBCryptGenRandom genRandom = null)
{
- CryptoUtil.Assert(KEY_MODIFIER_SIZE_IN_BYTES <= symmetricAlgorithmKeySizeInBytes && symmetricAlgorithmKeySizeInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "KEY_MODIFIER_SIZE_IN_BYTES <= symmetricAlgorithmKeySizeInBytes && symmetricAlgorithmKeySizeInBytes <= Constants.MAX_STACKALLOC_BYTES");
-
+ // 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;
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs
new file mode 100644
index 0000000000..2fa1164d04
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionConfiguration.cs
@@ -0,0 +1,372 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.ComponentModel;
+using System.IO;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.AspNet.DataProtection.XmlEncryption;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Internal;
+using Microsoft.Win32;
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+using System.Security.Cryptography.X509Certificates;
+#endif
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ /// 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
+ /// should generally be accompanied by
+ /// a call to , or exceptions may
+ /// occur at runtime due to the data protection system not knowing where to persist keys.
+ ///
+ ///
+ /// 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
+ /// should therefore generally be paired with a call to ,
+ /// for example.
+ ///
+ ///
+ /// When the default cryptographic algorithms or at-rest key protection mechanisms are
+ /// changed, they only affect new keys in the repository. The repository may
+ /// contain existing keys that use older algorithms or protection mechanisms.
+ ///
+ ///
+ public class DataProtectionConfiguration
+ {
+ ///
+ /// Creates a new configuration object linked to a .
+ ///
+ public DataProtectionConfiguration([NotNull] IServiceCollection services)
+ {
+ Services = services;
+ }
+
+ ///
+ /// Provides access to the passed to this object's constructor.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public IServiceCollection Services { get; }
+
+ ///
+ /// Registers a to perform escrow before keys are persisted to storage.
+ ///
+ /// The instance of the to register.
+ /// The 'this' instance.
+ ///
+ /// Registrations are additive.
+ ///
+ public DataProtectionConfiguration AddKeyEscrowSink([NotNull] IKeyEscrowSink sink)
+ {
+ Services.AddInstance(sink);
+ return this;
+ }
+
+ ///
+ /// Registers a to perform escrow before keys are persisted to storage.
+ ///
+ /// The concrete type of the to register.
+ /// The 'this' instance.
+ ///
+ /// Registrations are additive.
+ ///
+ public DataProtectionConfiguration AddKeyEscrowSink()
+ where TImplementation : IKeyEscrowSink
+ {
+ Services.AddSingleton();
+ return this;
+ }
+
+ ///
+ /// Registers a to perform escrow before keys are persisted to storage.
+ ///
+ /// A factory that creates the instance.
+ /// The 'this' instance.
+ ///
+ /// Registrations are additive.
+ ///
+ public DataProtectionConfiguration AddKeyEscrowSink([NotNull] Func factory)
+ {
+ Services.AddSingleton(factory);
+ return this;
+ }
+
+ ///
+ /// Configures miscellaneous global options.
+ ///
+ /// A callback that configures the global options.
+ /// The 'this' instance.
+ public DataProtectionConfiguration ConfigureGlobalOptions([NotNull] Action setupAction)
+ {
+ Services.Configure(setupAction);
+ return this;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The directory in which to store keys.
+ /// The 'this' instance.
+ public DataProtectionConfiguration PersistKeysToFileSystem([NotNull] DirectoryInfo directory)
+ {
+ Use(DataProtectionServiceDescriptors.IXmlRepository_FileSystem(directory));
+ return this;
+ }
+
+ ///
+ /// Configures the data protection system to persist keys to the Windows registry.
+ ///
+ /// The location in the registry where keys should be stored.
+ /// The 'this' instance.
+ public DataProtectionConfiguration PersistKeysToRegistry([NotNull] RegistryKey registryKey)
+ {
+ Use(DataProtectionServiceDescriptors.IXmlRepository_Registry(registryKey));
+ return this;
+ }
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+ ///
+ /// Configures keys to be encrypted to a given certificate before being persisted to storage.
+ ///
+ /// The certificate to use when encrypting keys.
+ /// The 'this' instance.
+ public DataProtectionConfiguration ProtectKeysWithCertificate([NotNull] X509Certificate2 certificate)
+ {
+ Use(DataProtectionServiceDescriptors.IXmlEncryptor_Certificate(certificate));
+ return this;
+ }
+
+ ///
+ /// Configures keys to be encrypted to a given certificate before being persisted to storage.
+ ///
+ /// The thumbprint of the certificate to use when encrypting keys.
+ /// The 'this' instance.
+ public DataProtectionConfiguration ProtectKeysWithCertificate([NotNull] string 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.
+ Services.TryAdd(DataProtectionServiceDescriptors.ICertificateResolver_Default());
+ Use(DataProtectionServiceDescriptors.IXmlEncryptor_Certificate(thumbprint));
+ return this;
+ }
+
+#endif
+
+ ///
+ /// 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.
+ ///
+ /// The 'this' instance.
+ ///
+ /// This API is only supported on Windows platforms.
+ ///
+ public DataProtectionConfiguration ProtectKeysWithDpapi()
+ {
+ return ProtectKeysWithDpapi(protectToLocalMachine: false);
+ }
+
+ ///
+ /// Configures keys to be encrypted with Windows DPAPI before being persisted to
+ /// storage.
+ ///
+ /// '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.
+ /// The 'this' instance.
+ ///
+ /// This API is only supported on Windows platforms.
+ ///
+ public DataProtectionConfiguration ProtectKeysWithDpapi(bool protectToLocalMachine)
+ {
+ Use(DataProtectionServiceDescriptors.IXmlEncryptor_Dpapi(protectToLocalMachine));
+ return this;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The 'this' instance.
+ ///
+ /// 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.
+ ///
+ public DataProtectionConfiguration ProtectKeysWithDpapiNG()
+ {
+ return ProtectKeysWithDpapiNG(
+ protectionDescriptorRule: DpapiNGXmlEncryptor.GetDefaultProtectionDescriptorString(),
+ flags: DpapiNGProtectionDescriptorFlags.None);
+ }
+
+ ///
+ /// Configures keys to be encrypted with Windows CNG DPAPI before being persisted to storage.
+ ///
+ /// The descriptor rule string with which to protect the key material.
+ /// Flags that should be passed to the call to 'NCryptCreateProtectionDescriptor'.
+ /// The default value of this parameter is .
+ /// The 'this' instance.
+ ///
+ /// 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
+ /// and arguments.
+ /// This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ ///
+ public DataProtectionConfiguration ProtectKeysWithDpapiNG([NotNull] string protectionDescriptorRule, DpapiNGProtectionDescriptorFlags flags)
+ {
+ Use(DataProtectionServiceDescriptors.IXmlEncryptor_DpapiNG(protectionDescriptorRule, flags));
+ return this;
+ }
+
+ ///
+ /// Sets the default lifetime of keys created by the data protection system.
+ ///
+ /// The lifetime (time before expiration) for newly-created keys.
+ /// See for more information and
+ /// usage notes.
+ /// The 'this' instance.
+ public DataProtectionConfiguration SetDefaultKeyLifetime(TimeSpan lifetime)
+ {
+ Services.Configure(options =>
+ {
+ options.NewKeyLifetime = lifetime;
+ });
+ return this;
+ }
+
+ ///
+ /// Configures the data protection system to persist keys in storage as plaintext.
+ ///
+ /// The 'this' instance.
+ ///
+ /// Caution: cryptographic key material will not be protected at rest.
+ ///
+ public DataProtectionConfiguration SuppressProtectionOfKeysAtRest()
+ {
+ RemoveAllServicesOfType(typeof(IXmlEncryptor));
+ return this;
+ }
+
+ ///
+ /// Configures the data protection system to use the specified cryptographic algorithms
+ /// by default when generating protected payloads.
+ ///
+ /// Information about what cryptographic algorithms should be used.
+ /// The 'this' instance.
+ public DataProtectionConfiguration UseCryptographicAlgorithms([NotNull] AuthenticatedEncryptionOptions options)
+ {
+ return UseCryptographicAlgorithmsCore(options);
+ }
+
+ ///
+ /// 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 and
+ /// enumerations.
+ ///
+ /// Information about what cryptographic algorithms should be used.
+ /// The 'this' instance.
+ ///
+ /// This API is only available on Windows.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public DataProtectionConfiguration UseCustomCryptographicAlgorithms([NotNull] CngCbcAuthenticatedEncryptionOptions options)
+ {
+ return UseCryptographicAlgorithmsCore(options);
+ }
+
+ ///
+ /// 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 and
+ /// enumerations.
+ ///
+ /// Information about what cryptographic algorithms should be used.
+ /// The 'this' instance.
+ ///
+ /// This API is only available on Windows.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public DataProtectionConfiguration UseCustomCryptographicAlgorithms([NotNull] CngGcmAuthenticatedEncryptionOptions options)
+ {
+ return UseCryptographicAlgorithmsCore(options);
+ }
+
+ ///
+ /// 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 and
+ /// enumerations.
+ ///
+ /// Information about what cryptographic algorithms should be used.
+ /// The 'this' instance.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public DataProtectionConfiguration UseCustomCryptographicAlgorithms([NotNull] ManagedAuthenticatedEncryptionOptions options)
+ {
+ return UseCryptographicAlgorithmsCore(options);
+ }
+
+ private DataProtectionConfiguration UseCryptographicAlgorithmsCore(IInternalAuthenticatedEncryptionOptions options)
+ {
+ options.Validate(); // perform self-test
+ Use(DataProtectionServiceDescriptors.IAuthenticatedEncryptorConfiguration_FromOptions(options));
+ return this;
+ }
+
+ ///
+ /// Configures the data protection system to use the
+ /// for data protection services.
+ ///
+ /// The 'this' instance.
+ ///
+ /// If this option is used, payloads protected by the data protection system will
+ /// be permanently undecipherable after the application exits.
+ ///
+ public DataProtectionConfiguration UseEphemeralDataProtectionProvider()
+ {
+ Use(DataProtectionServiceDescriptors.IDataProtectionProvider_Ephemeral());
+ return this;
+ }
+
+ /*
+ * UTILITY ISERVICECOLLECTION METHODS
+ */
+
+ private void RemoveAllServicesOfType(Type serviceType)
+ {
+ // We go backward since we're modifying the collection in-place.
+ for (int i = Services.Count - 1; i >= 0; i--)
+ {
+ if (Services[i]?.ServiceType == serviceType)
+ {
+ Services.RemoveAt(i);
+ }
+ }
+ }
+
+ private void Use(ServiceDescriptor descriptor)
+ {
+ RemoveAllServicesOfType(descriptor.ServiceType);
+ Services.Add(descriptor);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs
index 38397f0c68..f2709b584f 100644
--- a/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionExtensions.cs
@@ -2,8 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using System.Diagnostics;
-using Microsoft.AspNet.Cryptography;
+using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.DataProtection
{
@@ -22,81 +21,5 @@ namespace Microsoft.AspNet.DataProtection
return (protector as ITimeLimitedDataProtector)
?? new TimeLimitedDataProtector(protector.CreateProtector(TimeLimitedDataProtector.PurposeString));
}
-
- ///
- /// Creates an IDataProtector given an array of purposes.
- ///
- /// The provider from which to generate the purpose chain.
- ///
- /// This is a convenience method used for chaining several purposes together
- /// in a single call to CreateProtector. See the documentation of
- /// IDataProtectionProvider.CreateProtector for more information.
- ///
- /// An IDataProtector tied to the provided purpose chain.
- public static IDataProtector CreateProtector([NotNull] this IDataProtectionProvider provider, params string[] purposes)
- {
- if (purposes == null || purposes.Length == 0)
- {
- throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesArray, nameof(purposes));
- }
-
- IDataProtectionProvider retVal = provider;
- foreach (string purpose in purposes)
- {
- if (String.IsNullOrEmpty(purpose))
- {
- throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesArray, nameof(purposes));
- }
- retVal = retVal.CreateProtector(purpose) ?? CryptoUtil.Fail("CreateProtector returned null.");
- }
-
- Debug.Assert(retVal is IDataProtector); // CreateProtector is supposed to return an instance of this interface
- return (IDataProtector)retVal;
- }
-
- ///
- /// Cryptographically protects a piece of plaintext data.
- ///
- /// The data protector to use for this operation.
- /// The plaintext data to protect.
- /// The protected form of the plaintext data.
- public static string Protect([NotNull] this IDataProtector protector, [NotNull] string unprotectedData)
- {
- try
- {
- byte[] unprotectedDataAsBytes = EncodingUtil.SecureUtf8Encoding.GetBytes(unprotectedData);
- byte[] protectedDataAsBytes = protector.Protect(unprotectedDataAsBytes);
- return WebEncoders.Base64UrlEncode(protectedDataAsBytes);
- }
- catch (Exception ex) when (ex.RequiresHomogenization())
- {
- // Homogenize exceptions to CryptographicException
- throw Error.CryptCommon_GenericError(ex);
- }
- }
-
- ///
- /// Cryptographically unprotects a piece of protected data.
- ///
- /// The data protector to use for this operation.
- /// The protected data to unprotect.
- /// The plaintext form of the protected data.
- ///
- /// This method will throw CryptographicException if the input is invalid or malformed.
- ///
- public static string Unprotect([NotNull] this IDataProtector protector, [NotNull] string protectedData)
- {
- try
- {
- byte[] protectedDataAsBytes = WebEncoders.Base64UrlDecode(protectedData);
- byte[] unprotectedDataAsBytes = protector.Unprotect(protectedDataAsBytes);
- return EncodingUtil.SecureUtf8Encoding.GetString(unprotectedDataAsBytes);
- }
- catch (Exception ex) when (ex.RequiresHomogenization())
- {
- // Homogenize exceptions to CryptographicException
- throw Error.CryptCommon_GenericError(ex);
- }
- }
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionOptions.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionOptions.cs
index ccd32586f8..1c6f998012 100644
--- a/src/Microsoft.AspNet.DataProtection/DataProtectionOptions.cs
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionOptions.cs
@@ -5,8 +5,21 @@ using System;
namespace Microsoft.AspNet.DataProtection
{
+ ///
+ /// Provides global options for the Data Protection system.
+ ///
public class DataProtectionOptions
{
+ ///
+ /// 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.
+ ///
+ ///
+ /// If two different applications need to share protected payloads, they should
+ /// ensure that this property is set to the same value across both applications.
+ ///
public string ApplicationDiscriminator { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs
new file mode 100644
index 0000000000..20d42ee09e
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionProvider.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.OptionsModel;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Contains static factory methods for creating instances.
+ ///
+ public static class DataProtectionProvider
+ {
+ ///
+ /// Creates an ephemeral .
+ ///
+ /// An ephemeral .
+ ///
+ /// Payloads generated by any given instance of an
+ /// can only be unprotected by that same provider instance. Once an instance of an ephemeral
+ /// provider is lost, all payloads generated by that provider are permanently undecipherable.
+ ///
+ public static EphemeralDataProtectionProvider CreateNewEphemeralProvider()
+ {
+ return CreateNewEphemeralProvider(services: null);
+ }
+
+ ///
+ /// Creates an ephemeral .
+ ///
+ /// Optional services (such as logging) for use by the provider.
+ /// An ephemeral .
+ ///
+ /// Payloads generated by any given instance of an
+ /// can only be unprotected by that same provider instance. Once an instance of an ephemeral
+ /// provider is lost, all payloads generated by that provider are permanently undecipherable.
+ ///
+ public static EphemeralDataProtectionProvider CreateNewEphemeralProvider(IServiceProvider services)
+ {
+ return new EphemeralDataProtectionProvider(services);
+ }
+
+ ///
+ /// Creates an given an .
+ ///
+ /// The global options to use when creating the provider.
+ /// Provides mandatory services for use by the provider.
+ /// An .
+ public static IDataProtectionProvider GetProviderFromServices([NotNull] DataProtectionOptions options, [NotNull] IServiceProvider services)
+ {
+ return GetProviderFromServices(options, services, mustCreateImmediately: false);
+ }
+
+ internal static IDataProtectionProvider GetProviderFromServices([NotNull] DataProtectionOptions options, [NotNull] IServiceProvider services, bool mustCreateImmediately)
+ {
+ IDataProtectionProvider dataProtectionProvider = null;
+
+ // If we're being asked to create the provider immediately, then it means that
+ // we're already in a call to GetService, and we're responsible for supplying
+ // the default implementation ourselves. We can't call GetService again or
+ // else we risk stack diving.
+ if (!mustCreateImmediately)
+ {
+ dataProtectionProvider = services.GetService();
+ }
+
+ // If all else fails, create a keyring manually based on the other registered services.
+ if (dataProtectionProvider == null)
+ {
+ var keyRingProvider = new KeyRingProvider(
+ keyManager: services.GetRequiredService(),
+ keyLifetimeOptions: services.GetService>()?.Options, // might be null
+ services: services);
+ dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyRingProvider, services);
+ }
+
+ // Finally, link the provider to the supplied discriminator
+ if (!String.IsNullOrEmpty(options.ApplicationDiscriminator))
+ {
+ dataProtectionProvider = dataProtectionProvider.CreateProtector(options.ApplicationDiscriminator);
+ }
+
+ return dataProtectionProvider;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceCollectionExtensions.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceCollectionExtensions.cs
index 14832f8d5d..c7f2ca16b7 100644
--- a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceCollectionExtensions.cs
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceCollectionExtensions.cs
@@ -2,156 +2,39 @@
// 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.Security.Cryptography;
-using Microsoft.AspNet.Cryptography.Cng;
using Microsoft.AspNet.DataProtection;
-using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
-using Microsoft.AspNet.DataProtection.Dpapi;
-using Microsoft.AspNet.DataProtection.KeyManagement;
-using Microsoft.AspNet.DataProtection.Repositories;
-using Microsoft.AspNet.DataProtection.XmlEncryption;
+using Microsoft.Framework.Internal;
namespace Microsoft.Framework.DependencyInjection
{
+ ///
+ /// Allows registering and configuring Data Protection in the application.
+ ///
public static class DataProtectionServiceCollectionExtensions
{
- public static IServiceCollection AddDataProtection(this IServiceCollection services)
+ ///
+ /// Adds default Data Protection services to an .
+ ///
+ /// The service collection to which to add DataProtection services.
+ /// The instance.
+ public static IServiceCollection AddDataProtection([NotNull] this IServiceCollection services)
{
services.AddOptions();
- services.TryAdd(OSVersionUtil.IsBCryptOnWin7OrLaterAvailable()
- ? GetDefaultServicesWindows()
- : GetDefaultServicesNonWindows());
+ services.TryAdd(DataProtectionServices.GetDefaultServices());
return services;
}
- private static IEnumerable GetDefaultServicesNonWindows()
+ ///
+ /// Configures the behavior of the Data Protection system.
+ ///
+ /// A service collection to which Data Protection has already been added.
+ /// A callback which takes a parameter.
+ /// This callback will be responsible for configuring the system.
+ /// The instance.
+ public static IServiceCollection ConfigureDataProtection([NotNull] this IServiceCollection services, [NotNull] Action configure)
{
- // If we're not running on Windows, we can't use CNG.
-
- // TODO: Replace this with something else. Mono's implementation of the
- // DPAPI routines don't provide authenticity.
- return new[]
- {
- ServiceDescriptor.Instance(new DpapiDataProtectionProvider(DataProtectionScope.CurrentUser))
- };
- }
-
- private static IEnumerable GetDefaultServicesWindows()
- {
- List descriptors = new List();
-
- // Are we running in Azure Web Sites?
- DirectoryInfo azureWebSitesKeysFolder = TryGetKeysFolderForAzureWebSites();
- if (azureWebSitesKeysFolder != null)
- {
- // We'll use a null protector at the moment until the
- // cloud DPAPI service comes online.
- descriptors.AddRange(new[]
- {
- ServiceDescriptor.Singleton(),
- ServiceDescriptor.Instance(new FileSystemXmlRepository(azureWebSitesKeysFolder))
- });
- }
- else
- {
- // Are we running with the user profile loaded?
- DirectoryInfo localAppDataKeysFolder = TryGetLocalAppDataKeysFolderForUser();
- if (localAppDataKeysFolder != null)
- {
- descriptors.AddRange(new[]
- {
- ServiceDescriptor.Instance(new DpapiXmlEncryptor(protectToLocalMachine: false)),
- ServiceDescriptor.Instance(new FileSystemXmlRepository(localAppDataKeysFolder))
- });
- }
- else
- {
- // If we've reached this point, we have no user profile loaded.
-
- RegistryXmlRepository hklmRegXmlRepository = RegistryXmlRepository.GetDefaultRepositoryForHKLMRegistry();
- if (hklmRegXmlRepository != null)
- {
- // Have WAS and IIS created an auto-gen key folder in the HKLM registry for us?
- // If so, use it as the repository, and use DPAPI as the key protection mechanism.
- // We use same-machine DPAPI since we already know no user profile is loaded.
- descriptors.AddRange(new[]
- {
- ServiceDescriptor.Instance(new DpapiXmlEncryptor(protectToLocalMachine: true)),
- ServiceDescriptor.Instance(hklmRegXmlRepository)
- });
- }
- else
- {
- // Fall back to DPAPI for now
- return new[] {
- ServiceDescriptor.Instance(new DpapiDataProtectionProvider(DataProtectionScope.LocalMachine))
- };
- }
- }
- }
-
- // We use CNG CBC + HMAC by default.
- descriptors.AddRange(new[]
- {
- ServiceDescriptor.Singleton(),
- ServiceDescriptor.Singleton(),
- ServiceDescriptor.Singleton()
- });
-
- return descriptors;
- }
-
- private static DirectoryInfo TryGetKeysFolderForAzureWebSites()
- {
- // There are two environment variables we care about.
- if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")))
- {
- return null;
- }
-
- string homeEnvVar = Environment.GetEnvironmentVariable("HOME");
- if (String.IsNullOrEmpty(homeEnvVar))
- {
- return null;
- }
-
- // TODO: Remove BETA moniker from below.
- string fullPathToKeys = Path.Combine(homeEnvVar, "ASP.NET", "keys-BETA6");
- return new DirectoryInfo(fullPathToKeys);
- }
-
- private static DirectoryInfo TryGetLocalAppDataKeysFolderForUser()
- {
-#if !DNXCORE50
- // Environment.GetFolderPath returns null if the user profile isn't loaded.
- string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- if (!String.IsNullOrEmpty(folderPath))
- {
- // TODO: Remove BETA moniker from below.
- return new DirectoryInfo(Path.Combine(folderPath, "ASP.NET", "keys-BETA6"));
- }
- else
- {
- return null;
- }
-#else
- // On core CLR, we need to fall back to environment variables.
- string folderPath = Environment.GetEnvironmentVariable("LOCALAPPDATA")
- ?? Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "Local");
-
- // TODO: Remove BETA moniker from below.
- DirectoryInfo retVal = new DirectoryInfo(Path.Combine(folderPath, "ASP.NET", "keys-BETA6"));
- try
- {
- retVal.Create(); // throws if we don't have access, e.g., user profile not loaded
- return retVal;
- } catch
- {
- return null;
- }
-#endif
+ configure(new DataProtectionConfiguration(services));
+ return services;
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs
new file mode 100644
index 0000000000..5a1c08ca29
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServiceDescriptors.cs
@@ -0,0 +1,187 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.AspNet.DataProtection.Repositories;
+using Microsoft.AspNet.DataProtection.XmlEncryption;
+using Microsoft.Framework.OptionsModel;
+using Microsoft.Win32;
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+using System.Security.Cryptography.X509Certificates;
+#endif
+
+namespace Microsoft.Framework.DependencyInjection
+{
+ ///
+ /// Default instances for the Data Protection system.
+ ///
+ internal static class DataProtectionServiceDescriptors
+ {
+ ///
+ /// An backed by the host-provided defaults.
+ ///
+ public static ServiceDescriptor ConfigureOptions_DataProtectionOptions()
+ {
+ return ServiceDescriptor.Transient>(services =>
+ {
+ return new ConfigureOptions(options =>
+ {
+ options.ApplicationDiscriminator = services.GetService()?.Discriminator;
+ });
+ });
+ }
+
+ ///
+ /// An where the key lifetime is specified explicitly.
+ ///
+
+ public static ServiceDescriptor ConfigureOptions_DefaultKeyLifetime(int numDays)
+ {
+ return ServiceDescriptor.Transient>(services =>
+ {
+ return new ConfigureOptions(options =>
+ {
+ options.NewKeyLifetime = TimeSpan.FromDays(numDays);
+ });
+ });
+ }
+
+ ///
+ /// An backed by default algorithmic options.
+ ///
+ public static ServiceDescriptor IAuthenticatedEncryptorConfiguration_Default()
+ {
+ return IAuthenticatedEncryptorConfiguration_FromOptions(new AuthenticatedEncryptionOptions());
+ }
+
+ ///
+ /// An backed by an .
+ ///
+ public static ServiceDescriptor IAuthenticatedEncryptorConfiguration_FromOptions(IInternalAuthenticatedEncryptionOptions options)
+ {
+ // We don't flow services since there's nothing interesting to flow.
+ return ServiceDescriptor.Singleton(services => options.ToConfiguration());
+ }
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+ ///
+ /// An backed by the default implementation.
+ ///
+ public static ServiceDescriptor ICertificateResolver_Default()
+ {
+ return ServiceDescriptor.Singleton();
+ }
+#endif
+
+ ///
+ /// An backed by the default keyring.
+ ///
+ public static ServiceDescriptor IDataProtectionProvider_Default()
+ {
+ return ServiceDescriptor.Singleton(
+ services => DataProtectionProvider.GetProviderFromServices(
+ options: services.GetRequiredService>().Options,
+ services: services,
+ mustCreateImmediately: true /* this is the ultimate fallback */));
+ }
+
+ ///
+ /// An ephemeral .
+ ///
+ public static ServiceDescriptor IDataProtectionProvider_Ephemeral()
+ {
+ return ServiceDescriptor.Singleton(services => new EphemeralDataProtectionProvider(services));
+ }
+
+ ///
+ /// An backed by a given implementation type.
+ ///
+ ///
+ /// The implementation type name is provided as a string so that we can provide activation services.
+ ///
+ public static ServiceDescriptor IKeyEscrowSink_FromTypeName(string implementationTypeName)
+ {
+ return ServiceDescriptor.Singleton(services => services.GetActivator().CreateInstance(implementationTypeName));
+ }
+
+ ///
+ /// An backed by the default XML key manager.
+ ///
+ public static ServiceDescriptor IKeyManager_Default()
+ {
+ return ServiceDescriptor.Singleton(services => new XmlKeyManager(services));
+ }
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+ ///
+ /// An backed by an X.509 certificate.
+ ///
+ public static ServiceDescriptor IXmlEncryptor_Certificate(X509Certificate2 certificate)
+ {
+ return ServiceDescriptor.Singleton(services => new CertificateXmlEncryptor(certificate, services));
+ }
+
+ ///
+ /// An backed by an X.509 certificate.
+ ///
+ public static ServiceDescriptor IXmlEncryptor_Certificate(string thumbprint)
+ {
+ return ServiceDescriptor.Singleton(services => new CertificateXmlEncryptor(
+ thumbprint: thumbprint,
+ certificateResolver: services.GetRequiredService(),
+ services: services));
+ }
+
+#endif
+
+ ///
+ /// An backed by DPAPI.
+ ///
+ public static ServiceDescriptor IXmlEncryptor_Dpapi(bool protectToMachine)
+ {
+ CryptoUtil.AssertPlatformIsWindows();
+ return ServiceDescriptor.Singleton(services => new DpapiXmlEncryptor(protectToMachine, services));
+ }
+
+ ///
+ /// An backed by DPAPI-NG.
+ ///
+ public static ServiceDescriptor IXmlEncryptor_DpapiNG(string protectionDescriptorRule, DpapiNGProtectionDescriptorFlags flags)
+ {
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
+ return ServiceDescriptor.Singleton(services => new DpapiNGXmlEncryptor(protectionDescriptorRule, flags, services));
+ }
+
+ ///
+ /// An backed by a file system.
+ ///
+ public static ServiceDescriptor IXmlRepository_FileSystem(DirectoryInfo directory)
+ {
+ return ServiceDescriptor.Singleton(services => new FileSystemXmlRepository(directory, services));
+ }
+
+ ///
+ /// An backed by volatile in-process memory.
+ ///
+ public static ServiceDescriptor IXmlRepository_InMemory()
+ {
+ return ServiceDescriptor.Singleton(services => new EphemeralXmlRepository(services));
+ }
+
+ ///
+ /// An backed by the Windows registry.
+ ///
+ public static ServiceDescriptor IXmlRepository_Registry(RegistryKey registryKey)
+ {
+ return ServiceDescriptor.Singleton(services => new RegistryXmlRepository(registryKey, services));
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs b/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs
new file mode 100644
index 0000000000..3589d588ba
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/DataProtectionServices.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.DataProtection;
+using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.AspNet.DataProtection.Repositories;
+
+namespace Microsoft.Framework.DependencyInjection
+{
+ ///
+ /// Provides access to default Data Protection instances.
+ ///
+ public static class DataProtectionServices
+ {
+ ///
+ /// Returns a collection of default instances that can be
+ /// used to bootstrap the Data Protection system.
+ ///
+ public static IEnumerable GetDefaultServices()
+ {
+ // Provide the default algorithmic information.
+ yield return DataProtectionServiceDescriptors.IAuthenticatedEncryptorConfiguration_Default();
+
+ // The default key services are a strange beast. We don't want to return
+ // IXmlEncryptor and IXmlRepository as-is because they almost always have to be
+ // set as a matched pair. Instead, our built-in key manager will use a meta-service
+ // which represents the default pairing (logic based on hosting environment as
+ // demonstrated below), and if the developer explicitly specifies one or the other
+ // we'll not use the fallback at all.
+ yield return ServiceDescriptor.Singleton(services =>
+ {
+ ServiceDescriptor keyEncryptorDescriptor = null;
+ ServiceDescriptor keyRepositoryDescriptor = null;
+
+ // If we're running in Azure Web Sites, the key repository goes in the %HOME% directory.
+ var azureWebSitesKeysFolder = FileSystemXmlRepository.GetKeyStorageDirectoryForAzureWebSites();
+ if (azureWebSitesKeysFolder != null)
+ {
+ // 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.
+ keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_FileSystem(azureWebSitesKeysFolder);
+ }
+ else
+ {
+ // If the user profile is available, store keys in the user profile directory.
+ var localAppDataKeysFolder = FileSystemXmlRepository.DefaultKeyStorageDirectory;
+ 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.
+ keyEncryptorDescriptor = DataProtectionServiceDescriptors.IXmlEncryptor_Dpapi(protectToMachine: !DpapiSecretSerializerHelper.CanProtectToCurrentUserAccount());
+ }
+ keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_FileSystem(localAppDataKeysFolder);
+ }
+ else
+ {
+ // Use profile isn't available - can we use the HKLM registry?
+ var regKeyStorageKey = RegistryXmlRepository.DefaultRegistryKey;
+ if (regKeyStorageKey != null)
+ {
+ if (OSVersionUtil.IsWindows())
+ {
+ // If the user profile isn't available, we can protect using DPAPI (to machine).
+ keyEncryptorDescriptor = DataProtectionServiceDescriptors.IXmlEncryptor_Dpapi(protectToMachine: true);
+ }
+ keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_Registry(regKeyStorageKey);
+ }
+ else
+ {
+ // Final fallback - use an ephemeral repository since we don't know where else to go.
+ // This can only be used for development scenarios.
+ keyRepositoryDescriptor = DataProtectionServiceDescriptors.IXmlRepository_InMemory();
+ }
+ }
+ }
+
+ return new DefaultKeyServices(
+ services: services,
+ keyEncryptorDescriptor: keyEncryptorDescriptor,
+ keyRepositoryDescriptor: keyRepositoryDescriptor);
+ });
+
+ // Provide root key management and data protection services
+ yield return DataProtectionServiceDescriptors.IKeyManager_Default();
+ yield return DataProtectionServiceDescriptors.IDataProtectionProvider_Default();
+
+ // Provide services required for XML encryption
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+ yield return DataProtectionServiceDescriptors.ICertificateResolver_Default();
+#endif
+
+ // Hook up the logic which allows populating default options
+ yield return DataProtectionServiceDescriptors.ConfigureOptions_DataProtectionOptions();
+
+ // Finally, read and apply policy from the registry, overriding any other defaults.
+ foreach (var descriptor in RegistryPolicyResolver.ResolveDefaultPolicy())
+ {
+ yield return descriptor;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/DefaultDataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/DefaultDataProtectionProvider.cs
deleted file mode 100644
index 1aa439e917..0000000000
--- a/src/Microsoft.AspNet.DataProtection/DefaultDataProtectionProvider.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Microsoft.AspNet.DataProtection.KeyManagement;
-using Microsoft.Framework.DependencyInjection;
-using Microsoft.Framework.OptionsModel;
-
-namespace Microsoft.AspNet.DataProtection
-{
- public class DefaultDataProtectionProvider : IDataProtectionProvider
- {
- private readonly IDataProtectionProvider _innerProvider;
-
- public DefaultDataProtectionProvider()
- {
- // use DI defaults
- var serviceProvider = new ServiceCollection().AddDataProtection().BuildServiceProvider();
-
- _innerProvider = serviceProvider.GetRequiredService();
- }
-
- public DefaultDataProtectionProvider(
- [NotNull] IOptions optionsAccessor,
- [NotNull] IKeyManager keyManager)
- {
- KeyRingBasedDataProtectionProvider rootProvider = new KeyRingBasedDataProtectionProvider(new KeyRingProvider(keyManager));
- var options = optionsAccessor.Options;
- _innerProvider = (!String.IsNullOrEmpty(options.ApplicationDiscriminator))
- ? (IDataProtectionProvider)rootProvider.CreateProtector(options.ApplicationDiscriminator)
- : rootProvider;
- }
-
- public IDataProtector CreateProtector([NotNull] string purpose)
- {
- return _innerProvider.CreateProtector(purpose);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/Dpapi/DataProtectionScope.cs b/src/Microsoft.AspNet.DataProtection/Dpapi/DataProtectionScope.cs
deleted file mode 100644
index e55496e2af..0000000000
--- a/src/Microsoft.AspNet.DataProtection/Dpapi/DataProtectionScope.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-// We only define this type in core CLR since desktop CLR already contains it.
-#if DNXCORE50
-using System;
-
-namespace System.Security.Cryptography
-{
- //
- // Summary:
- // Specifies the scope of the data protection to be applied by the System.Security.Cryptography.ProtectedData.Protect(System.Byte[],System.Byte[],System.Security.Cryptography.DataProtectionScope)
- // method.
- internal enum DataProtectionScope
- {
- //
- // Summary:
- // The protected data is associated with the current user. Only threads running
- // under the current user context can unprotect the data.
- CurrentUser,
- //
- // Summary:
- // The protected data is associated with the machine context. Any process running
- // on the computer can unprotect data. This enumeration value is usually used in
- // server-specific applications that run on a server where untrusted users are not
- // allowed access.
- LocalMachine
- }
-}
-#endif
diff --git a/src/Microsoft.AspNet.DataProtection/Dpapi/DpapiDataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/Dpapi/DpapiDataProtectionProvider.cs
deleted file mode 100644
index e3c3dad792..0000000000
--- a/src/Microsoft.AspNet.DataProtection/Dpapi/DpapiDataProtectionProvider.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Security.Cryptography;
-
-namespace Microsoft.AspNet.DataProtection.Dpapi
-{
- // Provides a temporary implementation of IDataProtectionProvider for non-Windows machines
- // or for Windows machines where we can't depend on the user profile.
- internal sealed class DpapiDataProtectionProvider : IDataProtectionProvider
- {
- private readonly DpapiDataProtector _innerProtector;
-
- public DpapiDataProtectionProvider(DataProtectionScope scope)
- {
- _innerProtector = new DpapiDataProtector(new ProtectedDataImpl(), new byte[0], scope);
- }
-
- public IDataProtector CreateProtector([NotNull] string purpose)
- {
- return _innerProtector.CreateProtector(purpose);
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/Dpapi/DpapiDataProtector.cs b/src/Microsoft.AspNet.DataProtection/Dpapi/DpapiDataProtector.cs
deleted file mode 100644
index df1c6d54a7..0000000000
--- a/src/Microsoft.AspNet.DataProtection/Dpapi/DpapiDataProtector.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.IO;
-using System.Security.Cryptography;
-using Microsoft.AspNet.Cryptography;
-
-namespace Microsoft.AspNet.DataProtection.Dpapi
-{
- // Provides a temporary implementation of IDataProtector for non-Windows machines
- // or for Windows machines where we can't depend on the user profile.
- internal sealed class DpapiDataProtector : IDataProtector
- {
- private readonly byte[] _combinedPurposes;
- private readonly DataProtectionScope _scope;
- private readonly IProtectedData _shim;
-
- internal DpapiDataProtector(IProtectedData shim, byte[] combinedPurposes, DataProtectionScope scope)
- {
- _combinedPurposes = combinedPurposes;
- _scope = scope;
- _shim = shim;
- }
-
- public IDataProtector CreateProtector([NotNull] string purpose)
- {
- // Appends the provided purpose to the existing list
- using (var memoryStream = new MemoryStream())
- {
- memoryStream.Write(_combinedPurposes, 0, _combinedPurposes.Length);
- using (var writer = new BinaryWriter(memoryStream, EncodingUtil.SecureUtf8Encoding, leaveOpen: true))
- {
- writer.Write(purpose);
- }
- return new DpapiDataProtector(_shim, memoryStream.ToArray(), _scope);
- }
- }
-
- public byte[] Protect([NotNull] byte[] unprotectedData)
- {
- try
- {
- return _shim.Protect(unprotectedData, _combinedPurposes, _scope)
- ?? CryptoUtil.Fail("Null return value.");
- }
- catch (Exception ex) when (ex.RequiresHomogenization())
- {
- // Homogenize to CryptographicException
- throw Error.CryptCommon_GenericError(ex);
- }
- }
-
- public byte[] Unprotect([NotNull] byte[] protectedData)
- {
- try
- {
- return _shim.Unprotect(protectedData, _combinedPurposes, _scope)
- ?? CryptoUtil.Fail("Null return value.");
- }
- catch (Exception ex) when (ex.RequiresHomogenization())
- {
- // Homogenize to CryptographicException
- throw Error.CryptCommon_GenericError(ex);
- }
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/Dpapi/IProtectedData.cs b/src/Microsoft.AspNet.DataProtection/Dpapi/IProtectedData.cs
deleted file mode 100644
index a12de6c77a..0000000000
--- a/src/Microsoft.AspNet.DataProtection/Dpapi/IProtectedData.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Security.Cryptography;
-
-namespace Microsoft.AspNet.DataProtection.Dpapi
-{
- internal interface IProtectedData
- {
- byte[] Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope);
-
- byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/Dpapi/ProtectedDataImpl.cs b/src/Microsoft.AspNet.DataProtection/Dpapi/ProtectedDataImpl.cs
deleted file mode 100644
index 74929a0d4d..0000000000
--- a/src/Microsoft.AspNet.DataProtection/Dpapi/ProtectedDataImpl.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Security.Cryptography;
-using Microsoft.AspNet.DataProtection.Cng;
-
-namespace Microsoft.AspNet.DataProtection.Dpapi
-{
- internal unsafe sealed class ProtectedDataImpl : IProtectedData
- {
- public byte[] Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope)
- {
-#if DNXCORE50
- fixed (byte* pbUserData = userData)
- {
- fixed (byte* pbOptionalEntropy = optionalEntropy)
- {
- return DpapiSecretSerializerHelper.ProtectWithDpapiImpl(
- pbSecret: pbUserData,
- cbSecret: (userData != null) ? (uint)userData.Length : 0,
- pbOptionalEntropy: pbOptionalEntropy,
- cbOptionalEntropy: (optionalEntropy != null) ? (uint)optionalEntropy.Length : 0,
- fLocalMachine: (scope == DataProtectionScope.LocalMachine));
- }
- }
-#else
- return ProtectedData.Protect(userData, optionalEntropy, scope);
-#endif
- }
-
- public byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)
- {
-#if DNXCORE50
- Secret blob;
- fixed (byte* pbEncryptedData = encryptedData)
- {
- fixed (byte* pbOptionalEntropy = optionalEntropy)
- {
- blob = DpapiSecretSerializerHelper.UnprotectWithDpapiImpl(
- pbProtectedData: pbEncryptedData,
- cbProtectedData: (encryptedData != null) ? (uint)encryptedData.Length : 0,
- pbOptionalEntropy: pbOptionalEntropy,
- cbOptionalEntropy: (optionalEntropy != null) ? (uint)optionalEntropy.Length : 0);
- }
- }
- using (blob)
- {
- byte[] retVal = new byte[blob.Length];
- blob.WriteSecretIntoBuffer(new ArraySegment(retVal));
- return retVal;
- }
-#else
- return ProtectedData.Unprotect(encryptedData, optionalEntropy, scope);
-#endif
- }
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/EphemeralDataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/EphemeralDataProtectionProvider.cs
index d5c323bcd5..262d978e0d 100644
--- a/src/Microsoft.AspNet.DataProtection/EphemeralDataProtectionProvider.cs
+++ b/src/Microsoft.AspNet.DataProtection/EphemeralDataProtectionProvider.cs
@@ -4,16 +4,17 @@
using System;
using Microsoft.AspNet.Cryptography.Cng;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
-using Microsoft.AspNet.DataProtection.Cng;
using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection
{
///
- /// An IDataProtectionProvider that is transient.
+ /// An that is transient.
///
///
- /// Payloads generated by a given EphemeralDataProtectionProvider instance can only
+ /// Payloads generated by a given instance can only
/// be deciphered by that same instance. Once the instance is lost, all ciphertexts
/// generated by that instance are permanently undecipherable.
///
@@ -21,22 +22,39 @@ namespace Microsoft.AspNet.DataProtection
{
private readonly KeyRingBasedDataProtectionProvider _dataProtectionProvider;
+ ///
+ /// Creates an ephemeral .
+ ///
public EphemeralDataProtectionProvider()
+ : this(services: null)
+ {
+ }
+
+ ///
+ /// Creates an ephemeral , optionally providing
+ /// services (such as logging) for consumption by the provider.
+ ///
+ public EphemeralDataProtectionProvider(IServiceProvider services)
{
IKeyRingProvider keyringProvider;
-
- if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
+ if (OSVersionUtil.IsWindows())
{
- // Fastest implementation: AES-GCM
- keyringProvider = new EphemeralKeyRing();
+ // Fastest implementation: AES-256-GCM [CNG]
+ keyringProvider = new EphemeralKeyRing();
}
else
{
- // Slowest implementation: managed CBC + HMAC
- keyringProvider = new EphemeralKeyRing();
+ // Slowest implementation: AES-256-CBC + HMACSHA256 [Managed]
+ keyringProvider = new EphemeralKeyRing();
}
- _dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider);
+ var logger = services.GetLogger();
+ if (logger.IsWarningLevelEnabled())
+ {
+ logger.LogWarning("Using ephemeral data protection provider. Payloads will be undecipherable upon application shutdown.");
+ }
+
+ _dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider, services);
}
public IDataProtector CreateProtector([NotNull] string purpose)
@@ -46,12 +64,12 @@ namespace Microsoft.AspNet.DataProtection
}
private sealed class EphemeralKeyRing : IKeyRing, IKeyRingProvider
- where T : IInternalConfigurationOptions, new()
+ where T : IInternalAuthenticatedEncryptionOptions, new()
{
// Currently hardcoded to a 512-bit KDK.
private const int NUM_BYTES_IN_KDK = 512 / 8;
- public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new T().CreateAuthenticatedEncryptor(Secret.Random(NUM_BYTES_IN_KDK));
+ public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new T().ToConfiguration().CreateNewDescriptor().CreateEncryptorInstance();
public Guid DefaultKeyId { get; } = default(Guid);
diff --git a/src/Microsoft.AspNet.DataProtection/Error.cs b/src/Microsoft.AspNet.DataProtection/Error.cs
index 309625bbb7..5d954946ee 100644
--- a/src/Microsoft.AspNet.DataProtection/Error.cs
+++ b/src/Microsoft.AspNet.DataProtection/Error.cs
@@ -9,9 +9,20 @@ namespace Microsoft.AspNet.DataProtection
{
internal static class Error
{
+ public static InvalidOperationException CertificateXmlEncryptor_CertificateNotFound(string thumbprint)
+ {
+ string 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)
{
- string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_BufferIncorrectlySized, actualSize, expectedSize);
+ string message = Resources.FormatCommon_BufferIncorrectlySized(actualSize, expectedSize);
return new ArgumentException(message, parameterName);
}
@@ -29,7 +40,13 @@ namespace Microsoft.AspNet.DataProtection
public static InvalidOperationException Common_PropertyCannotBeNullOrEmpty(string propertyName)
{
string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_PropertyCannotBeNullOrEmpty, propertyName);
- throw new InvalidOperationException(message);
+ return new InvalidOperationException(message);
+ }
+
+ public static InvalidOperationException Common_PropertyMustBeNonNegative(string propertyName)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_PropertyMustBeNonNegative, propertyName);
+ return new InvalidOperationException(message);
}
public static CryptographicException Common_EncryptionFailed(Exception inner = null)
@@ -49,14 +66,9 @@ namespace Microsoft.AspNet.DataProtection
return new CryptographicException(message);
}
- public static CryptographicException Common_NotAValidProtectedPayload()
+ public static ArgumentOutOfRangeException Common_ValueMustBeNonNegative(string paramName)
{
- return new CryptographicException(Resources.Common_NotAValidProtectedPayload);
- }
-
- public static CryptographicException Common_PayloadProducedByNewerVersion()
- {
- return new CryptographicException(Resources.Common_PayloadProducedByNewerVersion);
+ return new ArgumentOutOfRangeException(paramName, Resources.Common_ValueMustBeNonNegative);
}
public static CryptographicException DecryptionFailed(Exception inner)
@@ -64,11 +76,27 @@ namespace Microsoft.AspNet.DataProtection
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 CryptographicException TimeLimitedDataProtector_PayloadExpired(ulong utcTicksExpiration)
{
DateTimeOffset expiration = new DateTimeOffset((long)utcTicksExpiration, TimeSpan.Zero).ToLocalTime();
string message = String.Format(CultureInfo.CurrentCulture, Resources.TimeLimitedDataProtector_PayloadExpired, expiration);
return new CryptographicException(message);
}
+
+ public static InvalidOperationException XmlKeyManager_DuplicateKey(Guid keyId)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, Resources.XmlKeyManager_DuplicateKey, keyId);
+ return new InvalidOperationException(message);
+ }
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/IActivator.cs b/src/Microsoft.AspNet.DataProtection/IActivator.cs
new file mode 100644
index 0000000000..a8827f58fa
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/IActivator.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// An interface into that also supports
+ /// limited dependency injection (of ).
+ ///
+ internal interface IActivator
+ {
+ ///
+ /// Creates an instance of and ensures
+ /// that it is assignable to .
+ ///
+ object CreateInstance(Type expectedBaseType, string implementationTypeName);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/IDataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/IDataProtectionProvider.cs
deleted file mode 100644
index 7c44fea90a..0000000000
--- a/src/Microsoft.AspNet.DataProtection/IDataProtectionProvider.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNet.DataProtection
-{
- ///
- /// An interface that can be used to create IDataProtector instances.
- ///
- public interface IDataProtectionProvider
- {
- ///
- /// Creates an IDataProtector given a purpose.
- ///
- ///
- /// The purpose to be assigned to the newly-created IDataProtector.
- /// This parameter must be unique for the intended use case; two different IDataProtector
- /// instances created with two different 'purpose' strings will not be able
- /// to understand each other's payloads. The 'purpose' parameter is not intended to be
- /// kept secret.
- ///
- /// An IDataProtector tied to the provided purpose.
- IDataProtector CreateProtector(string purpose);
- }
-}
diff --git a/src/Microsoft.AspNet.DataProtection/IPersistedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/IPersistedDataProtector.cs
new file mode 100644
index 0000000000..a1fe9ef00b
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/IPersistedDataProtector.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// An interface that can provide data protection services for data which has been persisted
+ /// to long-term storage.
+ ///
+ public interface IPersistedDataProtector : IDataProtector
+ {
+ ///
+ /// Cryptographically unprotects a piece of data, optionally ignoring failures due to
+ /// revocation of the cryptographic keys used to protect the payload.
+ ///
+ /// The protected data to unprotect.
+ /// '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.
+ /// '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.
+ /// '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.
+ /// The plaintext form of the protected data.
+ ///
+ /// Implementations should throw CryptographicException if the protected data is
+ /// invalid or malformed.
+ ///
+ byte[] DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs
index acada25c6e..7e168a93bc 100644
--- a/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs
+++ b/src/Microsoft.AspNet.DataProtection/ITimeLimitedDataProtector.cs
@@ -26,10 +26,10 @@ namespace Microsoft.AspNet.DataProtection
///
/// Cryptographically protects a piece of plaintext data and assigns an expiration date to the data.
///
- /// The plaintext data to protect.
+ /// The plaintext data to protect.
/// The date after which the data can no longer be unprotected.
/// The protected form of the plaintext data.
- byte[] Protect(byte[] unprotectedData, DateTimeOffset expiration);
+ byte[] Protect(byte[] plaintext, DateTimeOffset expiration);
///
/// Cryptographically unprotects a piece of protected data.
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs
new file mode 100644
index 0000000000..5ad6d238f8
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ ///
+ /// Wraps both a keyring and its expiration policy.
+ ///
+ internal sealed class CacheableKeyRing
+ {
+ private readonly CancellationToken _expirationToken;
+
+ internal CacheableKeyRing(CancellationToken expirationToken, DateTimeOffset expirationTime, IKey defaultKey, IEnumerable allKeys)
+ : this(expirationToken, expirationTime, keyRing: new KeyRing(defaultKey.KeyId, 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs
new file mode 100644
index 0000000000..63f035b057
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ internal struct DefaultKeyResolution
+ {
+ ///
+ /// The default key, may be null if no key is a good default candidate.
+ ///
+ public IKey DefaultKey;
+
+ ///
+ /// '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.
+ ///
+ public bool ShouldGenerateNewKey;
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs
new file mode 100644
index 0000000000..624b23e53f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs
@@ -0,0 +1,135 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Framework.Logging;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ ///
+ /// Implements policy for resolving the default key from a candidate keyring.
+ ///
+ internal sealed class DefaultKeyResolver : IDefaultKeyResolver
+ {
+ ///
+ /// The window of time before the key expires when a new key should be created
+ /// and persisted to the keyring to ensure uninterrupted service.
+ ///
+ ///
+ /// If the expiration window is 5 days and the current key expires within 5 days,
+ /// a new key will be generated.
+ ///
+ private readonly TimeSpan _keyGenBeforeExpirationWindow;
+
+ private readonly ILogger _logger;
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ private readonly TimeSpan _maxServerToServerClockSkew;
+
+ public DefaultKeyResolver(TimeSpan keyGenBeforeExpirationWindow, TimeSpan maxServerToServerClockSkew, IServiceProvider services)
+ {
+ _keyGenBeforeExpirationWindow = keyGenBeforeExpirationWindow;
+ _maxServerToServerClockSkew = maxServerToServerClockSkew;
+ _logger = services.GetLogger();
+ }
+
+ public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable allKeys)
+ {
+ DefaultKeyResolution retVal = default(DefaultKeyResolution);
+ retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.ShouldGenerateNewKey);
+ return retVal;
+ }
+
+ private IKey FindDefaultKey(DateTimeOffset now, IEnumerable allKeys, out bool callerShouldGenerateNewKey)
+ {
+ // the key with the most recent activation date where the activation date is in the past
+ IKey keyMostRecentlyActivated = (from key in allKeys
+ where key.ActivationDate <= now
+ orderby key.ActivationDate descending
+ select key).FirstOrDefault();
+
+ if (keyMostRecentlyActivated != null)
+ {
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Considering key '{0:D}' with expiration date {1:u} as default key candidate.", keyMostRecentlyActivated.KeyId, keyMostRecentlyActivated.ExpirationDate);
+ }
+
+ // if the key has been revoked or is expired, it is no longer a candidate
+ if (keyMostRecentlyActivated.IsExpired(now) || keyMostRecentlyActivated.IsRevoked)
+ {
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Key '{0:D}' no longer eligible as default key candidate because it is expired or revoked.", keyMostRecentlyActivated.KeyId);
+ }
+ keyMostRecentlyActivated = null;
+ }
+ }
+
+ // There's an interesting edge case here. If two keys have an activation date in the past and
+ // an expiration date in the future, and if the most recently activated of those two keys is
+ // revoked, we won't consider the older key a valid candidate. This is intentional: generating
+ // a new key is an implicit signal that we should stop using older keys without explicitly
+ // revoking them.
+
+ // if the key's expiration is beyond our safety window, we can use this key
+ if (keyMostRecentlyActivated != null && keyMostRecentlyActivated.ExpirationDate - now > _keyGenBeforeExpirationWindow)
+ {
+ callerShouldGenerateNewKey = false;
+ return keyMostRecentlyActivated;
+ }
+
+ // the key with the nearest activation date where the activation date is in the future
+ // and the key isn't expired or revoked
+ IKey keyNextPendingActivation = (from key in allKeys
+ where key.ActivationDate > now && !key.IsExpired(now) && !key.IsRevoked
+ orderby key.ActivationDate ascending
+ select key).FirstOrDefault();
+
+ // if we have a valid current key, return it, and signal to the caller that he must perform
+ // the keygen step only if the next key pending activation won't be activated until *after*
+ // the current key expires (allowing for server-to-server skew)
+ if (keyMostRecentlyActivated != null)
+ {
+ callerShouldGenerateNewKey = (keyNextPendingActivation == null || (keyNextPendingActivation.ActivationDate - keyMostRecentlyActivated.ExpirationDate > _maxServerToServerClockSkew));
+ if (callerShouldGenerateNewKey && _logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Default key expiration imminent and repository contains no viable successor. Caller should generate a successor.");
+ }
+
+ return keyMostRecentlyActivated;
+ }
+
+ // if there's no valid current key but there is a key pending activation, we can use
+ // it only if its activation period is within the server-to-server clock skew
+ if (keyNextPendingActivation != null && keyNextPendingActivation.ActivationDate - now <= _maxServerToServerClockSkew)
+ {
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Considering key '{0:D}' with expiration date {1:u} as default key candidate.", keyNextPendingActivation.KeyId, keyNextPendingActivation.ExpirationDate);
+ }
+
+ callerShouldGenerateNewKey = false;
+ return keyNextPendingActivation;
+ }
+
+ // if we got this far, there was no valid default key in the keyring
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Repository contains no viable default key. Caller should generate a key with immediate activation.");
+ }
+ callerShouldGenerateNewKey = true;
+ return null;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyServices.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyServices.cs
new file mode 100644
index 0000000000..c9dd42484a
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyServices.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection.Repositories;
+using Microsoft.AspNet.DataProtection.XmlEncryption;
+using Microsoft.Framework.DependencyInjection;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ internal sealed class DefaultKeyServices : IDefaultKeyServices
+ {
+ private readonly Lazy
///
/// A revoked key may still be used to decrypt existing payloads, but the payloads
- /// must be treated as potentially unauthentic unless the application has some
- /// other assurance that the payloads are authentic.
+ /// must be treated as tampered unless the application has some other assurance
+ /// that the payloads are authentic.
///
bool IsRevoked { get; }
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyEscrowSink.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyEscrowSink.cs
new file mode 100644
index 0000000000..4223085202
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyEscrowSink.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.AspNet.DataProtection.Repositories;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ ///
+ /// The basic interface for implementing a key escrow sink.
+ ///
+ ///
+ /// is distinct from in that
+ /// provides a write-only interface and instances handle unencrypted key material,
+ /// while provides a read+write interface and instances handle encrypted key material.
+ ///
+ public interface IKeyEscrowSink
+ {
+ ///
+ /// Stores the given key material to the escrow service.
+ ///
+ /// The id of the key being persisted to escrow.
+ /// The unencrypted XML element that comprises the key material.
+ void Store(Guid keyId, XElement element);
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyManager.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyManager.cs
index 9f64f7f9d2..104c51a73d 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyManager.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyManager.cs
@@ -3,23 +3,25 @@
using System;
using System.Collections.Generic;
+using System.Threading;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
///
/// The basic interface for performing key management operations.
///
+ ///
+ /// Instantiations of this interface are expected to be thread-safe.
+ ///
public interface IKeyManager
{
///
- /// Creates a new key with the specified activation and expiration dates.
+ /// Creates a new key with the specified activation and expiration dates and persists
+ /// the new key to the underlying repository.
///
/// The date on which encryptions to this key may begin.
/// The date after which encryptions to this key may no longer take place.
/// The newly-created IKey instance.
- ///
- /// This method also persists the newly-created IKey instance to the underlying repository.
- ///
IKey CreateNewKey(DateTimeOffset activationDate, DateTimeOffset expirationDate);
///
@@ -29,7 +31,27 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
IReadOnlyCollection GetAllKeys();
///
- /// Revokes a specific key.
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ CancellationToken GetCacheExpirationToken();
+
+ ///
+ /// Revokes a specific key and persists the revocation to the underlying repository.
///
/// The id of the key to revoke.
/// An optional human-readable reason for revocation.
@@ -40,7 +62,8 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
void RevokeKey(Guid keyId, string reason = null);
///
- /// Revokes all keys created before a specified date.
+ /// Revokes all keys created before a specified date and persists the revocation to the
+ /// underlying repository.
///
/// The revocation date. All keys with a creation date before
/// this value will be revoked.
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyRing.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyRing.cs
index b71aaedd1e..d046a5242a 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyRing.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKeyRing.cs
@@ -6,12 +6,31 @@ using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
+ ///
+ /// The basic interface for accessing a read-only keyring.
+ ///
internal interface IKeyRing
{
+ ///
+ /// The authenticated encryptor that shall be used for new encryption operations.
+ ///
+ ///
+ /// Activation of the encryptor instance is deferred until first access.
+ ///
IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; }
+ ///
+ /// The id of the key associated with .
+ ///
Guid DefaultKeyId { get; }
+ ///
+ /// Returns an encryptor instance for the given key, or 'null' if the key with the
+ /// specified id cannot be found in the keyring.
+ ///
+ ///
+ /// Activation of the encryptor instance is deferred until first access.
+ ///
IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked);
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs
index 5366536ced..d436b18498 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs
@@ -3,56 +3,40 @@
using System;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
+ ///
+ /// The basic implementation of .
+ ///
internal sealed class Key : IKey
{
- private readonly IAuthenticatedEncryptorConfiguration _encryptorConfiguration;
+ private readonly IAuthenticatedEncryptorDescriptor _descriptor;
- public Key(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IAuthenticatedEncryptorConfiguration encryptorConfiguration)
+ public Key(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IAuthenticatedEncryptorDescriptor descriptor)
{
KeyId = keyId;
CreationDate = creationDate;
ActivationDate = activationDate;
ExpirationDate = expirationDate;
- _encryptorConfiguration = encryptorConfiguration;
+ _descriptor = descriptor;
}
- public DateTimeOffset ActivationDate
- {
- get;
- private set;
- }
+ public DateTimeOffset ActivationDate { get; }
- public DateTimeOffset CreationDate
- {
- get;
- private set;
- }
+ public DateTimeOffset CreationDate { get; }
- public DateTimeOffset ExpirationDate
- {
- get;
- private set;
- }
+ public DateTimeOffset ExpirationDate { get; }
- public bool IsRevoked
- {
- get;
- private set;
- }
+ public bool IsRevoked { get; private set; }
- public Guid KeyId
- {
- get;
- private set;
- }
+ public Guid KeyId { get; }
public IAuthenticatedEncryptor CreateEncryptorInstance()
{
- return _encryptorConfiguration.CreateEncryptorInstance();
+ return _descriptor.CreateEncryptorInstance();
}
internal void SetRevoked()
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs
new file mode 100644
index 0000000000..6794a4884f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyEscrowServiceProviderExtensions.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Framework.DependencyInjection;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ internal static class KeyEscrowServiceProviderExtensions
+ {
+ ///
+ /// Gets an aggregate from the underlying .
+ /// This method may return null if no sinks are registered.
+ ///
+ public static IKeyEscrowSink GetKeyEscrowSink(this IServiceProvider services)
+ {
+ var escrowSinks = services?.GetService>()?.ToList();
+ return (escrowSinks != null && escrowSinks.Count > 0) ? new AggregateKeyEscrowSink(escrowSinks) : null;
+ }
+
+ private sealed class AggregateKeyEscrowSink : IKeyEscrowSink
+ {
+ private readonly List _sinks;
+
+ public AggregateKeyEscrowSink(List sinks)
+ {
+ _sinks = sinks;
+ }
+
+ public void Store(Guid keyId, XElement element)
+ {
+ foreach (var sink in _sinks)
+ {
+ sink.Store(keyId, element);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyExtensions.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyExtensions.cs
index bed820e872..665be69320 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyExtensions.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyExtensions.cs
@@ -7,9 +7,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{
internal static class KeyExtensions
{
- public static bool IsExpired(this IKey key, DateTime utcNow)
+ public static bool IsExpired(this IKey key, DateTimeOffset now)
{
- return (key.ExpirationDate.UtcDateTime <= utcNow);
+ return (key.ExpirationDate <= now);
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyLifetimeOptions.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyLifetimeOptions.cs
new file mode 100644
index 0000000000..7316cdb3f7
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyLifetimeOptions.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ public class KeyLifetimeOptions
+ {
+ private readonly TimeSpan _keyExpirationSafetyPeriod = TimeSpan.FromDays(2);
+ private readonly TimeSpan _keyRingRefreshPeriod = TimeSpan.FromHours(24);
+ private readonly TimeSpan _maxServerClockSkew = TimeSpan.FromMinutes(5);
+ private TimeSpan _newKeyLifetime = TimeSpan.FromDays(90);
+
+ public KeyLifetimeOptions()
+ {
+ }
+
+ // copy ctor
+ internal KeyLifetimeOptions(KeyLifetimeOptions other)
+ {
+ if (other != null)
+ {
+ this._newKeyLifetime = other._newKeyLifetime;
+ }
+ }
+
+ ///
+ /// Specifies the period before key expiration in which a new key should be generated.
+ /// For example, if this period is 72 hours, then a new key will be created and
+ /// persisted to storage approximately 72 hours before expiration.
+ ///
+ ///
+ /// This value is currently fixed at 48 hours.
+ ///
+ internal TimeSpan KeyExpirationSafetyPeriod
+ {
+ get
+ {
+ // This value is not settable since there's a complex interaction between
+ // it and the key ring refresh period.
+ return _keyExpirationSafetyPeriod;
+ }
+ }
+
+ ///
+ /// Controls the auto-refresh period where the key ring provider will
+ /// flush its collection of cached keys and reread the collection from
+ /// backing storage.
+ ///
+ ///
+ /// This value is currently fixed at 24 hours.
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 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
+ /// if key activation and expiration times should be strictly honored by this server.
+ ///
+ ///
+ /// This value is currently fixed at 5 minutes.
+ ///
+ internal TimeSpan MaxServerClockSkew
+ {
+ get
+ {
+ return _maxServerClockSkew;
+ }
+ }
+
+ ///
+ /// Controls the lifetime (number of days before expiration)
+ /// for newly-generated keys.
+ ///
+ ///
+ /// The lifetime cannot be less than one week.
+ /// The default value is 90 days.
+ ///
+ public TimeSpan NewKeyLifetime
+ {
+ get
+ {
+ return _newKeyLifetime;
+ }
+ set
+ {
+ if (value < TimeSpan.FromDays(7))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), Resources.KeyLifetimeOptions_MinNewKeyLifetimeViolated);
+ }
+ _newKeyLifetime = value;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs
index 6a15e227ac..38d8b20099 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs
@@ -8,66 +8,52 @@ using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
+ ///
+ /// A basic implementation of .
+ ///
internal sealed class KeyRing : IKeyRing
{
- private readonly AuthenticatedEncryptorHolder _defaultEncryptorHolder;
- private readonly Dictionary _keyToEncryptorMap;
+ private readonly KeyHolder _defaultKeyHolder;
+ private readonly Dictionary _keyIdToKeyHolderMap;
- public KeyRing(Guid defaultKeyId, IKey[] keys)
+ public KeyRing(Guid defaultKeyId, IEnumerable keys)
{
- DefaultKeyId = defaultKeyId;
- _keyToEncryptorMap = CreateEncryptorMap(defaultKeyId, keys, out _defaultEncryptorHolder);
- }
+ _keyIdToKeyHolderMap = new Dictionary();
+ foreach (IKey key in keys)
+ {
+ _keyIdToKeyHolderMap.Add(key.KeyId, new KeyHolder(key));
+ }
- public KeyRing(Guid defaultKeyId, KeyRing other)
- {
DefaultKeyId = defaultKeyId;
- _keyToEncryptorMap = other._keyToEncryptorMap;
- _defaultEncryptorHolder = _keyToEncryptorMap[defaultKeyId];
+ _defaultKeyHolder = _keyIdToKeyHolderMap[defaultKeyId];
}
-
+
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor
{
get
{
bool unused;
- return _defaultEncryptorHolder.GetEncryptorInstance(out unused);
+ return _defaultKeyHolder.GetEncryptorInstance(out unused);
}
}
- public Guid DefaultKeyId { get; private set; }
-
- private static Dictionary CreateEncryptorMap(Guid defaultKeyId, IKey[] keys, out AuthenticatedEncryptorHolder defaultEncryptorHolder)
- {
- defaultEncryptorHolder = null;
-
- var encryptorMap = new Dictionary(keys.Length);
- foreach (var key in keys)
- {
- var holder = new AuthenticatedEncryptorHolder(key);
- encryptorMap.Add(key.KeyId, holder);
- if (key.KeyId == defaultKeyId)
- {
- defaultEncryptorHolder = holder;
- }
- }
- return encryptorMap;
- }
+ public Guid DefaultKeyId { get; }
public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked)
{
isRevoked = false;
- AuthenticatedEncryptorHolder holder;
- _keyToEncryptorMap.TryGetValue(keyId, out holder);
+ KeyHolder holder;
+ _keyIdToKeyHolderMap.TryGetValue(keyId, out holder);
return holder?.GetEncryptorInstance(out isRevoked);
}
- private sealed class AuthenticatedEncryptorHolder
+ // used for providing lazy activation of the authenticated encryptor instance
+ private sealed class KeyHolder
{
private readonly IKey _key;
private IAuthenticatedEncryptor _encryptor;
- internal AuthenticatedEncryptorHolder(IKey key)
+ internal KeyHolder(IKey key)
{
_key = key;
}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs
index 0837c0dc2d..dc89a53aa8 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs
@@ -2,21 +2,29 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
internal unsafe sealed class KeyRingBasedDataProtectionProvider : IDataProtectionProvider
{
- private readonly IKeyRingProvider _keyringProvider;
+ private readonly IKeyRingProvider _keyRingProvider;
+ private readonly ILogger _logger;
- public KeyRingBasedDataProtectionProvider(IKeyRingProvider keyringProvider)
+ public KeyRingBasedDataProtectionProvider(IKeyRingProvider keyRingProvider, IServiceProvider services)
{
- _keyringProvider = keyringProvider;
+ _keyRingProvider = keyRingProvider;
+ _logger = services.GetLogger(); // note: for protector (not provider!) type, could be null
}
public IDataProtector CreateProtector([NotNull] string purpose)
{
- return new KeyRingBasedDataProtector(_keyringProvider, new[] { purpose });
+ return new KeyRingBasedDataProtector(
+ logger: _logger,
+ keyRingProvider: _keyRingProvider,
+ originalPurposes: null,
+ newPurpose: purpose);
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs
index e7bac85c14..5528cc45e9 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs
@@ -2,159 +2,139 @@
// 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.AspNet.Cryptography;
using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
- internal unsafe sealed class KeyRingBasedDataProtector : IDataProtector
+ 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.MultiplexingDataProtector" [US-ASCII].
- // 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.
+ // 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 byte[] _additionalAuthenticatedDataTemplate;
- private readonly IKeyRingProvider _keyringProvider;
- private readonly string[] _purposes;
+ private AdditionalAuthenticatedDataTemplate _aadTemplate;
+ private readonly IKeyRingProvider _keyRingProvider;
+ private readonly ILogger _logger;
- public KeyRingBasedDataProtector(IKeyRingProvider keyringProvider, string[] purposes)
+ public KeyRingBasedDataProtector(IKeyRingProvider keyRingProvider, ILogger logger, string[] originalPurposes, string newPurpose)
{
- _additionalAuthenticatedDataTemplate = GenerateAdditionalAuthenticatedDataTemplateFromPurposes(purposes);
- _keyringProvider = keyringProvider;
- _purposes = purposes;
+ Debug.Assert(keyRingProvider != null);
+
+ Purposes = ConcatPurposes(originalPurposes, newPurpose);
+ _logger = logger; // can be null
+ _keyRingProvider = keyRingProvider;
+ _aadTemplate = new AdditionalAuthenticatedDataTemplate(Purposes);
}
- private static byte[] ApplyEncryptorIdToAdditionalAuthenticatedDataTemplate(Guid encryptorId, byte[] additionalAuthenticatedDataTemplate)
+ internal string[] Purposes { get; }
+
+ private static string[] ConcatPurposes(string[] originalPurposes, string newPurpose)
{
- CryptoUtil.Assert(additionalAuthenticatedDataTemplate.Length >= sizeof(uint) + sizeof(Guid), "additionalAuthenticatedDataTemplate.Length >= sizeof(uint) + sizeof(Guid)");
-
- // Optimization: just return the original template if the GUID already matches.
- fixed (byte* pbOriginal = additionalAuthenticatedDataTemplate)
+ if (originalPurposes != null && originalPurposes.Length > 0)
{
- if (Read32bitAlignedGuid(&pbOriginal[sizeof(uint)]) == encryptorId)
- {
- return additionalAuthenticatedDataTemplate;
- }
+ string[] newPurposes = new string[originalPurposes.Length + 1];
+ Array.Copy(originalPurposes, 0, newPurposes, 0, originalPurposes.Length);
+ newPurposes[originalPurposes.Length] = newPurpose;
+ return newPurposes;
}
-
- // Clone the template since the input is immutable, then inject the encryptor ID into the new template
- byte[] cloned = (byte[])additionalAuthenticatedDataTemplate.Clone();
- fixed (byte* pbCloned = cloned)
+ else
{
- Write32bitAlignedGuid(&pbCloned[sizeof(uint)], encryptorId);
+ return new string[] { newPurpose };
}
- return cloned;
}
public IDataProtector CreateProtector([NotNull] string purpose)
{
- // Append the incoming purpose to the end of the original array to form a hierarchy
- string[] newPurposes = new string[_purposes.Length + 1];
- Array.Copy(_purposes, 0, newPurposes, 0, _purposes.Length);
- newPurposes[newPurposes.Length - 1] = purpose;
-
- // Use the same keyring as the current instance
- return new KeyRingBasedDataProtector(_keyringProvider, newPurposes);
+ return new KeyRingBasedDataProtector(
+ logger: _logger,
+ keyRingProvider: _keyRingProvider,
+ originalPurposes: Purposes,
+ newPurpose: purpose);
}
- private static byte[] GenerateAdditionalAuthenticatedDataTemplateFromPurposes(string[] purposes)
- {
- const int MEMORYSTREAM_DEFAULT_CAPACITY = 0x100; // matches MemoryStream.EnsureCapacity
- var ms = new MemoryStream(MEMORYSTREAM_DEFAULT_CAPACITY);
-
- // additionalAuthenticatedData := { magicHeader || encryptor-GUID || purposeCount || (purpose)* }
- // purpose := { utf8ByteCount || utf8Text }
- using (var writer = new PurposeBinaryWriter(ms))
- {
- writer.WriteBigEndian(MAGIC_HEADER_V0);
- Debug.Assert(ms.Position == sizeof(uint));
- writer.Seek(sizeof(Guid), SeekOrigin.Current); // skip over where the encryptor GUID will be stored; we'll fill it in later
- if (purposes != null)
- {
- writer.Write7BitEncodedInt(purposes.Length);
- foreach (var purpose in purposes)
- {
- if (String.IsNullOrEmpty(purpose))
- {
- writer.Write7BitEncodedInt(0); // blank purpose
- }
- else
- {
- writer.Write(purpose);
- }
- }
- }
- else
- {
- writer.Write7BitEncodedInt(0); // empty purposes array
- }
- }
-
- return ms.ToArray();
- }
-
- public byte[] Protect(byte[] unprotectedData)
+ // 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 (unprotectedData == null)
+ if (protectedData == null)
{
- throw new ArgumentNullException("unprotectedData");
+ throw new ArgumentNullException(nameof(protectedData));
}
- // 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");
+ UnprotectStatus status;
+ byte[] retVal = UnprotectCore(protectedData, ignoreRevocationErrors, status: out status);
+ requiresMigration = (status != UnprotectStatus.Ok);
+ wasRevoked = (status == UnprotectStatus.DecryptionKeyWasRevoked);
+ return retVal;
+ }
- // We'll need to apply the default encryptor ID to the template if it hasn't already been applied.
- // If the default encryptor ID has been updated since the last call to Protect, also write back the updated template.
- byte[] aadTemplate = Volatile.Read(ref _additionalAuthenticatedDataTemplate);
- byte[] aadForInvocation = ApplyEncryptorIdToAdditionalAuthenticatedDataTemplate(defaultKeyId, aadTemplate);
- if (aadTemplate != aadForInvocation)
+ public byte[] Protect(byte[] plaintext)
+ {
+ // argument & state checking
+ if (plaintext == null)
{
- Volatile.Write(ref _additionalAuthenticatedDataTemplate, aadForInvocation);
+ throw new ArgumentNullException(nameof(plaintext));
}
- // We allocate a 20-byte pre-buffer so that we can inject the magic header and encryptor id into the return value.
- byte[] retVal;
try
{
- retVal = defaultEncryptorInstance.Encrypt(
- plaintext: new ArraySegment(unprotectedData),
- additionalAuthenticatedData: new ArraySegment(aadForInvocation),
+ // 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.LogDebug("Performing protect operation to key '{0:D}' with purposes ({1}).",
+ defaultKeyId, String.Join(", ", Purposes.Select(p => "'" + p + "'")));
+ }
+
+ // 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.
+ byte[] 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.
+ byte[] retVal = defaultEncryptorInstance.Encrypt(
+ plaintext: new ArraySegment(plaintext),
+ additionalAuthenticatedData: new ArraySegment(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);
}
-
- // At this point: retVal := { 000..000 || encryptorSpecificProtectedPayload },
- // where 000..000 is a placeholder for our magic header and encryptor ID.
-
- // Write out the magic header and encryptor ID
- fixed (byte* pbRetVal = retVal)
- {
- WriteBigEndianInteger(pbRetVal, MAGIC_HEADER_V0);
- Write32bitAlignedGuid(&pbRetVal[sizeof(uint)], defaultKeyId);
- }
-
- // At this point, retVal := { magicHeader || encryptor-GUID || encryptorSpecificProtectedPayload }
- // And we're done!
- return retVal;
}
- // Helper function to read a GUID from a 32-bit alignment; useful on ARM where unaligned reads
+ // 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)
{
@@ -193,61 +173,104 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
public byte[] Unprotect(byte[] protectedData)
{
- // argument & state checking
- if (protectedData == null)
- {
- throw new ArgumentNullException("protectedData");
- }
- if (protectedData.Length < sizeof(uint) /* magic header */ + sizeof(Guid) /* key id */)
- {
- throw Error.Common_NotAValidProtectedPayload();
- }
+ // Argument checking will be done by the callee
+ bool requiresMigration, wasRevoked; // unused
+ return DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: false,
+ requiresMigration: out requiresMigration,
+ wasRevoked: out wasRevoked);
+ }
- // Need to check that protectedData := { magicHeader || encryptor-GUID || encryptorSpecificProtectedPayload }
-
- // Parse the payload version number and encryptor ID.
- uint payloadMagicHeader;
- Guid payloadEncryptorId;
- fixed (byte* pbInput = protectedData)
- {
- payloadMagicHeader = ReadBigEndian32BitInteger(pbInput);
- payloadEncryptorId = Read32bitAlignedGuid(&pbInput[sizeof(uint)]);
- }
-
- // Are the magic header and version information correct?
- int payloadVersion;
- if (!TryGetVersionFromMagicHeader(payloadMagicHeader, out payloadVersion))
- {
- throw Error.Common_NotAValidProtectedPayload();
- }
- else if (payloadVersion != 0)
- {
- throw Error.Common_PayloadProducedByNewerVersion();
- }
-
- // Find the correct encryptor in the keyring.
- bool keyWasRevoked;
- var requestedEncryptor = _keyringProvider.GetCurrentKeyRing().GetAuthenticatedEncryptorByKeyId(payloadEncryptorId, out keyWasRevoked);
- if (requestedEncryptor == null)
- {
- throw Error.Common_KeyNotFound(payloadEncryptorId);
- }
- if (keyWasRevoked)
- {
- throw Error.Common_KeyRevoked(payloadEncryptorId);
- }
-
- // Perform the decryption operation.
- ArraySegment ciphertext = new ArraySegment(protectedData, sizeof(uint) + sizeof(Guid), protectedData.Length - (sizeof(uint) + sizeof(Guid))); // chop off magic header + encryptor id
- ArraySegment additionalAuthenticatedData = new ArraySegment(ApplyEncryptorIdToAdditionalAuthenticatedDataTemplate(payloadEncryptorId, Volatile.Read(ref _additionalAuthenticatedDataTemplate)));
+ 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.LogDebug("Performing unprotect operation to key '{0:D}' with purposes ({1}).",
+ keyIdFromPayload, String.Join(", ", Purposes.Select(p => "'" + p + "'")));
+ }
+
+ // Find the correct encryptor in the keyring.
+ bool keyWasRevoked;
+ var currentKeyRing = _keyRingProvider.GetCurrentKeyRing();
+ var requestedEncryptor = currentKeyRing.GetAuthenticatedEncryptorByKeyId(keyIdFromPayload, out keyWasRevoked);
+ if (requestedEncryptor == null)
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("Key '{0:D}' was not found in the key ring. Unprotect operation cannot proceed.", 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)
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("Key '{0:D}' was revoked. Caller requested unprotect operation proceed regardless.", keyIdFromPayload);
+ }
+ status = UnprotectStatus.DecryptionKeyWasRevoked;
+ }
+ else
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("Key '{0:D}' was revoked. Unprotect operation cannot proceed.", keyIdFromPayload);
+ }
+ throw Error.Common_KeyRevoked(keyIdFromPayload);
+ }
+ }
+
+ // Perform the decryption operation.
+ ArraySegment ciphertext = new ArraySegment(protectedData, sizeof(uint) + sizeof(Guid), protectedData.Length - (sizeof(uint) + sizeof(Guid))); // chop off magic header + encryptor id
+ ArraySegment additionalAuthenticatedData = new ArraySegment(_aadTemplate.GetAadForKey(keyIdFromPayload, isProtecting: false));
+
// At this point, cipherText := { encryptorSpecificPayload },
// so all that's left is to invoke the decryption routine directly.
- byte[] retVal = requestedEncryptor.Decrypt(ciphertext, additionalAuthenticatedData);
- CryptoUtil.Assert(retVal != null, "retVal != null");
- return retVal;
+ return requestedEncryptor.Decrypt(ciphertext, additionalAuthenticatedData)
+ ?? CryptoUtil.Fail("IAuthenticatedEncryptor.Decrypt returned null.");
}
catch (Exception ex) when (ex.RequiresHomogenization())
{
@@ -276,27 +299,95 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
ptr[3] = (byte)(value);
}
- private sealed class PurposeBinaryWriter : BinaryWriter
+ private struct AdditionalAuthenticatedDataTemplate
{
- // Strings should never contain invalid UTF16 chars, so we'll use a secure encoding.
- private static readonly byte[] _guidBuffer = new byte[sizeof(Guid)];
+ private byte[] _aadTemplate;
- public PurposeBinaryWriter(MemoryStream stream) : base(stream, EncodingUtil.SecureUtf8Encoding, leaveOpen: true) { }
-
- public new void Write7BitEncodedInt(int value)
+ public AdditionalAuthenticatedDataTemplate(IEnumerable purposes)
{
- base.Write7BitEncodedInt(value);
+ 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));
+ long 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();
}
- // Writes a big-endian 32-bit integer to the underlying stream.
- public void WriteBigEndian(uint value)
+ public byte[] GetAadForKey(Guid keyId, bool isProtecting)
{
- 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));
+ // 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.
+ byte[] 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/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs
index ce37200737..ec8c878c04 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs
@@ -2,205 +2,162 @@
// 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.Linq;
using System.Threading;
using Microsoft.AspNet.Cryptography;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
- internal sealed class KeyRingProvider : IKeyRingProvider
+ internal sealed class KeyRingProvider : ICacheableKeyRingProvider, IKeyRingProvider
{
- // TODO: Should the below be 3 months?
- private static readonly TimeSpan KEY_DEFAULT_LIFETIME = TimeSpan.FromDays(30 * 6); // how long should keys be active once created?
- private static readonly TimeSpan KEYRING_REFRESH_PERIOD = TimeSpan.FromDays(1); // how often should we check for updates to the repository?
- private static readonly TimeSpan KEY_EXPIRATION_BUFFER = TimeSpan.FromDays(7); // how close to key expiration should we generate a new key?
- private static readonly TimeSpan MAX_SERVER_TO_SERVER_CLOCK_SKEW = TimeSpan.FromMinutes(10); // max skew we expect to see between servers using the key ring
-
- private CachedKeyRing _cachedKeyRing;
- private readonly object _cachedKeyRingLockObj = new object();
+ private CacheableKeyRing _cacheableKeyRing;
+ private readonly object _cacheableKeyRingLockObj = new object();
+ private readonly ICacheableKeyRingProvider _cacheableKeyRingProvider;
+ private readonly IDefaultKeyResolver _defaultKeyResolver;
+ private readonly KeyLifetimeOptions _keyLifetimeOptions;
private readonly IKeyManager _keyManager;
+ private readonly ILogger _logger;
- public KeyRingProvider(IKeyManager keyManager)
+ public KeyRingProvider(IKeyManager keyManager, KeyLifetimeOptions keyLifetimeOptions, IServiceProvider services)
{
+ _keyLifetimeOptions = new KeyLifetimeOptions(keyLifetimeOptions); // clone so new instance is immutable
_keyManager = keyManager;
+ _cacheableKeyRingProvider = services?.GetService() ?? this;
+ _logger = services?.GetLogger();
+ _defaultKeyResolver = services?.GetService()
+ ?? new DefaultKeyResolver(_keyLifetimeOptions.KeyExpirationSafetyPeriod, _keyLifetimeOptions.MaxServerClockSkew, services);
+ }
+
+ private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowRecursiveCalls = false)
+ {
+ // 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);
+ }
+
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Policy resolution states that a new key should be added to the key ring.");
+ }
+
+ // At this point, we know we need to generate a new key.
+
+ // This should only occur if a call to CreateNewKey immediately followed by a call to
+ // GetAllKeys returned 'you need to add a key to the key ring'. This should never happen
+ // in practice unless there's corruption in the backing store. Regardless, we can't recurse
+ // forever, so we have to bail now.
+ if (!allowRecursiveCalls)
+ {
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey.");
+ }
+ throw CryptoUtil.Fail("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey.");
+ }
+
+ 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.
+ _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyLifetimeOptions.NewKeyLifetime);
+ return CreateCacheableKeyRingCore(now); // 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.
+ _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyLifetimeOptions.NewKeyLifetime);
+ return CreateCacheableKeyRingCore(now); // recursively call
+ }
}
- private CachedKeyRing CreateCachedKeyRingInstanceUnderLock(DateTime utcNow, CachedKeyRing existingCachedKeyRing)
+ private CacheableKeyRing CreateCacheableKeyRingCoreStep2(DateTimeOffset now, CancellationToken cacheExpirationToken, IKey defaultKey, IEnumerable allKeys)
{
- bool shouldCreateNewKeyWithDeferredActivation; // flag stating whether the default key will soon expire and doesn't have a suitable replacement
-
- // Must we discard the cached keyring and refresh directly from the manager?
- if (existingCachedKeyRing != null && existingCachedKeyRing.HardRefreshTimeUtc <= utcNow)
+ if (_logger.IsVerboseLevelEnabled())
{
- existingCachedKeyRing = null;
+ _logger.LogVerbose("Using key '{0:D}' as the default key.", defaultKey.KeyId);
}
- // Try to locate the current default key, using the cached keyring if we can.
- IKey defaultKey;
- if (existingCachedKeyRing != null)
- {
- defaultKey = FindDefaultKey(utcNow, existingCachedKeyRing.Keys, out shouldCreateNewKeyWithDeferredActivation);
- if (defaultKey != null && !shouldCreateNewKeyWithDeferredActivation)
- {
- return new CachedKeyRing
- {
- KeyRing = new KeyRing(defaultKey.KeyId, existingCachedKeyRing.KeyRing), // this overload allows us to use existing IAuthenticatedEncryptor instances
- Keys = existingCachedKeyRing.Keys,
- HardRefreshTimeUtc = existingCachedKeyRing.HardRefreshTimeUtc,
- SoftRefreshTimeUtc = MinDateTime(existingCachedKeyRing.HardRefreshTimeUtc, utcNow + KEYRING_REFRESH_PERIOD)
- };
- }
- }
-
- // That didn't work, so refresh from the underlying key manager.
- var allKeys = _keyManager.GetAllKeys().ToArray();
- defaultKey = FindDefaultKey(utcNow, allKeys, out shouldCreateNewKeyWithDeferredActivation);
-
- if (defaultKey != null && shouldCreateNewKeyWithDeferredActivation)
- {
- // If we need to create a new key with deferred activation, do so now.
- _keyManager.CreateNewKey(activationDate: defaultKey.ExpirationDate, expirationDate: utcNow + KEY_DEFAULT_LIFETIME);
- allKeys = _keyManager.GetAllKeys().ToArray();
- defaultKey = FindDefaultKey(utcNow, allKeys);
- }
- else if (defaultKey == null)
- {
- // If there's no default key, create one now with immediate activation.
- _keyManager.CreateNewKey(utcNow, utcNow + KEY_DEFAULT_LIFETIME);
- allKeys = _keyManager.GetAllKeys().ToArray();
- defaultKey = FindDefaultKey(utcNow, allKeys);
- }
-
- // We really should have a default key at this point.
- CryptoUtil.Assert(defaultKey != null, "defaultKey != null");
-
- var cachedKeyRingHardRefreshTime = GetNextHardRefreshTime(utcNow);
- return new CachedKeyRing
- {
- KeyRing = new KeyRing(defaultKey.KeyId, allKeys),
- Keys = allKeys,
- HardRefreshTimeUtc = cachedKeyRingHardRefreshTime,
- SoftRefreshTimeUtc = MinDateTime(defaultKey.ExpirationDate.UtcDateTime, cachedKeyRingHardRefreshTime)
- };
- }
-
- private static IKey FindDefaultKey(DateTime utcNow, IKey[] allKeys)
- {
- bool unused;
- return FindDefaultKey(utcNow, allKeys, out unused);
- }
-
- private static IKey FindDefaultKey(DateTime utcNow, IKey[] allKeys, out bool callerShouldGenerateNewKey)
- {
- callerShouldGenerateNewKey = false;
-
- // Find the keys with the nearest past and future activation dates.
- IKey keyWithNearestPastActivationDate = null;
- IKey keyWithNearestFutureActivationDate = null;
- foreach (var candidateKey in allKeys)
- {
- // Revoked keys are never eligible candidates to be the default key.
- if (candidateKey.IsRevoked)
- {
- continue;
- }
-
- if (candidateKey.ActivationDate.UtcDateTime <= utcNow)
- {
- if (keyWithNearestPastActivationDate == null || keyWithNearestPastActivationDate.ActivationDate < candidateKey.ActivationDate)
- {
- keyWithNearestPastActivationDate = candidateKey;
- }
- }
- else
- {
- if (keyWithNearestFutureActivationDate == null || keyWithNearestFutureActivationDate.ActivationDate > candidateKey.ActivationDate)
- {
- keyWithNearestFutureActivationDate = candidateKey;
- }
- }
- }
-
- // If the most recently activated key hasn't yet expired, use it as the default key.
- if (keyWithNearestPastActivationDate != null && !keyWithNearestPastActivationDate.IsExpired(utcNow))
- {
- // Additionally, if it's about to expire and there will be a gap in the keyring during which there
- // is no valid default encryption key, the caller should generate a new key with deferred activation.
- if (keyWithNearestPastActivationDate.ExpirationDate.UtcDateTime - utcNow <= KEY_EXPIRATION_BUFFER)
- {
- if (keyWithNearestFutureActivationDate == null || keyWithNearestFutureActivationDate.ActivationDate > keyWithNearestPastActivationDate.ExpirationDate)
- {
- callerShouldGenerateNewKey = true;
- }
- }
-
- return keyWithNearestPastActivationDate;
- }
-
- // Failing that, is any key due for imminent activation? If so, use it as the default key.
- // This allows us to account for clock skew when multiple servers touch the repository.
- if (keyWithNearestFutureActivationDate != null
- && (keyWithNearestFutureActivationDate.ActivationDate.UtcDateTime - utcNow) < MAX_SERVER_TO_SERVER_CLOCK_SKEW
- && !keyWithNearestFutureActivationDate.IsExpired(utcNow) /* sanity check: expiration can't occur before activation */)
- {
- return keyWithNearestFutureActivationDate;
- }
-
- // Otherwise, there's no default key.
- return null;
+ // 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.
+ return new CacheableKeyRing(
+ expirationToken: cacheExpirationToken,
+ expirationTime: Min(defaultKey.ExpirationDate, now + GetRefreshPeriodWithJitter(_keyLifetimeOptions.KeyRingRefreshPeriod)),
+ defaultKey: defaultKey,
+ allKeys: allKeys);
}
public IKeyRing GetCurrentKeyRing()
{
- DateTime utcNow = DateTime.UtcNow;
+ 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 existingCachedKeyRing = Volatile.Read(ref _cachedKeyRing);
- if (existingCachedKeyRing != null && existingCachedKeyRing.SoftRefreshTimeUtc > utcNow)
+ var existingCacheableKeyRing = Volatile.Read(ref _cacheableKeyRing);
+ if (CacheableKeyRing.IsValid(existingCacheableKeyRing, utcNow))
{
- return existingCachedKeyRing.KeyRing;
+ return existingCacheableKeyRing.KeyRing;
}
// The cached keyring hasn't been created or must be refreshed.
- lock (_cachedKeyRingLockObj)
+ lock (_cacheableKeyRingLockObj)
{
// Did somebody update the keyring while we were waiting for the lock?
- existingCachedKeyRing = Volatile.Read(ref _cachedKeyRing);
- if (existingCachedKeyRing != null && existingCachedKeyRing.SoftRefreshTimeUtc > utcNow)
+ existingCacheableKeyRing = Volatile.Read(ref _cacheableKeyRing);
+ if (CacheableKeyRing.IsValid(existingCacheableKeyRing, utcNow))
{
- return existingCachedKeyRing.KeyRing;
+ return existingCacheableKeyRing.KeyRing;
+ }
+
+ if (existingCacheableKeyRing != null && _logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Existing cached key ring is expired. Refreshing.");
}
// It's up to us to refresh the cached keyring.
- var newCachedKeyRing = CreateCachedKeyRingInstanceUnderLock(utcNow, existingCachedKeyRing);
- Volatile.Write(ref _cachedKeyRing, newCachedKeyRing);
- return newCachedKeyRing.KeyRing;
+ // This call is performed *under lock*.
+ var newCacheableKeyRing = _cacheableKeyRingProvider.GetCacheableKeyRing(utcNow);
+ Volatile.Write(ref _cacheableKeyRing, newCacheableKeyRing);
+ return newCacheableKeyRing.KeyRing;
}
}
- private static DateTime GetNextHardRefreshTime(DateTime utcNow)
+ private static TimeSpan GetRefreshPeriodWithJitter(TimeSpan refreshPeriod)
{
- // We'll fudge the refresh period up to 20% so that multiple applications don't try to
+ // 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 calculate the new refresh time as somewhere between 48 - 60 minutes from now.
- var skewedRefreshPeriod = TimeSpan.FromTicks((long)(KEYRING_REFRESH_PERIOD.Ticks * ((new Random().NextDouble() / 5) + 0.8d)));
- return utcNow + skewedRefreshPeriod;
+ // 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 DateTime MinDateTime(DateTime a, DateTime b)
+ private static DateTimeOffset Min(DateTimeOffset a, DateTimeOffset b)
{
- Debug.Assert(a.Kind == DateTimeKind.Utc);
- Debug.Assert(b.Kind == DateTimeKind.Utc);
return (a < b) ? a : b;
}
- private sealed class CachedKeyRing
+ CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now)
{
- internal DateTime HardRefreshTimeUtc;
- internal KeyRing KeyRing;
- internal IKey[] Keys;
- internal DateTime SoftRefreshTimeUtc;
+ // the entry point allows one recursive call
+ return CreateCacheableKeyRingCore(now, allowRecursiveCalls: true);
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs
index e31cd5353a..4466158062 100644
--- a/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs
+++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs
@@ -7,98 +7,121 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNet.Cryptography;
-using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNet.DataProtection.Repositories;
using Microsoft.AspNet.DataProtection.XmlEncryption;
using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.KeyManagement
{
- public sealed class XmlKeyManager : IKeyManager
+ ///
+ /// A key manager backed by an .
+ ///
+ public sealed class XmlKeyManager : IKeyManager, IInternalXmlKeyManager
{
- private const string KEY_MANAGEMENT_XML_NAMESPACE_STRING = "http://www.asp.net/dataProtection/2014";
- internal static readonly XNamespace KeyManagementXmlNamespace = XNamespace.Get(KEY_MANAGEMENT_XML_NAMESPACE_STRING);
+ // 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";
- internal static readonly XName ActivationDateElementName = KeyManagementXmlNamespace.GetName("activationDate");
- internal static readonly XName AuthenticatedEncryptorElementName = KeyManagementXmlNamespace.GetName("authenticatedEncryptor");
- internal static readonly XName CreationDateElementName = KeyManagementXmlNamespace.GetName("creationDate");
- internal static readonly XName ExpirationDateElementName = KeyManagementXmlNamespace.GetName("expirationDate");
- internal static readonly XName IdAttributeName = XNamespace.None.GetName("id");
- internal static readonly XName KeyElementName = KeyManagementXmlNamespace.GetName("key");
- internal static readonly XName ReaderAttributeName = XNamespace.None.GetName("reader");
- internal static readonly XName ReasonElementName = KeyManagementXmlNamespace.GetName("reason");
- internal static readonly XName RevocationDateElementName = KeyManagementXmlNamespace.GetName("revocationDate");
- internal static readonly XName RevocationElementName = KeyManagementXmlNamespace.GetName("revocation");
- internal static readonly XName VersionAttributeName = XNamespace.None.GetName("version");
+ private const string RevokeAllKeysValue = "*";
- private readonly IAuthenticatedEncryptorConfigurationFactory _authenticatedEncryptorConfigurationFactory;
- private readonly IServiceProvider _serviceProvider;
- private readonly IXmlRepository _xmlRepository;
- private readonly IXmlEncryptor _xmlEncryptor;
+ private readonly IActivator _activator;
+ private readonly IAuthenticatedEncryptorConfiguration _authenticatedEncryptorConfiguration;
+ private readonly IInternalXmlKeyManager _internalKeyManager;
+ private readonly IKeyEscrowSink _keyEscrowSink;
+ private readonly ILogger _logger;
+ private CancellationTokenSource _cacheExpirationTokenSource;
+
+ ///
+ /// Creates an .
+ ///
+ /// The repository where keys are stored.
+ /// Configuration for newly-created keys.
+ /// A provider of optional services.
public XmlKeyManager(
- [NotNull] IServiceProvider serviceProvider,
- [NotNull] IAuthenticatedEncryptorConfigurationFactory authenticatedEncryptorConfigurationFactory,
- [NotNull] IXmlRepository xmlRepository,
- [NotNull] IXmlEncryptor xmlEncryptor)
+ [NotNull] IXmlRepository repository,
+ [NotNull] IAuthenticatedEncryptorConfiguration configuration,
+ IServiceProvider services)
{
- _serviceProvider = serviceProvider;
- _authenticatedEncryptorConfigurationFactory = authenticatedEncryptorConfigurationFactory;
- _xmlRepository = xmlRepository;
- _xmlEncryptor = xmlEncryptor;
+ KeyEncryptor = services.GetService(); // optional
+ KeyRepository = repository;
+
+ _activator = services.GetActivator(); // returns non-null
+ _authenticatedEncryptorConfiguration = configuration;
+ _internalKeyManager = services.GetService() ?? this;
+ _keyEscrowSink = services.GetKeyEscrowSink(); // not required
+ _logger = services.GetLogger(); // not required
+ TriggerAndResetCacheExpirationToken(suppressLogging: true);
}
+ internal XmlKeyManager(IServiceProvider services)
+ {
+ // First, see if an explicit encryptor or repository was specified.
+ // If either was specified, then we won't use the fallback.
+ KeyEncryptor = services.GetService(); // optional
+ KeyRepository = (KeyEncryptor != null)
+ ? services.GetRequiredService() // required if encryptor is specified
+ : services.GetService(); // optional if encryptor not specified
+
+ // If the repository is missing, then we get both the encryptor and the repository from the fallback.
+ // If the fallback is missing, the final call to GetRequiredService below will throw.
+ if (KeyRepository == null)
+ {
+ var defaultKeyServices = services.GetService();
+ KeyEncryptor = defaultKeyServices?.GetKeyEncryptor(); // optional
+ KeyRepository = defaultKeyServices?.GetKeyRepository() ?? services.GetRequiredService();
+ }
+
+ _activator = services.GetActivator(); // returns non-null
+ _authenticatedEncryptorConfiguration = services.GetRequiredService();
+ _internalKeyManager = services.GetService() ?? this;
+ _keyEscrowSink = services.GetKeyEscrowSink(); // not required
+ _logger = services.GetLogger(); // not required
+ TriggerAndResetCacheExpirationToken(suppressLogging: true);
+ }
+
+ internal IXmlEncryptor KeyEncryptor { get; }
+
+ internal IXmlRepository KeyRepository { get; }
+
public IKey CreateNewKey(DateTimeOffset activationDate, DateTimeOffset expirationDate)
{
- return CreateNewKey(Guid.NewGuid(), DateTimeOffset.UtcNow, activationDate, expirationDate);
+ return _internalKeyManager.CreateNewKey(
+ keyId: Guid.NewGuid(),
+ creationDate: DateTimeOffset.UtcNow,
+ activationDate: activationDate,
+ expirationDate: expirationDate);
}
- private IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate)
+ private static string DateTimeOffsetToFilenameSafeString(DateTimeOffset dateTime)
{
- //
- // ...
- // ...
- // ...
- //
- // <... parser="{TYPE}" />
- //
- //
-
- // Create the element and make sure it's well-formed.
- var encryptorConfiguration = _authenticatedEncryptorConfigurationFactory.CreateNewConfiguration();
- var encryptorElementAsXml = encryptorConfiguration.ToXml(_xmlEncryptor);
- CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptorElementAsXml.Attribute(ReaderAttributeName)), "!String.IsNullOrEmpty((string)encryptorElementAsXml.Attribute(ParserAttributeName))");
-
- // Create the 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(AuthenticatedEncryptorElementName,
- encryptorElementAsXml));
-
- // Persist it to the underlying repository
- string friendlyName = String.Format(CultureInfo.InvariantCulture, "key-{0:D}", keyId);
- _xmlRepository.StoreElement(keyElement, friendlyName);
-
- // And we're done!
- return new Key(
- keyId: keyId,
- creationDate: creationDate,
- activationDate: activationDate,
- expirationDate: expirationDate,
- encryptorConfiguration: encryptorConfiguration);
+ // similar to the XML format for dates, but with punctuation stripped
+ return dateTime.UtcDateTime.ToString("yyyyMMddTHHmmssFFFFFFFZ");
}
public IReadOnlyCollection GetAllKeys()
{
- var allElements = _xmlRepository.GetAllElements();
+ var allElements = KeyRepository.GetAllElements();
- Dictionary idToKeyMap = new Dictionary();
+ // We aggregate all the information we read into three buckets
+ Dictionary keyIdToKeyMap = new Dictionary();
HashSet revokedKeyIds = null;
DateTimeOffset? mostRecentMassRevocationDate = null;
@@ -106,149 +129,344 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement
{
if (element.Name == KeyElementName)
{
- var thisKey = ParseKeyElement(element);
- if (idToKeyMap.ContainsKey(thisKey.KeyId))
+ // 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.
+ Key key = ProcessKeyElement(element);
+ if (key != null)
{
- throw CryptoUtil.Fail("TODO: Duplicate key.");
+ if (keyIdToKeyMap.ContainsKey(key.KeyId))
+ {
+ throw Error.XmlKeyManager_DuplicateKey(key.KeyId);
+ }
+ keyIdToKeyMap[key.KeyId] = key;
}
- idToKeyMap.Add(thisKey.KeyId, thisKey);
}
else if (element.Name == RevocationElementName)
{
- object revocationInfo = ParseRevocationElement(element);
- DateTimeOffset? revocationInfoAsDate = revocationInfo as DateTimeOffset?;
- if (revocationInfoAsDate != null)
+ object revocationInfo = ProcessRevocationElement(element);
+ if (revocationInfo is Guid)
{
- // We're revoking all keys created on or after a specific date.
- if (!mostRecentMassRevocationDate.HasValue || mostRecentMassRevocationDate < revocationInfoAsDate)
- {
- // This new value is the most recent mass revocation date.
- mostRecentMassRevocationDate = revocationInfoAsDate;
- }
- }
- else
- {
- // We're revoking only a specific key
+ // a single key was revoked
if (revokedKeyIds == null)
{
revokedKeyIds = new HashSet();
}
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
{
- throw CryptoUtil.Fail("TODO: Unknown element.");
- }
- }
-
- // Now process all revocations
- if (revokedKeyIds != null || mostRecentMassRevocationDate.HasValue)
- {
- foreach (Key key in idToKeyMap.Values)
- {
- if ((revokedKeyIds != null && revokedKeyIds.Contains(key.KeyId))
- || (mostRecentMassRevocationDate.HasValue && mostRecentMassRevocationDate >= key.CreationDate))
+ // Skip unknown elements.
+ if (_logger.IsWarningLevelEnabled())
{
- key.SetRevoked();
+ _logger.LogWarning("Unknown element with name '{0}' found in keyring, skipping.", element.Name);
}
}
}
- // And we're done!
- return idToKeyMap.Values.ToArray();
+ // Apply individual revocations
+ if (revokedKeyIds != null)
+ {
+ foreach (Guid revokedKeyId in revokedKeyIds)
+ {
+ Key key;
+ keyIdToKeyMap.TryGetValue(revokedKeyId, out key);
+ if (key != null)
+ {
+ key.SetRevoked();
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Marked key '{0:D}' as revoked in the keyring.", revokedKeyId);
+ }
+ }
+ else
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("Tried to process revocation of key '{0:D}', but no such key was found in keyring. Skipping.", revokedKeyId);
+ }
+ }
+ }
+ }
+
+ // Apply mass revocations
+ if (mostRecentMassRevocationDate.HasValue)
+ {
+ foreach (var key in keyIdToKeyMap.Values)
+ {
+ if (key.CreationDate <= mostRecentMassRevocationDate)
+ {
+ key.SetRevoked();
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Marked key '{0:D}' as revoked in the keyring.", key.KeyId);
+ }
+ }
+ }
+ }
+
+ // And we're finished!
+ return keyIdToKeyMap.Values.ToList().AsReadOnly();
}
- private Key ParseKeyElement(XElement keyElement)
+ public CancellationToken GetCacheExpirationToken()
+ {
+ return Interlocked.CompareExchange(ref _cacheExpirationTokenSource, null, null).Token;
+ }
+
+ private Key ProcessKeyElement(XElement keyElement)
{
Debug.Assert(keyElement.Name == KeyElementName);
- int version = (int)keyElement.Attribute(VersionAttributeName);
- CryptoUtil.Assert(version == 1, "TODO: version == 1");
+ try
+ {
+ // Read metadata
+ Guid keyId = (Guid)keyElement.Attribute(IdAttributeName);
+ DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName);
+ DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName);
+ DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName);
- XElement encryptorConfigurationAsXml = keyElement.Element(AuthenticatedEncryptorElementName).Elements().Single();
- string encryptorConfigurationParserTypeName = (string)encryptorConfigurationAsXml.Attribute(ReaderAttributeName);
- Type encryptorConfigurationParserType = Type.GetType(encryptorConfigurationParserTypeName, throwOnError: true);
- CryptoUtil.Assert(typeof(IAuthenticatedEncryptorConfigurationXmlReader).IsAssignableFrom(encryptorConfigurationParserType),
- "TODO: typeof(IAuthenticatedEncryptorConfigurationXmlReader).IsAssignableFrom(encryptorConfigurationParserType)");
+ // Figure out who will be deserializing this
+ XElement descriptorElement = keyElement.Element(DescriptorElementName);
+ string descriptorDeserializerTypeName = (string)descriptorElement.Attribute(DeserializerTypeAttributeName);
- var parser = (IAuthenticatedEncryptorConfigurationXmlReader)ActivatorUtilities.CreateInstance(_serviceProvider, encryptorConfigurationParserType);
- var encryptorConfiguration = parser.FromXml(encryptorConfigurationAsXml);
+ // Decrypt the descriptor element and pass it to the descriptor for consumption
+ XElement unencryptedInputToDeserializer = descriptorElement.Elements().Single().DecryptElement(_activator);
+ var deserializerInstance = _activator.CreateInstance(descriptorDeserializerTypeName);
+ var descriptorInstance = deserializerInstance.ImportFromXml(unencryptedInputToDeserializer);
- Guid keyId = (Guid)keyElement.Attribute(IdAttributeName);
- DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName);
- DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName);
- DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName);
+ // Finally, create the Key instance
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Found key '{0:D}'.", keyId);
+ }
+ return new Key(
+ keyId: keyId,
+ creationDate: creationDate,
+ activationDate: activationDate,
+ expirationDate: expirationDate,
+ descriptor: descriptorInstance);
+ }
+ catch (Exception ex)
+ {
+ // We only write the exception out to the 'debug' log since it could contain sensitive
+ // information and we don't want to leak it.
+ if (_logger.IsDebugLevelEnabled())
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("An exception of type '{0}' occurred while processing the key element '{1}', so the key will not be included in the keyring." + Environment.NewLine
+ + "Full details of the exception will be written to the 'Debug' log.",
+ ex.GetType().FullName, keyElement.WithoutChildNodes());
+ }
+ _logger.LogDebug(ex, "An exception occurred while processing the key element '{0}'.", keyElement);
+ }
+ else
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("An exception of type '{0}' occurred while processing the key element '{1}', so the key will not be included in the keyring." + Environment.NewLine
+ + "To prevent accidental disclosure of sensitive information the full exception details are not being logged. To enable logging full exception details, enable 'Debug' level logging for this provider.",
+ ex.GetType().FullName, keyElement.WithoutChildNodes());
+ }
+ }
- return new Key(
- keyId: keyId,
- creationDate: creationDate,
- activationDate: activationDate,
- expirationDate: expirationDate,
- encryptorConfiguration: encryptorConfiguration);
+ // If an error occurs, we just skip this key.
+ return null;
+ }
}
// returns a Guid (for specific keys) or a DateTimeOffset (for all keys created on or before a specific date)
- private object ParseRevocationElement(XElement revocationElement)
+ private object ProcessRevocationElement(XElement revocationElement)
{
Debug.Assert(revocationElement.Name == RevocationElementName);
- string keyIdAsString = revocationElement.Element(KeyElementName).Attribute(IdAttributeName).Value;
- if (keyIdAsString == "*")
+ try
{
- // all keys
- return (DateTimeOffset)revocationElement.Element(RevocationDateElementName);
+ 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);
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Found revocation of all keys created prior to {0:u}.", massRevocationDate);
+ }
+ return massRevocationDate;
+ }
+ else
+ {
+ // only one key is being revoked
+ Guid keyId = XmlConvert.ToGuid(keyIdAsString);
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Found revocation of key '{0:D}'.", keyId);
+ }
+ return keyId;
+ }
}
- else
+ catch (Exception ex)
{
- // only one key
- return new Guid(keyIdAsString);
+ // Any exceptions that occur are fatal - we don't want to continue if we cannot process
+ // revocation information.
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(ex, "An exception occurred while processing the revocation element '{0}'. Cannot continue keyring processing.", revocationElement);
+ }
+ throw;
}
}
public void RevokeAllKeys(DateTimeOffset revocationDate, string reason = null)
{
- //
+ //
// ...
+ //
//
// ...
//
+ if (_logger.IsInformationLevelEnabled())
+ {
+ _logger.LogInformation("Revoking all keys as of {0:u} for reason '{1}'.", 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.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Key cache expiration token triggered by '{0}' operation.", opName);
+ }
+
+ Interlocked.Exchange(ref _cacheExpirationTokenSource, new CancellationTokenSource())?.Cancel();
+ }
+
+ IKey IInternalXmlKeyManager.CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate)
+ {
+ //
+ // ...
+ // ...
+ // ...
+ //
+ // ...
+ //
+ //
+
+ if (_logger.IsInformationLevelEnabled())
+ {
+ _logger.LogInformation("Creating key {0:D} with creation date {1:u}, activation date {2:u}, and expiration date {3:u}.", keyId, creationDate, activationDate, expirationDate);
+ }
+
+ var newDescriptor = _authenticatedEncryptorConfiguration.CreateNewDescriptor()
+ ?? CryptoUtil.Fail("CreateNewDescriptor returned null.");
+ var descriptorXmlInfo = newDescriptor.ExportToXml();
+
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Descriptor deserializer type for key {0:D} is {1}.", keyId, descriptorXmlInfo.DeserializerType.AssemblyQualifiedName);
+ }
+
+ // build the 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 (_logger.IsVerboseLevelEnabled())
+ {
+ if (_keyEscrowSink != null)
+ {
+ _logger.LogVerbose("Key escrow sink found. Writing key {0:D} to escrow.", keyId);
+ }
+ else
+ {
+ _logger.LogVerbose("No key escrow sink found. Not writing key {0:D} to escrow.", keyId);
+ }
+ }
+ _keyEscrowSink?.Store(keyId, keyElement);
+
+ // If an XML encryptor has been configured, protect secret key material now.
+ if (KeyEncryptor == null && _logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("No XML encryptor configured. Key {0:D} may be persisted to storage in unencrypted form.", keyId);
+ }
+ var possiblyEncryptedKeyElement = KeyEncryptor?.EncryptIfNecessary(keyElement) ?? keyElement;
+
+ // Persist it to the underlying repository and trigger the cancellation token.
+ string 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);
+ }
+
+ void IInternalXmlKeyManager.RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason)
+ {
+ //
+ // ...
+ //
+ // ...
+ //
+
+ if (_logger.IsInformationLevelEnabled())
+ {
+ _logger.LogInformation("Revoking key {0:D} at {1:u} for reason '{2}'.", keyId, revocationDate, reason);
+ }
+
var revocationElement = new XElement(RevocationElementName,
new XAttribute(VersionAttributeName, 1),
new XElement(RevocationDateElementName, revocationDate),
- new XElement(KeyElementName,
- new XAttribute(IdAttributeName, "*")),
- new XElement(ReasonElementName, reason));
-
- // Persist it to the underlying repository
- string friendlyName = String.Format(CultureInfo.InvariantCulture, "revocation-{0:X16}", (ulong)revocationDate.UtcTicks);
- _xmlRepository.StoreElement(revocationElement, friendlyName);
- }
-
- public void RevokeKey(Guid keyId, string reason = null)
- {
- RevokeSingleKey(keyId, DateTimeOffset.UtcNow, reason);
- }
-
- private void RevokeSingleKey(Guid keyId, DateTimeOffset utcNow, string reason)
- {
- //
- // ...
- //
- // ...
- //
-
- var revocationElement = new XElement(RevocationElementName,
- new XAttribute(VersionAttributeName, 1),
- new XElement(RevocationDateElementName, utcNow),
new XElement(KeyElementName,
new XAttribute(IdAttributeName, keyId)),
new XElement(ReasonElementName, reason));
- // Persist it to the underlying repository
+ // Persist it to the underlying repository and trigger the cancellation token
string friendlyName = String.Format(CultureInfo.InvariantCulture, "revocation-{0:D}", keyId);
- _xmlRepository.StoreElement(revocationElement, friendlyName);
+ KeyRepository.StoreElement(revocationElement, friendlyName);
+ TriggerAndResetCacheExpirationToken();
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/LoggingExtensions.cs b/src/Microsoft.AspNet.DataProtection/LoggingExtensions.cs
new file mode 100644
index 0000000000..ee7735fe2f
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/LoggingExtensions.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Framework.Logging.Internal;
+
+namespace Microsoft.Framework.Logging
+{
+ ///
+ /// Helpful extension methods on ILogger.
+ ///
+ internal static class LoggingExtensions
+ {
+ ///
+ /// Returns a value stating whether the 'debug' log level is enabled.
+ /// Returns false if the logger instance is null.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsDebugLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Debug);
+ }
+
+ ///
+ /// Returns a value stating whether the 'error' log level is enabled.
+ /// Returns false if the logger instance is null.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsErrorLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Error);
+ }
+
+ ///
+ /// Returns a value stating whether the 'information' log level is enabled.
+ /// Returns false if the logger instance is null.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsInformationLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Information);
+ }
+
+ ///
+ /// Returns a value stating whether the 'verbose' log level is enabled.
+ /// Returns false if the logger instance is null.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsVerboseLevelEnabled(this ILogger logger)
+ {
+ return IsLogLevelEnabledCore(logger, LogLevel.Verbose);
+ }
+
+ ///
+ /// Returns a value stating whether the 'warning' log level is enabled.
+ /// Returns false if the logger instance is null.
+ ///
+ [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 LogDebug(this ILogger logger, Exception error, string message, params object[] args)
+ {
+ logger.LogDebug(new FormattedLogValues(message, args), error);
+ }
+
+ public static void LogError(this ILogger logger, Exception error, string message, params object[] args)
+ {
+ logger.LogError(new FormattedLogValues(message, args), error);
+ }
+
+ public static void LogWarning(this ILogger logger, Exception error, string message, params object[] args)
+ {
+ logger.LogWarning(new FormattedLogValues(message, args), error);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs b/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs
new file mode 100644
index 0000000000..4b9f05ec59
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/LoggingServiceProviderExtensions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Logging;
+
+namespace System
+{
+ ///
+ /// Helpful extension methods on IServiceProvider.
+ ///
+ internal static class LoggingServiceProviderExtensions
+ {
+ ///
+ /// Retrieves an instance of ILogger given the type name of the caller.
+ /// The caller's type name is used as the name of the ILogger created.
+ /// This method returns null if the IServiceProvider is null or if it
+ /// does not contain a registered ILoggerFactory.
+ ///
+ public static ILogger GetLogger(this IServiceProvider services)
+ {
+ return services?.GetService()?.CreateLogger();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs
index 687f5002a6..5e67f3ac07 100644
--- a/src/Microsoft.AspNet.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs
@@ -26,13 +26,6 @@ namespace Microsoft.AspNet.DataProtection.Managed
// probability of collision, and this is acceptable for the expected KDK lifetime.
private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
- // Our analysis re: IV collision resistance only holds if we're working with block ciphers
- // with a block length of 64 bits or greater.
- internal const int SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES = 64 / 8;
-
- // Min security bar: authentication tag must have at least 128 bits of output.
- internal const int HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES = 128 / 8;
-
private static readonly Func _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
private readonly byte[] _contextHeader;
@@ -47,9 +40,6 @@ namespace Microsoft.AspNet.DataProtection.Managed
public ManagedAuthenticatedEncryptor(Secret keyDerivationKey, Func symmetricAlgorithmFactory, int symmetricAlgorithmKeySizeInBytes, Func validationAlgorithmFactory, IManagedGenRandom genRandom = null)
{
- CryptoUtil.Assert(KEY_MODIFIER_SIZE_IN_BYTES <= symmetricAlgorithmKeySizeInBytes && symmetricAlgorithmKeySizeInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "KEY_MODIFIER_SIZE_IN_BYTES <= symmetricAlgorithmKeySizeInBytes && symmetricAlgorithmKeySizeInBytes <= Constants.MAX_STACKALLOC_BYTES");
-
_genRandom = genRandom ?? ManagedGenRandomImpl.Instance;
_keyDerivationKey = keyDerivationKey;
@@ -69,14 +59,10 @@ namespace Microsoft.AspNet.DataProtection.Managed
_validationAlgorithmSubkeyLengthInBytes = _validationAlgorithmDigestLengthInBytes; // for simplicity we'll generate MAC subkeys with a length equal to the digest length
}
- CryptoUtil.Assert(SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES <= _symmetricAlgorithmBlockSizeInBytes && _symmetricAlgorithmBlockSizeInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "SYMMETRIC_ALG_MIN_BLOCK_SIZE_IN_BYTES <= _symmetricAlgorithmBlockSizeInBytes && _symmetricAlgorithmBlockSizeInBytes <= Constants.MAX_STACKALLOC_BYTES");
-
- CryptoUtil.Assert(HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES <= _validationAlgorithmDigestLengthInBytes,
- "HASH_ALG_MIN_DIGEST_LENGTH_IN_BYTES <= _validationAlgorithmDigestLengthInBytes");
-
- CryptoUtil.Assert(KEY_MODIFIER_SIZE_IN_BYTES <= _validationAlgorithmSubkeyLengthInBytes && _validationAlgorithmSubkeyLengthInBytes <= Constants.MAX_STACKALLOC_BYTES,
- "KEY_MODIFIER_SIZE_IN_BYTES <= _validationAlgorithmSubkeyLengthInBytes && _validationAlgorithmSubkeyLengthInBytes <= Constants.MAX_STACKALLOC_BYTES");
+ // 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();
}
diff --git a/src/Microsoft.AspNet.DataProtection/MemoryProtection.cs b/src/Microsoft.AspNet.DataProtection/MemoryProtection.cs
index 2be0be5db0..6171796765 100644
--- a/src/Microsoft.AspNet.DataProtection/MemoryProtection.cs
+++ b/src/Microsoft.AspNet.DataProtection/MemoryProtection.cs
@@ -8,7 +8,7 @@ using Microsoft.AspNet.Cryptography;
namespace Microsoft.AspNet.DataProtection
{
///
- /// Support for generating random data.
+ /// Wrappers around CryptProtectMemory / CryptUnprotectMemory.
///
internal unsafe static class MemoryProtection
{
diff --git a/src/Microsoft.AspNet.DataProtection/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.DataProtection/Properties/AssemblyInfo.cs
index c262afe4c7..68aea95cb4 100644
--- a/src/Microsoft.AspNet.DataProtection/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNet.DataProtection/Properties/AssemblyInfo.cs
@@ -6,3 +6,4 @@ using System.Runtime.CompilerServices;
// for unit testing
[assembly: InternalsVisibleTo("Microsoft.AspNet.DataProtection.Test")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs
index 563030c9b4..fad1928f16 100644
--- a/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.DataProtection/Properties/Resources.Designer.cs
@@ -107,7 +107,7 @@ namespace Microsoft.AspNet.DataProtection
}
///
- /// The key {0:B} was not found in the keyring.
+ /// The key '{0:D}' was not found in the key ring.
///
internal static string Common_KeyNotFound
{
@@ -115,7 +115,7 @@ namespace Microsoft.AspNet.DataProtection
}
///
- /// The key {0:B} was not found in the keyring.
+ /// The key '{0:D}' was not found in the key ring.
///
internal static string FormatCommon_KeyNotFound()
{
@@ -123,7 +123,7 @@ namespace Microsoft.AspNet.DataProtection
}
///
- /// The key {0:B} has been revoked.
+ /// The key '{0:D}' has been revoked.
///
internal static string Common_KeyRevoked
{
@@ -131,7 +131,7 @@ namespace Microsoft.AspNet.DataProtection
}
///
- /// The key {0:B} has been revoked.
+ /// The key '{0:D}' has been revoked.
///
internal static string FormatCommon_KeyRevoked()
{
@@ -139,35 +139,35 @@ namespace Microsoft.AspNet.DataProtection
}
///
- /// The provided payload was not protected with this protection provider.
+ /// The provided payload cannot be decrypted because it was not protected with this protection provider.
///
- internal static string Common_NotAValidProtectedPayload
+ internal static string ProtectionProvider_BadMagicHeader
{
- get { return GetString("Common_NotAValidProtectedPayload"); }
+ get { return GetString("ProtectionProvider_BadMagicHeader"); }
}
///
- /// The provided payload was not protected with this protection provider.
+ /// The provided payload cannot be decrypted because it was not protected with this protection provider.
///
- internal static string FormatCommon_NotAValidProtectedPayload()
+ internal static string FormatProtectionProvider_BadMagicHeader()
{
- return GetString("Common_NotAValidProtectedPayload");
+ return GetString("ProtectionProvider_BadMagicHeader");
}
///
- /// The protected payload cannot be decrypted because it was protected with a newer version of the protection provider.
+ /// The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
///
- internal static string Common_PayloadProducedByNewerVersion
+ internal static string ProtectionProvider_BadVersion
{
- get { return GetString("Common_PayloadProducedByNewerVersion"); }
+ get { return GetString("ProtectionProvider_BadVersion"); }
}
///
- /// The protected payload cannot be decrypted because it was protected with a newer version of the protection provider.
+ /// The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
///
- internal static string FormatCommon_PayloadProducedByNewerVersion()
+ internal static string FormatProtectionProvider_BadVersion()
{
- return GetString("Common_PayloadProducedByNewerVersion");
+ return GetString("ProtectionProvider_BadVersion");
}
///
@@ -187,19 +187,195 @@ namespace Microsoft.AspNet.DataProtection
}
///
- /// The purposes array cannot be null or empty and cannot contain null or empty elements.
+ /// Value must be non-negative.
///
- internal static string DataProtectionExtensions_NullPurposesArray
+ internal static string Common_ValueMustBeNonNegative
{
- get { return GetString("DataProtectionExtensions_NullPurposesArray"); }
+ get { return GetString("Common_ValueMustBeNonNegative"); }
}
///
- /// The purposes array cannot be null or empty and cannot contain null or empty elements.
+ /// Value must be non-negative.
///
- internal static string FormatDataProtectionExtensions_NullPurposesArray()
+ internal static string FormatCommon_ValueMustBeNonNegative()
{
- return GetString("DataProtectionExtensions_NullPurposesArray");
+ return GetString("Common_ValueMustBeNonNegative");
+ }
+
+ ///
+ /// The type '{1}' is not assignable to '{0}'.
+ ///
+ internal static string TypeExtensions_BadCast
+ {
+ get { return GetString("TypeExtensions_BadCast"); }
+ }
+
+ ///
+ /// The type '{1}' is not assignable to '{0}'.
+ ///
+ internal static string FormatTypeExtensions_BadCast(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TypeExtensions_BadCast"), p0, p1);
+ }
+
+ ///
+ /// The default new key lifetime must be at least one week.
+ ///
+ internal static string KeyLifetimeOptions_MinNewKeyLifetimeViolated
+ {
+ get { return GetString("KeyLifetimeOptions_MinNewKeyLifetimeViolated"); }
+ }
+
+ ///
+ /// The default new key lifetime must be at least one week.
+ ///
+ internal static string FormatKeyLifetimeOptions_MinNewKeyLifetimeViolated()
+ {
+ return GetString("KeyLifetimeOptions_MinNewKeyLifetimeViolated");
+ }
+
+ ///
+ /// The key '{0:D}' already exists in the keyring.
+ ///
+ internal static string XmlKeyManager_DuplicateKey
+ {
+ get { return GetString("XmlKeyManager_DuplicateKey"); }
+ }
+
+ ///
+ /// The key '{0:D}' already exists in the keyring.
+ ///
+ internal static string FormatXmlKeyManager_DuplicateKey()
+ {
+ return GetString("XmlKeyManager_DuplicateKey");
+ }
+
+ ///
+ /// Argument cannot be null or empty.
+ ///
+ internal static string Common_ArgumentCannotBeNullOrEmpty
+ {
+ get { return GetString("Common_ArgumentCannotBeNullOrEmpty"); }
+ }
+
+ ///
+ /// Argument cannot be null or empty.
+ ///
+ internal static string FormatCommon_ArgumentCannotBeNullOrEmpty()
+ {
+ return GetString("Common_ArgumentCannotBeNullOrEmpty");
+ }
+
+ ///
+ /// Property {0} must have a non-negative value.
+ ///
+ internal static string Common_PropertyMustBeNonNegative
+ {
+ get { return GetString("Common_PropertyMustBeNonNegative"); }
+ }
+
+ ///
+ /// Property {0} must have a non-negative value.
+ ///
+ internal static string FormatCommon_PropertyMustBeNonNegative(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyMustBeNonNegative"), p0);
+ }
+
+ ///
+ /// GCM algorithms require the Windows platform.
+ ///
+ internal static string Platform_WindowsRequiredForGcm
+ {
+ get { return GetString("Platform_WindowsRequiredForGcm"); }
+ }
+
+ ///
+ /// GCM algorithms require the Windows platform.
+ ///
+ internal static string FormatPlatform_WindowsRequiredForGcm()
+ {
+ return GetString("Platform_WindowsRequiredForGcm");
+ }
+
+ ///
+ /// A certificate with the thumbprint '{0}' could not be found.
+ ///
+ internal static string CertificateXmlEncryptor_CertificateNotFound
+ {
+ get { return GetString("CertificateXmlEncryptor_CertificateNotFound"); }
+ }
+
+ ///
+ /// A certificate with the thumbprint '{0}' could not be found.
+ ///
+ internal static string FormatCertificateXmlEncryptor_CertificateNotFound(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("CertificateXmlEncryptor_CertificateNotFound"), p0);
+ }
+
+ ///
+ /// Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
+ ///
+ internal static string EncryptedXmlDecryptor_DoesNotWorkOnCoreClr
+ {
+ get { return GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr"); }
+ }
+
+ ///
+ /// Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
+ ///
+ internal static string FormatEncryptedXmlDecryptor_DoesNotWorkOnCoreClr()
+ {
+ return GetString("EncryptedXmlDecryptor_DoesNotWorkOnCoreClr");
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string AlgorithmAssert_BadBlockSize
+ {
+ get { return GetString("AlgorithmAssert_BadBlockSize"); }
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string FormatAlgorithmAssert_BadBlockSize(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadBlockSize"), p0);
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string AlgorithmAssert_BadDigestSize
+ {
+ get { return GetString("AlgorithmAssert_BadDigestSize"); }
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string FormatAlgorithmAssert_BadDigestSize(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadDigestSize"), p0);
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string AlgorithmAssert_BadKeySize
+ {
+ get { return GetString("AlgorithmAssert_BadKeySize"); }
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static string FormatAlgorithmAssert_BadKeySize(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AlgorithmAssert_BadKeySize"), p0);
}
private static string GetString(string name, params string[] formatterNames)
diff --git a/src/Microsoft.AspNet.DataProtection/RegistryPolicyResolver.cs b/src/Microsoft.AspNet.DataProtection/RegistryPolicyResolver.cs
new file mode 100644
index 0000000000..c4b2bfb703
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/RegistryPolicyResolver.cs
@@ -0,0 +1,149 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Win32;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// A type which allows reading policy from the system registry.
+ ///
+ internal sealed class RegistryPolicyResolver
+ {
+ private readonly RegistryKey _policyRegKey;
+
+ internal RegistryPolicyResolver(RegistryKey policyRegKey)
+ {
+ _policyRegKey = policyRegKey;
+ }
+
+ // 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)))
+ {
+ object 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 ReadKeyEscrowSinks(RegistryKey key)
+ {
+ List sinks = new List();
+
+ // The format of this key is "type1; type2; ...".
+ // We call Type.GetType to perform an eager check that the type exists.
+ string sinksFromRegistry = (string)key.GetValue("KeyEscrowSinks");
+ if (sinksFromRegistry != null)
+ {
+ foreach (string sinkFromRegistry in sinksFromRegistry.Split(';'))
+ {
+ string candidate = sinkFromRegistry.Trim();
+ if (!String.IsNullOrEmpty(candidate))
+ {
+ typeof(IKeyEscrowSink).AssertIsAssignableFrom(Type.GetType(candidate, throwOnError: true));
+ sinks.Add(candidate);
+ }
+ }
+ }
+
+ return sinks;
+ }
+
+ ///
+ /// Returns a object from the default registry location.
+ ///
+ public static ServiceDescriptor[] ResolveDefaultPolicy()
+ {
+ RegistryKey subKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\DotNetPackages\Microsoft.AspNet.DataProtection");
+ if (subKey != null)
+ {
+ using (subKey)
+ {
+ return new RegistryPolicyResolver(subKey).ResolvePolicy();
+ }
+ }
+ else
+ {
+ return new ServiceDescriptor[0];
+ }
+ }
+
+ internal ServiceDescriptor[] ResolvePolicy()
+ {
+ return ResolvePolicyCore().ToArray(); // fully evaluate enumeration while the reg key is open
+ }
+
+ private IEnumerable ResolvePolicyCore()
+ {
+ // Read the encryption options type: CNG-CBC, CNG-GCM, Managed
+ IInternalAuthenticatedEncryptionOptions options = null;
+ string encryptionType = (string)_policyRegKey.GetValue("EncryptionType");
+ if (String.Equals(encryptionType, "CNG-CBC", StringComparison.OrdinalIgnoreCase))
+ {
+ options = new CngCbcAuthenticatedEncryptionOptions();
+ }
+ else if (String.Equals(encryptionType, "CNG-GCM", StringComparison.OrdinalIgnoreCase))
+ {
+ options = new CngGcmAuthenticatedEncryptionOptions();
+ }
+ else if (String.Equals(encryptionType, "Managed", StringComparison.OrdinalIgnoreCase))
+ {
+ options = new ManagedAuthenticatedEncryptionOptions();
+ }
+ else if (!String.IsNullOrEmpty(encryptionType))
+ {
+ throw CryptoUtil.Fail("Unrecognized EncryptionType: " + encryptionType);
+ }
+ if (options != null)
+ {
+ PopulateOptions(options, _policyRegKey);
+ yield return DataProtectionServiceDescriptors.IAuthenticatedEncryptorConfiguration_FromOptions(options);
+ }
+
+ // Read ancillary data
+
+ int? defaultKeyLifetime = (int?)_policyRegKey.GetValue("DefaultKeyLifetime");
+ if (defaultKeyLifetime.HasValue)
+ {
+ yield return DataProtectionServiceDescriptors.ConfigureOptions_DefaultKeyLifetime(defaultKeyLifetime.Value);
+ }
+
+ var keyEscrowSinks = ReadKeyEscrowSinks(_policyRegKey);
+ foreach (var keyEscrowSink in keyEscrowSinks)
+ {
+ yield return DataProtectionServiceDescriptors.IKeyEscrowSink_FromTypeName(keyEscrowSink);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/Repositories/EphemeralXmlRepository.cs b/src/Microsoft.AspNet.DataProtection/Repositories/EphemeralXmlRepository.cs
new file mode 100644
index 0000000000..46e993e979
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/Repositories/EphemeralXmlRepository.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
+
+namespace Microsoft.AspNet.DataProtection.Repositories
+{
+ ///
+ /// 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.
+ ///
+ internal class EphemeralXmlRepository : IXmlRepository
+ {
+ private readonly List _storedElements = new List();
+
+ public EphemeralXmlRepository(IServiceProvider services)
+ {
+ var logger = services?.GetLogger();
+ if (logger.IsWarningLevelEnabled())
+ {
+ logger.LogWarning("Using an in-memory repository. Keys will not be persisted to storage.");
+ }
+ }
+
+ public virtual IReadOnlyCollection GetAllElements()
+ {
+ // force complete enumeration under lock to avoid races
+ lock (_storedElements)
+ {
+ return GetAllElementsCore().ToList().AsReadOnly();
+ }
+ }
+
+ private IEnumerable 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([NotNull] XElement element, string friendlyName)
+ {
+ XElement cloned = new XElement(element); // makes a deep copy so caller doesn't inadvertently modify it
+
+ // under lock to avoid races
+ lock (_storedElements)
+ {
+ _storedElements.Add(cloned);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/Repositories/FileSystemXmlRepository.cs b/src/Microsoft.AspNet.DataProtection/Repositories/FileSystemXmlRepository.cs
index a5e219e50b..52c1718aa9 100644
--- a/src/Microsoft.AspNet.DataProtection/Repositories/FileSystemXmlRepository.cs
+++ b/src/Microsoft.AspNet.DataProtection/Repositories/FileSystemXmlRepository.cs
@@ -3,10 +3,11 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.Repositories
{
@@ -15,76 +16,190 @@ namespace Microsoft.AspNet.DataProtection.Repositories
///
public class FileSystemXmlRepository : IXmlRepository
{
+ private static readonly Lazy _defaultDirectoryLazy = new Lazy(GetDefaultKeyStorageDirectory);
+
+ private readonly ILogger _logger;
+
+ ///
+ /// Creates a with keys stored at the given directory.
+ ///
+ /// The directory in which to persist key material.
public FileSystemXmlRepository([NotNull] DirectoryInfo directory)
+ : this(directory, services: null)
{
- Directory = directory;
}
- protected DirectoryInfo Directory
+ ///
+ /// Creates a with keys stored at the given directory.
+ ///
+ /// The directory in which to persist key material.
+ /// An optional to provide ancillary services.
+ public FileSystemXmlRepository([NotNull] DirectoryInfo directory, IServiceProvider services)
{
- get;
- private set;
+ Directory = directory;
+ Services = services;
+ _logger = services?.GetLogger();
+ }
+
+ ///
+ /// The default key storage directory, which currently corresponds to
+ /// "%LOCALAPPDATA%\ASP.NET\DataProtection-Keys".
+ ///
+ ///
+ /// This property can return null if no suitable default key storage directory can
+ /// be found, such as the case when the user profile is unavailable.
+ ///
+ public static DirectoryInfo DefaultKeyStorageDirectory => _defaultDirectoryLazy.Value;
+
+ ///
+ /// The directory into which key material will be written.
+ ///
+ public DirectoryInfo Directory { get; }
+
+ ///
+ /// The provided to the constructor.
+ ///
+ protected IServiceProvider Services { get; }
+
+ private static DirectoryInfo GetKeyStorageDirectoryFromBaseAppDataPath(string basePath)
+ {
+ return new DirectoryInfo(Path.Combine(basePath, "ASP.NET", "DataProtection-Keys"));
}
public virtual IReadOnlyCollection GetAllElements()
{
// forces complete enumeration
- return GetAllElementsImpl().ToArray();
+ return GetAllElementsCore().ToList().AsReadOnly();
}
- private IEnumerable GetAllElementsImpl()
+ private IEnumerable GetAllElementsCore()
{
Directory.Create(); // won't throw if the directory already exists
- // Find all files matching the pattern "{guid}.xml"
+ // 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))
{
- string simpleFilename = fileSystemInfo.Name;
- if (simpleFilename.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
- {
- simpleFilename = simpleFilename.Substring(0, simpleFilename.Length - ".xml".Length);
- }
+ yield return ReadElementFromFile(fileSystemInfo.FullName);
+ }
+ }
- Guid unused;
- if (Guid.TryParseExact(simpleFilename, "D" /* registry format */, out unused))
- {
- XDocument document;
- using (var fileStream = File.OpenRead(fileSystemInfo.FullName))
- {
- document = XDocument.Load(fileStream);
- }
+ private static DirectoryInfo GetDefaultKeyStorageDirectory()
+ {
+#if !DNXCORE50
+ // Environment.GetFolderPath returns null if the user profile isn't loaded.
+ string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ if (!String.IsNullOrEmpty(folderPath))
+ {
+ return GetKeyStorageDirectoryFromBaseAppDataPath(folderPath);
+ }
+ else
+ {
+ return null;
+ }
+#else
+ // On core CLR, we need to fall back to environment variables.
+ string folderPath = Environment.GetEnvironmentVariable("LOCALAPPDATA")
+ ?? Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "Local");
- // 'yield return' outside the preceding 'using' block so we don't hold files open longer than necessary
- yield return document.Root;
+ DirectoryInfo retVal = GetKeyStorageDirectoryFromBaseAppDataPath(folderPath);
+ try
+ {
+ retVal.Create(); // throws if we don't have access, e.g., user profile not loaded
+ return retVal;
+ }
+ catch
+ {
+ return null;
+ }
+#endif
+ }
+
+ internal static DirectoryInfo GetKeyStorageDirectoryForAzureWebSites()
+ {
+ // Azure Web Sites needs to be treated specially, as we need to store the keys in a
+ // correct persisted location. We use the existence of the %WEBSITE_INSTANCE_ID% env
+ // variable to determine if we're running in this environment, and if so we then use
+ // the %HOME% variable to build up our base key storage path.
+ if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")))
+ {
+ string homeEnvVar = Environment.GetEnvironmentVariable("HOME");
+ if (!String.IsNullOrEmpty(homeEnvVar))
+ {
+ return GetKeyStorageDirectoryFromBaseAppDataPath(homeEnvVar);
}
}
+
+ // nope
+ return null;
+ }
+
+ private static bool IsSafeFilename(string filename)
+ {
+ // Must be non-empty and contain only a-zA-Z0-9, hyphen, and underscore.
+ 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)
+ {
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Reading data from file '{0}'.", fullPath);
+ }
+
+ using (var fileStream = File.OpenRead(fullPath))
+ {
+ return XElement.Load(fileStream);
+ }
}
public virtual void StoreElement([NotNull] XElement element, string friendlyName)
{
- // We're going to ignore the friendly name for now and just use a GUID.
- StoreElement(element, Guid.NewGuid());
+ if (!IsSafeFilename(friendlyName))
+ {
+ string newFriendlyName = Guid.NewGuid().ToString();
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("The name '{0}' is not a safe file name, using '{1}' instead.", friendlyName, newFriendlyName);
+ }
+ friendlyName = newFriendlyName;
+ }
+
+ StoreElementCore(element, friendlyName);
}
- private void StoreElement(XElement element, Guid id)
+ 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
- string tempFilename = Path.Combine(Directory.FullName, String.Format(CultureInfo.InvariantCulture, "{0:D}.tmp", id));
- string finalFilename = Path.Combine(Directory.FullName, String.Format(CultureInfo.InvariantCulture, "{0:D}.xml", id));
+ string tempFilename = Path.Combine(Directory.FullName, Guid.NewGuid().ToString() + ".tmp");
+ string finalFilename = Path.Combine(Directory.FullName, filename + ".xml");
try
{
using (var tempFileStream = File.OpenWrite(tempFilename))
{
- new XDocument(element).Save(tempFileStream);
+ element.Save(tempFileStream);
}
// Once the file has been fully written, perform the rename.
// Renames are atomic operations on the file systems we support.
+ if (_logger.IsInformationLevelEnabled())
+ {
+ _logger.LogInformation("Writing data to file '{0}.", finalFilename);
+ }
File.Move(tempFilename, finalFilename);
}
finally
diff --git a/src/Microsoft.AspNet.DataProtection/Repositories/IXmlRepository.cs b/src/Microsoft.AspNet.DataProtection/Repositories/IXmlRepository.cs
index e5e649594c..b17b395407 100644
--- a/src/Microsoft.AspNet.DataProtection/Repositories/IXmlRepository.cs
+++ b/src/Microsoft.AspNet.DataProtection/Repositories/IXmlRepository.cs
@@ -28,6 +28,10 @@ namespace Microsoft.AspNet.DataProtection.Repositories
/// 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.
+ ///
+ /// The 'friendlyName' parameter must be unique if specified. For instance, it could
+ /// be the id of the key being stored.
+ ///
void StoreElement(XElement element, string friendlyName);
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/Repositories/RegistryXmlRepository.cs b/src/Microsoft.AspNet.DataProtection/Repositories/RegistryXmlRepository.cs
index a25fc6a3d4..bc42ef4a23 100644
--- a/src/Microsoft.AspNet.DataProtection/Repositories/RegistryXmlRepository.cs
+++ b/src/Microsoft.AspNet.DataProtection/Repositories/RegistryXmlRepository.cs
@@ -4,10 +4,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
using Microsoft.Win32;
namespace Microsoft.AspNet.DataProtection.Repositories
@@ -17,70 +18,96 @@ namespace Microsoft.AspNet.DataProtection.Repositories
///
public class RegistryXmlRepository : IXmlRepository
{
+ private static readonly Lazy _defaultRegistryKeyLazy = new Lazy(GetDefaultHklmStorageKey);
+
+ private readonly ILogger _logger;
+
+ ///
+ /// Creates a with keys stored in the given registry key.
+ ///
+ /// The registry key in which to persist key material.
public RegistryXmlRepository([NotNull] RegistryKey registryKey)
+ : this(registryKey, services: null)
{
- RegistryKey = registryKey;
}
- protected RegistryKey RegistryKey
+ ///
+ /// Creates a with keys stored in the given registry key.
+ ///
+ /// The registry key in which to persist key material.
+ public RegistryXmlRepository([NotNull] RegistryKey registryKey, IServiceProvider services)
{
- get;
- private set;
+ RegistryKey = registryKey;
+ Services = services;
+ _logger = services?.GetLogger();
}
+ ///
+ /// The default key storage directory, which currently corresponds to
+ /// "HKLM\SOFTWARE\Microsoft\ASP.NET\4.0.30319.0\AutoGenKeys\{SID}".
+ ///
+ ///
+ /// 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.
+ ///
+ public static RegistryKey DefaultRegistryKey => _defaultRegistryKeyLazy.Value;
+
+ ///
+ /// The registry key into which key material will be written.
+ ///
+ public RegistryKey RegistryKey { get; }
+
+ ///
+ /// The provided to the constructor.
+ ///
+ protected IServiceProvider Services { get; }
+
public virtual IReadOnlyCollection GetAllElements()
{
// forces complete enumeration
- return GetAllElementsImpl().ToArray();
+ return GetAllElementsCore().ToList().AsReadOnly();
}
- private IEnumerable GetAllElementsImpl()
+ private IEnumerable GetAllElementsCore()
{
- string[] allValueNames = RegistryKey.GetValueNames();
- foreach (var valueName in allValueNames)
- {
- string thisValue = RegistryKey.GetValue(valueName) as string;
- if (!String.IsNullOrEmpty(thisValue))
- {
- XDocument document;
- using (var textReader = new StringReader(thisValue))
- {
- document = XDocument.Load(textReader);
- }
+ // 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.
- // 'yield return' outside the preceding 'using' block so we can release the reader
- yield return document.Root;
+ foreach (string valueName in RegistryKey.GetValueNames())
+ {
+ XElement element = ReadElementFromRegKey(RegistryKey, valueName);
+ if (element != null)
+ {
+ yield return element;
}
}
}
- internal static RegistryXmlRepository GetDefaultRepositoryForHKLMRegistry()
+ private static RegistryKey GetDefaultHklmStorageKey()
{
try
{
// Try reading the auto-generated machine key from HKLM
using (var hklmBaseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
{
- // TODO: Do we need to change the version number below?
+ // 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.
string 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)
+ if (aspnetBaseKey != null)
{
- return null; // couldn't find the auto-generated machine key
- }
-
- using (aspnetBaseKey) {
- // TODO: Remove the ".BETA" moniker.
- var dataProtectionKey = aspnetBaseKey.OpenSubKey("DataProtection.BETA6", writable: true);
- if (dataProtectionKey == null)
+ using (aspnetBaseKey)
{
- // TODO: Remove the ".BETA" moniker from here, also.
- dataProtectionKey = aspnetBaseKey.CreateSubKey("DataProtection.BETA6");
+ // We'll create a 'DataProtection' subkey under the auto-gen keys base
+ return aspnetBaseKey.OpenSubKey("DataProtection", writable: true)
+ ?? aspnetBaseKey.CreateSubKey("DataProtection");
}
-
- // Once we've opened the HKLM reg key, return a repository which wraps it.
- return new RegistryXmlRepository(dataProtectionKey);
}
+ return null; // couldn't find the auto-generated machine key
}
}
catch
@@ -90,28 +117,50 @@ namespace Microsoft.AspNet.DataProtection.Repositories
}
}
- public virtual void StoreElement([NotNull] XElement element, string friendlyName)
+ private static bool IsSafeRegistryValueName(string filename)
{
- // We're going to ignore the friendly name for now and just use a GUID.
- StoreElement(element, Guid.NewGuid());
+ // 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 void StoreElement(XElement element, Guid id)
+ private XElement ReadElementFromRegKey(RegistryKey regKey, string valueName)
{
- // First, serialize the XElement to a string.
- string serializedString;
- using (var writer = new StringWriter())
+ if (_logger.IsVerboseLevelEnabled())
{
- new XDocument(element).Save(writer);
- serializedString = writer.ToString();
+ _logger.LogVerbose("Reading data from registry key '{0}', value '{1}'.", regKey.ToString(), valueName);
}
+ string data = regKey.GetValue(valueName) as string;
+ return (!String.IsNullOrEmpty(data)) ? XElement.Parse(data) : null;
+ }
+
+ public virtual void StoreElement([NotNull] XElement element, string friendlyName)
+ {
+ if (!IsSafeRegistryValueName(friendlyName))
+ {
+ string newFriendlyName = Guid.NewGuid().ToString();
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("The name '{0}' is not a safe registry value name, using '{1}' instead.", 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.
- string idAsString = id.ToString("D");
- RegistryKey.SetValue(idAsString, serializedString, RegistryValueKind.String);
+ RegistryKey.SetValue(valueName, element.ToString(), RegistryValueKind.String);
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/Resources.resx b/src/Microsoft.AspNet.DataProtection/Resources.resx
index 3db16f062c..ad1f4512df 100644
--- a/src/Microsoft.AspNet.DataProtection/Resources.resx
+++ b/src/Microsoft.AspNet.DataProtection/Resources.resx
@@ -136,21 +136,54 @@
An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information.
- The key {0:B} was not found in the keyring.
+ The key '{0:D}' was not found in the key ring.
- The key {0:B} has been revoked.
+ The key '{0:D}' has been revoked.
-
- The provided payload was not protected with this protection provider.
+
+ The provided payload cannot be decrypted because it was not protected with this protection provider.
-
- The protected payload cannot be decrypted because it was protected with a newer version of the protection provider.
+
+ The provided payload cannot be decrypted because it was protected with a newer version of the protection provider.
The payload expired at {0}.
-
- The purposes array cannot be null or empty and cannot contain null or empty elements.
+
+ Value must be non-negative.
+
+
+ The type '{1}' is not assignable to '{0}'.
+
+
+ The default new key lifetime must be at least one week.
+
+
+ The key '{0:D}' already exists in the keyring.
+
+
+ Argument cannot be null or empty.
+
+
+ Property {0} must have a non-negative value.
+
+
+ GCM algorithms require the Windows platform.
+
+
+ A certificate with the thumbprint '{0}' could not be found.
+
+
+ Decrypting EncryptedXml-encapsulated payloads is not yet supported on Core CLR.
+
+
+ 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.
+
+
+ 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.
+
+
+ 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.
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs b/src/Microsoft.AspNet.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs
index 9105d95fc3..e93eb7da4a 100644
--- a/src/Microsoft.AspNet.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs
+++ b/src/Microsoft.AspNet.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs
@@ -26,7 +26,7 @@ namespace Microsoft.AspNet.DataProtection.SP800_108
// Creates a provider from the given key.
public static ISP800_108_CTR_HMACSHA512Provider CreateProvider(byte* pbKdk, uint cbKdk)
{
- if (OSVersionUtil.IsBCryptOnWin8OrLaterAvailable())
+ if (OSVersionUtil.IsWindows8OrLater())
{
return new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk);
}
diff --git a/src/Microsoft.AspNet.DataProtection/Secret.cs b/src/Microsoft.AspNet.DataProtection/Secret.cs
index 6f04529c52..991624e6a6 100644
--- a/src/Microsoft.AspNet.DataProtection/Secret.cs
+++ b/src/Microsoft.AspNet.DataProtection/Secret.cs
@@ -6,6 +6,7 @@ using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.Cryptography.Cng;
using Microsoft.AspNet.Cryptography.SafeHandles;
using Microsoft.AspNet.DataProtection.Managed;
+using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.DataProtection
{
@@ -36,7 +37,7 @@ namespace Microsoft.AspNet.DataProtection
/// Creates a new Secret from the provided input value, where the input value
/// is specified as an array.
///
- public Secret(byte[] value)
+ public Secret([NotNull] byte[] value)
: this(new ArraySegment(value))
{
}
@@ -49,11 +50,11 @@ namespace Microsoft.AspNet.DataProtection
{
if (secret == null)
{
- throw new ArgumentNullException("secret");
+ throw new ArgumentNullException(nameof(secret));
}
if (secretLength < 0)
{
- throw new ArgumentOutOfRangeException("secretLength");
+ throw Error.Common_ValueMustBeNonNegative(nameof(secretLength));
}
_localAllocHandle = Protect(secret, (uint)secretLength);
@@ -63,13 +64,8 @@ namespace Microsoft.AspNet.DataProtection
///
/// Creates a new Secret from another secret object.
///
- public Secret(ISecret secret)
+ public Secret([NotNull] ISecret secret)
{
- if (secret == null)
- {
- throw new ArgumentNullException("secret");
- }
-
Secret other = secret as Secret;
if (other != null)
{
@@ -130,7 +126,7 @@ namespace Microsoft.AspNet.DataProtection
// 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.IsBCryptOnWin7OrLaterAvailable())
+ if (!OSVersionUtil.IsWindows())
{
SecureLocalAllocHandle handle = SecureLocalAllocHandle.Allocate((IntPtr)checked((int)cbPlaintext));
UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: handle, byteCount: cbPlaintext);
@@ -165,7 +161,10 @@ namespace Microsoft.AspNet.DataProtection
///
public static Secret Random(int numBytes)
{
- CryptoUtil.Assert(numBytes >= 0, "numBytes >= 0");
+ if (numBytes < 0)
+ {
+ throw Error.Common_ValueMustBeNonNegative(nameof(numBytes));
+ }
if (numBytes == 0)
{
@@ -175,7 +174,7 @@ namespace Microsoft.AspNet.DataProtection
else
{
// Don't use CNG if we're not on Windows.
- if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
+ if (!OSVersionUtil.IsWindows())
{
return new Secret(ManagedGenRandomImpl.Instance.GenRandom(numBytes));
}
@@ -200,7 +199,7 @@ namespace Microsoft.AspNet.DataProtection
{
// If we're not running on a platform that supports CryptProtectMemory,
// the handle contains plaintext bytes.
- if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
+ if (!OSVersionUtil.IsWindows())
{
UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
return;
@@ -209,7 +208,6 @@ namespace Microsoft.AspNet.DataProtection
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.
- // We go through this code path even for empty plaintexts since we still want SafeHandle dispose semantics.
UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength);
}
@@ -237,7 +235,7 @@ namespace Microsoft.AspNet.DataProtection
buffer.Validate();
if (buffer.Count != Length)
{
- throw Error.Common_BufferIncorrectlySized("buffer", actualSize: buffer.Count, expectedSize: 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
@@ -253,6 +251,8 @@ namespace Microsoft.AspNet.DataProtection
///
/// Writes the secret value to the specified buffer.
///
+ /// The buffer into which to write the secret value.
+ /// The size (in bytes) of the provided buffer.
///
/// The 'bufferLength' parameter must exactly match the length of the secret value.
///
@@ -260,18 +260,17 @@ namespace Microsoft.AspNet.DataProtection
{
if (buffer == null)
{
- throw new ArgumentNullException("buffer");
- }
- if (bufferLength < 0)
- {
- throw new ArgumentOutOfRangeException("bufferLength");
+ throw new ArgumentNullException(nameof(buffer));
}
if (bufferLength != Length)
{
- throw Error.Common_BufferIncorrectlySized("bufferLength", actualSize: bufferLength, expectedSize: Length);
+ throw Error.Common_BufferIncorrectlySized(nameof(bufferLength), actualSize: bufferLength, expectedSize: Length);
}
- UnprotectInto(buffer);
+ if (Length != 0)
+ {
+ UnprotectInto(buffer);
+ }
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs b/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs
index a1c4ef1454..a9033d4c25 100644
--- a/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs
+++ b/src/Microsoft.AspNet.DataProtection/TimeLimitedDataProtector.cs
@@ -2,8 +2,8 @@
// 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.AspNet.Cryptography;
+using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.DataProtection
{
@@ -27,28 +27,28 @@ namespace Microsoft.AspNet.DataProtection
return new TimeLimitedDataProtector(InnerProtector.CreateProtector(purpose));
}
- public byte[] Protect([NotNull] byte[] unprotectedData)
+ public byte[] Protect([NotNull] byte[] plaintext)
{
- return Protect(unprotectedData, DateTimeOffset.MaxValue);
+ return Protect(plaintext, DateTimeOffset.MaxValue);
}
- public byte[] Protect([NotNull] byte[] unprotectedData, DateTimeOffset expiration)
+ public byte[] Protect([NotNull] byte[] plaintext, DateTimeOffset expiration)
{
// We prepend the expiration time (as a big-endian 64-bit UTC tick count) to the unprotected data.
ulong utcTicksExpiration = (ulong)expiration.UtcTicks;
- byte[] unprotectedDataWithHeader = new byte[checked(8 + unprotectedData.Length)];
- unprotectedDataWithHeader[0] = (byte)(utcTicksExpiration >> 56);
- unprotectedDataWithHeader[1] = (byte)(utcTicksExpiration >> 48);
- unprotectedDataWithHeader[2] = (byte)(utcTicksExpiration >> 40);
- unprotectedDataWithHeader[3] = (byte)(utcTicksExpiration >> 32);
- unprotectedDataWithHeader[4] = (byte)(utcTicksExpiration >> 24);
- unprotectedDataWithHeader[5] = (byte)(utcTicksExpiration >> 16);
- unprotectedDataWithHeader[6] = (byte)(utcTicksExpiration >> 8);
- unprotectedDataWithHeader[7] = (byte)(utcTicksExpiration);
- Buffer.BlockCopy(unprotectedData, 0, unprotectedDataWithHeader, 8, unprotectedData.Length);
+ byte[] plaintextWithHeader = new byte[checked(8 + plaintext.Length)];
+ plaintextWithHeader[0] = (byte)(utcTicksExpiration >> 56);
+ plaintextWithHeader[1] = (byte)(utcTicksExpiration >> 48);
+ plaintextWithHeader[2] = (byte)(utcTicksExpiration >> 40);
+ plaintextWithHeader[3] = (byte)(utcTicksExpiration >> 32);
+ plaintextWithHeader[4] = (byte)(utcTicksExpiration >> 24);
+ plaintextWithHeader[5] = (byte)(utcTicksExpiration >> 16);
+ plaintextWithHeader[6] = (byte)(utcTicksExpiration >> 8);
+ plaintextWithHeader[7] = (byte)(utcTicksExpiration);
+ Buffer.BlockCopy(plaintext, 0, plaintextWithHeader, 8, plaintext.Length);
- return InnerProtector.Protect(unprotectedDataWithHeader);
+ return InnerProtector.Protect(plaintextWithHeader);
}
public byte[] Unprotect([NotNull] byte[] protectedData)
@@ -61,18 +61,18 @@ namespace Microsoft.AspNet.DataProtection
{
try
{
- byte[] unprotectedDataWithHeader = InnerProtector.Unprotect(protectedData);
- CryptoUtil.Assert(unprotectedDataWithHeader.Length >= 8, "No header present.");
+ byte[] plaintextWithHeader = InnerProtector.Unprotect(protectedData);
+ CryptoUtil.Assert(plaintextWithHeader.Length >= 8, "No header present.");
// Read expiration time back out of the payload
- ulong utcTicksExpiration = (((ulong)unprotectedDataWithHeader[0]) << 56)
- | (((ulong)unprotectedDataWithHeader[1]) << 48)
- | (((ulong)unprotectedDataWithHeader[2]) << 40)
- | (((ulong)unprotectedDataWithHeader[3]) << 32)
- | (((ulong)unprotectedDataWithHeader[4]) << 24)
- | (((ulong)unprotectedDataWithHeader[5]) << 16)
- | (((ulong)unprotectedDataWithHeader[6]) << 8)
- | (ulong)unprotectedDataWithHeader[7];
+ ulong utcTicksExpiration = (((ulong)plaintextWithHeader[0]) << 56)
+ | (((ulong)plaintextWithHeader[1]) << 48)
+ | (((ulong)plaintextWithHeader[2]) << 40)
+ | (((ulong)plaintextWithHeader[3]) << 32)
+ | (((ulong)plaintextWithHeader[4]) << 24)
+ | (((ulong)plaintextWithHeader[5]) << 16)
+ | (((ulong)plaintextWithHeader[6]) << 8)
+ | (ulong)plaintextWithHeader[7];
// Are we expired?
DateTime utcNow = DateTime.UtcNow;
@@ -81,8 +81,8 @@ namespace Microsoft.AspNet.DataProtection
throw Error.TimeLimitedDataProtector_PayloadExpired(utcTicksExpiration);
}
- byte[] retVal = new byte[unprotectedDataWithHeader.Length - 8];
- Buffer.BlockCopy(unprotectedDataWithHeader, 8, retVal, 0, retVal.Length);
+ byte[] retVal = new byte[plaintextWithHeader.Length - 8];
+ Buffer.BlockCopy(plaintextWithHeader, 8, retVal, 0, retVal.Length);
expiration = new DateTimeOffset((long)utcTicksExpiration, TimeSpan.Zero);
return retVal;
diff --git a/src/Microsoft.AspNet.DataProtection/TypeExtensions.cs b/src/Microsoft.AspNet.DataProtection/TypeExtensions.cs
new file mode 100644
index 0000000000..7f4c12b529
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/TypeExtensions.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Helpful extension methods on .
+ ///
+ internal static class TypeExtensions
+ {
+ ///
+ /// Throws if
+ /// is not assignable to .
+ ///
+ 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/Microsoft.AspNet.DataProtection/XmlConstants.cs b/src/Microsoft.AspNet.DataProtection/XmlConstants.cs
new file mode 100644
index 0000000000..e41785f59a
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlConstants.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Contains XLinq constants.
+ ///
+ internal static class XmlConstants
+ {
+ ///
+ /// The root namespace used for all DataProtection-specific XML elements and attributes.
+ ///
+ private static readonly XNamespace RootNamespace = XNamespace.Get("http://schemas.asp.net/2015/03/dataProtection");
+
+ ///
+ /// Represents the type of decryptor that can be used when reading 'encryptedSecret' elements.
+ ///
+ internal static readonly XName DecryptorTypeAttributeName = "decryptorType";
+
+ ///
+ /// Elements with this attribute will be read with the specified deserializer type.
+ ///
+ internal static readonly XName DeserializerTypeAttributeName = "deserializerType";
+
+ ///
+ /// Elements with this name will be automatically decrypted when read by the XML key manager.
+ ///
+ internal static readonly XName EncryptedSecretElementName = RootNamespace.GetName("encryptedSecret");
+
+ ///
+ /// Elements where this attribute has a value of 'true' should be encrypted before storage.
+ ///
+ internal static readonly XName RequiresEncryptionAttributeName = RootNamespace.GetName("requiresEncryption");
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateResolver.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateResolver.cs
new file mode 100644
index 0000000000..d16a4f9af6
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateResolver.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+using System;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ ///
+ /// A default implementation of that looks in the current user
+ /// and local machine certificate stores.
+ ///
+ public class CertificateResolver : ICertificateResolver
+ {
+ ///
+ /// Locates an given its thumbprint.
+ ///
+ /// The thumbprint (as a hex string) of the certificate to resolve.
+ /// The resolved , or null if the certificate cannot be found.
+ public virtual X509Certificate2 ResolveCertificate(string 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);
+ var matchingCerts = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: true);
+ return (matchingCerts != null && matchingCerts.Count > 0) ? matchingCerts[0] : null;
+ }
+ finally
+ {
+ store.Close();
+ }
+ }
+ }
+}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs
index 39f6d3e1a5..f89820b02c 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs
@@ -1,37 +1,168 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
using System;
using System.Security.Cryptography.X509Certificates;
+using System.Security.Cryptography.Xml;
+using System.Xml;
using System.Xml.Linq;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// A class that performs XML encryption using an X.509 certificate.
+ /// An that can perform XML encryption by using an X.509 certificate.
///
- ///
- /// This type currently requires Windows 8.1 (Windows Server 2012 R2) or higher.
- ///
- public sealed class CertificateXmlEncryptor : IXmlEncryptor
+ public sealed class CertificateXmlEncryptor : IInternalCertificateXmlEncryptor, IXmlEncryptor
{
- private readonly DpapiNGXmlEncryptor _dpapiEncryptor;
+ private readonly Func _certFactory;
+ private readonly IInternalCertificateXmlEncryptor _encryptor;
+ private readonly ILogger _logger;
- public CertificateXmlEncryptor([NotNull] X509Certificate2 cert)
+ ///
+ /// Creates a given a certificate's thumbprint and an
+ /// that can be used to resolve the certificate.
+ ///
+ /// The thumbprint (as a hex string) of the certificate with which to
+ /// encrypt the key material. The certificate must be locatable by .
+ /// A resolver which can locate objects.
+ public CertificateXmlEncryptor([NotNull] string thumbprint, [NotNull] ICertificateResolver certificateResolver)
+ : this(thumbprint, certificateResolver, services: null)
{
- byte[] certAsBytes = cert.Export(X509ContentType.Cert);
- string protectionDescriptor = "CERTIFICATE=CertBlob:" + Convert.ToBase64String(certAsBytes);
- _dpapiEncryptor = new DpapiNGXmlEncryptor(protectionDescriptor, DpapiNGProtectionDescriptorFlags.None);
}
///
- /// Encrypts the specified XML element using an X.509 certificate.
+ /// Creates a given a certificate's thumbprint, an
+ /// that can be used to resolve the certificate, and
+ /// an .
///
- /// The plaintext XML element to encrypt. This element is unchanged by the method.
- /// The encrypted form of the XML element.
- public XElement Encrypt([NotNull] XElement plaintextElement)
+ /// The thumbprint (as a hex string) of the certificate with which to
+ /// encrypt the key material. The certificate must be locatable by .
+ /// A resolver which can locate objects.
+ /// An optional to provide ancillary services.
+ public CertificateXmlEncryptor([NotNull] string thumbprint, [NotNull] ICertificateResolver certificateResolver, IServiceProvider services)
+ : this(services)
{
- return _dpapiEncryptor.Encrypt(plaintextElement);
+ _certFactory = CreateCertFactory(thumbprint, certificateResolver);
+ }
+
+ ///
+ /// Creates a given an instance.
+ ///
+ /// The with which to encrypt the key material.
+ public CertificateXmlEncryptor([NotNull] X509Certificate2 certificate)
+ : this(certificate, services: null)
+ {
+ }
+
+ ///
+ /// Creates a given an instance
+ /// and an .
+ ///
+ /// The with which to encrypt the key material.
+ /// An optional to provide ancillary services.
+ public CertificateXmlEncryptor([NotNull] X509Certificate2 certificate, IServiceProvider services)
+ : this(services)
+ {
+ _certFactory = () => certificate;
+ }
+
+ internal CertificateXmlEncryptor(IServiceProvider services)
+ {
+ _encryptor = services?.GetService() ?? this;
+ _logger = services.GetLogger();
+ }
+
+ ///
+ /// Encrypts the specified with an X.509 certificate.
+ ///
+ /// The plaintext to encrypt.
+ ///
+ /// An that contains the encrypted value of
+ /// along with information about how to
+ /// decrypt it.
+ ///
+ public EncryptedXmlInfo Encrypt([NotNull] XElement plaintextElement)
+ {
+ //
+ // ...
+ //
+
+ XElement 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 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 element back off and convert the XmlDocument to an XElement.
+ return XElement.Load(xmlDocument.DocumentElement.FirstChild.CreateNavigator().ReadSubtree());
+ }
+
+ private Func 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)
+ {
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(ex, "An exception occurred while trying to resolve certificate with thumbprint '{0}'.", thumbprint);
+ }
+ throw;
+ }
+ };
+ }
+
+ EncryptedData IInternalCertificateXmlEncryptor.PerformEncryption(EncryptedXml encryptedXml, XmlElement elementToEncrypt)
+ {
+ var cert = _certFactory()
+ ?? CryptoUtil.Fail("Cert factory returned null.");
+
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Encrypting to X.509 certificate with thumbprint '{0}'.", cert.Thumbprint);
+ }
+
+ try
+ {
+ return encryptedXml.Encrypt(elementToEncrypt, cert);
+ }
+ catch (Exception ex)
+ {
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(ex, "An error occurred while encrypting to X.509 certificate with thumbprint '{0}'.", cert.Thumbprint);
+ }
+ throw;
+ }
}
}
}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs
index dd7d0938d2..17b9a762c2 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs
@@ -5,12 +5,31 @@ using System;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
- // from ncrypt.h and ncryptprotect.h
+ ///
+ /// Flags used to control the creation of protection descriptors.
+ ///
+ ///
+ /// 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.
+ ///
[Flags]
public enum DpapiNGProtectionDescriptorFlags
{
+ ///
+ /// No special handling is necessary.
+ ///
None = 0,
+
+ ///
+ /// The provided descriptor is a reference to a full descriptor stored
+ /// in the system registry.
+ ///
NamedDescriptor = 0x00000001,
+
+ ///
+ /// When combined with , uses the HKLM registry
+ /// instead of the HKCU registry when locating the full descriptor.
+ ///
MachineKey = 0x00000020,
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs
index debd74b5a0..e804c1d7cb 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs
@@ -2,48 +2,90 @@
// 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.Xml.Linq;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// A class that can decrypt XML elements which were encrypted using Windows DPAPI:NG.
+ /// An that decrypts XML elements that were encrypted with .
///
- internal unsafe sealed class DpapiNGXmlDecryptor : IXmlDecryptor
+ ///
+ /// This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ ///
+ public sealed class DpapiNGXmlDecryptor : IXmlDecryptor
{
+ private readonly ILogger _logger;
+
///
- /// Decrypts the specified XML element using Windows DPAPI:NG.
+ /// Creates a new instance of a .
///
- /// The encrypted XML element to decrypt. This element is unchanged by the method.
- /// The decrypted form of the XML element.
+ public DpapiNGXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ ///
+ /// Creates a new instance of a .
+ ///
+ /// An optional to provide ancillary services.
+ public DpapiNGXmlDecryptor(IServiceProvider services)
+ {
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
+
+ _logger = services.GetLogger();
+ }
+
+ ///
+ /// Decrypts the specified XML element.
+ ///
+ /// An encrypted XML element.
+ /// The decrypted form of .
+ ///
public XElement Decrypt([NotNull] XElement encryptedElement)
{
- CryptoUtil.Assert(encryptedElement.Name == DpapiNGXmlEncryptor.DpapiNGEncryptedSecretElementName,
- "TODO: Incorrect element.");
-
- int version = (int)encryptedElement.Attribute("version");
- CryptoUtil.Assert(version == 1, "TODO: Bad version.");
-
- byte[] dpapiNGProtectedBytes = Convert.FromBase64String(encryptedElement.Value);
- using (var secret = DpapiSecretSerializerHelper.UnprotectWithDpapiNG(dpapiNGProtectedBytes))
+ try
{
- byte[] plaintextXmlBytes = new byte[secret.Length];
- try
+ //
+ //
+ //
+ // {base64}
+ //
+
+ byte[] protectedSecret = Convert.FromBase64String((string)encryptedElement.Element("value"));
+ if (_logger.IsVerboseLevelEnabled())
{
- secret.WriteSecretIntoBuffer(new ArraySegment(plaintextXmlBytes));
- using (var memoryStream = new MemoryStream(plaintextXmlBytes, writable: false))
+ string protectionDescriptorRule;
+ try
{
- return XElement.Load(memoryStream);
+ protectionDescriptorRule = DpapiSecretSerializerHelper.GetRuleFromDpapiNGProtectedPayload(protectedSecret);
}
+ catch
+ {
+ // swallow all errors - it's just a log
+ protectionDescriptorRule = null;
+ }
+ _logger.LogVerbose("Decrypting secret element using Windows DPAPI-NG with protection descriptor '{0}'.", protectionDescriptorRule);
}
- finally
+
+ using (Secret secret = DpapiSecretSerializerHelper.UnprotectWithDpapiNG(protectedSecret))
{
- Array.Clear(plaintextXmlBytes, 0, plaintextXmlBytes.Length);
+ 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.
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(ex, "An exception occurred while trying to decrypt the element.");
+ }
+ throw;
+ }
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs
index 498df42350..1a11ce10a7 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs
@@ -3,79 +3,112 @@
using System;
using System.Globalization;
-using System.IO;
using System.Security.Principal;
using System.Xml.Linq;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.Cryptography.SafeHandles;
using Microsoft.AspNet.DataProtection.Cng;
-using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
/// A class that can encrypt XML elements using Windows DPAPI:NG.
///
+ ///
+ /// This API is only supported on Windows 8 / Windows Server 2012 and higher.
+ ///
public sealed class DpapiNGXmlEncryptor : IXmlEncryptor
{
- internal static readonly XName DpapiNGEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("dpapiNGEncryptedSecret");
-
+ private readonly ILogger _logger;
private readonly NCryptDescriptorHandle _protectionDescriptorHandle;
- public DpapiNGXmlEncryptor()
- : this(GetDefaultProtectionDescriptorString(), DpapiNGProtectionDescriptorFlags.None)
+ ///
+ /// Creates a new instance of a .
+ ///
+ /// The rule string from which to create the protection descriptor.
+ /// Flags controlling the creation of the protection descriptor.
+ public DpapiNGXmlEncryptor([NotNull] string protectionDescriptorRule, DpapiNGProtectionDescriptorFlags flags)
+ : this(protectionDescriptorRule, flags, services: null)
{
}
- public DpapiNGXmlEncryptor(string protectionDescriptor, DpapiNGProtectionDescriptorFlags protectionDescriptorFlags = DpapiNGProtectionDescriptorFlags.None)
- {
- if (String.IsNullOrEmpty(protectionDescriptor))
- {
- throw new Exception("TODO: Null or empty.");
- }
-
- int ntstatus = UnsafeNativeMethods.NCryptCreateProtectionDescriptor(protectionDescriptor, (uint)protectionDescriptorFlags, out _protectionDescriptorHandle);
- UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
- CryptoUtil.AssertSafeHandleIsValid(_protectionDescriptorHandle);
- }
-
///
- /// Encrypts the specified XML element using Windows DPAPI:NG.
+ /// Creates a new instance of a .
///
- /// The plaintext XML element to encrypt. This element is unchanged by the method.
- /// The encrypted form of the XML element.
- public XElement Encrypt([NotNull] XElement plaintextElement)
+ /// The rule string from which to create the protection descriptor.
+ /// Flags controlling the creation of the protection descriptor.
+ /// An optional to provide ancillary services.
+ public DpapiNGXmlEncryptor([NotNull] string protectionDescriptorRule, DpapiNGProtectionDescriptorFlags flags, IServiceProvider services)
{
- // First, convert the XML element to a byte[] so that it can be encrypted.
- Secret secret;
- using (var memoryStream = new MemoryStream())
- {
- plaintextElement.Save(memoryStream);
-#if !DNXCORE50
- // If we're on full desktop CLR, utilize the underlying buffer directly as an optimization.
- byte[] underlyingBuffer = memoryStream.GetBuffer();
- secret = new Secret(new ArraySegment(underlyingBuffer, 0, checked((int)memoryStream.Length)));
- Array.Clear(underlyingBuffer, 0, underlyingBuffer.Length);
-#else
- // Otherwise, need to make a copy of the buffer.
- byte[] clonedBuffer = memoryStream.ToArray();
- secret = new Secret(clonedBuffer);
- Array.Clear(clonedBuffer, 0, clonedBuffer.Length);
-#endif
- }
+ CryptoUtil.AssertPlatformIsWindows8OrLater();
- //
- // ... base64 data ...
- //
- byte[] encryptedBytes = DpapiSecretSerializerHelper.ProtectWithDpapiNG(secret, _protectionDescriptorHandle);
- return new XElement(DpapiNGEncryptedSecretElementName,
- new XAttribute("decryptor", typeof(DpapiNGXmlDecryptor).AssemblyQualifiedName),
- new XAttribute("version", 1),
- Convert.ToBase64String(encryptedBytes));
+ int ntstatus = UnsafeNativeMethods.NCryptCreateProtectionDescriptor(protectionDescriptorRule, (uint)flags, out _protectionDescriptorHandle);
+ UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus);
+ CryptoUtil.AssertSafeHandleIsValid(_protectionDescriptorHandle);
+
+ _logger = services.GetLogger();
}
- private static string GetDefaultProtectionDescriptorString()
+ ///
+ /// Encrypts the specified .
+ ///
+ /// The plaintext to encrypt.
+ ///
+ /// An that contains the encrypted value of
+ /// along with information about how to
+ /// decrypt it.
+ ///
+ public EncryptedXmlInfo Encrypt([NotNull] XElement plaintextElement)
{
+ string protectionDescriptorRuleString = _protectionDescriptorHandle.GetProtectionDescriptorRuleString();
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ _logger.LogVerbose("Encrypting to Windows DPAPI-NG using protection descriptor '{0}'.", protectionDescriptorRuleString);
+ }
+
+ // Convert the XML element to a binary secret so that it can be run through DPAPI
+ byte[] cngDpapiEncryptedData;
+ try
+ {
+ using (Secret plaintextElementAsSecret = plaintextElement.ToSecret())
+ {
+ cngDpapiEncryptedData = DpapiSecretSerializerHelper.ProtectWithDpapiNG(plaintextElementAsSecret, _protectionDescriptorHandle);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(ex, "An error occurred while encrypting to Windows DPAPI-NG.");
+ }
+ throw;
+ }
+
+ //
+ //
+ //
+ // {base64}
+ //
+
+ 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));
+ }
+
+ ///
+ /// Creates a rule string tied to the current Windows user and which is transferrable
+ /// across machines (backed up in AD).
+ ///
+ 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 (WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent())
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs
index c55a6ba47d..10d0b81a84 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs
@@ -2,47 +2,75 @@
// 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.Xml.Linq;
using Microsoft.AspNet.Cryptography;
using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// A class that can decrypt XML elements which were encrypted using Windows DPAPI.
+ /// An that decrypts XML elements that were encrypted with .
///
- internal unsafe sealed class DpapiXmlDecryptor : IXmlDecryptor
+ public sealed class DpapiXmlDecryptor : IXmlDecryptor
{
+ private readonly ILogger _logger;
+
///
- /// Decrypts the specified XML element using Windows DPAPI.
+ /// Creates a new instance of a .
///
- /// The encrypted XML element to decrypt. This element is unchanged by the method.
- /// The decrypted form of the XML element.
+ public DpapiXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ ///
+ /// Creates a new instance of a .
+ ///
+ /// An optional to provide ancillary services.
+ public DpapiXmlDecryptor(IServiceProvider services)
+ {
+ CryptoUtil.AssertPlatformIsWindows();
+
+ _logger = services.GetLogger();
+ }
+
+ ///
+ /// Decrypts the specified XML element.
+ ///
+ /// An encrypted XML element.
+ /// The decrypted form of .
+ ///
public XElement Decrypt([NotNull] XElement encryptedElement)
{
- CryptoUtil.Assert(encryptedElement.Name == DpapiXmlEncryptor.DpapiEncryptedSecretElementName,
- "TODO: Incorrect element.");
-
- int version = (int)encryptedElement.Attribute("version");
- CryptoUtil.Assert(version == 1, "TODO: Bad version.");
-
- byte[] dpapiProtectedBytes = Convert.FromBase64String(encryptedElement.Value);
- using (var secret = DpapiSecretSerializerHelper.UnprotectWithDpapi(dpapiProtectedBytes))
+ if (_logger.IsVerboseLevelEnabled())
{
- byte[] plaintextXmlBytes = new byte[secret.Length];
- try
+ _logger.LogVerbose("Decrypting secret element using Windows DPAPI.");
+ }
+
+ try
+ {
+ //
+ //
+ // {base64}
+ //
+
+ byte[] protectedSecret = Convert.FromBase64String((string)encryptedElement.Element("value"));
+ using (Secret secret = DpapiSecretSerializerHelper.UnprotectWithDpapi(protectedSecret))
{
- secret.WriteSecretIntoBuffer(new ArraySegment(plaintextXmlBytes));
- using (var memoryStream = new MemoryStream(plaintextXmlBytes, writable: false))
- {
- return XElement.Load(memoryStream);
- }
+ return secret.ToXElement();
}
- finally
+ }
+ catch (Exception ex)
+ {
+ // It's OK for us to log the error, as we control the exception, and it doesn't contain
+ // sensitive information.
+ if (_logger.IsErrorLevelEnabled())
{
- Array.Clear(plaintextXmlBytes, 0, plaintextXmlBytes.Length);
+ _logger.LogError(ex, "An exception occurred while trying to decrypt the element.");
}
+ throw;
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs
index 121384d7bc..d0b5908092 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs
@@ -2,61 +2,103 @@
// 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.Principal;
using System.Xml.Linq;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNet.DataProtection.Cng;
-using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// A class that can encrypt XML elements using Windows DPAPI.
+ /// An that encrypts XML by using Windows DPAPI.
///
+ ///
+ /// This API is only supported on Windows platforms.
+ ///
public sealed class DpapiXmlEncryptor : IXmlEncryptor
{
- internal static readonly XName DpapiEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("dpapiEncryptedSecret");
-
+ private readonly ILogger _logger;
private readonly bool _protectToLocalMachine;
+ ///
+ /// Creates a given a protection scope.
+ ///
+ /// '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.
public DpapiXmlEncryptor(bool protectToLocalMachine)
+ : this(protectToLocalMachine, services: null)
{
- _protectToLocalMachine = protectToLocalMachine;
}
///
- /// Encrypts the specified XML element using Windows DPAPI.
+ /// Creates a given a protection scope and an .
///
- /// The plaintext XML element to encrypt. This element is unchanged by the method.
- /// The encrypted form of the XML element.
- public XElement Encrypt([NotNull] XElement plaintextElement)
+ /// '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.
+ /// An optional to provide ancillary services.
+ public DpapiXmlEncryptor(bool protectToLocalMachine, IServiceProvider services)
{
- // First, convert the XML element to a byte[] so that it can be encrypted.
- Secret secret;
- using (var memoryStream = new MemoryStream())
- {
- plaintextElement.Save(memoryStream);
+ CryptoUtil.AssertPlatformIsWindows();
-#if !DNXCORE50
- // If we're on full desktop CLR, utilize the underlying buffer directly as an optimization.
- byte[] underlyingBuffer = memoryStream.GetBuffer();
- secret = new Secret(new ArraySegment(underlyingBuffer, 0, checked((int)memoryStream.Length)));
- Array.Clear(underlyingBuffer, 0, underlyingBuffer.Length);
-#else
- // Otherwise, need to make a copy of the buffer.
- byte[] clonedBuffer = memoryStream.ToArray();
- secret = new Secret(clonedBuffer);
- Array.Clear(clonedBuffer, 0, clonedBuffer.Length);
-#endif
+ _protectToLocalMachine = protectToLocalMachine;
+ _logger = services.GetLogger();
+ }
+
+ ///
+ /// Encrypts the specified .
+ ///
+ /// The plaintext to encrypt.
+ ///
+ /// An that contains the encrypted value of
+ /// along with information about how to
+ /// decrypt it.
+ ///
+ public EncryptedXmlInfo Encrypt([NotNull] XElement plaintextElement)
+ {
+ if (_logger.IsVerboseLevelEnabled())
+ {
+ if (_protectToLocalMachine)
+ {
+ _logger.LogVerbose("Encrypting to Windows DPAPI for local machine account.");
+ }
+ else
+ {
+ _logger.LogVerbose("Encrypting to Windows DPAPI for current user account ({0}).", WindowsIdentity.GetCurrent().Name);
+ }
}
- //
- // ... base64 data ...
- //
- byte[] encryptedBytes = DpapiSecretSerializerHelper.ProtectWithDpapi(secret, protectToLocalMachine: _protectToLocalMachine);
- return new XElement(DpapiEncryptedSecretElementName,
- new XAttribute("decryptor", typeof(DpapiXmlDecryptor).AssemblyQualifiedName),
- new XAttribute("version", 1),
- Convert.ToBase64String(encryptedBytes));
+ // Convert the XML element to a binary secret so that it can be run through DPAPI
+ byte[] dpapiEncryptedData;
+ try
+ {
+ using (Secret plaintextElementAsSecret = plaintextElement.ToSecret())
+ {
+ dpapiEncryptedData = DpapiSecretSerializerHelper.ProtectWithDpapi(plaintextElementAsSecret, protectToLocalMachine: _protectToLocalMachine);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(ex, "An error occurred while encrypting to Windows DPAPI.");
+ }
+ throw;
+ }
+
+ //
+ //
+ // {base64}
+ //
+
+ 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/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlDecryptor.core50.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlDecryptor.core50.cs
new file mode 100644
index 0000000000..d3889429b9
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlDecryptor.core50.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if DNXCORE50
+// [[ISSUE60]] Remove this entire file when Core CLR gets support for EncryptedXml.
+// This is just a dummy implementation of the class that always throws.
+// The only reason it's here (albeit internal) is to provide a nice error message if key
+// material that was generated by Desktop CLR needs to be read by Core CLR.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ internal sealed class EncryptedXmlDecryptor : IXmlDecryptor
+ {
+ private readonly ILogger _logger;
+
+ public EncryptedXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ public EncryptedXmlDecryptor(IServiceProvider services)
+ {
+ _logger = services.GetLogger();
+ }
+
+ public XElement Decrypt([NotNull] XElement encryptedElement)
+ {
+ if (_logger.IsErrorLevelEnabled())
+ {
+ _logger.LogError(Resources.EncryptedXmlDecryptor_DoesNotWorkOnCoreClr);
+ }
+
+ throw new PlatformNotSupportedException(Resources.EncryptedXmlDecryptor_DoesNotWorkOnCoreClr);
+ }
+ }
+}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
new file mode 100644
index 0000000000..870cdda96c
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlDecryptor.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+using System;
+using System.Security.Cryptography.Xml;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ ///
+ /// An that decrypts XML elements by using the class.
+ ///
+ public sealed class EncryptedXmlDecryptor : IInternalEncryptedXmlDecryptor, IXmlDecryptor
+ {
+ private readonly IInternalEncryptedXmlDecryptor _decryptor;
+
+ ///
+ /// Creates a new instance of an .
+ ///
+ public EncryptedXmlDecryptor()
+ : this(services: null)
+ {
+ }
+
+ ///
+ /// Creates a new instance of an .
+ ///
+ /// An optional to provide ancillary services.
+ public EncryptedXmlDecryptor(IServiceProvider services)
+ {
+ _decryptor = services?.GetService() ?? this;
+ }
+
+ ///
+ /// Decrypts the specified XML element.
+ ///
+ /// An encrypted XML element.
+ /// The decrypted form of .
+ ///
+ public XElement Decrypt([NotNull] XElement encryptedElement)
+ {
+ //
+ // ...
+ //
+
+ // EncryptedXml works with XmlDocument, not XLinq. When we perform the conversion
+ // we'll wrap the incoming element in a dummy 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 EncryptedXml(xmlDocument);
+ _decryptor.PerformPreDecryptionSetup(encryptedXml);
+ encryptedXml.DecryptDocument();
+
+ // Strip the 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
+ }
+ }
+}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlInfo.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlInfo.cs
new file mode 100644
index 0000000000..f9e4141054
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/EncryptedXmlInfo.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using System.Xml.Linq;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ ///
+ /// Wraps an that contains a blob of encrypted XML
+ /// and information about the class which can be used to decrypt it.
+ ///
+ public sealed class EncryptedXmlInfo
+ {
+ ///
+ /// Creates an instance of an .
+ ///
+ /// A piece of encrypted XML.
+ /// The class whose
+ /// method can be used to decrypt .
+ public EncryptedXmlInfo([NotNull] XElement encryptedElement, [NotNull] Type decryptorType)
+ {
+ if (!typeof(IXmlDecryptor).IsAssignableFrom(decryptorType))
+ {
+ throw new ArgumentException(
+ Resources.FormatTypeExtensions_BadCast(decryptorType.FullName, typeof(IXmlDecryptor).FullName),
+ nameof(decryptorType));
+ }
+
+ EncryptedElement = encryptedElement;
+ DecryptorType = decryptorType;
+ }
+
+ ///
+ /// The class whose method can be used to
+ /// decrypt the value stored in .
+ ///
+ public Type DecryptorType { get; }
+
+ ///
+ /// A piece of encrypted XML.
+ ///
+ public XElement EncryptedElement { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/ICertificateResolver.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/ICertificateResolver.cs
new file mode 100644
index 0000000000..037c7fcc07
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/ICertificateResolver.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+using System;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Framework.Internal;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ ///
+ /// Provides services for locating instances.
+ ///
+ public interface ICertificateResolver
+ {
+ ///
+ /// Locates an given its thumbprint.
+ ///
+ /// The thumbprint (as a hex string) of the certificate to resolve.
+ /// The resolved , or null if the certificate cannot be found.
+ X509Certificate2 ResolveCertificate([NotNull] string thumbprint);
+ }
+}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.cs
new file mode 100644
index 0000000000..1a0169cf42
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IInternalCertificateXmlEncryptor.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+using System;
+using System.Xml;
+using System.Security.Cryptography.Xml;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ ///
+ /// Internal implementation details of for unit testing.
+ ///
+ internal interface IInternalCertificateXmlEncryptor
+ {
+ EncryptedData PerformEncryption(EncryptedXml encryptedXml, XmlElement elementToEncrypt);
+ }
+}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs
new file mode 100644
index 0000000000..441a300e49
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IInternalEncryptedXmlDecryptor.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+#if !DNXCORE50 // [[ISSUE60]] Remove this #ifdef when Core CLR gets support for EncryptedXml
+
+using System;
+using System.Security.Cryptography.Xml;
+
+namespace Microsoft.AspNet.DataProtection.XmlEncryption
+{
+ ///
+ /// Internal implementation details of for unit testing.
+ ///
+ internal interface IInternalEncryptedXmlDecryptor
+ {
+ void PerformPreDecryptionSetup(EncryptedXml encryptedXml);
+ }
+}
+
+#endif
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlDecryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlDecryptor.cs
index 3b7f2a516c..474a6d0dda 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlDecryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlDecryptor.cs
@@ -3,6 +3,7 @@
using System;
using System.Xml.Linq;
+using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
@@ -14,8 +15,12 @@ namespace Microsoft.AspNet.DataProtection.XmlEncryption
///
/// Decrypts the specified XML element.
///
- /// The encrypted XML element to decrypt. This element is unchanged by the method.
- /// The decrypted form of the XML element.
- XElement Decrypt(XElement encryptedElement);
+ /// An encrypted XML element.
+ /// The decrypted form of .
+ ///
+ /// Implementations of this method must not mutate the
+ /// instance provided by .
+ ///
+ XElement Decrypt([NotNull] XElement encryptedElement);
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlEncryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlEncryptor.cs
index 019c32d7f4..ebb5f092ba 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/IXmlEncryptor.cs
@@ -3,19 +3,29 @@
using System;
using System.Xml.Linq;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// The basic interface for encrypting an XML element.
+ /// The basic interface for encrypting XML elements.
///
public interface IXmlEncryptor
{
///
- /// Encrypts the specified XML element.
+ /// Encrypts the specified .
///
- /// The plaintext XML element to encrypt. This element is unchanged by the method.
- /// The encrypted form of the XML element.
- XElement Encrypt(XElement plaintextElement);
+ /// The plaintext to encrypt.
+ ///
+ /// An that contains the encrypted value of
+ /// along with information about how to
+ /// decrypt it.
+ ///
+ ///
+ /// Implementations of this method must not mutate the
+ /// instance provided by .
+ ///
+ EncryptedXmlInfo Encrypt([NotNull] XElement plaintextElement);
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlDecryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlDecryptor.cs
index e5b8b1ab5b..d43c068e6b 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlDecryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlDecryptor.cs
@@ -4,21 +4,30 @@
using System;
using System.Linq;
using System.Xml.Linq;
-using Microsoft.AspNet.Cryptography;
+using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// A class that can decrypt XML elements which were encrypted using a null encryptor.
+ /// An that decrypts XML elements with a null decryptor.
///
- internal unsafe sealed class NullXmlDecryptor : IXmlDecryptor
+ public sealed class NullXmlDecryptor : IXmlDecryptor
{
+ ///
+ /// Decrypts the specified XML element.
+ ///
+ /// An encrypted XML element.
+ /// The decrypted form of .
+ ///
public XElement Decrypt([NotNull] XElement encryptedElement)
{
- CryptoUtil.Assert(encryptedElement.Name == NullXmlEncryptor.NullEncryptedSecretElementName,
- "TODO: Incorrect element.");
+ //
+ //
+ //
+ //
- return encryptedElement.Elements().Single();
+ // Return a clone of the single child node.
+ return new XElement(encryptedElement.Elements().Single());
}
}
}
diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlEncryptor.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlEncryptor.cs
index 170f4eb6e3..9343053537 100644
--- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlEncryptor.cs
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/NullXmlEncryptor.cs
@@ -3,30 +3,62 @@
using System;
using System.Xml.Linq;
-using Microsoft.AspNet.DataProtection.KeyManagement;
+using Microsoft.Framework.Internal;
+using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.DataProtection.XmlEncryption
{
///
- /// A class that performs null XML encryption (just returns the plaintext).
+ /// An that encrypts XML elements with a null encryptor.
///
public sealed class NullXmlEncryptor : IXmlEncryptor
{
- internal static readonly XName NullEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("nullEncryptedSecret");
+ private readonly ILogger _logger;
///
- /// Encrypts the specified XML element using a null encryptor.
+ /// Creates a new instance of .
///
- /// The plaintext XML element to encrypt. This element is unchanged by the method.
- /// The null-encrypted form of the XML element.
- public XElement Encrypt([NotNull] XElement plaintextElement)
+ public NullXmlEncryptor()
+ : this(services: null)
{
- //
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// An optional to provide ancillary services.
+ public NullXmlEncryptor(IServiceProvider services)
+ {
+ _logger = services.GetLogger();
+ }
+
+ ///
+ /// Encrypts the specified with a null encryptor, i.e.,
+ /// by returning the original value of unencrypted.
+ ///
+ /// The plaintext to echo back.
+ ///
+ /// An that contains the null-encrypted value of
+ /// along with information about how to
+ /// decrypt it.
+ ///
+ public EncryptedXmlInfo Encrypt([NotNull] XElement plaintextElement)
+ {
+ if (_logger.IsWarningLevelEnabled())
+ {
+ _logger.LogWarning("Encrypting using a null encryptor; secret information isn't being protected.");
+ }
+
+ //
+ //
//
- //
- return new XElement(NullEncryptedSecretElementName,
- new XAttribute("decryptor", typeof(NullXmlDecryptor).AssemblyQualifiedName),
- plaintextElement);
+ //
+
+ 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/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs
new file mode 100644
index 0000000000..e97bc112e9
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs
@@ -0,0 +1,201 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+
+namespace Microsoft.AspNet.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();
+
+ 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);
+ var innerDoc = new XDocument(clonedElementWhichRequiresDecryption);
+ string decryptorTypeName = (string)clonedElementWhichRequiresDecryption.Attribute(XmlConstants.DecryptorTypeAttributeName);
+ var decryptorInstance = activator.CreateInstance(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();
+
+ 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)
+ {
+ //
+ //
+ //
+ entry.Key.ReplaceWith(
+ new XElement(XmlConstants.EncryptedSecretElementName,
+ new XAttribute(XmlConstants.DecryptorTypeAttributeName, entry.Value.DecryptorType.AssemblyQualifiedName),
+ entry.Value.EncryptedElement));
+ }
+ return doc.Root;
+ }
+
+ ///
+ /// Converts an to a so that it can be run through
+ /// the DPAPI routines.
+ ///
+ 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);
+
+#if !DNXCORE50
+ byte[] underlyingBuffer = memoryStream.GetBuffer();
+ fixed (byte* __unused__ = underlyingBuffer) // try to limit this moving around in memory while we allocate
+ {
+ try
+ {
+ return new Secret(new ArraySegment(underlyingBuffer, 0, checked((int)memoryStream.Length)));
+ }
+ finally
+ {
+ Array.Clear(underlyingBuffer, 0, underlyingBuffer.Length);
+ }
+ }
+#else
+ ArraySegment underlyingBuffer;
+ CryptoUtil.Assert(memoryStream.TryGetBuffer(out underlyingBuffer), "Underlying buffer isn't exposable.");
+ fixed (byte* __unused__ = underlyingBuffer.Array) // try to limit this moving around in memory while we allocate
+ {
+ try
+ {
+ return new Secret(underlyingBuffer);
+ }
+ finally
+ {
+ Array.Clear(underlyingBuffer.Array, underlyingBuffer.Offset, underlyingBuffer.Count);
+ }
+ }
+#endif
+ }
+
+ ///
+ /// Converts a provided by the DPAPI routines back into an .
+ ///
+ public static XElement ToXElement(this Secret secret)
+ {
+ byte[] plaintextSecret = new byte[secret.Length];
+ fixed (byte* __unused__ = plaintextSecret) // try to keep the GC from moving it around
+ {
+ try
+ {
+ secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret));
+ MemoryStream 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/Microsoft.AspNet.DataProtection/XmlExtensions.cs b/src/Microsoft.AspNet.DataProtection/XmlExtensions.cs
new file mode 100644
index 0000000000..6021878bc9
--- /dev/null
+++ b/src/Microsoft.AspNet.DataProtection/XmlExtensions.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Contains helpers to work with XElement objects.
+ ///
+ internal static class XmlExtensions
+ {
+ ///
+ /// 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.
+ ///
+ 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/Microsoft.AspNet.DataProtection/project.json b/src/Microsoft.AspNet.DataProtection/project.json
index eb11984e4d..8de03164c8 100644
--- a/src/Microsoft.AspNet.DataProtection/project.json
+++ b/src/Microsoft.AspNet.DataProtection/project.json
@@ -3,19 +3,25 @@
"description": "ASP.NET 5 logic to protect and unprotect data, similar to DPAPI.",
"dependencies": {
"Microsoft.AspNet.Cryptography.Internal": "1.0.0-*",
- "Microsoft.Framework.DependencyInjection": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Interfaces": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Shared": { "type": "build", "version": "" },
+ "Microsoft.Framework.DependencyInjection.Interfaces": "1.0.0-*",
+ "Microsoft.Framework.Logging.Interfaces": "1.0.0-*",
+ "Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.OptionsModel": "1.0.0-*"
},
"frameworks": {
"net451": {
- "frameworkAssemblies": {
+ "frameworkAssemblies": {
+ "System.Runtime": { "version": "", "type": "build" },
"System.Security": "",
"System.Xml": "",
"System.Xml.Linq": ""
}
},
"dnx451": {
- "frameworkAssemblies": {
+ "frameworkAssemblies": {
+ "System.Runtime": { "version": "", "type": "build" },
"System.Security": "",
"System.Xml": "",
"System.Xml.Linq": ""
@@ -25,6 +31,8 @@
"dependencies": {
"Microsoft.Win32.Registry": "4.0.0-beta-*",
"System.IO": "4.0.10-beta-*",
+ "System.Linq": "4.0.0-beta-*",
+ "System.Reflection.Extensions": "4.0.0-beta-*",
"System.Reflection.TypeExtensions": "4.0.0-beta-*",
"System.Security.Cryptography.X509Certificates": "4.0.0-beta-*",
"System.Security.Cryptography.Encryption.Aes": "4.0.0-beta-*",
diff --git a/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs
new file mode 100644
index 0000000000..74f9da1b98
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.Cryptography.Cng
+{
+ public unsafe class BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_Tests
+ {
+ [Fact]
+ public void Init_SetsProperties()
+ {
+ // Arrange
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO cipherModeInfo;
+
+ // Act
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out 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/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs
new file mode 100644
index 0000000000..9817dcb205
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCRYPT_KEY_LENGTHS_STRUCT_Tests.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Cryptography.Internal;
+using Microsoft.AspNet.Testing;
+using Xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs
new file mode 100644
index 0000000000..4166f51e32
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/BCryptUtilTests.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using Microsoft.AspNet.DataProtection.Test.Shared;
+using Microsoft.AspNet.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs
new file mode 100644
index 0000000000..dd5547efeb
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/Cng/CachedAlgorithmHandlesTests.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Text;
+using Microsoft.AspNet.Cryptography.SafeHandles;
+using Microsoft.AspNet.DataProtection.Test.Shared;
+using Microsoft.AspNet.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNet.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 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 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 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/test/Microsoft.AspNet.Cryptography.Internal.Test/CryptoUtilTests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/CryptoUtilTests.cs
new file mode 100644
index 0000000000..1ddd951e7f
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/CryptoUtilTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs
new file mode 100644
index 0000000000..f892af7d63
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/SafeHandles/SecureLocalAllocHandleTests.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs b/test/Microsoft.AspNet.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs
new file mode 100644
index 0000000000..9835b11131
--- /dev/null
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/UnsafeBufferUtilTests.cs
@@ -0,0 +1,162 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using Microsoft.AspNet.Cryptography.SafeHandles;
+using Xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.Cryptography.Internal.Test/project.json b/test/Microsoft.AspNet.Cryptography.Internal.Test/project.json
index 6f59035881..d34f53f281 100644
--- a/test/Microsoft.AspNet.Cryptography.Internal.Test/project.json
+++ b/test/Microsoft.AspNet.Cryptography.Internal.Test/project.json
@@ -1,6 +1,7 @@
{
"dependencies": {
"Microsoft.AspNet.Cryptography.Internal": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Test.Shared": { "type": "build", "version": "" },
"Microsoft.AspNet.Testing": "1.0.0-*",
"xunit.runner.kre": "1.0.0-*"
},
@@ -9,5 +10,8 @@
},
"commands": {
"test": "xunit.runner.kre"
+ },
+ "compilationOptions": {
+ "allowUnsafe": true
}
}
diff --git a/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/ConditionalRunTestOnlyIfBcryptAvailableAttribute.cs b/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/ConditionalRunTestOnlyIfBcryptAvailableAttribute.cs
deleted file mode 100644
index e435d081dc..0000000000
--- a/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/ConditionalRunTestOnlyIfBcryptAvailableAttribute.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Globalization;
-using Microsoft.AspNet.Cryptography.SafeHandles;
-using Microsoft.AspNet.Testing.xunit;
-
-namespace Microsoft.AspNet.Cryptography
-{
- public class ConditionalRunTestOnlyIfBcryptAvailableAttribute : Attribute, ITestCondition
- {
- private static readonly SafeLibraryHandle _bcryptLibHandle = GetBCryptLibHandle();
-
- private readonly string _requiredExportFunction;
-
- public ConditionalRunTestOnlyIfBcryptAvailableAttribute(string requiredExportFunction = null)
- {
- _requiredExportFunction = requiredExportFunction;
- }
-
- public bool IsMet
- {
- get
- {
- if (_bcryptLibHandle == null)
- {
- return false; // no bcrypt.dll available
- }
-
- return (_requiredExportFunction == null || _bcryptLibHandle.DoesProcExist(_requiredExportFunction));
- }
- }
-
- public string SkipReason
- {
- get
- {
- return (_bcryptLibHandle != null)
- ? String.Format(CultureInfo.InvariantCulture, "Export {0} not found in bcrypt.dll", _requiredExportFunction)
- : "bcrypt.dll not found on this platform.";
- }
- }
-
- private static SafeLibraryHandle GetBCryptLibHandle()
- {
- try
- {
- return SafeLibraryHandle.Open("bcrypt.dll");
- }
- catch
- {
- // If we're not on an OS with BCRYPT.DLL, just bail.
- return null;
- }
- }
- }
-}
diff --git a/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs b/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs
index 6fc684797d..81b0908ce0 100644
--- a/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs
+++ b/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs
@@ -4,6 +4,7 @@
using System;
using System.Text;
using Microsoft.AspNet.Cryptography.KeyDerivation.PBKDF2;
+using Microsoft.AspNet.DataProtection.Test.Shared;
using Microsoft.AspNet.Testing.xunit;
using Xunit;
@@ -40,7 +41,8 @@ namespace Microsoft.AspNet.Cryptography.KeyDerivation
// 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]
+ [ConditionalTheory]
+ [ConditionalRunTestOnlyOnWindows]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
@@ -67,7 +69,7 @@ namespace Microsoft.AspNet.Cryptography.KeyDerivation
// this value straddles the digest length of the PRF. We only use 5 iterations so
// that our unit tests are fast.
[ConditionalTheory]
- [ConditionalRunTestOnlyIfBcryptAvailable("BCryptKeyDerivation")]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
@@ -97,14 +99,14 @@ namespace Microsoft.AspNet.Cryptography.KeyDerivation
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable("BCryptDeriveKeyPBKDF2")]
+ [ConditionalRunTestOnlyOnWindows]
public void RunTest_WithLongPassword_Win7()
{
RunTest_WithLongPassword_Impl();
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable("BCryptKeyDerivation")]
+ [ConditionalRunTestOnlyOnWindows8OrLater]
public void RunTest_WithLongPassword_Win8()
{
RunTest_WithLongPassword_Impl();
diff --git a/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/project.json b/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/project.json
index 90dcd88b09..7fbc91ad6c 100644
--- a/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/project.json
+++ b/test/Microsoft.AspNet.Cryptography.KeyDerivation.Test/project.json
@@ -2,6 +2,7 @@
"dependencies": {
"Microsoft.AspNet.Cryptography.Internal": "1.0.0-*",
"Microsoft.AspNet.Cryptography.KeyDerivation": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Test.Shared": "",
"Microsoft.AspNet.Testing": "1.0.0-*",
"Moq": "4.2.1312.1622",
"xunit.runner.kre": "1.0.0-*"
diff --git a/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs
new file mode 100644
index 0000000000..268e3e1d21
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/DataProtectionExtensionsTests.cs
@@ -0,0 +1,179 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNet.DataProtection.Interfaces;
+using Microsoft.AspNet.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ public class DataProtectionExtensionsTests
+ {
+ [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();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny())).Returns(mockProtector.Object);
+ var provider = mockProtector.Object;
+
+ // Act & assert
+ ExceptionAssert.ThrowsArgument(
+ testCode: () => provider.CreateProtector((IEnumerable)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();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny())).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().Object;
+
+ var thirdMock = new Mock();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
+
+ // Act
+ var retVal = firstMock.Object.CreateProtector((IEnumerable)new string[] { "first", "second", "third" });
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Fact]
+ public void CreateProtector_ChainedAsParams_NonEmptyParams_SuccessCase()
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock().Object;
+
+ var thirdMock = new Mock();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock();
+ 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().Object;
+ var firstMock = new Mock();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(finalExpectedProtector);
+
+ // Act
+ var retVal = firstMock.Object.CreateProtector("first", subPurposes);
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
+ [Fact]
+ public void Protect_InvalidUtf8_Failure()
+ {
+ // Arrange
+ Mock mockProtector = new Mock();
+
+ // Act & assert
+ var ex = Assert.Throws(() =>
+ {
+ mockProtector.Object.Protect("Hello\ud800");
+ });
+ Assert.IsAssignableFrom(typeof(EncoderFallbackException), ex.InnerException);
+ }
+
+ [Fact]
+ public void Protect_Success()
+ {
+ // Arrange
+ Mock mockProtector = new Mock();
+ 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 mockProtector = new Mock();
+
+ // Act & assert
+ var ex = Assert.Throws(() =>
+ {
+ mockProtector.Object.Unprotect("A");
+ });
+ }
+
+ [Fact]
+ public void Unprotect_InvalidUtf8AfterDecryption_Failure()
+ {
+ // Arrange
+ Mock mockProtector = new Mock();
+ mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0xff });
+
+ // Act & assert
+ var ex = Assert.Throws(() =>
+ {
+ mockProtector.Object.Unprotect("AQIDBAU");
+ });
+ Assert.IsAssignableFrom(typeof(DecoderFallbackException), ex.InnerException);
+ }
+
+ [Fact]
+ public void Unprotect_Success()
+ {
+ // Arrange
+ Mock mockProtector = new Mock();
+ mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f });
+
+ // Act
+ string retVal = DataProtectionExtensions.Unprotect(mockProtector.Object, "AQIDBAU");
+
+ // Assert
+ Assert.Equal("Hello", retVal);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Interfaces.Test/Microsoft.AspNet.DataProtection.Interfaces.Test.kproj b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/Microsoft.AspNet.DataProtection.Interfaces.Test.kproj
new file mode 100644
index 0000000000..85d49cd927
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/Microsoft.AspNet.DataProtection.Interfaces.Test.kproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ ff650a69-dee4-4b36-9e30-264ee7cfb478
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+
+
+
diff --git a/test/Microsoft.AspNet.DataProtection.Interfaces.Test/project.json b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/project.json
new file mode 100644
index 0000000000..2be80d1ab4
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Interfaces.Test/project.json
@@ -0,0 +1,19 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.Cryptography.Internal": "1.0.0-*",
+ "Microsoft.AspNet.DataProtection.Interfaces": "1.0.0-*",
+ "Microsoft.AspNet.Testing": "1.0.0-*",
+ "Moq": "4.2.1312.1622",
+ "xunit.runner.kre": "1.0.0-*"
+ },
+ "frameworks": {
+ "dnx451": { }
+ },
+ "commands": {
+ "test": "xunit.runner.kre"
+ },
+ "code": "**\\*.cs;..\\common\\**\\*.cs",
+ "compilationOptions": {
+
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test.Shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs b/test/Microsoft.AspNet.DataProtection.Test.Shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs
new file mode 100644
index 0000000000..1a41ae9d7c
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test.Shared/ConditionalRunTestOnlyWindows8OrLaterAttribute.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.Testing.xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.DataProtection.Test.Shared/ConditionalRunTestOnlyWindowsAttribute.cs b/test/Microsoft.AspNet.DataProtection.Test.Shared/ConditionalRunTestOnlyWindowsAttribute.cs
new file mode 100644
index 0000000000..37b05192be
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test.Shared/ConditionalRunTestOnlyWindowsAttribute.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.Testing.xunit;
+
+namespace Microsoft.AspNet.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/test/Microsoft.AspNet.DataProtection.Test.Shared/ExceptionAssert2.cs b/test/Microsoft.AspNet.DataProtection.Test.Shared/ExceptionAssert2.cs
new file mode 100644
index 0000000000..79c53bb99f
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test.Shared/ExceptionAssert2.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Xunit;
+
+namespace Microsoft.AspNet.Testing
+{
+ internal static class ExceptionAssert2
+ {
+ ///
+ /// Verifies that the code throws an .
+ ///
+ /// A delegate to the code to be tested
+ /// The name of the parameter that should throw the exception
+ /// The that was thrown, when successful
+ /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown
+ public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName)
+ {
+ var ex = Assert.Throws(testCode);
+ Assert.Equal(paramName, ex.ParamName);
+ return ex;
+ }
+
+ ///
+ /// Verifies that the code throws a .
+ ///
+ /// A delegate to the code to be tested
+ /// The that was thrown, when successful
+ /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown
+ public static CryptographicException ThrowsCryptographicException(Action testCode)
+ {
+ return Assert.Throws(testCode);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test.Shared/Microsoft.AspNet.DataProtection.Test.Shared.kproj b/test/Microsoft.AspNet.DataProtection.Test.Shared/Microsoft.AspNet.DataProtection.Test.Shared.kproj
new file mode 100644
index 0000000000..35909b7c73
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test.Shared/Microsoft.AspNet.DataProtection.Test.Shared.kproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 4f14ba2a-4f04-4676-8586-ec380977ee2e
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+
+
+
diff --git a/test/Microsoft.AspNet.DataProtection.Test.Shared/project.json b/test/Microsoft.AspNet.DataProtection.Test.Shared/project.json
new file mode 100644
index 0000000000..03f270e861
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test.Shared/project.json
@@ -0,0 +1,17 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.Cryptography.Internal": "1.0.0-*",
+ "Microsoft.AspNet.Testing": "1.0.0-*",
+ "xunit.runner.kre": "1.0.0-*"
+ },
+ "frameworks": {
+ "dnx451": { },
+ "dnxcore50": { }
+ },
+ "commands": {
+
+ },
+ "compilationOptions": {
+ },
+ "shared": "**\\*.cs"
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/ActivatorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/ActivatorTests.cs
new file mode 100644
index 0000000000..ae0fdba4df
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/ActivatorTests.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Framework.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.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(typeof(ClassWithParameterlessCtor).AssemblyQualifiedName);
+ var retVal2 = (ClassWithServiceProviderCtor)activator.CreateInstance(typeof(ClassWithServiceProviderCtor).AssemblyQualifiedName);
+ var retVal3 = (ClassWithBothCtors)activator.CreateInstance(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(typeof(ClassWithParameterlessCtor).AssemblyQualifiedName);
+ var retVal2 = (ClassWithServiceProviderCtor)activator.CreateInstance(typeof(ClassWithServiceProviderCtor).AssemblyQualifiedName);
+ var retVal3 = (ClassWithBothCtors)activator.CreateInstance(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(
+ () => activator.CreateInstance(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().Object;
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddInstance(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/test/Microsoft.AspNet.DataProtection.Test/AnonymousImpersonation.cs b/test/Microsoft.AspNet.DataProtection.Test/AnonymousImpersonation.cs
new file mode 100644
index 0000000000..b0793bc5d2
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AnonymousImpersonation.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNet.DataProtection
+{
+ ///
+ /// Helpers for working with the anonymous Windows identity.
+ ///
+ internal static class AnonymousImpersonation
+ {
+ ///
+ /// Performs an action while impersonated under the anonymous user (NT AUTHORITY\ANONYMOUS LOGIN).
+ ///
+ 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);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..829d478ede
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class AuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [Fact]
+ public void ImportFromXml_Cbc_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var control = new AuthenticatedEncryptorDescriptor(
+ new AuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = EncryptionAlgorithm.AES_192_CBC,
+ ValidationAlgorithm = ValidationAlgorithm.HMACSHA512
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret()).CreateEncryptorInstance();
+
+ const string xml = @"
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+ ";
+ var test = new AuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml)).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..d707579cbb
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/AuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+using Microsoft.AspNet.Cryptography.Cng;
+using Microsoft.AspNet.Cryptography.SafeHandles;
+using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.AspNet.DataProtection.Managed;
+using Microsoft.AspNet.DataProtection.Test.Shared;
+using Microsoft.AspNet.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNet.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_(?\d{3})_CBC$").Groups["keyLength"].Value, CultureInfo.InvariantCulture);
+ string hashAlgorithm = Regex.Match(validationAlgorithm.ToString(), @"^HMAC(?.*)$").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 = CreateDescriptor(encryptionAlgorithm, validationAlgorithm, masterKey).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(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_(?\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 = CreateDescriptor(encryptionAlgorithm, ValidationAlgorithm.HMACSHA256 /* unused */, masterKey).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ [Theory]
+ [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_ManagedImplementation(EncryptionAlgorithm encryptionAlgorithm, ValidationAlgorithm validationAlgorithm)
+ {
+ // Parse test input
+ int keyLengthInBits = Int32.Parse(Regex.Match(encryptionAlgorithm.ToString(), @"^AES_(?\d{3})_CBC$").Groups["keyLength"].Value, CultureInfo.InvariantCulture);
+
+ // Arrange
+ var masterKey = Secret.Random(512 / 8);
+ var control = new ManagedAuthenticatedEncryptor(
+ keyDerivationKey: masterKey,
+ symmetricAlgorithmFactory: () => new AesCryptoServiceProvider(),
+ symmetricAlgorithmKeySizeInBytes: keyLengthInBits / 8,
+ validationAlgorithmFactory: () => KeyedHashAlgorithm.Create(validationAlgorithm.ToString()));
+ var test = CreateDescriptor(encryptionAlgorithm, validationAlgorithm, masterKey).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(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 = @"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ";
+ 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 = @"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ private static AuthenticatedEncryptorDescriptor CreateDescriptor(EncryptionAlgorithm encryptionAlgorithm, ValidationAlgorithm validationAlgorithm, ISecret masterKey)
+ {
+ return new AuthenticatedEncryptorDescriptor(new AuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = encryptionAlgorithm,
+ ValidationAlgorithm = validationAlgorithm
+ }, masterKey);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs
new file mode 100644
index 0000000000..12b4e75b2d
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorConfigurationTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngCbcAuthenticatedEncryptorConfigurationTests
+ {
+ [Fact]
+ public void CreateNewDescriptor_CreatesUniqueCorrectlySizedMasterKey()
+ {
+ // Arrange
+ var configuration = new CngCbcAuthenticatedEncryptorConfiguration(new CngCbcAuthenticatedEncryptionOptions());
+
+ // 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(new CngCbcAuthenticatedEncryptionOptions());
+
+ // Act
+ var descriptor = (CngCbcAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor();
+
+ // Assert
+ Assert.Equal(configuration.Options, descriptor.Options);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..b0aede26e5
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection.Test.Shared;
+using Microsoft.AspNet.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngCbcAuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void ImportFromXml_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var control = new CngCbcAuthenticatedEncryptorDescriptor(
+ new CngCbcAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = Constants.BCRYPT_AES_ALGORITHM,
+ EncryptionAlgorithmKeySize = 192,
+ EncryptionAlgorithmProvider = null,
+ HashAlgorithm = Constants.BCRYPT_SHA512_ALGORITHM,
+ HashAlgorithmProvider = null
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret()).CreateEncryptorInstance();
+
+ const string xml = @"
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+ ";
+ var test = new CngCbcAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml)).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..baa19dde89
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngCbcAuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngCbcAuthenticatedEncryptorDescriptorTests
+ {
+ [Fact]
+ public void ExportToXml_WithProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngCbcAuthenticatedEncryptorDescriptor(new CngCbcAuthenticatedEncryptionOptions()
+ {
+ 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 = @"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ [Fact]
+ public void ExportToXml_WithoutProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngCbcAuthenticatedEncryptorDescriptor(new CngCbcAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048,
+ HashAlgorithm = "hash-alg"
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(CngCbcAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs
new file mode 100644
index 0000000000..d3af69a74d
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorConfigurationTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngGcmAuthenticatedEncryptorConfigurationTests
+ {
+ [Fact]
+ public void CreateNewDescriptor_CreatesUniqueCorrectlySizedMasterKey()
+ {
+ // Arrange
+ var configuration = new CngGcmAuthenticatedEncryptorConfiguration(new CngGcmAuthenticatedEncryptionOptions());
+
+ // 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(new CngGcmAuthenticatedEncryptionOptions());
+
+ // Act
+ var descriptor = (CngGcmAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor();
+
+ // Assert
+ Assert.Equal(configuration.Options, descriptor.Options);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..5e0c48d72b
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Xml.Linq;
+using Microsoft.AspNet.Cryptography;
+using Microsoft.AspNet.DataProtection.Test.Shared;
+using Microsoft.AspNet.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngGcmAuthenticatedEncryptorDescriptorDeserializerTests
+ {
+ [ConditionalFact]
+ [ConditionalRunTestOnlyOnWindows]
+ public void ImportFromXml_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var control = new CngGcmAuthenticatedEncryptorDescriptor(
+ new CngGcmAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = Constants.BCRYPT_AES_ALGORITHM,
+ EncryptionAlgorithmKeySize = 192,
+ EncryptionAlgorithmProvider = null
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret()).CreateEncryptorInstance();
+
+ const string xml = @"
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+ ";
+ var test = new CngGcmAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml)).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..96fd83afdb
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/CngGcmAuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class CngGcmAuthenticatedEncryptorDescriptorTests
+ {
+ [Fact]
+ public void ExportToXml_WithProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngGcmAuthenticatedEncryptorDescriptor(new CngGcmAuthenticatedEncryptionOptions()
+ {
+ 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 = @"
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+
+ [Fact]
+ public void ExportToXml_WithoutProviders_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new CngGcmAuthenticatedEncryptorDescriptor(new CngGcmAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithm = "enc-alg",
+ EncryptionAlgorithmKeySize = 2048
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(CngGcmAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ const string expectedXml = @"
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ";
+ XmlAssert.Equal(expectedXml, retVal.SerializedDescriptorElement);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs
new file mode 100644
index 0000000000..dcc8d365ee
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfigurationTests.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class ManagedAuthenticatedEncryptorConfigurationTests
+ {
+ [Fact]
+ public void CreateNewDescriptor_CreatesUniqueCorrectlySizedMasterKey()
+ {
+ // Arrange
+ var configuration = new ManagedAuthenticatedEncryptorConfiguration(new ManagedAuthenticatedEncryptionOptions());
+
+ // 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(new ManagedAuthenticatedEncryptionOptions());
+
+ // Act
+ var descriptor = (ManagedAuthenticatedEncryptorDescriptor)configuration.CreateNewDescriptor();
+
+ // Assert
+ Assert.Equal(configuration.Options, descriptor.Options);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs
new file mode 100644
index 0000000000..6b249c1072
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.AspNet.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 control = new ManagedAuthenticatedEncryptorDescriptor(
+ new ManagedAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithmType = encryptionAlgorithmType,
+ EncryptionAlgorithmKeySize = 192,
+ ValidationAlgorithmType = validationAlgorithmType
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret()).CreateEncryptorInstance();
+
+ string xml = String.Format(@"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ",
+ encryptionAlgorithmType.Name, validationAlgorithmType.Name);
+ var test = new ManagedAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml)).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+
+ [Fact]
+ public void ImportFromXml_CustomType_CreatesAppropriateDescriptor()
+ {
+ // Arrange
+ var control = new ManagedAuthenticatedEncryptorDescriptor(
+ new ManagedAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithmType = typeof(AesCryptoServiceProvider),
+ EncryptionAlgorithmKeySize = 192,
+ ValidationAlgorithmType = typeof(HMACSHA384)
+ },
+ "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret()).CreateEncryptorInstance();
+
+ string xml = String.Format(@"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ",
+ typeof(AesCryptoServiceProvider).AssemblyQualifiedName, typeof(HMACSHA384).AssemblyQualifiedName);
+ var test = new ManagedAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml)).CreateEncryptorInstance();
+
+ // 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(plaintext), new ArraySegment(aad));
+ byte[] roundTripPlaintext = test.Decrypt(new ArraySegment(ciphertext), new ArraySegment(aad));
+ Assert.Equal(plaintext, roundTripPlaintext);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs
new file mode 100644
index 0000000000..f944037880
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorTests.cs
@@ -0,0 +1,115 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel
+{
+ public class ManagedAuthenticatedEncryptorDescriptorTests
+ {
+ [Fact]
+ public void ExportToXml_CustomTypes_ProducesCorrectPayload()
+ {
+ // Arrange
+ var masterKey = "k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==".ToSecret();
+ var descriptor = new ManagedAuthenticatedEncryptorDescriptor(new ManagedAuthenticatedEncryptionOptions()
+ {
+ 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(@"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ",
+ 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 ManagedAuthenticatedEncryptionOptions()
+ {
+ EncryptionAlgorithmType = encryptionAlgorithmType,
+ EncryptionAlgorithmKeySize = 2048,
+ ValidationAlgorithmType = validationAlgorithmType
+ }, masterKey);
+
+ // Act
+ var retVal = descriptor.ExportToXml();
+
+ // Assert
+ Assert.Equal(typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer), retVal.DeserializerType);
+ string expectedXml = String.Format(@"
+
+
+
+
+ k88VrwGLINfVAqzlAp7U4EAjdlmUG17c756McQGdjHU8Ajkfc/A3YOKdqlMcF6dXaIxATED+g2f62wkRRRRRzA==
+
+ ",
+ 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/test/Microsoft.AspNet.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
index 85a80c25af..8e0b8e4a8e 100644
--- a/test/Microsoft.AspNet.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
+++ b/test/Microsoft.AspNet.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
@@ -6,16 +6,16 @@ using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Cryptography.Cng;
-using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.AspNet.DataProtection.Test.Shared;
using Microsoft.AspNet.Testing.xunit;
using Xunit;
-namespace Microsoft.AspNet.DataProtection.Test.Cng
+namespace Microsoft.AspNet.DataProtection.Cng
{
public class CbcAuthenticatedEncryptorTests
{
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Encrypt_Decrypt_RoundTrips()
{
// Arrange
@@ -36,7 +36,7 @@ namespace Microsoft.AspNet.DataProtection.Test.Cng
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Encrypt_Decrypt_Tampering_Fails()
{
// Arrange
@@ -83,7 +83,7 @@ namespace Microsoft.AspNet.DataProtection.Test.Cng
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Encrypt_KnownKey()
{
// Arrange
diff --git a/test/Microsoft.AspNet.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs
index 1aa5a8afb7..84335935b2 100644
--- a/test/Microsoft.AspNet.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs
+++ b/test/Microsoft.AspNet.DataProtection.Test/Cng/CngAuthenticatedEncryptorBaseTests.cs
@@ -2,17 +2,17 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.AspNet.DataProtection.Test.Shared;
using Microsoft.AspNet.Testing.xunit;
using Moq;
using Xunit;
-namespace Microsoft.AspNet.DataProtection.Test.Cng
+namespace Microsoft.AspNet.DataProtection.Cng
{
public unsafe class CngAuthenticatedEncryptorBaseTests
{
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Decrypt_ForwardsArraySegment()
{
// Arrange
@@ -38,7 +38,7 @@ namespace Microsoft.AspNet.DataProtection.Test.Cng
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Decrypt_HandlesEmptyAADPointerFixup()
{
// Arrange
@@ -64,7 +64,7 @@ namespace Microsoft.AspNet.DataProtection.Test.Cng
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Decrypt_HandlesEmptyCiphertextPointerFixup()
{
// Arrange
diff --git a/test/Microsoft.AspNet.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
index b5d1b757ad..80b57a14f1 100644
--- a/test/Microsoft.AspNet.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
+++ b/test/Microsoft.AspNet.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
@@ -6,16 +6,16 @@ using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Cryptography.Cng;
-using Microsoft.AspNet.DataProtection.Cng;
+using Microsoft.AspNet.DataProtection.Test.Shared;
using Microsoft.AspNet.Testing.xunit;
using Xunit;
-namespace Microsoft.AspNet.DataProtection.Test.Cng
+namespace Microsoft.AspNet.DataProtection.Cng
{
public class GcmAuthenticatedEncryptorTests
{
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Encrypt_Decrypt_RoundTrips()
{
// Arrange
@@ -33,7 +33,7 @@ namespace Microsoft.AspNet.DataProtection.Test.Cng
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Encrypt_Decrypt_Tampering_Fails()
{
// Arrange
@@ -77,7 +77,7 @@ namespace Microsoft.AspNet.DataProtection.Test.Cng
}
[ConditionalFact]
- [ConditionalRunTestOnlyIfBcryptAvailable]
+ [ConditionalRunTestOnlyOnWindows]
public void Encrypt_KnownKey()
{
// Arrange
diff --git a/test/Microsoft.AspNet.DataProtection.Test/ConditionalRunTestOnlyIfBcryptAvailableAttribute.cs b/test/Microsoft.AspNet.DataProtection.Test/ConditionalRunTestOnlyIfBcryptAvailableAttribute.cs
deleted file mode 100644
index 99e1762625..0000000000
--- a/test/Microsoft.AspNet.DataProtection.Test/ConditionalRunTestOnlyIfBcryptAvailableAttribute.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Globalization;
-using Microsoft.AspNet.Cryptography.SafeHandles;
-using Microsoft.AspNet.Testing.xunit;
-
-namespace Microsoft.AspNet.DataProtection.Test
-{
- public class ConditionalRunTestOnlyIfBcryptAvailableAttribute : Attribute, ITestCondition
- {
- private static readonly SafeLibraryHandle _bcryptLibHandle = GetBcryptLibHandle();
-
- private readonly string _requiredExportFunction;
-
- public ConditionalRunTestOnlyIfBcryptAvailableAttribute(string requiredExportFunction = null)
- {
- _requiredExportFunction = requiredExportFunction;
- }
-
- public bool IsMet
- {
- get
- {
- if (_bcryptLibHandle == null)
- {
- return false; // no bcrypt.dll available
- }
-
- return (_requiredExportFunction == null || _bcryptLibHandle.DoesProcExist(_requiredExportFunction));
- }
- }
-
- public string SkipReason
- {
- get
- {
- return (_bcryptLibHandle != null)
- ? String.Format(CultureInfo.InvariantCulture, "Export {0} not found in bcrypt.dll", _requiredExportFunction)
- : "bcrypt.dll not found on this platform.";
- }
- }
-
- private static SafeLibraryHandle GetBcryptLibHandle()
- {
- try
- {
- return SafeLibraryHandle.Open("bcrypt.dll");
- }
- catch
- {
- // If we're not on an OS with BCRYPT.DLL, just bail.
- return null;
- }
- }
- }
-}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs
index c7f50b17cb..2b2c122265 100644
--- a/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs
+++ b/test/Microsoft.AspNet.DataProtection.Test/DataProtectionExtensionsTests.cs
@@ -2,12 +2,10 @@
// 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 Moq;
using Xunit;
-namespace Microsoft.AspNet.DataProtection.Test
+namespace Microsoft.AspNet.DataProtection
{
public class DataProtectionExtensionsTests
{
@@ -38,113 +36,5 @@ namespace Microsoft.AspNet.DataProtection.Test
// Assert
Assert.Same(innerProtector, timeLimitedProtector.InnerProtector);
}
-
- [Theory]
- [InlineData(new object[] { null })]
- [InlineData(new object[] { new string[0] })]
- [InlineData(new object[] { new string[] { null } })]
- [InlineData(new object[] { new string[] { "the next value is bad", "" } })]
- public void CreateProtector_Chained_FailureCases(string[] purposes)
- {
- // Arrange
- var mockProtector = new Mock();
- mockProtector.Setup(o => o.CreateProtector(It.IsAny())).Returns(mockProtector.Object);
- var provider = mockProtector.Object;
-
- // Act & assert
- var ex = Assert.Throws(() => provider.CreateProtector(purposes));
- ex.AssertMessage("purposes", Resources.DataProtectionExtensions_NullPurposesArray);
- }
-
- [Fact]
- public void CreateProtector_Chained_SuccessCase()
- {
- // Arrange
- var finalExpectedProtector = new Mock().Object;
-
- var thirdMock = new Mock();
- thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
- var secondMock = new Mock();
- secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
- var firstMock = new Mock();
- firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
-
- // Act
- var retVal = firstMock.Object.CreateProtector("first", "second", "third");
-
- // Assert
- Assert.Same(finalExpectedProtector, retVal);
- }
-
- [Fact]
- public void Protect_InvalidUtf_Failure()
- {
- // Arrange
- Mock mockProtector = new Mock();
-
- // Act & assert
- var ex = Assert.Throws(() =>
- {
- DataProtectionExtensions.Protect(mockProtector.Object, "Hello\ud800");
- });
- Assert.IsAssignableFrom(typeof(EncoderFallbackException), ex.InnerException);
- }
-
- [Fact]
- public void Protect_Success()
- {
- // Arrange
- Mock mockProtector = new Mock();
- mockProtector.Setup(p => p.Protect(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f })).Returns(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 });
-
- // Act
- string retVal = DataProtectionExtensions.Protect(mockProtector.Object, "Hello");
-
- // Assert
- Assert.Equal("AQIDBAU", retVal);
- }
-
- [Fact]
- public void Unprotect_InvalidBase64BeforeDecryption_Failure()
- {
- // Arrange
- Mock mockProtector = new Mock();
-
- // Act & assert
- var ex = Assert.Throws(() =>
- {
- DataProtectionExtensions.Unprotect(mockProtector.Object, "A");
- });
- Assert.IsAssignableFrom(typeof(FormatException), ex.InnerException);
- }
-
- [Fact]
- public void Unprotect_InvalidUtfAfterDecryption_Failure()
- {
- // Arrange
- Mock mockProtector = new Mock();
- mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0xff });
-
- // Act & assert
- var ex = Assert.Throws(() =>
- {
- DataProtectionExtensions.Unprotect(mockProtector.Object, "AQIDBAU");
- });
- Assert.IsAssignableFrom(typeof(DecoderFallbackException), ex.InnerException);
- }
-
- [Fact]
- public void Unprotect_Success()
- {
- // Arrange
- Mock mockProtector = new Mock();
- mockProtector.Setup(p => p.Unprotect(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 })).Returns(new byte[] { 0x48, 0x65, 0x6c, 0x6c, 0x6f });
-
- // Act
- string retVal = DataProtectionExtensions.Unprotect(mockProtector.Object, "AQIDBAU");
-
- // Assert
- Assert.Equal("Hello", retVal);
- }
}
}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/EphemeralDataProtectionProviderTests.cs b/test/Microsoft.AspNet.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
index 17e86f2279..04acee0a65 100644
--- a/test/Microsoft.AspNet.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
+++ b/test/Microsoft.AspNet.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
@@ -6,7 +6,7 @@ using System.Security.Cryptography;
using System.Text;
using Xunit;
-namespace Microsoft.AspNet.DataProtection.Test
+namespace Microsoft.AspNet.DataProtection
{
public class EphemeralDataProtectionProviderTests
{
diff --git a/test/Microsoft.AspNet.DataProtection.Test/ExceptionHelpers.cs b/test/Microsoft.AspNet.DataProtection.Test/ExceptionHelpers.cs
deleted file mode 100644
index a05290105c..0000000000
--- a/test/Microsoft.AspNet.DataProtection.Test/ExceptionHelpers.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using Xunit;
-
-namespace Microsoft.AspNet.DataProtection.Test
-{
- internal static class ExceptionHelpers
- {
- public static void AssertMessage(this ArgumentException exception, string parameterName, string message)
- {
- Assert.Equal(parameterName, exception.ParamName);
-
- // We'll let ArgumentException handle the message formatting for us and treat it as our control value
- var controlException = new ArgumentException(message, parameterName);
- Assert.Equal(controlException.Message, exception.Message);
- }
- }
-}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs
new file mode 100644
index 0000000000..d92b38ec5a
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/CacheableKeyRingTests.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.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().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().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().Object;
+ var cacheableKeyRing = new CacheableKeyRing(CancellationToken.None, DateTimeOffset.Now, keyRing);
+
+ // Act & assert
+ Assert.Same(keyRing, cacheableKeyRing.KeyRing);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs
new file mode 100644
index 0000000000..7c66fdc3e0
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.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");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-04-01 00:00:00Z", key1);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_ApproachingSafetyWindow_ReturnsExistingKey_SignalsGenerateNewKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-04-01 00:00:00Z");
+ var key2 = CreateKey("2015-04-01 00:00:00Z", "2015-05-01 00:00:00Z", isRevoked: true);
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-03-30 00:00:00Z", key1, key2);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.True(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_ValidExistingKey_ApproachingSafetyWindow_FutureKeyIsValidAndWithinSkew_ReturnsExistingKey_NoSignalToGenerateNewKey()
+ {
+ // Arrange
+ var resolver = CreateDefaultKeyResolver();
+ var key1 = CreateKey("2015-03-01 00:00:00Z", "2015-04-01 00:00:00Z");
+ var key2 = CreateKey("2015-04-01 00:00:00Z", "2015-05-01 00:00:00Z", isRevoked: true);
+ var key3 = CreateKey("2015-04-01 00:01:00Z", "2015-05-01 00:00:00Z");
+
+ // Act
+ var resolution = resolver.ResolveDefaultKeyPolicy("2015-03-31 23:59:00Z", key1, key2, key3);
+
+ // Assert
+ Assert.Same(key1, resolution.DefaultKey);
+ Assert.False(resolution.ShouldGenerateNewKey);
+ }
+
+ [Fact]
+ public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_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_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:53: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);
+ }
+
+ private static IDefaultKeyResolver CreateDefaultKeyResolver()
+ {
+ return new DefaultKeyResolver(
+ keyGenBeforeExpirationWindow: TimeSpan.FromDays(2),
+ maxServerToServerClockSkew: TimeSpan.FromMinutes(7),
+ services: null);
+ }
+
+ private static IKey CreateKey(string activationDate, string expirationDate, bool isRevoked = false)
+ {
+ var mockKey = new Mock();
+ 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);
+ 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)allKeys);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs
new file mode 100644
index 0000000000..755509b42b
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyEscrowServiceProviderExtensionsTests.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml.Linq;
+using Microsoft.Framework.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.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 output = new List();
+
+ var mockKeyEscrowSink = new Mock();
+ mockKeyEscrowSink.Setup(o => o.Store(It.IsAny(), It.IsAny()))
+ .Callback((keyId, element) =>
+ {
+ output.Add(String.Format(CultureInfo.InvariantCulture, "{0:D}: {1}", keyId, element.Name.LocalName));
+ });
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddInstance(mockKeyEscrowSink.Object);
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Act
+ var sink = services.GetKeyEscrowSink();
+ sink.Store(new Guid("39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b"), XElement.Parse(""));
+
+ // Assert
+ Assert.Equal(new[] { "39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b: theElement" }, output);
+ }
+
+ [Fact]
+ public void GetKeyEscrowSink_MultipleKeyEscrowRegistration_ReturnsAggregate()
+ {
+ // Arrange
+ List output = new List();
+
+ var mockKeyEscrowSink1 = new Mock();
+ mockKeyEscrowSink1.Setup(o => o.Store(It.IsAny(), It.IsAny()))
+ .Callback((keyId, element) =>
+ {
+ output.Add(String.Format(CultureInfo.InvariantCulture, "[sink1] {0:D}: {1}", keyId, element.Name.LocalName));
+ });
+
+ var mockKeyEscrowSink2 = new Mock();
+ mockKeyEscrowSink2.Setup(o => o.Store(It.IsAny(), It.IsAny()))
+ .Callback((keyId, element) =>
+ {
+ output.Add(String.Format(CultureInfo.InvariantCulture, "[sink2] {0:D}: {1}", keyId, element.Name.LocalName));
+ });
+
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddInstance(mockKeyEscrowSink1.Object);
+ serviceCollection.AddInstance(mockKeyEscrowSink2.Object);
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Act
+ var sink = services.GetKeyEscrowSink();
+ sink.Store(new Guid("39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b"), XElement.Parse(""));
+
+ // Assert
+ Assert.Equal(new[] { "[sink1] 39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b: theElement", "[sink2] 39974d8e-3e53-4d78-b7e9-4ff64a2a5d7b: theElement" }, output);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs
new file mode 100644
index 0000000000..6bd46fc6c6
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs
@@ -0,0 +1,486 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption;
+using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel;
+using Microsoft.AspNet.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ public class KeyRingBasedDataProtectorTests
+ {
+ [Fact]
+ public void Protect_NullPlaintext_Throws()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock().Object,
+ logger: null,
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ ExceptionAssert2.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();
+ mockEncryptor
+ .Setup(o => o.Encrypt(It.IsAny>(), It.IsAny>()))
+ .Returns, ArraySegment>((actualPlaintext, actualAad) =>
+ {
+ Assert.Equal(expectedPlaintext, actualPlaintext);
+ Assert.Equal(expectedAad, actualAad);
+ return new byte[] { 0x23, 0x29, 0x31, 0x37 }; // ciphertext + tag
+ });
+
+ var mockKeyRing = new Mock(MockBehavior.Strict);
+ mockKeyRing.Setup(o => o.DefaultKeyId).Returns(defaultKey);
+ mockKeyRing.Setup(o => o.DefaultAuthenticatedEncryptor).Returns(mockEncryptor.Object);
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(mockKeyRing.Object);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ 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();
+ mockEncryptor
+ .Setup(o => o.Encrypt(It.IsAny>(), It.IsAny>()))
+ .Returns, ArraySegment>((actualPlaintext, actualAad) =>
+ {
+ Assert.Equal(expectedPlaintext, actualPlaintext);
+ Assert.Equal(expectedAad, actualAad);
+ return new byte[] { 0x23, 0x29, 0x31, 0x37 }; // ciphertext + tag
+ });
+
+ var mockKeyRing = new Mock(MockBehavior.Strict);
+ mockKeyRing.Setup(o => o.DefaultKeyId).Returns(defaultKey);
+ mockKeyRing.Setup(o => o.DefaultAuthenticatedEncryptor).Returns(mockEncryptor.Object);
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(mockKeyRing.Object);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ 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(MockBehavior.Strict).Object,
+ logger: null,
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ var ex = ExceptionAssert2.ThrowsCryptographicException(() => protector.Protect(new byte[0]));
+ Assert.IsAssignableFrom(typeof(MockException), ex.InnerException);
+ }
+
+ [Fact]
+ public void Unprotect_NullProtectedData_Throws()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock().Object,
+ logger: null,
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert
+ ExceptionAssert2.ThrowsArgumentNull(() => protector.Unprotect(protectedData: null), "protectedData");
+ }
+
+ [Fact]
+ public void Unprotect_PayloadTooShort_ThrowsBadMagicHeader()
+ {
+ // Arrange
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: new Mock().Object,
+ logger: null,
+ 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().Object,
+ logger: null,
+ 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().Object,
+ logger: null,
+ 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();
+ mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(new Mock().Object);
+
+ // the keyring has only one key
+ Key key = new Key(Guid.Empty, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
+ var keyRing = new KeyRing(Guid.Empty, new[] { key });
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ 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();
+ mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(new Mock().Object);
+
+ // the keyring has only one key
+ Key key = new Key(keyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
+ key.SetRevoked();
+ var keyRing = new KeyRing(keyId, new[] { key });
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ 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();
+ mockEncryptor
+ .Setup(o => o.Decrypt(It.IsAny>(), It.IsAny>()))
+ .Returns, ArraySegment>((actualCiphertext, actualAad) =>
+ {
+ Assert.Equal(expectedCiphertext, actualCiphertext);
+ Assert.Equal(expectedAad, actualAad);
+ return expectedPlaintext;
+ });
+ var mockDescriptor = new Mock();
+ mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(mockEncryptor.Object);
+
+ Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
+ defaultKey.SetRevoked();
+ var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey });
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act
+ bool requiresMigration, wasRevoked;
+ byte[] retVal = ((IPersistedDataProtector)protector).DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: true,
+ requiresMigration: out requiresMigration,
+ wasRevoked: out 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();
+ mockEncryptor
+ .Setup(o => o.Decrypt(It.IsAny>(), It.IsAny>()))
+ .Returns, ArraySegment>((actualCiphertext, actualAad) =>
+ {
+ Assert.Equal(expectedCiphertext, actualCiphertext);
+ Assert.Equal(expectedAad, actualAad);
+ return expectedPlaintext;
+ });
+ var mockDescriptor = new Mock();
+ mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(mockEncryptor.Object);
+
+ Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
+ var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey });
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert - IDataProtector
+ byte[] retVal = protector.Unprotect(protectedData);
+ Assert.Equal(expectedPlaintext, retVal);
+
+ // Act & assert - IPersistedDataProtector
+ bool requiresMigration, wasRevoked;
+ retVal = ((IPersistedDataProtector)protector).DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: false,
+ requiresMigration: out requiresMigration,
+ wasRevoked: out 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();
+ mockEncryptor
+ .Setup(o => o.Decrypt(It.IsAny>(), It.IsAny>()))
+ .Returns, ArraySegment>((actualCiphertext, actualAad) =>
+ {
+ Assert.Equal(expectedCiphertext, actualCiphertext);
+ Assert.Equal(expectedAad, actualAad);
+ return expectedPlaintext;
+ });
+ var mockDescriptor = new Mock();
+ mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(mockEncryptor.Object);
+
+ Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new Mock().Object);
+ Key embeddedKey = new Key(embeddedKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object);
+ var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey, embeddedKey });
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ originalPurposes: null,
+ newPurpose: "purpose");
+
+ // Act & assert - IDataProtector
+ byte[] retVal = protector.Unprotect(protectedData);
+ Assert.Equal(expectedPlaintext, retVal);
+
+ // Act & assert - IPersistedDataProtector
+ bool requiresMigration, wasRevoked;
+ retVal = ((IPersistedDataProtector)protector).DangerousUnprotect(protectedData,
+ ignoreRevocationErrors: false,
+ requiresMigration: out requiresMigration,
+ wasRevoked: out 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 };
+ Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration(new AuthenticatedEncryptionOptions()).CreateNewDescriptor());
+ var keyRing = new KeyRing(key.KeyId, new[] { key });
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing);
+
+ var protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ 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();
+ mockEncryptor
+ .Setup(o => o.Encrypt(It.IsAny>(), It.IsAny>()))
+ .Returns, ArraySegment>((actualPlaintext, actualAad) =>
+ {
+ Assert.Equal(expectedPlaintext, actualPlaintext);
+ Assert.Equal(expectedAad, actualAad);
+ return new byte[] { 0x23, 0x29, 0x31, 0x37 }; // ciphertext + tag
+ });
+
+ var mockKeyRing = new Mock(MockBehavior.Strict);
+ mockKeyRing.Setup(o => o.DefaultKeyId).Returns(defaultKey);
+ mockKeyRing.Setup(o => o.DefaultAuthenticatedEncryptor).Returns(mockEncryptor.Object);
+ var mockKeyRingProvider = new Mock();
+ mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(mockKeyRing.Object);
+
+ IDataProtector protector = new KeyRingBasedDataProtector(
+ keyRingProvider: mockKeyRingProvider.Object,
+ logger: null,
+ 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();
+
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs
new file mode 100644
index 0000000000..b117c9e215
--- /dev/null
+++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs
@@ -0,0 +1,397 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Framework.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.DataProtection.KeyManagement
+{
+ public class KeyRingProviderTests
+ {
+ [Fact]
+ public void CreateCacheableKeyRing_NoGenerationRequired_DefaultKeyExpiresAfterRefreshPeriod()
+ {
+ // Arrange
+ var callSequence = new List();
+ 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)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();
+ 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)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();
+ 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))
+ },
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable)allKeys1, new DefaultKeyResolution()
+ {
+ DefaultKey = null,
+ ShouldGenerateNewKey = true
+ }),
+ Tuple.Create((DateTimeOffset)now, (IEnumerable)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_WithDefaultKey_CreatesNewKeyWithDeferredActivationAndExpirationBasedOnCreationTime()
+ {
+ // Arrange
+ var callSequence = new List();
+ 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))
+ },
+ resolveDefaultKeyPolicyReturnValues: new[]
+ {
+ Tuple.Create((DateTimeOffset)now, (IEnumerable)allKeys1, new DefaultKeyResolution()
+ {
+ DefaultKey = key1,
+ ShouldGenerateNewKey = true
+ }),
+ Tuple.Create((DateTimeOffset)now, (IEnumerable)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);
+ }
+
+ private static ICacheableKeyRingProvider SetupCreateCacheableKeyRingTestAndCreateKeyManager(
+ IList callSequence,
+ IEnumerable getCacheExpirationTokenReturnValues,
+ IEnumerable> getAllKeysReturnValues,
+ IEnumerable> createNewKeyCallbacks,
+ IEnumerable, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues)
+ {
+ var getCacheExpirationTokenReturnValuesEnumerator = getCacheExpirationTokenReturnValues.GetEnumerator();
+ var mockKeyManager = new Mock(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(), It.IsAny()))
+ .Returns((activationDate, expirationDate) =>
+ {
+ callSequence.Add("CreateNewKey");
+ createNewKeyCallbacksEnumerator.MoveNext();
+ Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item1, activationDate);
+ Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item2, expirationDate);
+ return null; // nobody uses this return value
+ });
+ }
+
+ var resolveDefaultKeyPolicyReturnValuesEnumerator = resolveDefaultKeyPolicyReturnValues.GetEnumerator();
+ var mockDefaultKeyResolver = new Mock(MockBehavior.Strict);
+ mockDefaultKeyResolver.Setup(o => o.ResolveDefaultKeyPolicy(It.IsAny(), It.IsAny>()))
+ .Returns>((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);
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_NoKeyRingCached_CachesAndReturns()
+ {
+ // Arrange
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var expectedKeyRing = new Mock().Object;
+ var mockCacheableKeyRingProvider = new Mock();
+ 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()), Times.Once);
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_KeyRingCached_AfterExpiration_ClearsCache()
+ {
+ // Arrange
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var expectedKeyRing1 = new Mock().Object;
+ var expectedKeyRing2 = new Mock().Object;
+ var mockCacheableKeyRingProvider = new Mock();
+ 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()), Times.Exactly(2));
+ }
+
+ [Fact]
+ public void GetCurrentKeyRing_ImplementsDoubleCheckLockPatternCorrectly()
+ {
+ // Arrange
+ var now = StringToDateTime("2015-03-01 00:00:00Z");
+ var expectedKeyRing = new Mock().Object;
+ var mockCacheableKeyRingProvider = new Mock