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 _keyEncryptorLazy; + private readonly Lazy _keyRepositoryLazy; + + public DefaultKeyServices(IServiceProvider services, ServiceDescriptor keyEncryptorDescriptor, ServiceDescriptor keyRepositoryDescriptor) + { + if (keyEncryptorDescriptor != null) + { + // optional + CryptoUtil.Assert(keyEncryptorDescriptor.ServiceType == typeof(IXmlEncryptor), "Bad service type."); + _keyEncryptorLazy = GetLazyForService(services, keyEncryptorDescriptor); + } + + CryptoUtil.Assert(keyRepositoryDescriptor.ServiceType == typeof(IXmlRepository), "Bad service type."); + _keyRepositoryLazy = GetLazyForService(services, keyRepositoryDescriptor); + } + + /// + /// Gets the default service (could return null). + /// + /// + public IXmlEncryptor GetKeyEncryptor() + { + return (IXmlEncryptor)_keyEncryptorLazy?.Value; + } + + /// + /// Gets the default service (must not be null). + /// + /// + public IXmlRepository GetKeyRepository() + { + return (IXmlRepository)_keyRepositoryLazy.Value ?? CryptoUtil.Fail("GetKeyRepository returned null."); + } + + private static Lazy GetLazyForService(IServiceProvider services, ServiceDescriptor descriptor) + { + CryptoUtil.Assert(descriptor != null && descriptor.Lifetime == ServiceLifetime.Singleton, "Descriptor must represent singleton."); + CryptoUtil.Assert(descriptor.ImplementationFactory != null, "Descriptor must have an implementation factory."); + + // pull the factory out so we don't close over the whole descriptor instance + Func wrapped = descriptor.ImplementationFactory; + return new Lazy(() => wrapped(services)); + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection/NotNullAttribute.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/ICacheableKeyRingProvider.cs similarity index 56% rename from src/Microsoft.AspNet.DataProtection/NotNullAttribute.cs rename to src/Microsoft.AspNet.DataProtection/KeyManagement/ICacheableKeyRingProvider.cs index 5896ea15d8..008b15a607 100644 --- a/src/Microsoft.AspNet.DataProtection/NotNullAttribute.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/ICacheableKeyRingProvider.cs @@ -3,10 +3,10 @@ using System; -namespace Microsoft.AspNet.DataProtection +namespace Microsoft.AspNet.DataProtection.KeyManagement { - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - internal sealed class NotNullAttribute : Attribute + internal interface ICacheableKeyRingProvider { + CacheableKeyRing GetCacheableKeyRing(DateTimeOffset now); } } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IDefaultKeyResolver.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IDefaultKeyResolver.cs new file mode 100644 index 0000000000..a5ecdeda16 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IDefaultKeyResolver.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; +using System.Collections.Generic; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + /// + /// Implements policy for resolving the default key from a candidate keyring. + /// + internal interface IDefaultKeyResolver + { + /// + /// Locates the default key from the keyring. + /// + DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable allKeys); + } +} diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IDefaultKeyServices.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IDefaultKeyServices.cs new file mode 100644 index 0000000000..34a4f2ab8b --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IDefaultKeyServices.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 Microsoft.AspNet.DataProtection.Repositories; +using Microsoft.AspNet.DataProtection.XmlEncryption; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + /// + /// Provides default implementations of the services required by an . + /// + internal interface IDefaultKeyServices + { + /// + /// Gets the default service (could return null). + /// + /// + IXmlEncryptor GetKeyEncryptor(); + + /// + /// Gets the default service (must not be null). + /// + /// + IXmlRepository GetKeyRepository(); + } +} diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs new file mode 100644 index 0000000000..7c2cd20685 --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs @@ -0,0 +1,14 @@ +// 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 +{ + // Used for unit testing + internal interface IInternalXmlKeyManager + { + IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate); + void RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason); + } +} diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKey.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKey.cs index 5356351d7b..1d3288d68d 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/IKey.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IKey.cs @@ -34,8 +34,8 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement /// /// /// 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(); + var keyRingProvider = CreateKeyRingProvider(mockCacheableKeyRingProvider.Object); + + // This test spawns a background thread which calls GetCurrentKeyRing then waits + // for the foreground thread to call GetCurrentKeyRing. When the foreground thread + // blocks (inside the lock), the background thread will return the cached keyring + // object, and the foreground thread should consume that same object instance. + + TimeSpan testTimeout = TimeSpan.FromSeconds(10); + + Thread foregroundThread = Thread.CurrentThread; + ManualResetEventSlim mreBackgroundThreadHasCalledGetCurrentKeyRing = new ManualResetEventSlim(); + ManualResetEventSlim mreForegroundThreadIsCallingGetCurrentKeyRing = new ManualResetEventSlim(); + var backgroundGetKeyRingTask = Task.Run(() => + { + mockCacheableKeyRingProvider + .Setup(o => o.GetCacheableKeyRing(now)) + .Returns(() => + { + mreBackgroundThreadHasCalledGetCurrentKeyRing.Set(); + Assert.True(mreForegroundThreadIsCallingGetCurrentKeyRing.Wait(testTimeout), "Test timed out."); + SpinWait.SpinUntil(() => (foregroundThread.ThreadState & ThreadState.WaitSleepJoin) != 0, testTimeout); + return new CacheableKeyRing( + expirationToken: CancellationToken.None, + expirationTime: StringToDateTime("2015-03-02 00:00:00Z"), + keyRing: expectedKeyRing); + }); + + return keyRingProvider.GetCurrentKeyRingCore(now); + }); + + Assert.True(mreBackgroundThreadHasCalledGetCurrentKeyRing.Wait(testTimeout), "Test timed out."); + mreForegroundThreadIsCallingGetCurrentKeyRing.Set(); + var foregroundRetVal = keyRingProvider.GetCurrentKeyRingCore(now); + backgroundGetKeyRingTask.Wait(testTimeout); + var backgroundRetVal = backgroundGetKeyRingTask.GetAwaiter().GetResult(); + + // Assert - underlying provider only should have been called once + Assert.Same(expectedKeyRing, foregroundRetVal); + Assert.Same(expectedKeyRing, backgroundRetVal); + mockCacheableKeyRingProvider.Verify(o => o.GetCacheableKeyRing(It.IsAny()), Times.Once); + } + + private static KeyRingProvider CreateKeyRingProvider(ICacheableKeyRingProvider cacheableKeyRingProvider) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(cacheableKeyRingProvider); + return new KeyRingProvider( + keyManager: null, + keyLifetimeOptions: null, + services: serviceCollection.BuildServiceProvider()); + } + + private static ICacheableKeyRingProvider CreateKeyRingProvider(IKeyManager keyManager, IDefaultKeyResolver defaultKeyResolver) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(defaultKeyResolver); + return new KeyRingProvider( + keyManager: keyManager, + keyLifetimeOptions: null, + services: serviceCollection.BuildServiceProvider()); + } + + private static void AssertWithinJitterRange(DateTimeOffset actual, DateTimeOffset now) + { + // The jitter can cause the actual value to fall in the range [now + 80% of refresh period, now + 100% of refresh period) + Assert.InRange(actual, now + TimeSpan.FromHours(24 * 0.8), now + TimeSpan.FromHours(24)); + } + + private static DateTime StringToDateTime(string input) + { + return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime; + } + + private static IKey CreateKey(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; + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.cs new file mode 100644 index 0000000000..aa192fc4d6 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.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 Microsoft.AspNet.DataProtection.AuthenticatedEncryption; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + public class KeyRingTests + { + [Fact] + public void DefaultAuthenticatedEncryptor_Prop_InstantiationIsDeferred() + { + // Arrange + var expectedEncryptorInstance = new Mock().Object; + + var key1 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance); + var key2 = new MyKey(); + + // Act + var keyRing = new KeyRing(key1.KeyId, new[] { key1, key2 }); + + // Assert + Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance, keyRing.DefaultAuthenticatedEncryptor); + Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance, keyRing.DefaultAuthenticatedEncryptor); + Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled); // should've been cached + } + + [Fact] + public void DefaultKeyId_Prop() + { + // Arrange + var key1 = new MyKey(); + var key2 = new MyKey(); + + // Act + var keyRing = new KeyRing(key2.KeyId, new[] { key1, key2 }); + + // Assert + Assert.Equal(key2.KeyId, keyRing.DefaultKeyId); + } + + [Fact] + public void GetAuthenticatedEncryptorByKeyId_DefersInstantiation_AndReturnsRevocationInfo() + { + // Arrange + var expectedEncryptorInstance1 = new Mock().Object; + var expectedEncryptorInstance2 = new Mock().Object; + + var key1 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance1, isRevoked: true); + var key2 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance2); + + // Act + var keyRing = new KeyRing(key2.KeyId, new[] { key1, key2 }); + + // Assert + bool isRevoked; + Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance1, keyRing.GetAuthenticatedEncryptorByKeyId(key1.KeyId, out isRevoked)); + Assert.True(isRevoked); + Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance1, keyRing.GetAuthenticatedEncryptorByKeyId(key1.KeyId, out isRevoked)); + Assert.True(isRevoked); + Assert.Equal(1, key1.NumTimesCreateEncryptorInstanceCalled); + Assert.Equal(0, key2.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance2, keyRing.GetAuthenticatedEncryptorByKeyId(key2.KeyId, out isRevoked)); + Assert.False(isRevoked); + Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance2, keyRing.GetAuthenticatedEncryptorByKeyId(key2.KeyId, out isRevoked)); + Assert.False(isRevoked); + Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled); + Assert.Same(expectedEncryptorInstance2, keyRing.DefaultAuthenticatedEncryptor); + Assert.Equal(1, key2.NumTimesCreateEncryptorInstanceCalled); + } + + private sealed class MyKey : IKey + { + public int NumTimesCreateEncryptorInstanceCalled; + private readonly Func _encryptorFactory; + + public MyKey(bool isRevoked = false, IAuthenticatedEncryptor expectedEncryptorInstance = null) + { + CreationDate = DateTimeOffset.Now; + ActivationDate = CreationDate + TimeSpan.FromHours(1); + ExpirationDate = CreationDate + TimeSpan.FromDays(30); + IsRevoked = isRevoked; + KeyId = Guid.NewGuid(); + _encryptorFactory = () => expectedEncryptorInstance ?? new Mock().Object; + } + + public DateTimeOffset ActivationDate { get; } + public DateTimeOffset CreationDate { get; } + public DateTimeOffset ExpirationDate { get; } + public bool IsRevoked { get; } + public Guid KeyId { get; } + + public IAuthenticatedEncryptor CreateEncryptorInstance() + { + NumTimesCreateEncryptorInstanceCalled++; + return _encryptorFactory(); + } + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs new file mode 100644 index 0000000000..388b5bc67e --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs @@ -0,0 +1,747 @@ +// 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; +using System.Xml.Linq; +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.Logging; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + public class XmlKeyManagerTests + { + private static readonly XElement serializedDescriptor = XElement.Parse(@" + + + + + "); + + [Fact] + public void Ctor_WithoutEncryptorOrRepository_UsesFallback() + { + // Arrange + var expectedEncryptor = new Mock().Object; + var expectedRepository = new Mock().Object; + var mockFallback = new Mock(); + mockFallback.Setup(o => o.GetKeyEncryptor()).Returns(expectedEncryptor); + mockFallback.Setup(o => o.GetKeyRepository()).Returns(expectedRepository); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockFallback.Object); + serviceCollection.AddInstance(new Mock().Object); + var services = serviceCollection.BuildServiceProvider(); + + // Act + var keyManager = new XmlKeyManager(services); + + // Assert + Assert.Same(expectedEncryptor, keyManager.KeyEncryptor); + Assert.Same(expectedRepository, keyManager.KeyRepository); + } + + [Fact] + public void Ctor_WithEncryptorButNoRepository_IgnoresFallback_FailsWithServiceNotFound() + { + // Arrange + var mockFallback = new Mock(); + mockFallback.Setup(o => o.GetKeyEncryptor()).Returns(new Mock().Object); + mockFallback.Setup(o => o.GetKeyRepository()).Returns(new Mock().Object); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockFallback.Object); + serviceCollection.AddInstance(new Mock().Object); + serviceCollection.AddInstance(new Mock().Object); + var services = serviceCollection.BuildServiceProvider(); + + // Act & assert - we don't care about exception type, only exception message + Exception ex = Assert.ThrowsAny(() => new XmlKeyManager(services)); + Assert.Contains("IXmlRepository", ex.Message); + } + + [Fact] + public void CreateNewKey_Internal_NoEscrowOrEncryption() + { + // Constants + var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero); + var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero); + var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero); + var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093"); + + // Arrange - mocks + XElement elementStoredInRepository = null; + string friendlyNameStoredInRepository = null; + var expectedAuthenticatedEncryptor = new Mock().Object; + var mockDescriptor = new Mock(); + mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer))); + mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expectedAuthenticatedEncryptor); + var mockConfiguration = new Mock(); + mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(mockDescriptor.Object); + var mockXmlRepository = new Mock(); + mockXmlRepository + .Setup(o => o.StoreElement(It.IsAny(), It.IsAny())) + .Callback((el, friendlyName) => + { + elementStoredInRepository = el; + friendlyNameStoredInRepository = friendlyName; + }); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockXmlRepository.Object); + serviceCollection.AddInstance(mockConfiguration.Object); + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + // Act & assert + + // The cancellation token should not already be fired + var firstCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.False(firstCancellationToken.IsCancellationRequested); + + // After the call to CreateNewKey, the first CT should be fired, + // and we should've gotten a new CT. + var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey( + keyId: keyId, + creationDate: creationDate, + activationDate: activationDate, + expirationDate: expirationDate); + var secondCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.True(firstCancellationToken.IsCancellationRequested); + Assert.False(secondCancellationToken.IsCancellationRequested); + + // Does the IKey have the properties we requested? + Assert.Equal(keyId, newKey.KeyId); + Assert.Equal(creationDate, newKey.CreationDate); + Assert.Equal(activationDate, newKey.ActivationDate); + Assert.Equal(expirationDate, newKey.ExpirationDate); + Assert.False(newKey.IsRevoked); + Assert.Same(expectedAuthenticatedEncryptor, newKey.CreateEncryptorInstance()); + + // Finally, was the correct element stored in the repository? + string expectedXml = String.Format(@" + + 2014-01-01T00:00:00Z + 2014-02-01T00:00:00Z + 2014-03-01T00:00:00Z + + + + + + + + ", + typeof(MyDeserializer).AssemblyQualifiedName); + XmlAssert.Equal(expectedXml, elementStoredInRepository); + Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository); + } + + [Fact] + public void CreateNewKey_Internal_WithEscrowAndEncryption() + { + // Constants + var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero); + var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero); + var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero); + var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093"); + + // Arrange - mocks + XElement elementStoredInEscrow = null; + Guid? keyIdStoredInEscrow = null; + XElement elementStoredInRepository = null; + string friendlyNameStoredInRepository = null; + var expectedAuthenticatedEncryptor = new Mock().Object; + var mockDescriptor = new Mock(); + mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer))); + mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expectedAuthenticatedEncryptor); + var mockConfiguration = new Mock(); + mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(mockDescriptor.Object); + var mockXmlRepository = new Mock(); + mockXmlRepository + .Setup(o => o.StoreElement(It.IsAny(), It.IsAny())) + .Callback((el, friendlyName) => + { + elementStoredInRepository = el; + friendlyNameStoredInRepository = friendlyName; + }); + var mockKeyEscrow = new Mock(); + mockKeyEscrow + .Setup(o => o.Store(It.IsAny(), It.IsAny())) + .Callback((innerKeyId, el) => + { + keyIdStoredInEscrow = innerKeyId; + elementStoredInEscrow = el; + }); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockXmlRepository.Object); + serviceCollection.AddInstance(mockConfiguration.Object); + serviceCollection.AddInstance(mockKeyEscrow.Object); + serviceCollection.AddSingleton(); + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + // Act & assert + + // The cancellation token should not already be fired + var firstCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.False(firstCancellationToken.IsCancellationRequested); + + // After the call to CreateNewKey, the first CT should be fired, + // and we should've gotten a new CT. + var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey( + keyId: keyId, + creationDate: creationDate, + activationDate: activationDate, + expirationDate: expirationDate); + var secondCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.True(firstCancellationToken.IsCancellationRequested); + Assert.False(secondCancellationToken.IsCancellationRequested); + + // Does the IKey have the properties we requested? + Assert.Equal(keyId, newKey.KeyId); + Assert.Equal(creationDate, newKey.CreationDate); + Assert.Equal(activationDate, newKey.ActivationDate); + Assert.Equal(expirationDate, newKey.ExpirationDate); + Assert.False(newKey.IsRevoked); + Assert.Same(expectedAuthenticatedEncryptor, newKey.CreateEncryptorInstance()); + + // Was the correct element stored in escrow? + // This should not have gone through the encryptor. + string expectedEscrowXml = String.Format(@" + + 2014-01-01T00:00:00Z + 2014-02-01T00:00:00Z + 2014-03-01T00:00:00Z + + + + + + + + ", + typeof(MyDeserializer).AssemblyQualifiedName); + XmlAssert.Equal(expectedEscrowXml, elementStoredInEscrow); + Assert.Equal(keyId, keyIdStoredInEscrow.Value); + + // Finally, was the correct element stored in the repository? + // This should have gone through the encryptor (which we set to be the null encryptor in this test) + string expectedRepositoryXml = String.Format(@" + + 2014-01-01T00:00:00Z + 2014-02-01T00:00:00Z + 2014-03-01T00:00:00Z + + + + + + + + + + + + ", + typeof(MyDeserializer).AssemblyQualifiedName, + typeof(NullXmlDecryptor).AssemblyQualifiedName); + XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository); + Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository); + } + + [Fact] + public void CreateNewKey_CallsInternalManager() + { + // Arrange - mocks + DateTimeOffset minCreationDate = DateTimeOffset.UtcNow; + DateTimeOffset? actualCreationDate = null; + DateTimeOffset activationDate = minCreationDate + TimeSpan.FromDays(7); + DateTimeOffset expirationDate = activationDate.AddMonths(1); + var mockInternalKeyManager = new Mock(); + mockInternalKeyManager + .Setup(o => o.CreateNewKey(It.IsAny(), It.IsAny(), activationDate, expirationDate)) + .Callback((innerKeyId, innerCreationDate, innerActivationDate, innerExpirationDate) => + { + actualCreationDate = innerCreationDate; + }); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(new Mock().Object); + serviceCollection.AddInstance(new Mock().Object); + serviceCollection.AddInstance(mockInternalKeyManager.Object); + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + // Act + keyManager.CreateNewKey(activationDate, expirationDate); + + // Assert + Assert.InRange(actualCreationDate.Value, minCreationDate, DateTimeOffset.UtcNow); + } + + [Fact] + public void GetAllKeys_Empty() + { + // Arrange + const string xml = @""; + var activator = new Mock().Object; + + // Act + var keys = RunGetAllKeysCore(xml, activator); + + // Assert + Assert.Equal(0, keys.Count); + } + + [Fact] + public void GetAllKeys_IgnoresUnknownElements() + { + // Arrange + const string xml = @" + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + + + + + 2015-04-01T00:00:00Z + 2015-05-01T00:00:00Z + 2015-06-01T00:00:00Z + + + + + "; + + var encryptorA = new Mock().Object; + var encryptorB = new Mock().Object; + var mockActivator = new Mock(); + mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("deserializer-A", "", encryptorA); + mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("deserializer-B", "", encryptorB); + + // Act + var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray(); + + // Assert + Assert.Equal(2, keys.Length); + Assert.Equal(new Guid("62a72ad9-42d7-4e97-b3fa-05bad5d53d33"), keys[0].KeyId); + Assert.Equal(XmlConvert.ToDateTimeOffset("2015-01-01T00:00:00Z"), keys[0].CreationDate); + Assert.Equal(XmlConvert.ToDateTimeOffset("2015-02-01T00:00:00Z"), keys[0].ActivationDate); + Assert.Equal(XmlConvert.ToDateTimeOffset("2015-03-01T00:00:00Z"), keys[0].ExpirationDate); + Assert.False(keys[0].IsRevoked); + Assert.Same(encryptorA, keys[0].CreateEncryptorInstance()); + Assert.Equal(new Guid("041be4c0-52d7-48b4-8d32-f8c0ff315459"), keys[1].KeyId); + Assert.Equal(XmlConvert.ToDateTimeOffset("2015-04-01T00:00:00Z"), keys[1].CreationDate); + Assert.Equal(XmlConvert.ToDateTimeOffset("2015-05-01T00:00:00Z"), keys[1].ActivationDate); + Assert.Equal(XmlConvert.ToDateTimeOffset("2015-06-01T00:00:00Z"), keys[1].ExpirationDate); + Assert.False(keys[1].IsRevoked); + Assert.Same(encryptorB, keys[1].CreateEncryptorInstance()); + } + + [Fact] + public void GetAllKeys_UnderstandsRevocations() + { + // Arrange + const string xml = @" + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + + 2016-01-01T00:00:00Z + 2016-02-01T00:00:00Z + 2016-03-01T00:00:00Z + + + + + + 2017-01-01T00:00:00Z + 2017-02-01T00:00:00Z + 2017-03-01T00:00:00Z + + + + + + 2018-01-01T00:00:00Z + 2018-02-01T00:00:00Z + 2018-03-01T00:00:00Z + + + + + + + 2014-01-01T00:00:00Z + + + + + 2016-01-01T00:00:00Z + + + + + 2020-01-01T00:00:00Z + + + "; + + var mockActivator = new Mock(); + mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("theDeserializer", "", new Mock().Object); + + // Act + var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray(); + + // Assert + Assert.Equal(4, keys.Length); + Assert.Equal(new Guid("67f9cdea-83ba-41ed-b160-2b1d0ea30251"), keys[0].KeyId); + Assert.True(keys[0].IsRevoked); + Assert.Equal(new Guid("0cf83742-d175-42a8-94b5-1ec049b354c3"), keys[1].KeyId); + Assert.True(keys[1].IsRevoked); + Assert.Equal(new Guid("21580ac4-c83a-493c-bde6-29a1cc97ca0f"), keys[2].KeyId); + Assert.False(keys[2].IsRevoked); + Assert.Equal(new Guid("6bd14f12-0bb8-4822-91d7-04b360de0497"), keys[3].KeyId); + Assert.True(keys[3].IsRevoked); + } + + [Fact] + public void GetAllKeys_PerformsDecryption() + { + // Arrange + const string xml = @" + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + + + "; + + var expectedEncryptor = new Mock().Object; + var mockActivator = new Mock(); + mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput("theDecryptor", "", ""); + mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("theDeserializer", "", expectedEncryptor); + + // Act + var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray(); + + // Assert + Assert.Equal(1, keys.Length); + Assert.Equal(new Guid("09712588-ba68-438a-a5ee-fe842b3453b2"), keys[0].KeyId); + Assert.Same(expectedEncryptor, keys[0].CreateEncryptorInstance()); + } + + [Fact] + public void GetAllKeys_SwallowsKeyDeserializationErrors() + { + // Arrange + const string xml = @" + + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + "; + + var expectedEncryptor = new Mock().Object; + var mockActivator = new Mock(); + mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("goodDeserializer", "", expectedEncryptor); + mockActivator.Setup(o => o.CreateInstance(It.IsAny(), "badDeserializer")).Throws(new Exception("How exceptional!")); + + // Act + var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray(); + + // Assert + Assert.Equal(1, keys.Length); + Assert.Equal(new Guid("49c0cda9-0232-4d8c-a541-de20cc5a73d6"), keys[0].KeyId); + Assert.Same(expectedEncryptor, keys[0].CreateEncryptorInstance()); + } + + [Fact] + public void GetAllKeys_WithKeyDeserializationError_LogLevelVerbose_DoesNotWriteSensitiveInformation() + { + // Arrange + const string xml = @" + + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + + + "; + + var mockActivator = new Mock(); + mockActivator.Setup(o => o.CreateInstance(It.IsAny(), "badDeserializer")).Throws(new Exception("Secret information: 9Z8Y7X6W")); + + var loggerFactory = new StringLoggerFactory(LogLevel.Verbose); + + // Act + RunGetAllKeysCore(xml, mockActivator.Object, loggerFactory).ToArray(); + + // Assert + Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged."); + Assert.False(loggerFactory.ToString().Contains("9Z8Y7X6W"), "The secret '1A2B3C4D' should not have been logged."); + } + + [Fact] + public void GetAllKeys_WithKeyDeserializationError_LogLevelDebug_WritesSensitiveInformation() + { + // Arrange + const string xml = @" + + + + 2015-01-01T00:00:00Z + 2015-02-01T00:00:00Z + 2015-03-01T00:00:00Z + + + + + + + "; + + var mockActivator = new Mock(); + mockActivator.Setup(o => o.CreateInstance(It.IsAny(), "badDeserializer")).Throws(new Exception("Secret information: 9Z8Y7X6W")); + + var loggerFactory = new StringLoggerFactory(LogLevel.Debug); + + // Act + RunGetAllKeysCore(xml, mockActivator.Object, loggerFactory).ToArray(); + + // Assert + Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged."); + Assert.True(loggerFactory.ToString().Contains("9Z8Y7X6W"), "The secret '9Z8Y7X6W' should have been logged."); + } + + [Fact] + public void GetAllKeys_SurfacesRevocationDeserializationErrors() + { + // Arrange + const string xml = @" + + + 2015-01-01T00:00:00Z + + + "; + + // Act & assert + // Bad GUID will lead to FormatException + Assert.Throws(() => RunGetAllKeysCore(xml, new Mock().Object)); + } + + private static IReadOnlyCollection RunGetAllKeysCore(string xml, IActivator activator, ILoggerFactory loggerFactory = null) + { + // Arrange - mocks + var mockXmlRepository = new Mock(); + mockXmlRepository.Setup(o => o.GetAllElements()).Returns(XElement.Parse(xml).Elements().ToArray()); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockXmlRepository.Object); + serviceCollection.AddInstance(activator); + serviceCollection.AddInstance(new Mock().Object); + if (loggerFactory != null) + { + serviceCollection.AddInstance(loggerFactory); + } + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + // Act + return keyManager.GetAllKeys(); + } + + [Fact] + public void RevokeAllKeys() + { + // Arrange - mocks + XElement elementStoredInRepository = null; + string friendlyNameStoredInRepository = null; + var mockXmlRepository = new Mock(); + mockXmlRepository + .Setup(o => o.StoreElement(It.IsAny(), It.IsAny())) + .Callback((el, friendlyName) => + { + elementStoredInRepository = el; + friendlyNameStoredInRepository = friendlyName; + }); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockXmlRepository.Object); + serviceCollection.AddInstance(new Mock().Object); + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + var revocationDate = XmlConvert.ToDateTimeOffset("2015-03-01T19:13:19.7573854-08:00"); + + // Act & assert + + // The cancellation token should not already be fired + var firstCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.False(firstCancellationToken.IsCancellationRequested); + + // After the call to RevokeAllKeys, the first CT should be fired, + // and we should've gotten a new CT. + keyManager.RevokeAllKeys(revocationDate, "Here's some reason text."); + var secondCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.True(firstCancellationToken.IsCancellationRequested); + Assert.False(secondCancellationToken.IsCancellationRequested); + + // Was the correct element stored in the repository? + const string expectedRepositoryXml = @" + + 2015-03-01T19:13:19.7573854-08:00 + + + Here's some reason text. + "; + XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository); + Assert.Equal("revocation-20150302T0313197573854Z", friendlyNameStoredInRepository); + } + + [Fact] + public void RevokeSingleKey_Internal() + { + // Arrange - mocks + XElement elementStoredInRepository = null; + string friendlyNameStoredInRepository = null; + var mockXmlRepository = new Mock(); + mockXmlRepository + .Setup(o => o.StoreElement(It.IsAny(), It.IsAny())) + .Callback((el, friendlyName) => + { + elementStoredInRepository = el; + friendlyNameStoredInRepository = friendlyName; + }); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockXmlRepository.Object); + serviceCollection.AddInstance(new Mock().Object); + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + var revocationDate = DateTimeOffset.UtcNow; + + // Act & assert + + // The cancellation token should not already be fired + var firstCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.False(firstCancellationToken.IsCancellationRequested); + + // After the call to RevokeKey, the first CT should be fired, + // and we should've gotten a new CT. + ((IInternalXmlKeyManager)keyManager).RevokeSingleKey( + keyId: new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932"), + revocationDate: new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero), + reason: "Here's some reason text."); + var secondCancellationToken = keyManager.GetCacheExpirationToken(); + Assert.True(firstCancellationToken.IsCancellationRequested); + Assert.False(secondCancellationToken.IsCancellationRequested); + + // Was the correct element stored in the repository? + const string expectedRepositoryXml = @" + + 2014-01-01T00:00:00Z + + Here's some reason text. + "; + XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository); + Assert.Equal("revocation-a11f35fc-1fed-4bd4-b727-056a63b70932", friendlyNameStoredInRepository); + } + + [Fact] + public void RevokeKey_CallsInternalManager() + { + // Arrange - mocks + var keyToRevoke = new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932"); + DateTimeOffset minRevocationDate = DateTimeOffset.UtcNow; + DateTimeOffset? actualRevocationDate = null; + var mockInternalKeyManager = new Mock(); + mockInternalKeyManager + .Setup(o => o.RevokeSingleKey(keyToRevoke, It.IsAny(), "Here's some reason text.")) + .Callback((innerKeyId, innerRevocationDate, innerReason) => + { + actualRevocationDate = innerRevocationDate; + }); + + // Arrange - services + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(new Mock().Object); + serviceCollection.AddInstance(new Mock().Object); + serviceCollection.AddInstance(mockInternalKeyManager.Object); + var services = serviceCollection.BuildServiceProvider(); + var keyManager = new XmlKeyManager(services); + + // Act + keyManager.RevokeKey(keyToRevoke, "Here's some reason text."); + + // Assert + Assert.InRange(actualRevocationDate.Value, minRevocationDate, DateTimeOffset.UtcNow); + } + + private class MyDeserializer : IAuthenticatedEncryptorDescriptorDeserializer + { + public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs index 812e9bf653..9f5ae98d5f 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/Managed/ManagedAuthenticatedEncryptorTests.cs @@ -5,10 +5,9 @@ using System; using System.Linq; using System.Security.Cryptography; using System.Text; -using Microsoft.AspNet.DataProtection.Managed; using Xunit; -namespace Microsoft.AspNet.DataProtection.Test.Managed +namespace Microsoft.AspNet.DataProtection.Managed { public class ManagedAuthenticatedEncryptorTests { diff --git a/test/Microsoft.AspNet.DataProtection.Test/MockExtensions.cs b/test/Microsoft.AspNet.DataProtection.Test/MockExtensions.cs new file mode 100644 index 0000000000..92cc02c25c --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/MockExtensions.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; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel; +using Microsoft.AspNet.DataProtection.XmlEncryption; +using Moq; + +namespace Microsoft.AspNet.DataProtection +{ + internal static class MockExtensions + { + /// + /// Sets up a mock such that given the name of a deserializer class and the XML node that class's + /// Import method should expect returns a descriptor which produces the given authenticator. + /// + public static void ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput(this Mock mockActivator, string typeName, string xml, IAuthenticatedEncryptor encryptor) + { + mockActivator + .Setup(o => o.CreateInstance(typeof(IAuthenticatedEncryptorDescriptorDeserializer), typeName)) + .Returns(() => + { + var mockDeserializer = new Mock(); + mockDeserializer + .Setup(o => o.ImportFromXml(It.IsAny())) + .Returns(el => + { + // Only return the descriptor if the XML matches + XmlAssert.Equal(xml, el); + var mockDescriptor = new Mock(); + mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(encryptor); + return mockDescriptor.Object; + }); + return mockDeserializer.Object; + }); + } + + /// + /// Sets up a mock such that given the name of a decryptor class and the XML node that class's + /// Decrypt method should expect returns the specified XML elmeent. + /// + public static void ReturnDecryptedElementGivenDecryptorTypeNameAndInput(this Mock mockActivator, string typeName, string expectedInputXml, string outputXml) + { + mockActivator + .Setup(o => o.CreateInstance(typeof(IXmlDecryptor), typeName)) + .Returns(() => + { + var mockDecryptor = new Mock(); + mockDecryptor + .Setup(o => o.Decrypt(It.IsAny())) + .Returns(el => + { + // Only return the descriptor if the XML matches + XmlAssert.Equal(expectedInputXml, el); + return XElement.Parse(outputXml); + }); + return mockDecryptor.Object; + }); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs b/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs new file mode 100644 index 0000000000..12f2818957 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/RegistryPolicyResolverTests.cs @@ -0,0 +1,282 @@ +// 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.Security.Cryptography; +using System.Xml.Linq; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel; +using Microsoft.AspNet.DataProtection.KeyManagement; +using Microsoft.AspNet.Testing.xunit; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; +using Microsoft.Win32; +using Xunit; + +namespace Microsoft.AspNet.DataProtection +{ + public class RegistryPolicyResolverTests + { + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_NoEntries_ResultsInNoPolicies() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["unused"] = 42 + }); + + Assert.Empty(serviceCollection); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_KeyEscrowSinks() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["KeyEscrowSinks"] = String.Join(" ;; ; ", new Type[] { typeof(MyKeyEscrowSink1), typeof(MyKeyEscrowSink2) }.Select(t => t.AssemblyQualifiedName)) + }); + + var services = serviceCollection.BuildServiceProvider(); + var actualKeyEscrowSinks = services.GetService>().ToArray(); + Assert.Equal(2, actualKeyEscrowSinks.Length); + Assert.IsType(typeof(MyKeyEscrowSink1), actualKeyEscrowSinks[0]); + Assert.IsType(typeof(MyKeyEscrowSink2), actualKeyEscrowSinks[1]); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_DefaultKeyLifetime() + { + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection.AddOptions(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["DefaultKeyLifetime"] = 1024 // days + }); + + var services = serviceCollection.BuildServiceProvider(); + var keyLifetimeOptions = services.GetService>(); + Assert.Equal(TimeSpan.FromDays(1024), keyLifetimeOptions.Options.NewKeyLifetime); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_CngCbcEncryption_WithoutExplicitSettings() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["EncryptionType"] = "cng-cbc" + }); + + var services = serviceCollection.BuildServiceProvider(); + var expectedConfiguration = new CngCbcAuthenticatedEncryptorConfiguration(new CngCbcAuthenticatedEncryptionOptions()); + var actualConfiguration = (CngCbcAuthenticatedEncryptorConfiguration)services.GetService(); + + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithm, actualConfiguration.Options.EncryptionAlgorithm); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmKeySize, actualConfiguration.Options.EncryptionAlgorithmKeySize); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmProvider, actualConfiguration.Options.EncryptionAlgorithmProvider); + Assert.Equal(expectedConfiguration.Options.HashAlgorithm, actualConfiguration.Options.HashAlgorithm); + Assert.Equal(expectedConfiguration.Options.HashAlgorithmProvider, actualConfiguration.Options.HashAlgorithmProvider); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_CngCbcEncryption_WithExplicitSettings() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["EncryptionType"] = "cng-cbc", + ["EncryptionAlgorithm"] = "enc-alg", + ["EncryptionAlgorithmKeySize"] = 2048, + ["EncryptionAlgorithmProvider"] = "my-enc-alg-provider", + ["HashAlgorithm"] = "hash-alg", + ["HashAlgorithmProvider"] = "my-hash-alg-provider" + }); + + var services = serviceCollection.BuildServiceProvider(); + var expectedConfiguration = new CngCbcAuthenticatedEncryptorConfiguration(new CngCbcAuthenticatedEncryptionOptions() + { + EncryptionAlgorithm = "enc-alg", + EncryptionAlgorithmKeySize = 2048, + EncryptionAlgorithmProvider = "my-enc-alg-provider", + HashAlgorithm = "hash-alg", + HashAlgorithmProvider = "my-hash-alg-provider" + }); + var actualConfiguration = (CngCbcAuthenticatedEncryptorConfiguration)services.GetService(); + + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithm, actualConfiguration.Options.EncryptionAlgorithm); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmKeySize, actualConfiguration.Options.EncryptionAlgorithmKeySize); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmProvider, actualConfiguration.Options.EncryptionAlgorithmProvider); + Assert.Equal(expectedConfiguration.Options.HashAlgorithm, actualConfiguration.Options.HashAlgorithm); + Assert.Equal(expectedConfiguration.Options.HashAlgorithmProvider, actualConfiguration.Options.HashAlgorithmProvider); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_CngGcmEncryption_WithoutExplicitSettings() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["EncryptionType"] = "cng-gcm" + }); + + var services = serviceCollection.BuildServiceProvider(); + var expectedConfiguration = new CngGcmAuthenticatedEncryptorConfiguration(new CngGcmAuthenticatedEncryptionOptions()); + var actualConfiguration = (CngGcmAuthenticatedEncryptorConfiguration)services.GetService(); + + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithm, actualConfiguration.Options.EncryptionAlgorithm); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmKeySize, actualConfiguration.Options.EncryptionAlgorithmKeySize); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmProvider, actualConfiguration.Options.EncryptionAlgorithmProvider); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_CngGcmEncryption_WithExplicitSettings() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["EncryptionType"] = "cng-gcm", + ["EncryptionAlgorithm"] = "enc-alg", + ["EncryptionAlgorithmKeySize"] = 2048, + ["EncryptionAlgorithmProvider"] = "my-enc-alg-provider" + }); + + var services = serviceCollection.BuildServiceProvider(); + var expectedConfiguration = new CngGcmAuthenticatedEncryptorConfiguration(new CngGcmAuthenticatedEncryptionOptions() + { + EncryptionAlgorithm = "enc-alg", + EncryptionAlgorithmKeySize = 2048, + EncryptionAlgorithmProvider = "my-enc-alg-provider" + }); + var actualConfiguration = (CngGcmAuthenticatedEncryptorConfiguration)services.GetService(); + + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithm, actualConfiguration.Options.EncryptionAlgorithm); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmKeySize, actualConfiguration.Options.EncryptionAlgorithmKeySize); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmProvider, actualConfiguration.Options.EncryptionAlgorithmProvider); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_ManagedEncryption_WithoutExplicitSettings() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["EncryptionType"] = "managed" + }); + + var services = serviceCollection.BuildServiceProvider(); + var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration(new ManagedAuthenticatedEncryptionOptions()); + var actualConfiguration = (ManagedAuthenticatedEncryptorConfiguration)services.GetService(); + + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmType, actualConfiguration.Options.EncryptionAlgorithmType); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmKeySize, actualConfiguration.Options.EncryptionAlgorithmKeySize); + Assert.Equal(expectedConfiguration.Options.ValidationAlgorithmType, actualConfiguration.Options.ValidationAlgorithmType); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void ResolvePolicy_ManagedEncryption_WithExplicitSettings() + { + IServiceCollection serviceCollection = new ServiceCollection(); + RunTestWithRegValues(serviceCollection, new Dictionary() + { + ["EncryptionType"] = "managed", + ["EncryptionAlgorithmType"] = typeof(TripleDES).AssemblyQualifiedName, + ["EncryptionAlgorithmKeySize"] = 2048, + ["ValidationAlgorithmType"] = typeof(HMACMD5).AssemblyQualifiedName + }); + + var services = serviceCollection.BuildServiceProvider(); + var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration(new ManagedAuthenticatedEncryptionOptions() + { + EncryptionAlgorithmType = typeof(TripleDES), + EncryptionAlgorithmKeySize = 2048, + ValidationAlgorithmType = typeof(HMACMD5) + }); + var actualConfiguration = (ManagedAuthenticatedEncryptorConfiguration)services.GetService(); + + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmType, actualConfiguration.Options.EncryptionAlgorithmType); + Assert.Equal(expectedConfiguration.Options.EncryptionAlgorithmKeySize, actualConfiguration.Options.EncryptionAlgorithmKeySize); + Assert.Equal(expectedConfiguration.Options.ValidationAlgorithmType, actualConfiguration.Options.ValidationAlgorithmType); + } + + private static void RunTestWithRegValues(IServiceCollection services, Dictionary regValues) + { + WithUniqueTempRegKey(registryKey => + { + foreach (var entry in regValues) + { + registryKey.SetValue(entry.Key, entry.Value); + } + + var policyResolver = new RegistryPolicyResolver(registryKey); + services.Add(policyResolver.ResolvePolicy()); + }); + } + + /// + /// Runs a test and cleans up the registry key afterward. + /// + private static void WithUniqueTempRegKey(Action testCode) + { + string uniqueName = Guid.NewGuid().ToString(); + var uniqueSubkey = LazyHkcuTempKey.Value.CreateSubKey(uniqueName); + try + { + testCode(uniqueSubkey); + } + finally + { + // clean up when test is done + LazyHkcuTempKey.Value.DeleteSubKeyTree(uniqueName, throwOnMissingSubKey: false); + } + } + + private static readonly Lazy LazyHkcuTempKey = new Lazy(() => + { + try + { + return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\ASP.NET\temp"); + } + catch + { + // swallow all failures + return null; + } + }); + + private class ConditionalRunTestOnlyIfHkcuRegistryAvailable : Attribute, ITestCondition + { + public bool IsMet => (LazyHkcuTempKey.Value != null); + + public string SkipReason { get; } = "HKCU registry couldn't be opened."; + } + + private class MyKeyEscrowSink1 : IKeyEscrowSink + { + public void Store(Guid keyId, XElement element) + { + throw new NotImplementedException(); + } + } + + private class MyKeyEscrowSink2 : IKeyEscrowSink + { + public void Store(Guid keyId, XElement element) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.cs new file mode 100644 index 0000000000..3c9c2dd57c --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/Repositories/EphemeralXmlRepositoryTests.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 Xunit; + +namespace Microsoft.AspNet.DataProtection.Repositories +{ + public class EphemeralXmlRepositoryTests + { + [Fact] + public void GetAllElements_Empty() + { + // Arrange + var repository = new EphemeralXmlRepository(null); + + // Act & assert + Assert.Empty(repository.GetAllElements()); + } + + [Fact] + public void Store_Then_Get() + { + // Arrange + var element1 = XElement.Parse(@""); + var element2 = XElement.Parse(@""); + var element3 = XElement.Parse(@""); + var repository = new EphemeralXmlRepository(null); + + // Act & assert + repository.StoreElement(element1, "Invalid friendly name."); // nobody should care about the friendly name + repository.StoreElement(element2, "abcdefg"); + Assert.Equal(new[] { element1, element2 }, repository.GetAllElements(), XmlAssert.EqualityComparer); + repository.StoreElement(element3, null); + Assert.Equal(new[] { element1, element2, element3 }, repository.GetAllElements(), XmlAssert.EqualityComparer); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs new file mode 100644 index 0000000000..829acad4cb --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/Repositories/FileSystemXmlRepositoryTests.cs @@ -0,0 +1,166 @@ +// 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.Xml.Linq; +using Microsoft.AspNet.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.Repositories +{ + public class FileSystemXmlRepositoryTests + { + [ConditionalFact] + [ConditionalRunTestOnlyIfLocalAppDataAvailable] + public void DefaultKeyStorageDirectory_Property() + { + // Act + var defaultDirInfo = FileSystemXmlRepository.DefaultKeyStorageDirectory; + + // Assert + Assert.Equal(defaultDirInfo.FullName, + new DirectoryInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ASP.NET", "DataProtection-Keys")).FullName); + } + + [Fact] + public void Directory_Property() + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var repository = new FileSystemXmlRepository(dirInfo); + + // Act + var retVal = repository.Directory; + + // Assert + Assert.Equal(dirInfo, retVal); + }); + } + + [Fact] + public void GetAllElements_EmptyOrNonexistentDirectory_ReturnsEmptyCollection() + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var repository = new FileSystemXmlRepository(dirInfo); + + // Act + var allElements = repository.GetAllElements(); + + // Assert + Assert.Equal(0, allElements.Count); + }); + } + + [Fact] + public void StoreElement_WithValidFriendlyName_UsesFriendlyName() + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var element = XElement.Parse(""); + var repository = new FileSystemXmlRepository(dirInfo); + + // Act + repository.StoreElement(element, "valid-friendly-name"); + + // Assert + var fileInfos = dirInfo.GetFiles(); + var fileInfo = fileInfos.Single(); // only one file should've been created + + // filename should be "valid-friendly-name.xml" + Assert.Equal("valid-friendly-name.xml", fileInfo.Name, StringComparer.OrdinalIgnoreCase); + + // file contents should be "" + var parsedElement = XElement.Parse(File.ReadAllText(fileInfo.FullName)); + XmlAssert.Equal("", parsedElement); + }); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("..")] + [InlineData("not*friendly")] + public void StoreElement_WithInvalidFriendlyName_CreatesNewGuidAsName(string friendlyName) + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var element = XElement.Parse(""); + var repository = new FileSystemXmlRepository(dirInfo); + + // Act + repository.StoreElement(element, friendlyName); + + // Assert + var fileInfos = dirInfo.GetFiles(); + var fileInfo = fileInfos.Single(); // only one file should've been created + + // filename should be "{GUID}.xml" + var filename = fileInfo.Name; + Assert.EndsWith(".xml", filename, StringComparison.OrdinalIgnoreCase); + var filenameNoSuffix = filename.Substring(0, filename.Length - ".xml".Length); + Guid parsedGuid = Guid.Parse(filenameNoSuffix); + Assert.NotEqual(Guid.Empty, parsedGuid); + + // file contents should be "" + var parsedElement = XElement.Parse(File.ReadAllText(fileInfo.FullName)); + XmlAssert.Equal("", parsedElement); + }); + } + + [Fact] + public void StoreElements_ThenRetrieve_SeesAllElements() + { + WithUniqueTempDirectory(dirInfo => + { + // Arrange + var repository = new FileSystemXmlRepository(dirInfo); + + // Act + repository.StoreElement(new XElement("element1"), friendlyName: null); + repository.StoreElement(new XElement("element2"), friendlyName: null); + repository.StoreElement(new XElement("element3"), friendlyName: null); + var allElements = repository.GetAllElements(); + + // Assert + var orderedNames = allElements.Select(el => el.Name.LocalName).OrderBy(name => name); + Assert.Equal(new[] { "element1", "element2", "element3" }, orderedNames); + }); + } + + /// + /// Runs a test and cleans up the temp directory afterward. + /// + private static void WithUniqueTempDirectory(Action testCode) + { + string uniqueTempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var dirInfo = new DirectoryInfo(uniqueTempPath); + try + { + testCode(dirInfo); + } + finally + { + // clean up when test is done + if (dirInfo.Exists) + { + dirInfo.Delete(recursive: true); + } + } + } + + private class ConditionalRunTestOnlyIfLocalAppDataAvailable : Attribute, ITestCondition + { + public bool IsMet => (Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) != null); + + public string SkipReason { get; } = "%LOCALAPPDATA% couldn't be located."; + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs b/test/Microsoft.AspNet.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs new file mode 100644 index 0000000000..ce9178e092 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/Repositories/RegistryXmlRepositoryTests.cs @@ -0,0 +1,166 @@ +// 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.Testing.xunit; +using Microsoft.Win32; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.Repositories +{ + public class RegistryXmlRepositoryTests + { + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void RegistryKey_Property() + { + WithUniqueTempRegKey(regKey => + { + // Arrange + var repository = new RegistryXmlRepository(regKey); + + // Act + var retVal = repository.RegistryKey; + + // Assert + Assert.Equal(regKey, retVal); + }); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void GetAllElements_EmptyOrNonexistentDirectory_ReturnsEmptyCollection() + { + WithUniqueTempRegKey(regKey => + { + // Arrange + var repository = new RegistryXmlRepository(regKey); + + // Act + var allElements = repository.GetAllElements(); + + // Assert + Assert.Equal(0, allElements.Count); + }); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void StoreElement_WithValidFriendlyName_UsesFriendlyName() + { + WithUniqueTempRegKey(regKey => + { + // Arrange + var element = XElement.Parse(""); + var repository = new RegistryXmlRepository(regKey); + + // Act + repository.StoreElement(element, "valid-friendly-name"); + + // Assert + var valueNames = regKey.GetValueNames(); + var valueName = valueNames.Single(); // only one value should've been created + + // value name should be "valid-friendly-name" + Assert.Equal("valid-friendly-name", valueName, StringComparer.OrdinalIgnoreCase); + + // value contents should be "" + var parsedElement = XElement.Parse(regKey.GetValue(valueName) as string); + XmlAssert.Equal("", parsedElement); + }); + } + + [ConditionalTheory] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("..")] + [InlineData("not*friendly")] + public void StoreElement_WithInvalidFriendlyName_CreatesNewGuidAsName(string friendlyName) + { + WithUniqueTempRegKey(regKey => + { + // Arrange + var element = XElement.Parse(""); + var repository = new RegistryXmlRepository(regKey); + + // Act + repository.StoreElement(element, friendlyName); + + // Assert + var valueNames = regKey.GetValueNames(); + var valueName = valueNames.Single(); // only one value should've been created + + // value name should be "{GUID}" + Guid parsedGuid = Guid.Parse(valueName as string); + Assert.NotEqual(Guid.Empty, parsedGuid); + + // value contents should be "" + var parsedElement = XElement.Parse(regKey.GetValue(valueName) as string); + XmlAssert.Equal("", parsedElement); + }); + } + + [ConditionalFact] + [ConditionalRunTestOnlyIfHkcuRegistryAvailable] + public void StoreElements_ThenRetrieve_SeesAllElements() + { + WithUniqueTempRegKey(regKey => + { + // Arrange + var repository = new RegistryXmlRepository(regKey); + + // Act + repository.StoreElement(new XElement("element1"), friendlyName: null); + repository.StoreElement(new XElement("element2"), friendlyName: null); + repository.StoreElement(new XElement("element3"), friendlyName: null); + var allElements = repository.GetAllElements(); + + // Assert + var orderedNames = allElements.Select(el => el.Name.LocalName).OrderBy(name => name); + Assert.Equal(new[] { "element1", "element2", "element3" }, orderedNames); + }); + } + + /// + /// Runs a test and cleans up the registry key afterward. + /// + private static void WithUniqueTempRegKey(Action testCode) + { + string uniqueName = Guid.NewGuid().ToString(); + var uniqueSubkey = LazyHkcuTempKey.Value.CreateSubKey(uniqueName); + try + { + testCode(uniqueSubkey); + } + finally + { + // clean up when test is done + LazyHkcuTempKey.Value.DeleteSubKeyTree(uniqueName, throwOnMissingSubKey: false); + } + } + + private static readonly Lazy LazyHkcuTempKey = new Lazy(() => + { + try + { + return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\ASP.NET\temp"); + } + catch + { + // swallow all failures + return null; + } + }); + + private class ConditionalRunTestOnlyIfHkcuRegistryAvailable : Attribute, ITestCondition + { + public bool IsMet => (LazyHkcuTempKey.Value != null); + + public string SkipReason { get; } = "HKCU registry couldn't be opened."; + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/SP800_108/SP800_108Tests.cs b/test/Microsoft.AspNet.DataProtection.Test/SP800_108/SP800_108Tests.cs index 69ac3097bf..d37d696d07 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/SP800_108/SP800_108Tests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/SP800_108/SP800_108Tests.cs @@ -4,11 +4,11 @@ using System; using System.Security.Cryptography; using System.Text; -using Microsoft.AspNet.DataProtection.SP800_108; +using Microsoft.AspNet.DataProtection.Test.Shared; using Microsoft.AspNet.Testing.xunit; using Xunit; -namespace Microsoft.AspNet.DataProtection.Test.SP800_108 +namespace Microsoft.AspNet.DataProtection.SP800_108 { public unsafe class SP800_108Tests { @@ -35,7 +35,7 @@ namespace Microsoft.AspNet.DataProtection.Test.SP800_108 // The 'numBytesRequested' parameters below are chosen to exercise code paths where // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512). [ConditionalTheory] - [ConditionalRunTestOnlyIfBcryptAvailable] + [ConditionalRunTestOnlyOnWindows] [InlineData(512 / 8 - 1, "V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")] [InlineData(512 / 8 + 0, "mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")] [InlineData(512 / 8 + 1, "GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")] @@ -54,7 +54,7 @@ namespace Microsoft.AspNet.DataProtection.Test.SP800_108 // The 'numBytesRequested' parameters below are chosen to exercise code paths where // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512). [ConditionalTheory] - [ConditionalRunTestOnlyIfBcryptAvailable("BCryptKeyDerivation")] + [ConditionalRunTestOnlyOnWindows8OrLater] [InlineData(512 / 8 - 1, "V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")] [InlineData(512 / 8 + 0, "mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")] [InlineData(512 / 8 + 1, "GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")] @@ -96,7 +96,7 @@ namespace Microsoft.AspNet.DataProtection.Test.SP800_108 // The 'numBytesRequested' parameters below are chosen to exercise code paths where // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512). [ConditionalTheory] - [ConditionalRunTestOnlyIfBcryptAvailable] + [ConditionalRunTestOnlyOnWindows] [InlineData(512 / 8 - 1, "rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")] [InlineData(512 / 8 + 0, "RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")] [InlineData(512 / 8 + 1, "KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")] @@ -120,7 +120,7 @@ namespace Microsoft.AspNet.DataProtection.Test.SP800_108 // The 'numBytesRequested' parameters below are chosen to exercise code paths where // this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512). [ConditionalTheory] - [ConditionalRunTestOnlyIfBcryptAvailable("BCryptKeyDerivation")] + [ConditionalRunTestOnlyOnWindows8OrLater] [InlineData(512 / 8 - 1, "rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")] [InlineData(512 / 8 + 0, "RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")] [InlineData(512 / 8 + 1, "KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")] diff --git a/test/Microsoft.AspNet.DataProtection.Test/SecretAssert.cs b/test/Microsoft.AspNet.DataProtection.Test/SecretAssert.cs new file mode 100644 index 0000000000..ec4f5e0b7c --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/SecretAssert.cs @@ -0,0 +1,45 @@ +// 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 +{ + /// + /// Helpful ISecret-based assertions. + /// + public static class SecretAssert + { + /// + /// Asserts that two instances contain the same material. + /// + public static void Equal(ISecret secret1, ISecret secret2) + { + Assert.Equal(SecretToBase64String(secret1), SecretToBase64String(secret2)); + } + + /// + /// Asserts that has the length specified by . + /// + public static void LengthIs(int expectedLengthInBits, ISecret secret) + { + Assert.Equal(expectedLengthInBits, checked(secret.Length * 8)); + } + + /// + /// Asserts that two instances do not contain the same material. + /// + public static void NotEqual(ISecret secret1, ISecret secret2) + { + Assert.NotEqual(SecretToBase64String(secret1), SecretToBase64String(secret2)); + } + + private static string SecretToBase64String(ISecret secret) + { + byte[] secretBytes = new byte[secret.Length]; + secret.WriteSecretIntoBuffer(new ArraySegment(secretBytes)); + return Convert.ToBase64String(secretBytes); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/SecretTests.cs b/test/Microsoft.AspNet.DataProtection.Test/SecretTests.cs new file mode 100644 index 0000000000..d84decfad8 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/SecretTests.cs @@ -0,0 +1,269 @@ +// 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.Testing; +using Xunit; + +namespace Microsoft.AspNet.DataProtection +{ + public unsafe class SecretTests + { + [Fact] + public void Ctor_ArraySegment_Default_Throws() + { + // Act & assert + ExceptionAssert.ThrowsArgument( + testCode: () => new Secret(default(ArraySegment)), + paramName: "array", + exceptionMessage: null); + } + + [Fact] + public void Ctor_ArraySegment_Success() + { + // Arrange + var input = new ArraySegment(new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60 }, 1, 3); + + // Act + var secret = new Secret(input); + input.Array[2] = 0xFF; // mutate original array - secret shouldn't be modified + + // Assert - length + Assert.Equal(3, secret.Length); + + // Assert - managed buffer + var outputSegment = new ArraySegment(new byte[7], 2, 3); + secret.WriteSecretIntoBuffer(outputSegment); + Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputSegment.AsStandaloneArray()); + + // Assert - unmanaged buffer + var outputBuffer = new byte[3]; + fixed (byte* pOutputBuffer = outputBuffer) + { + secret.WriteSecretIntoBuffer(pOutputBuffer, 3); + } + Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputBuffer); + } + + [Fact] + public void Ctor_Buffer_Success() + { + // Arrange + var input = new byte[] { 0x20, 0x30, 0x40 }; + + // Act + var secret = new Secret(input); + input[1] = 0xFF; // mutate original array - secret shouldn't be modified + + // Assert - length + Assert.Equal(3, secret.Length); + + // Assert - managed buffer + var outputSegment = new ArraySegment(new byte[7], 2, 3); + secret.WriteSecretIntoBuffer(outputSegment); + Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputSegment.AsStandaloneArray()); + + // Assert - unmanaged buffer + var outputBuffer = new byte[3]; + fixed (byte* pOutputBuffer = outputBuffer) + { + secret.WriteSecretIntoBuffer(pOutputBuffer, 3); + } + Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputBuffer); + } + + [Fact] + public void Ctor_Buffer_ZeroLength_Success() + { + // Act + var secret = new Secret(new byte[0]); + + // Assert - none of these methods should throw + Assert.Equal(0, secret.Length); + secret.WriteSecretIntoBuffer(new ArraySegment(new byte[0])); + byte dummy; + secret.WriteSecretIntoBuffer(&dummy, 0); + } + + [Fact] + public void Ctor_Pointer_WithNullPointer_ThrowsArgumentNull() + { + // Act & assert + ExceptionAssert2.ThrowsArgumentNull( + testCode: () => new Secret(null, 0), + paramName: "secret"); + } + + [Fact] + public void Ctor_Pointer_WithNegativeLength_ThrowsArgumentOutOfRange() + { + // Act & assert + ExceptionAssert.ThrowsArgumentOutOfRange( + testCode: () => + { + byte dummy; + new Secret(&dummy, -1); + }, + paramName: "secretLength", + exceptionMessage: Resources.Common_ValueMustBeNonNegative); + } + + [Fact] + public void Ctor_Pointer_ZeroLength_Success() + { + // Arrange + byte input; + + // Act + var secret = new Secret(&input, 0); + + // Assert - none of these methods should throw + Assert.Equal(0, secret.Length); + secret.WriteSecretIntoBuffer(new ArraySegment(new byte[0])); + byte dummy; + secret.WriteSecretIntoBuffer(&dummy, 0); + } + + [Fact] + public void Ctor_Pointer_Success() + { + // Arrange + byte* input = stackalloc byte[3]; + input[0] = 0x20; + input[1] = 0x30; + input[2] = 0x40; + + // Act + var secret = new Secret(input, 3); + input[1] = 0xFF; // mutate original buffer - secret shouldn't be modified + + // Assert - length + Assert.Equal(3, secret.Length); + + // Assert - managed buffer + var outputSegment = new ArraySegment(new byte[7], 2, 3); + secret.WriteSecretIntoBuffer(outputSegment); + Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputSegment.AsStandaloneArray()); + + // Assert - unmanaged buffer + var outputBuffer = new byte[3]; + fixed (byte* pOutputBuffer = outputBuffer) + { + secret.WriteSecretIntoBuffer(pOutputBuffer, 3); + } + Assert.Equal(new byte[] { 0x20, 0x30, 0x40 }, outputBuffer); + } + + [Fact] + public void Random_ZeroLength_Success() + { + // Act + var secret = Secret.Random(0); + + // Assert + Assert.Equal(0, secret.Length); + } + + [Fact] + public void Random_LengthIsMultipleOf16_Success() + { + // Act + var secret = Secret.Random(32); + + // Assert + Assert.Equal(32, secret.Length); + Guid* pGuids = stackalloc Guid[2]; + secret.WriteSecretIntoBuffer((byte*)pGuids, 32); + Assert.NotEqual(Guid.Empty, pGuids[0]); + Assert.NotEqual(Guid.Empty, pGuids[1]); + Assert.NotEqual(pGuids[0], pGuids[1]); + } + + [Fact] + public void Random_LengthIsNotMultipleOf16_Success() + { + // Act + var secret = Secret.Random(31); + + // Assert + Assert.Equal(31, secret.Length); + Guid* pGuids = stackalloc Guid[2]; + secret.WriteSecretIntoBuffer((byte*)pGuids, 31); + Assert.NotEqual(Guid.Empty, pGuids[0]); + Assert.NotEqual(Guid.Empty, pGuids[1]); + Assert.NotEqual(pGuids[0], pGuids[1]); + Assert.Equal(0, ((byte*)pGuids)[31]); // last byte shouldn't have been overwritten + } + + [Fact] + public void WriteSecretIntoBuffer_ArraySegment_IncorrectlySizedBuffer_Throws() + { + // Arrange + var secret = Secret.Random(16); + + // Act & assert + ExceptionAssert.ThrowsArgument( + testCode: () => secret.WriteSecretIntoBuffer(new ArraySegment(new byte[100])), + paramName: "buffer", + exceptionMessage: Resources.FormatCommon_BufferIncorrectlySized(100, 16)); + } + + [Fact] + public void WriteSecretIntoBuffer_ArraySegment_Disposed_Throws() + { + // Arrange + var secret = Secret.Random(16); + secret.Dispose(); + + // Act & assert + Assert.Throws( + testCode: () => secret.WriteSecretIntoBuffer(new ArraySegment(new byte[16]))); + } + + [Fact] + public void WriteSecretIntoBuffer_Pointer_NullBuffer_Throws() + { + // Arrange + var secret = Secret.Random(16); + + // Act & assert + ExceptionAssert2.ThrowsArgumentNull( + testCode: () => secret.WriteSecretIntoBuffer(null, 100), + paramName: "buffer"); + } + + [Fact] + public void WriteSecretIntoBuffer_Pointer_IncorrectlySizedBuffer_Throws() + { + // Arrange + var secret = Secret.Random(16); + + // Act & assert + ExceptionAssert.ThrowsArgument( + testCode: () => + { + byte* pBuffer = stackalloc byte[100]; + secret.WriteSecretIntoBuffer(pBuffer, 100); + }, + paramName: "bufferLength", + exceptionMessage: Resources.FormatCommon_BufferIncorrectlySized(100, 16)); + } + + [Fact] + public void WriteSecretIntoBuffer_Pointer_Disposed_Throws() + { + // Arrange + var secret = Secret.Random(16); + secret.Dispose(); + + // Act & assert + Assert.Throws( + testCode: () => + { + byte* pBuffer = stackalloc byte[16]; + secret.WriteSecretIntoBuffer(pBuffer, 16); + }); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/SequentialGenRandom.cs b/test/Microsoft.AspNet.DataProtection.Test/SequentialGenRandom.cs index fd449f19c7..505e6f0913 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/SequentialGenRandom.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/SequentialGenRandom.cs @@ -5,7 +5,7 @@ using System; using Microsoft.AspNet.DataProtection.Cng; using Microsoft.AspNet.DataProtection.Managed; -namespace Microsoft.AspNet.DataProtection.Test +namespace Microsoft.AspNet.DataProtection { internal unsafe class SequentialGenRandom : IBCryptGenRandom, IManagedGenRandom { diff --git a/test/Microsoft.AspNet.DataProtection.Test/StringLoggerFactory.cs b/test/Microsoft.AspNet.DataProtection.Test/StringLoggerFactory.cs new file mode 100644 index 0000000000..2b2ca32a9e --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/StringLoggerFactory.cs @@ -0,0 +1,78 @@ +// 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.Text; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.DataProtection +{ + internal sealed class StringLoggerFactory : ILoggerFactory + { + private readonly StringBuilder _log = new StringBuilder(); + + public StringLoggerFactory(LogLevel logLevel) + { + MinimumLevel = logLevel; + } + + public LogLevel MinimumLevel { get; set; } + + public void AddProvider(ILoggerProvider provider) + { + // no-op + } + + public ILogger CreateLogger(string name) + { + return new StringLogger(name, this); + } + + public override string ToString() + { + return _log.ToString(); + } + + private sealed class StringLogger : ILogger + { + private readonly StringLoggerFactory _factory; + private readonly string _name; + + public StringLogger(string name, StringLoggerFactory factory) + { + _name = name; + _factory = factory; + } + + public IDisposable BeginScope(object state) + { + return new DummyDisposable(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return (logLevel >= _factory.MinimumLevel); + } + + public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) + { + string message = String.Format(CultureInfo.InvariantCulture, + "Provider: {0}" + Environment.NewLine + + "Log level: {1}" + Environment.NewLine + + "Event id: {2}" + Environment.NewLine + + "Exception: {3}" + Environment.NewLine + + "Message: {4}", _name, logLevel, eventId, exception?.ToString(), formatter(state, exception)); + _factory._log.AppendLine(message); + } + + private sealed class DummyDisposable : IDisposable + { + public void Dispose() + { + // no-op + } + } + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs index 459ced47ee..354078de05 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/TimeLimitedDataProtectorTests.cs @@ -6,7 +6,7 @@ using System.Security.Cryptography; using Moq; using Xunit; -namespace Microsoft.AspNet.DataProtection.Test +namespace Microsoft.AspNet.DataProtection { public class TimeLimitedDataProtectorTests { diff --git a/test/Microsoft.AspNet.DataProtection.Test/XmlAssert.cs b/test/Microsoft.AspNet.DataProtection.Test/XmlAssert.cs new file mode 100644 index 0000000000..e937122773 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/XmlAssert.cs @@ -0,0 +1,151 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.DataProtection +{ + /// + /// Helpful XML-based assertions. + /// + public static class XmlAssert + { + public static readonly IEqualityComparer EqualityComparer = new CallbackBasedEqualityComparer(Core.AreEqual); + + /// + /// Asserts that a and an are semantically equivalent. + /// + public static void Equal(string expected, XElement actual) + { + Assert.NotNull(expected); + Assert.NotNull(actual); + Equal(XElement.Parse(expected), actual); + } + + /// + /// Asserts that two instances are semantically equivalent. + /// + public static void Equal(XElement expected, XElement actual) + { + Assert.NotNull(expected); + Assert.NotNull(actual); + + if (!Core.AreEqual(expected, actual)) + { + Assert.True(false, + "Expected element:" + Environment.NewLine + + expected.ToString() + Environment.NewLine + + "Actual element:" + Environment.NewLine + + actual.ToString()); + } + } + + private static class Core + { + private static readonly IEqualityComparer AttributeEqualityComparer = new CallbackBasedEqualityComparer(AreEqual); + + private static bool AreEqual(XElement expected, XElement actual) + { + return expected.Name == actual.Name + && AreEqual(expected.Attributes(), actual.Attributes()) + && AreEqual(expected.Nodes(), actual.Nodes()); + } + + private static bool AreEqual(IEnumerable expected, IEnumerable actual) + { + List filteredExpected = expected.Where(ShouldIncludeNodeDuringComparison).ToList(); + List filteredActual = actual.Where(ShouldIncludeNodeDuringComparison).ToList(); + return filteredExpected.SequenceEqual(filteredActual, EqualityComparer); + } + + internal static bool AreEqual(XNode expected, XNode actual) + { + if (expected is XText && actual is XText) + { + return AreEqual((XText)expected, (XText)actual); + } + else if (expected is XElement && actual is XElement) + { + return AreEqual((XElement)expected, (XElement)actual); + } + else + { + return false; + } + } + + private static bool AreEqual(XText expected, XText actual) + { + return expected.Value == actual.Value; + } + + private static bool AreEqual(IEnumerable expected, IEnumerable actual) + { + List orderedExpected = expected + .Where(ShouldIncludeAttributeDuringComparison) + .OrderBy(attr => attr.Name.ToString()) + .ToList(); + + List orderedActual = actual + .Where(ShouldIncludeAttributeDuringComparison) + .OrderBy(attr => attr.Name.ToString()) + .ToList(); + + return orderedExpected.SequenceEqual(orderedActual, AttributeEqualityComparer); + } + + private static bool AreEqual(XAttribute expected, XAttribute actual) + { + return expected.Name == actual.Name + && expected.Value == actual.Value; + } + + private static bool ShouldIncludeAttributeDuringComparison(XAttribute attribute) + { + // exclude 'xmlns' attributes since they're already considered in the + // actual element and attribute names + return attribute.Name != (XName)"xmlns" + && attribute.Name.Namespace != XNamespace.Xmlns; + } + + private static bool ShouldIncludeNodeDuringComparison(XNode node) + { + if (node is XComment) + { + return false; // not contextually relevant + } + + if (node is XText /* includes XCData */ || node is XElement) + { + return true; // relevant + } + + throw new NotSupportedException(String.Format("Node of type '{0}' is not supported.", node.GetType().Name)); + } + } + + private sealed class CallbackBasedEqualityComparer : IEqualityComparer + { + private readonly Func _equalityCheck; + + public CallbackBasedEqualityComparer(Func equalityCheck) + { + _equalityCheck = equalityCheck; + } + + public bool Equals(T x, T y) + { + return _equalityCheck(x, y); + } + + public int GetHashCode(T obj) + { + return obj.ToString().GetHashCode(); + } + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.cs b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.cs new file mode 100644 index 0000000000..44be41b780 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/CertificateXmlEncryptionTests.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.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Framework.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.XmlEncryption +{ + public class CertificateXmlEncryptorTests + { + [Fact] + public void Encrypt_Decrypt_RoundTrips() + { + // Arrange + var aes = new AesCryptoServiceProvider(); + aes.GenerateKey(); + + var serviceCollection = new ServiceCollection(); + var mockInternalEncryptor = new Mock(); + mockInternalEncryptor.Setup(o => o.PerformEncryption(It.IsAny(), It.IsAny())) + .Returns((encryptedXml, element) => + { + encryptedXml.AddKeyNameMapping("theKey", aes); // use symmetric encryption + return encryptedXml.Encrypt(element, "theKey"); + }); + serviceCollection.AddInstance(mockInternalEncryptor.Object); + + var mockInternalDecryptor = new Mock(); + mockInternalDecryptor.Setup(o => o.PerformPreDecryptionSetup(It.IsAny())) + .Callback(encryptedXml => + { + encryptedXml.AddKeyNameMapping("theKey", aes); // use symmetric encryption + }); + serviceCollection.AddInstance(mockInternalDecryptor.Object); + + var services = serviceCollection.BuildServiceProvider(); + var encryptor = new CertificateXmlEncryptor(services); + var decryptor = new EncryptedXmlDecryptor(services); + + var originalXml = XElement.Parse(@""); + + // Act & assert - run through encryptor and make sure we get back element + var encryptedXmlInfo = encryptor.Encrypt(originalXml); + Assert.Equal(typeof(EncryptedXmlDecryptor), encryptedXmlInfo.DecryptorType); + Assert.Equal(XName.Get("EncryptedData", "http://www.w3.org/2001/04/xmlenc#"), encryptedXmlInfo.EncryptedElement.Name); + Assert.Equal("http://www.w3.org/2001/04/xmlenc#Element", (string)encryptedXmlInfo.EncryptedElement.Attribute("Type")); + Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase); + + // Act & assert - run through decryptor and make sure we get back the original value + var roundTrippedElement = decryptor.Decrypt(encryptedXmlInfo.EncryptedElement); + XmlAssert.Equal(originalXml, roundTrippedElement); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.cs b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.cs new file mode 100644 index 0000000000..321e29943c --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/DpapiNGXmlEncryptionTests.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.Xml.Linq; +using Microsoft.AspNet.DataProtection.Test.Shared; +using Microsoft.AspNet.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.XmlEncryption +{ + public class DpapiNGXmlEncryptionTests + { + [ConditionalFact] + [ConditionalRunTestOnlyOnWindows8OrLater] + public void Encrypt_Decrypt_RoundTrips() + { + // Arrange + var originalXml = XElement.Parse(@""); + var encryptor = new DpapiNGXmlEncryptor("LOCAL=user", DpapiNGProtectionDescriptorFlags.None); + var decryptor = new DpapiNGXmlDecryptor(); + + // Act & assert - run through encryptor and make sure we get back an obfuscated element + var encryptedXmlInfo = encryptor.Encrypt(originalXml); + Assert.Equal(typeof(DpapiNGXmlDecryptor), encryptedXmlInfo.DecryptorType); + Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase); + + // Act & assert - run through decryptor and make sure we get back the original value + var roundTrippedElement = decryptor.Decrypt(encryptedXmlInfo.EncryptedElement); + XmlAssert.Equal(originalXml, roundTrippedElement); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.cs b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.cs new file mode 100644 index 0000000000..bb4c2145e9 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/DpapiXmlEncryptionTests.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.AspNet.DataProtection.Test.Shared; +using Microsoft.AspNet.Testing; +using Microsoft.AspNet.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.XmlEncryption +{ + public class DpapiXmlEncryptionTests + { + [ConditionalTheory] + [ConditionalRunTestOnlyOnWindows] + [InlineData(true)] + [InlineData(false)] + public void Encrypt_CurrentUserOrLocalMachine_Decrypt_RoundTrips(bool protectToLocalMachine) + { + // Arrange + var originalXml = XElement.Parse(@""); + var encryptor = new DpapiXmlEncryptor(protectToLocalMachine); + var decryptor = new DpapiXmlDecryptor(); + + // Act & assert - run through encryptor and make sure we get back an obfuscated element + var encryptedXmlInfo = encryptor.Encrypt(originalXml); + Assert.Equal(typeof(DpapiXmlDecryptor), encryptedXmlInfo.DecryptorType); + Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase); + + // Act & assert - run through decryptor and make sure we get back the original value + var roundTrippedElement = decryptor.Decrypt(encryptedXmlInfo.EncryptedElement); + XmlAssert.Equal(originalXml, roundTrippedElement); + } + + [ConditionalFact] + [ConditionalRunTestOnlyOnWindows] + public void Encrypt_CurrentUser_Decrypt_ImpersonatedAsAnonymous_Fails() + { + // Arrange + var originalXml = XElement.Parse(@""); + var encryptor = new DpapiXmlEncryptor(protectToLocalMachine: false); + var decryptor = new DpapiXmlDecryptor(); + + // Act & assert - run through encryptor and make sure we get back an obfuscated element + var encryptedXmlInfo = encryptor.Encrypt(originalXml); + Assert.Equal(typeof(DpapiXmlDecryptor), encryptedXmlInfo.DecryptorType); + Assert.DoesNotContain("265ee4ea-ade2-43b1-b706-09b259e58b6b", encryptedXmlInfo.EncryptedElement.ToString(), StringComparison.OrdinalIgnoreCase); + + // Act & assert - run through decryptor (while impersonated as anonymous) and verify failure + ExceptionAssert2.ThrowsCryptographicException(() => + AnonymousImpersonation.Run(() => decryptor.Decrypt(encryptedXmlInfo.EncryptedElement))); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.cs b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.cs new file mode 100644 index 0000000000..1e2e92476e --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/NullXmlEncryptionTests.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 Xunit; + +namespace Microsoft.AspNet.DataProtection.XmlEncryption +{ + public class NullXmlEncryptionTests + { + [Fact] + public void NullDecryptor_ReturnsOriginalElement() + { + // Arrange + var decryptor = new NullXmlDecryptor(); + + // Act + var retVal = decryptor.Decrypt(XElement.Parse("")); + + // Assert + XmlAssert.Equal("", retVal); + } + + [Fact] + public void NullEncryptor_ReturnsOriginalElement() + { + // Arrange + var encryptor = new NullXmlEncryptor(); + + // Act + var retVal = encryptor.Encrypt(XElement.Parse("")); + + // Assert + Assert.Equal(typeof(NullXmlDecryptor), retVal.DecryptorType); + XmlAssert.Equal("", retVal.EncryptedElement); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs new file mode 100644 index 0000000000..c931d1bd48 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/XmlEncryption/XmlEncryptionExtensionsTests.cs @@ -0,0 +1,234 @@ +// 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.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.XmlEncryption +{ + public class XmlEncryptionExtensionsTests + { + [Fact] + public void DecryptElement_NothingToDecrypt_ReturnsOriginalElement() + { + // Arrange + var original = XElement.Parse(@""); + + // Act + var retVal = original.DecryptElement(activator: null); + + // Assert + Assert.Same(original, retVal); + XmlAssert.Equal("", original); // unmutated + } + + [Fact] + public void DecryptElement_RootNodeRequiresDecryption_Success() + { + // Arrange + var original = XElement.Parse(@" + + + "); + + var mockActivator = new Mock(); + mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput("theDecryptor", "", ""); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockActivator.Object); + var services = serviceCollection.BuildServiceProvider(); + var activator = services.GetActivator(); + + // Act + var retVal = original.DecryptElement(activator); + + // Assert + XmlAssert.Equal("", retVal); + } + + [Fact] + public void DecryptElement_MultipleNodesRequireDecryption_AvoidsRecursion_Success() + { + // Arrange + var original = XElement.Parse(@" + + + + + + + + + + + "); + + var expected = @" + + + nested + + + + + nested + + + "; + + var mockDecryptor = new Mock(); + mockDecryptor + .Setup(o => o.Decrypt(It.IsAny())) + .Returns(el => new XElement(el.Name.LocalName + "_decrypted", new XElement(XmlConstants.EncryptedSecretElementName, "nested"))); + + var mockActivator = new Mock(); + mockActivator.Setup(o => o.CreateInstance(typeof(IXmlDecryptor), "myDecryptor")).Returns(mockDecryptor.Object); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(mockActivator.Object); + var services = serviceCollection.BuildServiceProvider(); + var activator = services.GetActivator(); + + // Act + var retVal = original.DecryptElement(activator); + + // Assert + XmlAssert.Equal(expected, retVal); + } + + [Fact] + public void EncryptIfNecessary_NothingToEncrypt_ReturnsNull() + { + // Arrange + var original = XElement.Parse(@""); + var xmlEncryptor = new Mock(MockBehavior.Strict).Object; + + // Act + var retVal = xmlEncryptor.EncryptIfNecessary(original); + + // Assert + Assert.Null(retVal); + XmlAssert.Equal("", original); // unmutated + } + + [Fact] + public void EncryptIfNecessary_RootNodeRequiresEncryption_Success() + { + // Arrange + var original = XElement.Parse(@""); + var mockXmlEncryptor = new Mock(); + mockXmlEncryptor.Setup(o => o.Encrypt(It.IsAny())).Returns(new EncryptedXmlInfo(new XElement("theElement"), typeof(MyXmlDecryptor))); + + // Act + var retVal = mockXmlEncryptor.Object.EncryptIfNecessary(original); + + // Assert + XmlAssert.Equal(@"", original); // unmutated + Assert.Equal(XmlConstants.EncryptedSecretElementName, retVal.Name); + Assert.Equal(typeof(MyXmlDecryptor).AssemblyQualifiedName, (string)retVal.Attribute(XmlConstants.DecryptorTypeAttributeName)); + XmlAssert.Equal("", retVal.Descendants().Single()); + } + + [Fact] + public void EncryptIfNecessary_MultipleNodesRequireEncryption_Success() + { + // Arrange + var original = XElement.Parse(@" + + + + + + + + + + + "); + + var expected = String.Format(@" + + + + + + + + + + + ", + typeof(MyXmlDecryptor).AssemblyQualifiedName); + + var mockXmlEncryptor = new Mock(); + mockXmlEncryptor + .Setup(o => o.Encrypt(It.IsAny())) + .Returns(element => new EncryptedXmlInfo(new XElement(element.Name.LocalName + "_encrypted"), typeof(MyXmlDecryptor))); + + // Act + var retVal = mockXmlEncryptor.Object.EncryptIfNecessary(original); + + // Assert + XmlAssert.Equal(expected, retVal); + } + + [Fact] + public void EncryptIfNecessary_NullEncryptorWithRecursion_NoStackDive_Success() + { + // Arrange + var original = XElement.Parse(@" + + + + + + + + + + + "); + + var expected = String.Format(@" + + + + + + + + + + + + + + + ", + typeof(MyXmlDecryptor).AssemblyQualifiedName); + + var mockXmlEncryptor = new Mock(); + mockXmlEncryptor + .Setup(o => o.Encrypt(It.IsAny())) + .Returns(element => new EncryptedXmlInfo(new XElement(element), typeof(MyXmlDecryptor))); + + // Act + var retVal = mockXmlEncryptor.Object.EncryptIfNecessary(original); + + // Assert + XmlAssert.Equal(expected, retVal); + } + + private sealed class MyXmlDecryptor : IXmlDecryptor + { + public XElement Decrypt(XElement encryptedElement) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/project.json b/test/Microsoft.AspNet.DataProtection.Test/project.json index 2de96e4f7b..8203d16d13 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/project.json +++ b/test/Microsoft.AspNet.DataProtection.Test/project.json @@ -1,7 +1,9 @@ { "dependencies": { "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.DataProtection.Test.Shared": { "type": "build", "version": "" }, "Microsoft.AspNet.Testing": "1.0.0-*", + "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Moq": "4.2.1312.1622", "xunit.runner.kre": "1.0.0-*" },