diff --git a/DataProtection.sln b/DataProtection.sln index 97a36b45e9..a516327b4a 100644 --- a/DataProtection.sln +++ b/DataProtection.sln @@ -1,12 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22013.1 +VisualStudioVersion = 14.0.22115.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.DataProtection", "src\Microsoft.AspNet.Security.DataProtection\Microsoft.AspNet.Security.DataProtection.kproj", "{1E570CD4-6F12-44F4-961E-005EE2002BC2}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.DataProtection.Azure", "src\Microsoft.AspNet.Security.DataProtection.Azure\Microsoft.AspNet.Security.DataProtection.Azure.kproj", "{DF3671D7-A9B1-45F1-A195-0AD596001735}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.DataProtection.Compatibility", "src\Microsoft.AspNet.Security.DataProtection.Compatibility\Microsoft.AspNet.Security.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.Security.DataProtection.Test", "test\Microsoft.AspNet.Security.DataProtection.Test\Microsoft.AspNet.Security.DataProtection.Test.kproj", "{7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x86 = Debug|x86 @@ -15,11 +23,20 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Debug|x86.ActiveCfg = Debug|Any CPU {1E570CD4-6F12-44F4-961E-005EE2002BC2}.Release|x86.ActiveCfg = Release|Any CPU + {DF3671D7-A9B1-45F1-A195-0AD596001735}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF3671D7-A9B1-45F1-A195-0AD596001735}.Release|x86.ActiveCfg = Release|Any CPU + {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2FD9D02-AA0E-45FA-8561-EE357A94B73D}.Release|x86.ActiveCfg = Release|Any CPU + {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {7A637185-2BA1-437D-9D4C-7CC4F94CF7BF}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection 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} EndGlobalSection EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000000..cad39504d4 --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "sources": [ "src" ] +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Azure/BlobStorageXmlRepository.cs b/src/Microsoft.AspNet.Security.DataProtection.Azure/BlobStorageXmlRepository.cs new file mode 100644 index 0000000000..a08027f6a8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Azure/BlobStorageXmlRepository.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft 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.IO; +using System.Linq; +using System.Net; +using System.Runtime.ExceptionServices; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.Repositories; +using Microsoft.Framework.OptionsModel; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; + +namespace Microsoft.AspNet.Security.DataProtection.Azure +{ + /// + /// An XML repository backed by Azure blob storage. + /// + public class BlobStorageXmlRepository : IXmlRepository + { + private const int MAX_NUM_UPDATE_ATTEMPTS = 10; + + internal static readonly XNamespace XmlNamespace = XNamespace.Get("http://www.asp.net/dataProtection/2014/azure"); + internal static readonly XName KeyRingElementName = XmlNamespace.GetName("keyRing"); + + public BlobStorageXmlRepository([NotNull] IOptionsAccessor optionsAccessor) + { + Directory = optionsAccessor.Options.Directory; + CryptoUtil.Assert(Directory != null, "Directory != null"); + } + + protected CloudBlobDirectory Directory + { + get; + private set; + } + + // IXmlRepository objects are supposed to be thread-safe, but CloudBlockBlob + // instances do not meet this criterion. We'll create them on-demand so that each + // thread can have its own instance that doesn't impact others. + private CloudBlockBlob GetKeyRingBlockBlobReference() + { + return Directory.GetBlockBlobReference("keyring.xml"); + } + + public virtual IReadOnlyCollection GetAllElements() + { + var blobRef = GetKeyRingBlockBlobReference(); + XDocument document = ReadDocumentFromStorage(blobRef); + return document?.Root.Elements().ToArray() ?? new XElement[0]; + } + + private XDocument ReadDocumentFromStorage(CloudBlockBlob blobRef) + { + // Try downloading from Azure storage + using (var memoryStream = new MemoryStream()) + { + try + { + blobRef.DownloadToStream(memoryStream); + } + catch (StorageException ex) if (ex.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound) + { + // 404s are not a fatal error - empty keyring + return null; + } + + // Rewind the memory stream and read it into an XDocument + memoryStream.Position = 0; + XDocument document = XDocument.Load(memoryStream); + + // Format checks + CryptoUtil.Assert(document.Root.Name == KeyRingElementName, "TODO: Unknown element."); + CryptoUtil.Assert((int)document.Root.Attribute("version") == 1, "TODO: Unknown version."); + return document; + } + } + + public virtual void StoreElement([NotNull] XElement element, string friendlyName) + { + ExceptionDispatchInfo lastException = null; + + // To perform a transactional update of keyring.xml, we first need to get + // the original contents of the blob. + var blobRef = GetKeyRingBlockBlobReference(); + + for (int i = 0; i < MAX_NUM_UPDATE_ATTEMPTS; i++) + { + AccessCondition updateAccessCondition; + XDocument document = ReadDocumentFromStorage(blobRef); + + // Inject the new element into the existing root. + if (document != null) + { + document.Root.Add(element); + + // only update if the contents haven't changed (prevents overwrite) + updateAccessCondition = AccessCondition.GenerateIfMatchCondition(blobRef.Properties.ETag); + } + else + { + document = new XDocument( + new XElement(KeyRingElementName, + new XAttribute("version", 1), + element)); + + // only update if the file doesn't exist (prevents overwrite) + updateAccessCondition = AccessCondition.GenerateIfNoneMatchCondition("*"); + } + + // Write the updated document back out + MemoryStream memoryStream = new MemoryStream(); + document.Save(memoryStream); + try + { + blobRef.UploadFromByteArray(memoryStream.GetBuffer(), 0, checked((int)memoryStream.Length), accessCondition: updateAccessCondition); + return; // success! + } + catch (StorageException ex) + { + switch ((HttpStatusCode)ex.RequestInformation.HttpStatusCode) + { + // If we couldn't update the blob due to a conflict on the server, try again. + case HttpStatusCode.Conflict: + case HttpStatusCode.PreconditionFailed: + lastException = ExceptionDispatchInfo.Capture(ex); + continue; + + default: + throw; + } + } + } + + // If we got this far, too many conflicts occurred while trying to update the blob. + // Just bail. + lastException.Throw(); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Azure/BlobStorageXmlRepositoryOptions.cs b/src/Microsoft.AspNet.Security.DataProtection.Azure/BlobStorageXmlRepositoryOptions.cs new file mode 100644 index 0000000000..b694ea5dd8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Azure/BlobStorageXmlRepositoryOptions.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 Microsoft.WindowsAzure.Storage.Blob; + +namespace Microsoft.AspNet.Security.DataProtection.Azure +{ + /// + /// Specifies options for configuring an Azure blob storage-based repository. + /// + public class BlobStorageXmlRepositoryOptions + { + /// + /// The blob storage directory where the key ring will be stored. + /// + public CloudBlobDirectory Directory { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Azure/CryptoUtil.cs b/src/Microsoft.AspNet.Security.DataProtection.Azure/CryptoUtil.cs new file mode 100644 index 0000000000..b9fb8859f7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Azure/CryptoUtil.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 System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal static class CryptoUtil + { + // This isn't a typical Debug.Assert; the check is always performed, even in retail builds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool condition, string message) + { + if (!condition) + { + Fail(message); + } + } + + // This isn't a typical Debug.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); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Azure/Microsoft.AspNet.Security.DataProtection.Azure.kproj b/src/Microsoft.AspNet.Security.DataProtection.Azure/Microsoft.AspNet.Security.DataProtection.Azure.kproj new file mode 100644 index 0000000000..753c52ebda --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Azure/Microsoft.AspNet.Security.DataProtection.Azure.kproj @@ -0,0 +1,20 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + DF3671D7-A9B1-45F1-A195-0AD596001735 + Library + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection.Azure/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.DataProtection.Azure/NotNullAttribute.cs new file mode 100644 index 0000000000..00985c02f5 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Azure/NotNullAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Azure +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Azure/project.json b/src/Microsoft.AspNet.Security.DataProtection.Azure/project.json new file mode 100644 index 0000000000..3d898a14be --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Azure/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", + "Microsoft.Framework.OptionsModel": "1.0.0-*", + "WindowsAzure.Storage": "4.3.0" + }, + "frameworkDependencies": { + "System.Xml.Linq": "4.0.0.0" + }, + "frameworks": { + "net451": { + }, + "aspnet50": { + } + }, + "compilationOptions": { + "warningsAsErrors": true, + "languageVersion": "experimental" + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtectionProviderHelper.cs b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtectionProviderHelper.cs new file mode 100644 index 0000000000..f05a11cf8e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtectionProviderHelper.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft 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.Security.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.Security.DataProtection.Compatibility/DataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtector.cs new file mode 100644 index 0000000000..af6c6872fa --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtector.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft 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.Security.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.Security.DataProtection.Compatibility/DataProtectorHelper.cs b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtectorHelper.cs new file mode 100644 index 0000000000..03d3af7d41 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/DataProtectorHelper.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft 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.Security.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.Security.DataProtection.Compatibility/IDataProtectionProviderFactory.cs b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/IDataProtectionProviderFactory.cs new file mode 100644 index 0000000000..ddf3dbe191 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/IDataProtectionProviderFactory.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Compatibility +{ + public interface IDataProtectionProviderFactory + { + IDataProtectionProvider CreateDataProtectionProvider(); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Compatibility/IFactorySupportFunctions.cs b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/IFactorySupportFunctions.cs new file mode 100644 index 0000000000..a318be7460 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/IFactorySupportFunctions.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.Security.DataProtection.Compatibility +{ + internal interface IFactorySupportFunctions + { + IDataProtectionProvider CreateDataProtectionProvider(); + + IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection.Compatibility/Microsoft.AspNet.Security.DataProtection.Compatibility.kproj b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/Microsoft.AspNet.Security.DataProtection.Compatibility.kproj new file mode 100644 index 0000000000..01ea1f2f00 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/Microsoft.AspNet.Security.DataProtection.Compatibility.kproj @@ -0,0 +1,20 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + C2FD9D02-AA0E-45FA-8561-EE357A94B73D + Library + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection.Compatibility/project.json b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/project.json new file mode 100644 index 0000000000..dae44c5f5c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection.Compatibility/project.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0-*", + "frameworks": { + "net451": { + "dependencies": { + "Microsoft.AspNet.Security.DataProtection": "1.0.0-*" + }, + "frameworkAssemblies": { + "System.Security": "4.0.0.0" + } + } + }, + "compilationOptions": { + "warningsAsErrors": true, + "languageVersion": "experimental" + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs b/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs deleted file mode 100644 index f09f9709aa..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/Algorithms.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.Security.Cryptography; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal unsafe static class Algorithms - { - public static readonly BCryptAlgorithmHandle AESAlgorithmHandle = CreateAESAlgorithmHandle(); - public static readonly BCryptAlgorithmHandle HMACSHA256AlgorithmHandle = CreateHMACSHA256AlgorithmHandle(); - public static readonly BCryptAlgorithmHandle HMACSHA512AlgorithmHandle = CreateHMACSHA512AlgorithmHandle(); - - private static BCryptAlgorithmHandle CreateAESAlgorithmHandle() - { - // create the AES instance - BCryptAlgorithmHandle algHandle; - int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_AES_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: 0); - if (status != 0 || algHandle == null || algHandle.IsInvalid) - { - throw new CryptographicException(status); - } - - // change it to use CBC chaining; it already uses PKCS7 padding by default - fixed (char* pCbcMode = Constants.BCRYPT_CHAIN_MODE_CBC) - { - status = UnsafeNativeMethods.BCryptSetProperty(algHandle, Constants.BCRYPT_CHAINING_MODE, (IntPtr)pCbcMode, (uint)((Constants.BCRYPT_CHAIN_MODE_CBC.Length + 1 /* trailing null */) * sizeof(char)), dwFlags: 0); - } - if (status != 0) - { - throw new CryptographicException(status); - } - - return algHandle; - } - - internal static BCryptAlgorithmHandle CreateGenericHMACHandleFromPrimitiveProvider(string algorithmName) - { - BCryptAlgorithmHandle algHandle; - int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, algorithmName, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: BCryptAlgorithmFlags.BCRYPT_ALG_HANDLE_HMAC_FLAG); - if (status != 0 || algHandle == null || algHandle.IsInvalid) - { - throw new CryptographicException(status); - } - - return algHandle; - } - - private static BCryptAlgorithmHandle CreateHMACSHA256AlgorithmHandle() - { - // create the HMACSHA-256 instance - return CreateGenericHMACHandleFromPrimitiveProvider(Constants.BCRYPT_SHA256_ALGORITHM); - } - - private static BCryptAlgorithmHandle CreateHMACSHA512AlgorithmHandle() - { - // create the HMACSHA-512 instance - return CreateGenericHMACHandleFromPrimitiveProvider(Constants.BCRYPT_SHA512_ALGORITHM); - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/ArraySegmentExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/ArraySegmentExtensions.cs new file mode 100644 index 0000000000..cadff82795 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/ArraySegmentExtensions.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; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal static class ArraySegmentExtensions + { + public static byte[] AsStandaloneArray(this ArraySegment arraySegment) + { + // Fast-track: Don't need to duplicate the array. + if (arraySegment.Offset == 0 && arraySegment.Count == arraySegment.Array.Length) + { + return arraySegment.Array; + } + + byte[] retVal = new byte[arraySegment.Count]; + Buffer.BlockCopy(arraySegment.Array, arraySegment.Offset, retVal, 0, retVal.Length); + return retVal; + } + + public static void Validate(this ArraySegment arraySegment) + { + // Since ArraySegment is a struct, it can be improperly initialized or torn. + // We call the ctor again to make sure the instance data is valid. + var unused = new ArraySegment(arraySegment.Array, arraySegment.Offset, arraySegment.Count); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.cs new file mode 100644 index 0000000000..3bcd320cb8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/AuthenticatedEncryptorExtensions.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.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + internal static class AuthenticatedEncryptorExtensions + { + public static byte[] Encrypt(this IAuthenticatedEncryptor encryptor, ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize) + { + // Can we call the optimized version? + IAuthenticatedEncryptor2 optimizedEncryptor = encryptor as IAuthenticatedEncryptor2; + if (optimizedEncryptor != null) + { + return optimizedEncryptor.Encrypt(plaintext, additionalAuthenticatedData, preBufferSize, postBufferSize); + } + + // Fall back to the unoptimized version + if (preBufferSize == 0 && postBufferSize == 0) + { + // optimization: call through to inner encryptor with no modifications + return encryptor.Encrypt(plaintext, additionalAuthenticatedData); + } + else + { + byte[] temp = encryptor.Encrypt(plaintext, additionalAuthenticatedData); + byte[] retVal = new byte[checked(preBufferSize + temp.Length + postBufferSize)]; + Buffer.BlockCopy(temp, 0, retVal, checked((int)preBufferSize), temp.Length); + return retVal; + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfiguration.cs new file mode 100644 index 0000000000..763c8f6e93 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfiguration.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.XmlEncryption; + +namespace Microsoft.AspNet.Security.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.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationFactory.cs new file mode 100644 index 0000000000..375a7dc961 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationFactory.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 Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Security.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] IOptionsAccessor optionsAccessor) + { + _options = optionsAccessor.Options.Clone(); + } + + public IAuthenticatedEncryptorConfiguration CreateNewConfiguration() + { + // generate a 512-bit secret randomly + const int KDK_SIZE_IN_BYTES = 512 / 8; + var secret = ProtectedMemoryBlob.Random(KDK_SIZE_IN_BYTES); + return new CngCbcAuthenticatedEncryptorConfiguration(_options, secret); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs new file mode 100644 index 0000000000..2765421512 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + /// + /// Options for configuring an authenticated encryption mechanism which uses + /// Windows CNG algorithms in CBC encryption + HMAC validation modes. + /// + public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions + { + /// + /// 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 of 64 bits or greater. + /// 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; + + /// + /// The name of the algorithm to use for hashing data. + /// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider. + /// This property is required to have a value. + /// + /// + /// The algorithm must support being opened in HMAC mode and must have a digest length + /// of 128 bits or greater. + /// The default value is 'SHA256'. + /// + public string HashAlgorithm { get; set; } = Constants.BCRYPT_SHA256_ALGORITHM; + + /// + /// The name of the provider which contains the implementation of the hash algorithm. + /// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider. + /// This property is optional. + /// + /// + /// The default value is null. + /// + public string HashAlgorithmProvider { get; set; } = null; + + /// + /// Makes a duplicate of this object, which allows the original object to remain mutable. + /// + internal CngCbcAuthenticatedEncryptorConfigurationOptions Clone() + { + return new CngCbcAuthenticatedEncryptorConfigurationOptions() + { + EncryptionAlgorithm = this.EncryptionAlgorithm, + EncryptionAlgorithmKeySize = this.EncryptionAlgorithmKeySize, + EncryptionAlgorithmProvider = this.EncryptionAlgorithmProvider, + HashAlgorithm = this.HashAlgorithm, + HashAlgorithmProvider = this.HashAlgorithmProvider + }; + } + + 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); + + // 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 ProtectedMemoryBlob(secret), + symmetricAlgorithmHandle: encryptionAlgorithmHandle, + symmetricAlgorithmKeySizeInBytes: encryptionAlgorithmKeySizeInBits / 8, + hmacAlgorithmHandle: hashAlgorithmHandle); + } + + 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_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 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); + } + + // 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"); + + // 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; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationXmlReader.cs new file mode 100644 index 0000000000..d37f854c42 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationXmlReader.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.XmlEncryption; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + internal sealed class CngCbcAuthenticatedEncryptorConfigurationXmlReader : IAuthenticatedEncryptorConfigurationXmlReader + { + private readonly IServiceProvider _serviceProvider; + private readonly ITypeActivator _typeActivator; + + public CngCbcAuthenticatedEncryptorConfigurationXmlReader( + [NotNull] IServiceProvider serviceProvider, + [NotNull] ITypeActivator typeActivator) + { + _serviceProvider = serviceProvider; + _typeActivator = typeActivator; + } + + 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)_typeActivator.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 protectedMemoryBlob = new ProtectedMemoryBlob(decryptedSecretBytes); + return new CngCbcAuthenticatedEncryptorConfiguration(options, protectedMemoryBlob); + } + finally + { + Array.Clear(decryptedSecretBytes, 0, decryptedSecretBytes.Length); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfiguration.cs new file mode 100644 index 0000000000..3007f2eb72 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfiguration.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.XmlEncryption; + +namespace Microsoft.AspNet.Security.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.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationFactory.cs new file mode 100644 index 0000000000..ac074377f8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationFactory.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 Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Security.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] IOptionsAccessor optionsAccessor) + { + _options = optionsAccessor.Options.Clone(); + } + + public IAuthenticatedEncryptorConfiguration CreateNewConfiguration() + { + // generate a 512-bit secret randomly + const int KDK_SIZE_IN_BYTES = 512 / 8; + var secret = ProtectedMemoryBlob.Random(KDK_SIZE_IN_BYTES); + return new CngGcmAuthenticatedEncryptorConfiguration(_options, secret); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs new file mode 100644 index 0000000000..33ad8e8eb5 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.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 Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + /// + /// Options for configuring an authenticated encryption mechanism which uses + /// Windows CNG encryption algorithms in Galois/Counter Mode. + /// + public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions + { + /// + /// 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 ProtectedMemoryBlob(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; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationXmlReader.cs new file mode 100644 index 0000000000..e3fc4bad31 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationXmlReader.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.Linq; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.XmlEncryption; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + internal sealed class CngGcmAuthenticatedEncryptorConfigurationXmlReader : IAuthenticatedEncryptorConfigurationXmlReader + { + private readonly IServiceProvider _serviceProvider; + private readonly ITypeActivator _typeActivator; + + public CngGcmAuthenticatedEncryptorConfigurationXmlReader( + [NotNull] IServiceProvider serviceProvider, + [NotNull] ITypeActivator typeActivator) + { + _serviceProvider = serviceProvider; + _typeActivator = typeActivator; + } + + 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)_typeActivator.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 protectedMemoryBlob = new ProtectedMemoryBlob(decryptedSecretBytes); + return new CngGcmAuthenticatedEncryptorConfiguration(options, protectedMemoryBlob); + } + finally + { + Array.Clear(decryptedSecretBytes, 0, decryptedSecretBytes.Length); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.cs new file mode 100644 index 0000000000..b897d668a0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor.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.Security.DataProtection.AuthenticatedEncryption +{ + /// + /// The basic interface for providing an authenticated encryption and decryption routine. + /// + public interface IAuthenticatedEncryptor + { + /// + /// Validates the authentication tag of and decrypts a blob of encrypted data. + /// + /// The ciphertext (including authentication tag) to decrypt. + /// Any ancillary data which was used during computation + /// of the authentication tag. The same AAD must have been specified in the corresponding + /// call to 'Encrypt'. + /// The original plaintext data (if the authentication tag was validated and decryption succeeded). + /// All cryptography-related exceptions should be homogenized to CryptographicException. + byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData); + + /// + /// Encrypts and tamper-proofs a piece of data. + /// + /// The plaintext to encrypt. This input may be zero bytes in length. + /// A piece of data which will not be included in + /// the returned ciphertext but which will still be covered by the authentication tag. + /// This input may be zero bytes in length. The same AAD must be specified in the corresponding + /// call to Decrypt. + /// The ciphertext blob, including authentication tag. + /// All cryptography-related exceptions should be homogenized to CryptographicException. + byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor2.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor2.cs new file mode 100644 index 0000000000..2e36143dc3 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptor2.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.AuthenticatedEncryption +{ + internal interface IAuthenticatedEncryptor2 : IAuthenticatedEncryptor + { + byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfiguration.cs new file mode 100644 index 0000000000..0da7da4b5e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfiguration.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 System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.XmlEncryption; + +namespace Microsoft.AspNet.Security.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.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationFactory.cs new file mode 100644 index 0000000000..843de1540c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationFactory.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.Security.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.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationXmlReader.cs new file mode 100644 index 0000000000..0d1fcc38fc --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IAuthenticatedEncryptorConfigurationXmlReader.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; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.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.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfiguration.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfiguration.cs new file mode 100644 index 0000000000..e636713040 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfiguration.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.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.XmlEncryption; + +namespace Microsoft.AspNet.Security.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.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationFactory.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationFactory.cs new file mode 100644 index 0000000000..41cb60213e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationFactory.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 Microsoft.AspNet.Security.DataProtection.Managed; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + public sealed class ManagedAuthenticatedEncryptorConfigurationFactory : IAuthenticatedEncryptorConfigurationFactory + { + private readonly ManagedAuthenticatedEncryptorConfigurationOptions _options; + + public ManagedAuthenticatedEncryptorConfigurationFactory([NotNull] IOptionsAccessor 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); + ProtectedMemoryBlob secret; + try + { + secret = new ProtectedMemoryBlob(kdk); + } + finally + { + Array.Clear(kdk, 0, kdk.Length); + } + + return new ManagedAuthenticatedEncryptorConfiguration(_options, secret); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs new file mode 100644 index 0000000000..0a9036886c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.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.Reflection; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.Managed; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + /// + /// Options for configuring an authenticated encryption mechanism which uses + /// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations. + /// + public sealed class ManagedAuthenticatedEncryptorConfigurationOptions + { + /// + /// 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 ProtectedMemoryBlob(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; + } + + 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.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationXmlReader.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationXmlReader.cs new file mode 100644 index 0000000000..cfa38ed3ea --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationXmlReader.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.XmlEncryption; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption +{ + internal sealed class ManagedAuthenticatedEncryptorConfigurationXmlReader : IAuthenticatedEncryptorConfigurationXmlReader + { + private readonly IServiceProvider _serviceProvider; + private readonly ITypeActivator _typeActivator; + + public ManagedAuthenticatedEncryptorConfigurationXmlReader( + [NotNull] IServiceProvider serviceProvider, + [NotNull] ITypeActivator typeActivator) + { + _serviceProvider = serviceProvider; + _typeActivator = typeActivator; + } + + 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)_typeActivator.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 protectedMemoryBlob = new ProtectedMemoryBlob(decryptedSecretBytes); + return new ManagedAuthenticatedEncryptorConfiguration(options, protectedMemoryBlob); + } + finally + { + Array.Clear(decryptedSecretBytes, 0, decryptedSecretBytes.Length); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs b/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs deleted file mode 100644 index 67327aba4a..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.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.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Microsoft.AspNet.Security.DataProtection -{ - // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375524(v=vs.85).aspx - [StructLayout(LayoutKind.Sequential)] - internal struct BCRYPT_KEY_DATA_BLOB_HEADER - { - // from bcrypt.h - private const uint BCRYPT_KEY_DATA_BLOB_MAGIC = 0x4d42444b; //Key Data Blob Magic (KDBM) - private const uint BCRYPT_KEY_DATA_BLOB_VERSION1 = 0x1; - - public uint dwMagic; - public uint dwVersion; - public uint cbKeyData; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Initialize(ref BCRYPT_KEY_DATA_BLOB_HEADER pHeader) - { - pHeader.dwMagic = BCRYPT_KEY_DATA_BLOB_MAGIC; - pHeader.dwVersion = BCRYPT_KEY_DATA_BLOB_VERSION1; - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs deleted file mode 100644 index 38b5818e18..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs +++ /dev/null @@ -1,16 +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.Security.DataProtection -{ - // from bcrypt.h - [Flags] - internal enum BCryptAlgorithmFlags - { - BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008, - BCRYPT_CAPI_AES_FLAG = 0x00000010, - BCRYPT_HASH_REUSABLE_FLAG = 0x00000020, - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs deleted file mode 100644 index 5d05d68027..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs +++ /dev/null @@ -1,23 +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.Win32.SafeHandles; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal sealed class BCryptAlgorithmHandle : SafeHandleZeroOrMinusOneIsInvalid - { - // Called by P/Invoke when returning SafeHandles - private BCryptAlgorithmHandle() - : base(ownsHandle: true) - { - } - - // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. - protected override bool ReleaseHandle() - { - return (UnsafeNativeMethods.BCryptCloseAlgorithmProvider(handle, dwFlags: 0) == 0); - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs deleted file mode 100644 index 6144c3e3ec..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs +++ /dev/null @@ -1,23 +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.Win32.SafeHandles; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal sealed class BCryptHashHandle : SafeHandleZeroOrMinusOneIsInvalid - { - // Called by P/Invoke when returning SafeHandles - private BCryptHashHandle() - : base(ownsHandle: true) - { - } - - // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. - protected override bool ReleaseHandle() - { - return (UnsafeNativeMethods.BCryptDestroyHash(handle) == 0); - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs deleted file mode 100644 index ad60cc9ed0..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs +++ /dev/null @@ -1,295 +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.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Text; -using Microsoft.AspNet.Security.DataProtection.Util; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal unsafe static class BCryptUtil - { - // from dpapi.h - const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16; - const uint CRYPTPROTECTMEMORY_SAME_PROCESS = 0x00; - - private static readonly UTF8Encoding _secureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - - // constant-time buffer comparison - [MethodImpl(MethodImplOptions.NoOptimization)] - public static bool BuffersAreEqualSecure(byte* p1, byte* p2, uint count) - { - bool retVal = true; - while (count-- > 0) - { - retVal &= (*(p1++) == *(p2++)); - } - return retVal; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CheckOverflowUnderflow(int input) - { - var unused = checked((uint)input); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CheckOverflowUnderflow(uint input) - { - var unused = checked((int)input); - } - - // helper function to wrap BCryptCreateHash, passing in a key used for HMAC - public static BCryptHashHandle CreateHMACHandle(BCryptAlgorithmHandle algorithmHandle, byte* key, int keyLengthInBytes) - { - CheckOverflowUnderflow(keyLengthInBytes); - - BCryptHashHandle retVal; - int status = UnsafeNativeMethods.BCryptCreateHash(algorithmHandle, out retVal, IntPtr.Zero, 0, key, (uint)keyLengthInBytes, dwFlags: 0); - if (status != 0 || retVal == null || retVal.IsInvalid) - { - throw new CryptographicException(status); - } - - return retVal; - } - - // helper function to wrap BCryptEncrypt; returns number of bytes written to 'output' - // assumes the output buffer is large enough to hold the ciphertext + any necessary padding - public static int DecryptWithPadding(BCryptKeyHandle keyHandle, byte* input, int inputLength, byte* iv, int ivLength, byte* output, int outputLength) - { - CheckOverflowUnderflow(inputLength); - CheckOverflowUnderflow(ivLength); - CheckOverflowUnderflow(outputLength); - - // BCryptEncrypt destroys the 'iv' parameter, so we need to pass a duplicate instead of the original - if (ivLength > Constants.MAX_STACKALLOC_BYTES) - { - throw new InvalidOperationException(); - } - byte* pDuplicatedIV = stackalloc byte[ivLength]; - BufferUtil.BlockCopy(from: iv, to: pDuplicatedIV, byteCount: ivLength); - - uint retVal; - int status = UnsafeNativeMethods.BCryptDecrypt(keyHandle, input, (uint)inputLength, IntPtr.Zero, pDuplicatedIV, (uint)ivLength, output, (uint)outputLength, out retVal, BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); - if (status != 0) - { - throw new CryptographicException(status); - } - - return checked((int)retVal); - } - - // helper function to wrap BCryptKeyDerivation using SP800-108-CTR-HMAC-SHA512 - public static void DeriveKeysSP800108(byte[] protectedKdk, string purpose, BCryptAlgorithmHandle encryptionAlgorithmHandle, out BCryptKeyHandle encryptionKeyHandle, BCryptAlgorithmHandle hashAlgorithmHandle, out BCryptHashHandle hmacHandle, out byte[] kdfSubkey) - { - const int ENCRYPTION_KEY_SIZE_IN_BYTES = 256 / 8; - const int HMAC_KEY_SIZE_IN_BYTES = 256 / 8; - const int KDF_SUBKEY_SIZE_IN_BYTES = 512 / 8; - const int TOTAL_NUM_BYTES_TO_DERIVE = ENCRYPTION_KEY_SIZE_IN_BYTES + HMAC_KEY_SIZE_IN_BYTES + KDF_SUBKEY_SIZE_IN_BYTES; - - // keep our buffers on the stack while we're generating key material - byte* pBuffer = stackalloc byte[TOTAL_NUM_BYTES_TO_DERIVE]; // will be freed with frame pops - byte* pNewEncryptionKey = pBuffer; - byte* pNewHmacKey = &pNewEncryptionKey[ENCRYPTION_KEY_SIZE_IN_BYTES]; - byte* pNewKdfSubkey = &pNewHmacKey[HMAC_KEY_SIZE_IN_BYTES]; - - protectedKdk = (byte[])protectedKdk.Clone(); // CryptUnprotectMemory mutates its input, so we preserve the original - fixed (byte* pKdk = protectedKdk) - { - try - { - // Since the KDK is pinned, the GC won't move around the array containing the plaintext key before we - // have the opportunity to clear its contents. - UnprotectMemoryWithinThisProcess(pKdk, (uint)protectedKdk.Length); - - byte[] purposeBytes = (!String.IsNullOrEmpty(purpose)) ? _secureUtf8Encoding.GetBytes(purpose) : null; - SP800_108Helper.DeriveKeys(pKdk, protectedKdk.Length, purposeBytes, pBuffer, TOTAL_NUM_BYTES_TO_DERIVE); - - // Split into AES, HMAC, and KDF subkeys - encryptionKeyHandle = ImportKey(encryptionAlgorithmHandle, pNewEncryptionKey, ENCRYPTION_KEY_SIZE_IN_BYTES); - hmacHandle = CreateHMACHandle(hashAlgorithmHandle, pNewHmacKey, HMAC_KEY_SIZE_IN_BYTES); - kdfSubkey = BufferUtil.ToProtectedManagedByteArray(pNewKdfSubkey, KDF_SUBKEY_SIZE_IN_BYTES); - } - finally - { - BufferUtil.SecureZeroMemory(pKdk, protectedKdk.Length); - } - } - } - - // helper function to wrap BCryptDuplicateHash - public static BCryptHashHandle DuplicateHash(BCryptHashHandle hashHandle) - { - BCryptHashHandle retVal; - int status = UnsafeNativeMethods.BCryptDuplicateHash(hashHandle, out retVal, IntPtr.Zero, 0, dwFlags: 0); - if (status != 0 || retVal == null || retVal.IsInvalid) - { - throw new CryptographicException(status); - } - - return retVal; - } - - // helper function to wrap BCryptEncrypt; returns number of bytes written to 'output' - // assumes the output buffer is large enough to hold the ciphertext + any necessary padding - public static int EncryptWithPadding(BCryptKeyHandle keyHandle, byte* input, int inputLength, byte* iv, int ivLength, byte* output, int outputLength) - { - CheckOverflowUnderflow(inputLength); - CheckOverflowUnderflow(ivLength); - CheckOverflowUnderflow(outputLength); - - // BCryptEncrypt destroys the 'iv' parameter, so we need to pass a duplicate instead of the original - if (ivLength > Constants.MAX_STACKALLOC_BYTES) - { - throw new InvalidOperationException(); - } - byte* pDuplicatedIV = stackalloc byte[ivLength]; - BufferUtil.BlockCopy(from: iv, to: pDuplicatedIV, byteCount: ivLength); - - uint retVal; - int status = UnsafeNativeMethods.BCryptEncrypt(keyHandle, input, (uint)inputLength, IntPtr.Zero, pDuplicatedIV, (uint)ivLength, output, (uint)outputLength, out retVal, BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); - if (status != 0) - { - throw new CryptographicException(status); - } - - return checked((int)retVal); - } - - // helper function to take a key, apply a purpose, and generate a new subkey ("entropy") for DPAPI-specific scenarios - public static byte[] GenerateDpapiSubkey(byte[] previousKey, string purpose) - { - Debug.Assert(previousKey != null); - purpose = purpose ?? String.Empty; // cannot be null - - // create the HMAC object - BCryptHashHandle hashHandle; - fixed (byte* pPreviousKey = previousKey) - { - hashHandle = CreateHMACHandle(Algorithms.HMACSHA256AlgorithmHandle, pPreviousKey, previousKey.Length); - } - - // hash the purpose string, treating it as UTF-16LE - using (hashHandle) - { - byte[] retVal = new byte[256 / 8]; // fixed length output since we're hardcoded to HMACSHA256 - fixed (byte* pRetVal = retVal) - { - fixed (char* pPurpose = purpose) - { - HashData(hashHandle, (byte*)pPurpose, checked(purpose.Length * sizeof(char)), pRetVal, retVal.Length); - return retVal; - } - } - } - } - - // helper function that's similar to RNGCryptoServiceProvider, but works directly with pointers - public static void GenRandom(byte* buffer, int bufferBytes) - { - CheckOverflowUnderflow(bufferBytes); - - int status = UnsafeNativeMethods.BCryptGenRandom(IntPtr.Zero, buffer, (uint)bufferBytes, BCryptGenRandomFlags.BCRYPT_USE_SYSTEM_PREFERRED_RNG); - if (status != 0) - { - throw new CryptographicException(status); - } - } - - // helper function that wraps BCryptHashData / BCryptFinishHash - public static void HashData(BCryptHashHandle hashHandle, byte* input, int inputBytes, byte* output, int outputBytes) - { - CheckOverflowUnderflow(inputBytes); - CheckOverflowUnderflow(outputBytes); - - int status = UnsafeNativeMethods.BCryptHashData(hashHandle, input, (uint)inputBytes, dwFlags: 0); - if (status != 0) - { - throw new CryptographicException(status); - } - - status = UnsafeNativeMethods.BCryptFinishHash(hashHandle, output, (uint)outputBytes, dwFlags: 0); - if (status != 0) - { - throw new CryptographicException(status); - } - } - - // helper function that wraps BCryptImportKey with a key data blob - public static BCryptKeyHandle ImportKey(BCryptAlgorithmHandle algHandle, byte* key, int keyBytes) - { - CheckOverflowUnderflow(keyBytes); - - byte[] heapAllocatedKeyDataBlob = null; - int numBytesRequiredForKeyDataBlob = checked(keyBytes + sizeof(BCRYPT_KEY_DATA_BLOB_HEADER)); - if (numBytesRequiredForKeyDataBlob > Constants.MAX_STACKALLOC_BYTES) - { - heapAllocatedKeyDataBlob = new byte[numBytesRequiredForKeyDataBlob]; // allocate on heap if we cannot allocate on stack - } - - int status; - BCryptKeyHandle retVal; - fixed (byte* pHeapAllocatedKeyDataBlob = heapAllocatedKeyDataBlob) - { - // The header is first; if it wasn't heap-allocated we can stack-allocate now - BCRYPT_KEY_DATA_BLOB_HEADER* pKeyDataBlobHeader = (BCRYPT_KEY_DATA_BLOB_HEADER*)pHeapAllocatedKeyDataBlob; - if (pKeyDataBlobHeader == null) - { - byte* temp = stackalloc byte[numBytesRequiredForKeyDataBlob]; // won't be released until frame pops - pKeyDataBlobHeader = (BCRYPT_KEY_DATA_BLOB_HEADER*)temp; - } - BCRYPT_KEY_DATA_BLOB_HEADER.Initialize(ref *pKeyDataBlobHeader); - pKeyDataBlobHeader->cbKeyData = (uint)keyBytes; - - // the raw material immediately follows the header - byte* pKeyDataRawMaterial = (byte*)(&pKeyDataBlobHeader[1]); - - try - { - BufferUtil.BlockCopy(from: key, to: pKeyDataRawMaterial, byteCount: keyBytes); - status = UnsafeNativeMethods.BCryptImportKey(algHandle, IntPtr.Zero, Constants.BCRYPT_KEY_DATA_BLOB, out retVal, IntPtr.Zero, 0, (byte*)pKeyDataBlobHeader, (uint)numBytesRequiredForKeyDataBlob, dwFlags: 0); - } - finally - { - // zero out the key we just copied - BufferUtil.SecureZeroMemory(pKeyDataRawMaterial, keyBytes); - } - } - - if (status != 0 || retVal == null || retVal.IsInvalid) - { - throw new CryptographicException(status); - } - return retVal; - } - - internal static void ProtectMemoryWithinThisProcess(byte* pBuffer, uint bufferLength) - { - Debug.Assert(pBuffer != null); - Debug.Assert(bufferLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "Input buffer size must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE."); - - bool success = UnsafeNativeMethods.CryptProtectMemory(pBuffer, bufferLength, CRYPTPROTECTMEMORY_SAME_PROCESS); - if (!success) - { - throw new CryptographicException(Marshal.GetLastWin32Error()); - } - } - - internal static void UnprotectMemoryWithinThisProcess(byte* pBuffer, uint bufferLength) - { - Debug.Assert(pBuffer != null); - Debug.Assert(bufferLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "Input buffer size must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE."); - - bool success = UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, bufferLength, CRYPTPROTECTMEMORY_SAME_PROCESS); - if (!success) - { - throw new CryptographicException(Marshal.GetLastWin32Error()); - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BitHelpers.cs b/src/Microsoft.AspNet.Security.DataProtection/BitHelpers.cs new file mode 100644 index 0000000000..379b5cdf5d --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BitHelpers.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 System.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal unsafe static class BitHelpers + { + /// + /// Writes an unsigned 32-bit value to a memory address, big-endian. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteTo(void* ptr, uint value) + { + byte* bytePtr = (byte*)ptr; + bytePtr[0] = (byte)(value >> 24); + bytePtr[1] = (byte)(value >> 16); + bytePtr[2] = (byte)(value >> 8); + bytePtr[3] = (byte)(value); + } + + /// + /// Writes a signed 32-bit value to a memory address, big-endian. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteTo(byte[] buffer, ref int idx, int value) + { + WriteTo(buffer, ref idx, (uint)value); + } + + /// + /// Writes a signed 32-bit value to a memory address, big-endian. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteTo(byte[] buffer, ref int idx, uint value) + { + buffer[idx++] = (byte)(value >> 24); + buffer[idx++] = (byte)(value >> 16); + buffer[idx++] = (byte)(value >> 8); + buffer[idx++] = (byte)(value); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs new file mode 100644 index 0000000000..5909ddd9f9 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft 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; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + // http://msdn.microsoft.com/en-us/library/windows/desktop/cc562981(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO + { + public uint cbSize; + public uint dwInfoVersion; + public byte* pbNonce; + public uint cbNonce; + public byte* pbAuthData; + public uint cbAuthData; + public byte* pbTag; + public uint cbTag; + public byte* pbMacContext; + public uint cbMacContext; + public uint cbAAD; + public ulong cbData; + public uint dwFlags; + + // corresponds to the BCRYPT_INIT_AUTH_MODE_INFO macro in bcrypt.h + public static void Init(out BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info) + { + const uint BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION = 1; + info = new BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO + { + cbSize = (uint)sizeof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO), + dwInfoVersion = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION + }; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs new file mode 100644 index 0000000000..1660bea5a4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCRYPT_KEY_LENGTHS_STRUCT.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft 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.Runtime.InteropServices; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375525(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + internal struct BCRYPT_KEY_LENGTHS_STRUCT + { + // MSDN says these fields represent the key length in bytes. + // It's wrong: these key lengths are all actually in bits. + private uint dwMinLength; + private uint dwMaxLength; + private 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"); + } + CryptoUtil.Assert(keyLengthInBits % 8 == 0, "keyLengthInBits % 8 == 0"); + } + + private bool IsValidKeyLength(uint keyLengthInBits) + { + // If the step size is zero, then the key length must be exactly the min or the max. Otherwise, + // key length must be between min and max (inclusive) and a whole number of increments away from min. + if (dwIncrement == 0) + { + return (keyLengthInBits == dwMinLength || keyLengthInBits == dwMaxLength); + } + else + { + return (dwMinLength <= keyLengthInBits) + && (keyLengthInBits <= dwMaxLength) + && ((keyLengthInBits - dwMinLength) % dwIncrement == 0); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptBuffer.cs similarity index 76% rename from src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs rename to src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptBuffer.cs index 818a35360b..13d76f2f12 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptBuffer.cs @@ -1,11 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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 System.Runtime.InteropServices; -namespace Microsoft.AspNet.Security.DataProtection +namespace Microsoft.AspNet.Security.DataProtection.Cng { // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375368(v=vs.85).aspx [StructLayout(LayoutKind.Sequential)] diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptBufferDesc.cs similarity index 86% rename from src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs rename to src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptBufferDesc.cs index e27c12df36..477e9c4725 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptBufferDesc.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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 System.Runtime.InteropServices; -namespace Microsoft.AspNet.Security.DataProtection +namespace Microsoft.AspNet.Security.DataProtection.Cng { // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375370(v=vs.85).aspx [StructLayout(LayoutKind.Sequential)] diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptEncryptFlags.cs similarity index 62% rename from src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs rename to src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptEncryptFlags.cs index a435271ff3..9d46755dec 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptEncryptFlags.cs @@ -1,11 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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.Security.DataProtection +namespace Microsoft.AspNet.Security.DataProtection.Cng { - // from bcrypt.h [Flags] internal enum BCryptEncryptFlags { diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptGenRandomFlags.cs similarity index 71% rename from src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs rename to src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptGenRandomFlags.cs index 1e96354394..2fef69b319 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptGenRandomFlags.cs @@ -1,9 +1,9 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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.Security.DataProtection +namespace Microsoft.AspNet.Security.DataProtection.Cng { // from bcrypt.h [Flags] diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptGenRandomImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptGenRandomImpl.cs new file mode 100644 index 0000000000..6ce50391f1 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptGenRandomImpl.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.Security.DataProtection.Cng +{ + internal unsafe sealed class BCryptGenRandomImpl : IBCryptGenRandom + { + public static readonly BCryptGenRandomImpl Instance = new BCryptGenRandomImpl(); + + private BCryptGenRandomImpl() + { + } + + public void GenRandom(byte* pbBuffer, uint cbBuffer) + { + BCryptUtil.GenRandom(pbBuffer, cbBuffer); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptKeyDerivationBufferType.cs similarity index 85% rename from src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs rename to src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptKeyDerivationBufferType.cs index 6cc9882dd9..db47ba9b67 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptKeyDerivationBufferType.cs @@ -1,9 +1,9 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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.Security.DataProtection +namespace Microsoft.AspNet.Security.DataProtection.Cng { // from bcrypt.h internal enum BCryptKeyDerivationBufferType diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptUtil.cs new file mode 100644 index 0000000000..5afd9e2512 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/BCryptUtil.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.Security.DataProtection.Cng +{ + internal unsafe static class BCryptUtil + { + // helper function that's similar to RNGCryptoServiceProvider, but works directly with pointers + public static void GenRandom(byte* pbBuffer, uint cbBuffer) + { + if (cbBuffer != 0) + { + int ntstatus = UnsafeNativeMethods.BCryptGenRandom( + hAlgorithm: IntPtr.Zero, + pbBuffer: pbBuffer, + cbBuffer: cbBuffer, + dwFlags: BCryptGenRandomFlags.BCRYPT_USE_SYSTEM_PREFERRED_RNG); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/CachedAlgorithmHandles.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/CachedAlgorithmHandles.cs new file mode 100644 index 0000000000..ba6f5df025 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/CachedAlgorithmHandles.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + /// + /// Provides cached CNG algorithm provider instances, as calling BCryptOpenAlgorithmProvider is expensive. + /// Callers should use caution never to dispose of the algorithm provider instances returned by this type. + /// + internal static class CachedAlgorithmHandles + { + private static CachedAlgorithmInfo _aesCbc = new CachedAlgorithmInfo(() => GetAesAlgorithm(chainingMode: Constants.BCRYPT_CHAIN_MODE_CBC)); + private static CachedAlgorithmInfo _aesGcm = new CachedAlgorithmInfo(() => GetAesAlgorithm(chainingMode: Constants.BCRYPT_CHAIN_MODE_GCM)); + private static CachedAlgorithmInfo _hmacSha1 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA1_ALGORITHM)); + private static CachedAlgorithmInfo _hmacSha256 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA256_ALGORITHM)); + private static CachedAlgorithmInfo _hmacSha512 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA512_ALGORITHM)); + private static CachedAlgorithmInfo _pbkdf2 = new CachedAlgorithmInfo(GetPbkdf2Algorithm); + private static CachedAlgorithmInfo _sha1 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA1_ALGORITHM)); + private static CachedAlgorithmInfo _sha256 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA256_ALGORITHM)); + private static CachedAlgorithmInfo _sha512 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA512_ALGORITHM)); + private static CachedAlgorithmInfo _sp800_108_ctr_hmac = new CachedAlgorithmInfo(GetSP800_108_CTR_HMACAlgorithm); + + public static BCryptAlgorithmHandle AES_CBC + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesCbc); + } + } + + public static BCryptAlgorithmHandle AES_GCM + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _aesGcm); + } + } + + public static BCryptAlgorithmHandle HMAC_SHA1 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha1); + } + } + + public static BCryptAlgorithmHandle HMAC_SHA256 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha256); + } + } + + public static BCryptAlgorithmHandle HMAC_SHA512 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _hmacSha512); + } + } + + // Only available on Win8+. + public static BCryptAlgorithmHandle PBKDF2 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _pbkdf2); + } + } + + public static BCryptAlgorithmHandle SHA1 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha1); + } + } + + public static BCryptAlgorithmHandle SHA256 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha256); + } + } + + public static BCryptAlgorithmHandle SHA512 + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sha512); + } + } + + public static BCryptAlgorithmHandle SP800_108_CTR_HMAC + { + get + { + return CachedAlgorithmInfo.GetAlgorithmHandle(ref _sp800_108_ctr_hmac); + } + } + + private static BCryptAlgorithmHandle GetAesAlgorithm(string chainingMode) + { + var algHandle = BCryptAlgorithmHandle.OpenAlgorithmHandle(Constants.BCRYPT_AES_ALGORITHM); + algHandle.SetChainingMode(chainingMode); + return algHandle; + } + + private static BCryptAlgorithmHandle GetHashAlgorithm(string algorithm) + { + return BCryptAlgorithmHandle.OpenAlgorithmHandle(algorithm, hmac: false); + } + + private static BCryptAlgorithmHandle GetHmacAlgorithm(string algorithm) + { + return BCryptAlgorithmHandle.OpenAlgorithmHandle(algorithm, hmac: true); + } + + private static BCryptAlgorithmHandle GetPbkdf2Algorithm() + { + return BCryptAlgorithmHandle.OpenAlgorithmHandle(Constants.BCRYPT_PBKDF2_ALGORITHM, implementation: Constants.MS_PRIMITIVE_PROVIDER); + } + + private static BCryptAlgorithmHandle GetSP800_108_CTR_HMACAlgorithm() + { + return BCryptAlgorithmHandle.OpenAlgorithmHandle(Constants.BCRYPT_SP800108_CTR_HMAC_ALGORITHM, implementation: Constants.MS_PRIMITIVE_PROVIDER); + } + + // Warning: mutable struct! + private struct CachedAlgorithmInfo + { + private WeakReference _algorithmHandle; + private readonly Func _factory; + + public CachedAlgorithmInfo(Func factory) + { + _algorithmHandle = null; + _factory = factory; + } + + public static BCryptAlgorithmHandle GetAlgorithmHandle(ref CachedAlgorithmInfo cachedAlgorithmInfo) + { + return WeakReferenceHelpers.GetSharedInstance(ref cachedAlgorithmInfo._algorithmHandle, cachedAlgorithmInfo._factory); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/CbcAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/CbcAuthenticatedEncryptor.cs new file mode 100644 index 0000000000..cc65448056 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/CbcAuthenticatedEncryptor.cs @@ -0,0 +1,437 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.SafeHandles; +using Microsoft.AspNet.Security.DataProtection.SP800_108; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + // An encryptor which does Encrypt(CBC) + HMAC using the Windows CNG (BCrypt*) APIs. + // The payloads produced by this encryptor should be compatible with the payloads + // produced by the managed Encrypt(CBC) + HMAC encryptor. + internal unsafe sealed class CbcAuthenticatedEncryptor : CngAuthenticatedEncryptorBase + { + // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single + // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block + // encryption operations, which a high-traffic web server might perform in mere hours. + // AES and other 128-bit block ciphers are less susceptible to this due to the larger IV + // space, but unfortunately some organizations require older 64-bit block ciphers. To address + // the collision issue, we'll feed 128 bits of entropy to the KDF when performing subkey + // generation. This creates >= 192 bits total entropy for each operation, so we shouldn't + // expect a collision until >= 2^96 operations. Even 2^80 operations still maintains a <= 2^-32 + // probability of collision, and this is acceptable for the expected KDK lifetime. + private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8; + + // 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; + private readonly uint _hmacAlgorithmDigestLengthInBytes; + private readonly uint _hmacAlgorithmSubkeyLengthInBytes; + private readonly ISP800_108_CTR_HMACSHA512Provider _sp800_108_ctr_hmac_provider; + private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle; + private readonly uint _symmetricAlgorithmBlockSizeInBytes; + private readonly uint _symmetricAlgorithmSubkeyLengthInBytes; + + public CbcAuthenticatedEncryptor(ProtectedMemoryBlob 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; + _symmetricAlgorithmBlockSizeInBytes = symmetricAlgorithmHandle.GetCipherBlockLength(); + _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes; + _hmacAlgorithmHandle = hmacAlgorithmHandle; + _hmacAlgorithmDigestLengthInBytes = hmacAlgorithmHandle.GetHashDigestLength(); + _hmacAlgorithmSubkeyLengthInBytes = _hmacAlgorithmDigestLengthInBytes; // for simplicity we'll generate HMAC subkeys with a length equal to the digest length + + 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"); + + _contextHeader = CreateContextHeader(); + } + + private byte[] CreateContextHeader() + { + byte[] retVal = new byte[checked( + 1 /* KDF alg */ + + 1 /* chaining mode */ + + sizeof(uint) /* sym alg key size */ + + sizeof(uint) /* sym alg block size */ + + sizeof(uint) /* hmac alg key size */ + + sizeof(uint) /* hmac alg digest size */ + + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */ + + _hmacAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)]; + + fixed (byte* pbRetVal = retVal) + { + byte* ptr = pbRetVal; + + // First is the two-byte header + *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF + *(ptr++) = 0; // 0x00 = CBC encryption + HMAC authentication + + // Next is information about the symmetric algorithm (key size followed by block size) + BitHelpers.WriteTo(ptr, _symmetricAlgorithmSubkeyLengthInBytes); + ptr += sizeof(uint); + BitHelpers.WriteTo(ptr, _symmetricAlgorithmBlockSizeInBytes); + ptr += sizeof(uint); + + // Next is information about the HMAC algorithm (key size followed by digest size) + BitHelpers.WriteTo(ptr, _hmacAlgorithmSubkeyLengthInBytes); + ptr += sizeof(uint); + BitHelpers.WriteTo(ptr, _hmacAlgorithmDigestLengthInBytes); + ptr += sizeof(uint); + + // See the design document for an explanation of the following code. + byte[] tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes]; + fixed (byte* pbTempKeys = tempKeys) + { + byte dummy; + + // Derive temporary keys for encryption + HMAC. + using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider()) + { + provider.DeriveKey( + pbLabel: &dummy, + cbLabel: 0, + pbContext: &dummy, + cbContext: 0, + pbDerivedKey: pbTempKeys, + cbDerivedKey: (uint)tempKeys.Length); + } + + // At this point, tempKeys := { K_E || K_H }. + byte* pbSymmetricEncryptionSubkey = pbTempKeys; + byte* pbHmacSubkey = &pbTempKeys[_symmetricAlgorithmSubkeyLengthInBytes]; + + // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer. + using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) + { + fixed (byte* pbIV = new byte[_symmetricAlgorithmBlockSizeInBytes] /* will be zero-initialized */) + { + DoCbcEncrypt( + symmetricKeyHandle: symmetricKeyHandle, + pbIV: pbIV, + pbInput: &dummy, + cbInput: 0, + pbOutput: ptr, + cbOutput: _symmetricAlgorithmBlockSizeInBytes); + } + } + ptr += _symmetricAlgorithmBlockSizeInBytes; + + // MAC a zero-length input string and copy the digest to the return buffer. + using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + { + hashHandle.HashData( + pbInput: &dummy, + cbInput: 0, + pbHashDigest: ptr, + cbHashDigest: _hmacAlgorithmDigestLengthInBytes); + } + + ptr += _hmacAlgorithmDigestLengthInBytes; + CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length"); + } + } + + // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || hmacAlgKeySize || hmacAlgDigestSize || E("") || MAC("") }. + return retVal; + } + + protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) + { + // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC + if (cbCiphertext < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes)) + { + throw Error.CryptCommon_PayloadInvalid(); + } + + // Assumption: pbCipherText := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) } + + uint cbEncryptedData = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _hmacAlgorithmDigestLengthInBytes)); + + // Calculate offsets + byte* pbKeyModifier = pbCiphertext; + byte* pbIV = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbEncryptedData = &pbIV[_symmetricAlgorithmBlockSizeInBytes]; + byte* pbActualHmac = &pbEncryptedData[cbEncryptedData]; + + // Use the KDF to recreate the symmetric encryption and HMAC subkeys + // We'll need a temporary buffer to hold them + uint cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes); + byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)]; + try + { + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: cbAdditionalAuthenticatedData, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbTempSubkeys, + cbDerivedKey: cbTempSubkeys); + + // Calculate offsets + byte* pbSymmetricEncryptionSubkey = pbTempSubkeys; + byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes]; + + // First, perform an explicit integrity check over (iv | encryptedPayload) to ensure the + // data hasn't been tampered with. The integrity check is also implicitly performed over + // keyModifier since that value was provided to the KDF earlier. + using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + { + if (!ValidateHash(hashHandle, pbIV, _symmetricAlgorithmBlockSizeInBytes + cbEncryptedData, pbActualHmac)) + { + throw Error.CryptCommon_PayloadInvalid(); + } + } + + // If the integrity check succeeded, decrypt the payload. + using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) + { + return DoCbcDecrypt(decryptionSubkeyHandle, pbIV, pbEncryptedData, cbEncryptedData); + } + } + finally + { + // Buffer contains sensitive key material; nuke. + UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys); + } + } + + public override void Dispose() + { + _sp800_108_ctr_hmac_provider.Dispose(); + + // We don't want to dispose of the underlying algorithm instances because they + // might be reused. + } + + // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size. + private byte[] DoCbcDecrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput) + { + // BCryptDecrypt mutates the provided IV; we need to clone it to prevent mutation of the original value + byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes); + + // First, figure out how large an output buffer we require. + // Ideally we'd be able to transform the last block ourselves and strip + // off the padding before creating the return value array, but we don't + // know the actual padding scheme being used under the covers (we can't + // assume PKCS#7). So unfortunately we're stuck with the temporary buffer. + // (Querying the output size won't mutate the IV.) + uint dwEstimatedDecryptedByteCount; + int ntstatus = UnsafeNativeMethods.BCryptDecrypt( + hKey: symmetricKeyHandle, + pbInput: pbInput, + cbInput: cbInput, + pPaddingInfo: null, + pbIV: pbClonedIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: null, + cbOutput: 0, + pcbResult: out dwEstimatedDecryptedByteCount, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + byte[] decryptedPayload = new byte[dwEstimatedDecryptedByteCount]; + uint dwActualDecryptedByteCount; + fixed (byte* pbDecryptedPayload = decryptedPayload) + { + byte dummy; + + // Perform the actual decryption. + ntstatus = UnsafeNativeMethods.BCryptDecrypt( + hKey: symmetricKeyHandle, + pbInput: pbInput, + cbInput: cbInput, + pPaddingInfo: null, + pbIV: pbClonedIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: (pbDecryptedPayload != null) ? pbDecryptedPayload : &dummy, // CLR won't pin zero-length arrays + cbOutput: dwEstimatedDecryptedByteCount, + pcbResult: out dwActualDecryptedByteCount, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + } + + // Decryption finished! + CryptoUtil.Assert(dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount, "dwActualDecryptedByteCount <= dwEstimatedDecryptedByteCount"); + if (dwActualDecryptedByteCount == dwEstimatedDecryptedByteCount) + { + // payload takes up the entire buffer + return decryptedPayload; + } + else + { + // payload takes up only a partial buffer + byte[] resizedDecryptedPayload = new byte[dwActualDecryptedByteCount]; + Buffer.BlockCopy(decryptedPayload, 0, resizedDecryptedPayload, 0, resizedDecryptedPayload.Length); + return resizedDecryptedPayload; + } + } + + // 'pbIV' must be a pointer to a buffer equal in length to the symmetric algorithm block size. + private void DoCbcEncrypt(BCryptKeyHandle symmetricKeyHandle, byte* pbIV, byte* pbInput, uint cbInput, byte* pbOutput, uint cbOutput) + { + // BCryptEncrypt mutates the provided IV; we need to clone it to prevent mutation of the original value + byte* pbClonedIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + UnsafeBufferUtil.BlockCopy(from: pbIV, to: pbClonedIV, byteCount: _symmetricAlgorithmBlockSizeInBytes); + + uint dwEncryptedBytes; + int ntstatus = UnsafeNativeMethods.BCryptEncrypt( + hKey: symmetricKeyHandle, + pbInput: pbInput, + cbInput: cbInput, + pPaddingInfo: null, + pbIV: pbClonedIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: pbOutput, + cbOutput: cbOutput, + pcbResult: out dwEncryptedBytes, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + // Need to make sure we didn't underrun the buffer - means caller passed a bad value + CryptoUtil.Assert(dwEncryptedBytes == cbOutput, "dwEncryptedBytes == cbOutput"); + } + + protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer) + { + // This buffer will be used to hold the symmetric encryption and HMAC subkeys + // used in the generation of this payload. + uint cbTempSubkeys = checked(_symmetricAlgorithmSubkeyLengthInBytes + _hmacAlgorithmSubkeyLengthInBytes); + byte* pbTempSubkeys = stackalloc byte[checked((int)cbTempSubkeys)]; + + try + { + // Randomly generate the key modifier and IV. + uint cbKeyModifierAndIV = checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes); + byte* pbKeyModifierAndIV = stackalloc byte[checked((int)cbKeyModifierAndIV)]; + _genRandom.GenRandom(pbKeyModifierAndIV, cbKeyModifierAndIV); + + // Calculate offsets + byte* pbKeyModifier = pbKeyModifierAndIV; + byte* pbIV = &pbKeyModifierAndIV[KEY_MODIFIER_SIZE_IN_BYTES]; + + // Use the KDF to generate a new symmetric encryption and HMAC subkey + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: cbAdditionalAuthenticatedData, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbTempSubkeys, + cbDerivedKey: cbTempSubkeys); + + // Calculate offsets + byte* pbSymmetricEncryptionSubkey = pbTempSubkeys; + byte* pbHmacSubkey = &pbTempSubkeys[_symmetricAlgorithmSubkeyLengthInBytes]; + + using (var symmetricKeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) + { + // We can't assume PKCS#7 padding (maybe the underlying provided is using CTS), + // so we need to query the padded output size before we can allocate the return value array. + uint cbOutputCiphertext = GetCbcEncryptedOutputSizeWithPadding(symmetricKeyHandle, pbPlaintext, cbPlaintext); + + // Allocate return value array and start copying some data + byte[] retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext + _hmacAlgorithmDigestLengthInBytes + cbPostBuffer)]; + fixed (byte* pbRetVal = retVal) + { + // Calculate offsets + byte* pbOutputKeyModifier = &pbRetVal[cbPreBuffer]; + byte* pbOutputIV = &pbOutputKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbOutputCiphertext = &pbOutputIV[_symmetricAlgorithmBlockSizeInBytes]; + byte* pbOutputHmac = &pbOutputCiphertext[cbOutputCiphertext]; + + UnsafeBufferUtil.BlockCopy(from: pbKeyModifierAndIV, to: pbOutputKeyModifier, byteCount: cbKeyModifierAndIV); + + // retVal will eventually contain { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer } + // At this point, retVal := { preBuffer | keyModifier | iv | _____ | _____ | postBuffer } + + DoCbcEncrypt( + symmetricKeyHandle: symmetricKeyHandle, + pbIV: pbIV, + pbInput: pbPlaintext, + cbInput: cbPlaintext, + pbOutput: pbOutputCiphertext, + cbOutput: cbOutputCiphertext); + + // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | _____ | postBuffer } + + // Compute the HMAC over the IV and the ciphertext (prevents IV tampering). + // The HMAC is already implicitly computed over the key modifier since the key + // modifier is used as input to the KDF. + using (var hashHandle = _hmacAlgorithmHandle.CreateHmac(pbHmacSubkey, _hmacAlgorithmSubkeyLengthInBytes)) + { + hashHandle.HashData( + pbInput: pbOutputIV, + cbInput: checked(_symmetricAlgorithmBlockSizeInBytes + cbOutputCiphertext), + pbHashDigest: pbOutputHmac, + cbHashDigest: _hmacAlgorithmDigestLengthInBytes); + } + + // At this point, retVal := { preBuffer | keyModifier | iv | encryptedData | HMAC(iv | encryptedData) | postBuffer } + // And we're done! + return retVal; + } + } + } + finally + { + // Buffer contains sensitive material; nuke it. + UnsafeBufferUtil.SecureZeroMemory(pbTempSubkeys, cbTempSubkeys); + } + } + + private uint GetCbcEncryptedOutputSizeWithPadding(BCryptKeyHandle symmetricKeyHandle, byte* pbInput, uint cbInput) + { + // ok for this memory to remain uninitialized since nobody depends on it + byte* pbIV = stackalloc byte[checked((int)_symmetricAlgorithmBlockSizeInBytes)]; + + // Calling BCryptEncrypt with a null output pointer will cause it to return the total number + // of bytes required for the output buffer. + uint dwResult; + int ntstatus = UnsafeNativeMethods.BCryptEncrypt( + hKey: symmetricKeyHandle, + pbInput: pbInput, + cbInput: cbInput, + pPaddingInfo: null, + pbIV: pbIV, + cbIV: _symmetricAlgorithmBlockSizeInBytes, + pbOutput: null, + cbOutput: 0, + pcbResult: out dwResult, + dwFlags: BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + return dwResult; + } + + // 'pbExpectedDigest' must point to a '_hmacAlgorithmDigestLengthInBytes'-length buffer + private bool ValidateHash(BCryptHashHandle hashHandle, byte* pbInput, uint cbInput, byte* pbExpectedDigest) + { + byte* pbActualDigest = stackalloc byte[checked((int)_hmacAlgorithmDigestLengthInBytes)]; + hashHandle.HashData(pbInput, cbInput, pbActualDigest, _hmacAlgorithmDigestLengthInBytes); + return CryptoUtil.TimeConstantBuffersAreEqual(pbExpectedDigest, pbActualDigest, _hmacAlgorithmDigestLengthInBytes); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/CngAuthenticatedEncryptorBase.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/CngAuthenticatedEncryptorBase.cs new file mode 100644 index 0000000000..2e00fb1cb3 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/CngAuthenticatedEncryptorBase.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + internal unsafe abstract class CngAuthenticatedEncryptorBase : IAuthenticatedEncryptor, IDisposable + { + public byte[] Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) + { + // This wrapper simply converts ArraySegment to byte* and calls the impl method. + + // Input validation + ciphertext.Validate(); + additionalAuthenticatedData.Validate(); + + byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer + fixed (byte* pbCiphertextArray = ciphertext.Array) + { + fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array) + { + try + { + return DecryptImpl( + pbCiphertext: (pbCiphertextArray != null) ? &pbCiphertextArray[ciphertext.Offset] : &dummy, + cbCiphertext: (uint)ciphertext.Count, + pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy, + cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count); + } + catch (Exception ex) if (!(ex is CryptographicException)) + { + // Homogenize to CryptographicException. + throw Error.CryptCommon_GenericError(ex); + } + } + } + } + + protected abstract byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData); + + public abstract void Dispose(); + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) + { + return Encrypt(plaintext, additionalAuthenticatedData, 0, 0); + } + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData, uint preBufferSize, uint postBufferSize) + { + // This wrapper simply converts ArraySegment to byte* and calls the impl method. + + // Input validation + plaintext.Validate(); + additionalAuthenticatedData.Validate(); + + byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer + fixed (byte* pbPlaintextArray = plaintext.Array) + { + fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array) + { + try + { + return EncryptImpl( + pbPlaintext: (pbPlaintextArray != null) ? &pbPlaintextArray[plaintext.Offset] : &dummy, + cbPlaintext: (uint)plaintext.Count, + pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy, + cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count, + cbPreBuffer: preBufferSize, + cbPostBuffer: postBufferSize); + } + catch (Exception ex) if (!(ex is CryptographicException)) + { + // Homogenize to CryptographicException. + throw Error.CryptCommon_GenericError(ex); + } + } + } + } + + protected abstract byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs new file mode 100644 index 0000000000..6c0f368847 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft 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.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + internal unsafe static class DpapiSecretSerializerHelper + { + // from ncrypt.h + private const uint NCRYPT_SILENT_FLAG = 0x00000040; + + // from dpapi.h + private const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1; + private const uint CRYPTPROTECT_LOCAL_MACHINE = 0x4; + + private static readonly byte[] _purpose = Encoding.UTF8.GetBytes("DPAPI-Protected Secret"); + + public static byte[] ProtectWithDpapi(ISecret secret) + { + Debug.Assert(secret != null); + + byte[] plaintextSecret = new byte[secret.Length]; + fixed (byte* pbPlaintextSecret = plaintextSecret) + { + try + { + secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret)); + fixed (byte* pbPurpose = _purpose) + { + return ProtectWithDpapiImpl(pbPlaintextSecret, (uint)plaintextSecret.Length, pbPurpose, (uint)_purpose.Length); + } + } + finally + { + // To limit exposure to the GC. + Array.Clear(plaintextSecret, 0, plaintextSecret.Length); + } + } + } + + internal static byte[] ProtectWithDpapiImpl(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 + + DATA_BLOB dataIn = new DATA_BLOB() + { + cbData = cbSecret, + pbData = (pbSecret != null) ? pbSecret : &dummy + }; + DATA_BLOB entropy = new DATA_BLOB() + { + cbData = cbOptionalEntropy, + pbData = (pbOptionalEntropy != null) ? pbOptionalEntropy : &dummy + }; + DATA_BLOB dataOut = default(DATA_BLOB); + +#if !ASPNETCORE50 + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { + bool success = UnsafeNativeMethods.CryptProtectData( + pDataIn: &dataIn, + szDataDescr: IntPtr.Zero, + pOptionalEntropy: &entropy, + pvReserved: IntPtr.Zero, + pPromptStruct: IntPtr.Zero, + dwFlags: CRYPTPROTECT_UI_FORBIDDEN | ((fLocalMachine) ? CRYPTPROTECT_LOCAL_MACHINE : 0), + pDataOut: out dataOut); + if (!success) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new CryptographicException(errorCode); + } + + int dataLength = checked((int)dataOut.cbData); + byte[] retVal = new byte[dataLength]; + Marshal.Copy((IntPtr)dataOut.pbData, retVal, 0, dataLength); + return retVal; + } + finally + { + // Free memory so that we don't leak. + // FreeHGlobal actually calls LocalFree. + if (dataOut.pbData != null) + { + Marshal.FreeHGlobal((IntPtr)dataOut.pbData); + } + } + } + + public static byte[] ProtectWithDpapiNG(ISecret secret, NCryptDescriptorHandle protectionDescriptorHandle) + { + Debug.Assert(secret != null); + Debug.Assert(protectionDescriptorHandle != null); + + byte[] plaintextSecret = new byte[secret.Length]; + fixed (byte* pbPlaintextSecret = plaintextSecret) + { + try + { + secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret)); + + byte dummy; // used to provide a valid memory address if secret is zero-length + return ProtectWithDpapiNGImpl( + protectionDescriptorHandle: protectionDescriptorHandle, + pbData: (pbPlaintextSecret != null) ? pbPlaintextSecret : &dummy, + cbData: (uint)plaintextSecret.Length); + } + finally + { + // Limits secret exposure to garbage collector. + Array.Clear(plaintextSecret, 0, plaintextSecret.Length); + } + } + } + + private static byte[] ProtectWithDpapiNGImpl(NCryptDescriptorHandle protectionDescriptorHandle, byte* pbData, uint cbData) + { + Debug.Assert(protectionDescriptorHandle != null); + Debug.Assert(pbData != null); + + // Perform the encryption operation, putting the protected data into LocalAlloc-allocated memory. + LocalAllocHandle protectedData; + uint cbProtectedData; + int ntstatus = UnsafeNativeMethods.NCryptProtectSecret( + hDescriptor: protectionDescriptorHandle, + dwFlags: NCRYPT_SILENT_FLAG, + pbData: pbData, + cbData: cbData, + pMemPara: IntPtr.Zero, + hWnd: IntPtr.Zero, + ppbProtectedBlob: out protectedData, + pcbProtectedBlob: out cbProtectedData); + UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus); + CryptoUtil.Assert(protectedData != null && !protectedData.IsInvalid, "protectedData != null && !protectedData.IsInvalid"); + + // Copy the data from LocalAlloc-allocated memory into a managed memory buffer. + using (protectedData) + { + byte[] retVal = new byte[cbProtectedData]; + if (cbProtectedData > 0) + { + fixed (byte* pbRetVal = retVal) + { + bool handleAcquired = false; +#if !ASPNETCORE50 + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { + protectedData.DangerousAddRef(ref handleAcquired); + UnsafeBufferUtil.BlockCopy(from: (void*)protectedData.DangerousGetHandle(), to: pbRetVal, byteCount: cbProtectedData); + } + finally + { + if (handleAcquired) + { + protectedData.DangerousRelease(); + } + } + } + } + return retVal; + } + } + + public static ProtectedMemoryBlob UnprotectWithDpapi(byte[] protectedSecret) + { + Debug.Assert(protectedSecret != null); + + fixed (byte* pbProtectedSecret = protectedSecret) + { + fixed (byte* pbPurpose = _purpose) + { + return UnprotectWithDpapiImpl(pbProtectedSecret, (uint)protectedSecret.Length, pbPurpose, (uint)_purpose.Length); + } + } + } + + internal static ProtectedMemoryBlob UnprotectWithDpapiImpl(byte* pbProtectedData, uint cbProtectedData, byte* pbOptionalEntropy, uint cbOptionalEntropy) + { + byte dummy; // provides a valid memory address if the secret or entropy has zero length + + DATA_BLOB dataIn = new DATA_BLOB() + { + cbData = cbProtectedData, + pbData = (pbProtectedData != null) ? pbProtectedData : &dummy + }; + DATA_BLOB entropy = new DATA_BLOB() + { + cbData = cbOptionalEntropy, + pbData = (pbOptionalEntropy != null) ? pbOptionalEntropy : &dummy + }; + DATA_BLOB dataOut = default(DATA_BLOB); + +#if !ASPNETCORE50 + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { + bool success = UnsafeNativeMethods.CryptUnprotectData( + pDataIn: &dataIn, + ppszDataDescr: IntPtr.Zero, + pOptionalEntropy: &entropy, + pvReserved: IntPtr.Zero, + pPromptStruct: IntPtr.Zero, + dwFlags: CRYPTPROTECT_UI_FORBIDDEN, + pDataOut: out dataOut); + if (!success) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new CryptographicException(errorCode); + } + + return new ProtectedMemoryBlob(dataOut.pbData, checked((int)dataOut.cbData)); + } + finally + { + // Zero and free memory so that we don't leak secrets. + // FreeHGlobal actually calls LocalFree. + if (dataOut.pbData != null) + { + UnsafeBufferUtil.SecureZeroMemory(dataOut.pbData, dataOut.cbData); + Marshal.FreeHGlobal((IntPtr)dataOut.pbData); + } + } + } + + public static ProtectedMemoryBlob UnprotectWithDpapiNG(byte[] protectedData) + { + Debug.Assert(protectedData != null); + + fixed (byte* pbProtectedData = protectedData) + { + byte dummy; // used to provide a valid memory address if protected data is zero-length + return UnprotectWithDpapiNGImpl( + pbData: (pbProtectedData != null) ? pbProtectedData : &dummy, + cbData: (uint)protectedData.Length); + } + } + + private static ProtectedMemoryBlob UnprotectWithDpapiNGImpl(byte* pbData, uint cbData) + { + Debug.Assert(pbData != null); + + // First, decrypt the payload into LocalAlloc-allocated memory. + LocalAllocHandle unencryptedPayloadHandle; + uint cbUnencryptedPayload; + int ntstatus = UnsafeNativeMethods.NCryptUnprotectSecret( + phDescriptor: IntPtr.Zero, + dwFlags: NCRYPT_SILENT_FLAG, + pbProtectedBlob: pbData, + cbProtectedBlob: cbData, + pMemPara: IntPtr.Zero, + hWnd: IntPtr.Zero, + ppbData: out unencryptedPayloadHandle, + pcbData: out cbUnencryptedPayload); + UnsafeNativeMethods.ThrowExceptionForNCryptStatus(ntstatus); + CryptoUtil.Assert(unencryptedPayloadHandle != null && !unencryptedPayloadHandle.IsInvalid, "unencryptedPayloadHandle != null && !unencryptedPayloadHandle.IsInvalid"); + + // Copy the data from LocalAlloc-allocated memory into a CryptProtectMemory-protected buffer. + // There's a small window between NCryptUnprotectSecret returning and the call to PrepareConstrainedRegions + // below where the AppDomain could rudely unload. This won't leak memory (due to the SafeHandle), but it + // will cause the secret not to be zeroed out before the memory is freed. We won't worry about this since + // the window is extremely small and AppDomain unloads should not happen here in practice. + using (unencryptedPayloadHandle) + { + bool handleAcquired = false; +#if !ASPNETCORE50 + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { + unencryptedPayloadHandle.DangerousAddRef(ref handleAcquired); + return new ProtectedMemoryBlob((byte*)unencryptedPayloadHandle.DangerousGetHandle(), checked((int)cbUnencryptedPayload)); + } + finally + { + if (handleAcquired) + { + UnsafeBufferUtil.SecureZeroMemory((byte*)unencryptedPayloadHandle.DangerousGetHandle(), cbUnencryptedPayload); + unencryptedPayloadHandle.DangerousRelease(); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/GcmAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/GcmAuthenticatedEncryptor.cs new file mode 100644 index 0000000000..9e404851cd --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/GcmAuthenticatedEncryptor.cs @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.SafeHandles; +using Microsoft.AspNet.Security.DataProtection.SP800_108; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + // GCM is defined in NIST SP 800-38D (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf). + // Heed closely the uniqueness requirements called out in Sec. 8: the probability that the GCM encryption + // routine is ever invoked on two or more distinct sets of input data with the same (key, IV) shall not + // exceed 2^-32. If we fix the key and use a random 96-bit IV for each invocation, this means that after + // 2^32 encryption operations the odds of reusing any (key, IV) pair is 2^-32 (see Sec. 8.3). This won't + // work for our use since a high-traffic web server can go through 2^32 requests in mere days. Instead, + // we'll use 224 bits of entropy for each operation, with 128 bits going to the KDF and 96 bits + // going to the IV. This means that we'll only hit the 2^-32 probability limit after 2^96 encryption + // operations, which will realistically never happen. (At the absurd rate of one encryption operation + // per nanosecond, it would still take 180 times the age of the universe to hit 2^96 operations.) + internal unsafe sealed class GcmAuthenticatedEncryptor : CngAuthenticatedEncryptorBase + { + // Having a key modifier ensures with overwhelming probability that no two encryption operations + // will ever derive the same (encryption subkey, MAC subkey) pair. This limits an attacker's + // ability to mount a key-dependent chosen ciphertext attack. See also the class-level comment + // for how this is used to overcome GCM's IV limitations. + private const uint KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8; + + private const uint NONCE_SIZE_IN_BYTES = 96 / 8; // GCM has a fixed 96-bit IV + private const uint TAG_SIZE_IN_BYTES = 128 / 8; // we're hardcoding a 128-bit authentication tag size + + private readonly byte[] _contextHeader; + private readonly IBCryptGenRandom _genRandom; + private readonly ISP800_108_CTR_HMACSHA512Provider _sp800_108_ctr_hmac_provider; + private readonly BCryptAlgorithmHandle _symmetricAlgorithmHandle; + private readonly uint _symmetricAlgorithmSubkeyLengthInBytes; + + public GcmAuthenticatedEncryptor(ProtectedMemoryBlob 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"); + + _genRandom = genRandom ?? BCryptGenRandomImpl.Instance; + _sp800_108_ctr_hmac_provider = SP800_108_CTR_HMACSHA512Util.CreateProvider(keyDerivationKey); + _symmetricAlgorithmHandle = symmetricAlgorithmHandle; + _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes; + _contextHeader = CreateContextHeader(); + } + + private byte[] CreateContextHeader() + { + byte[] retVal = new byte[checked( + 1 /* KDF alg */ + + 1 /* chaining mode */ + + sizeof(uint) /* sym alg key size */ + + sizeof(uint) /* GCM nonce size */ + + sizeof(uint) /* sym alg block size */ + + sizeof(uint) /* GCM tag size */ + + TAG_SIZE_IN_BYTES /* tag of GCM-encrypted empty string */)]; + + fixed (byte* pbRetVal = retVal) + { + byte* ptr = pbRetVal; + + // First is the two-byte header + *(ptr++) = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF + *(ptr++) = 1; // 0x01 = GCM encryption + authentication + + // Next is information about the symmetric algorithm (key size, nonce size, block size, tag size) + BitHelpers.WriteTo(ptr, _symmetricAlgorithmSubkeyLengthInBytes); + ptr += sizeof(uint); + BitHelpers.WriteTo(ptr, NONCE_SIZE_IN_BYTES); + ptr += sizeof(uint); + BitHelpers.WriteTo(ptr, TAG_SIZE_IN_BYTES); // block size + ptr += sizeof(uint); + BitHelpers.WriteTo(ptr, TAG_SIZE_IN_BYTES); + ptr += sizeof(uint); + + // See the design document for an explanation of the following code. + byte[] tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; + fixed (byte* pbTempKeys = tempKeys) + { + byte dummy; + + // Derive temporary key for encryption. + using (var provider = SP800_108_CTR_HMACSHA512Util.CreateEmptyProvider()) + { + provider.DeriveKey( + pbLabel: &dummy, + cbLabel: 0, + pbContext: &dummy, + cbContext: 0, + pbDerivedKey: pbTempKeys, + cbDerivedKey: (uint)tempKeys.Length); + } + + // Encrypt a zero-length input string with an all-zero nonce and copy the tag to the return buffer. + byte* pbNonce = stackalloc byte[(int)NONCE_SIZE_IN_BYTES]; + UnsafeBufferUtil.SecureZeroMemory(pbNonce, NONCE_SIZE_IN_BYTES); + DoGcmEncrypt( + pbKey: pbTempKeys, + cbKey: _symmetricAlgorithmSubkeyLengthInBytes, + pbNonce: pbNonce, + pbPlaintextData: &dummy, + cbPlaintextData: 0, + pbEncryptedData: &dummy, + pbTag: ptr); + } + + ptr += TAG_SIZE_IN_BYTES; + CryptoUtil.Assert(ptr - pbRetVal == retVal.Length, "ptr - pbRetVal == retVal.Length"); + } + + // retVal := { version || chainingMode || symAlgKeySize || nonceSize || symAlgBlockSize || symAlgTagSize || TAG-of-E("") }. + return retVal; + } + + protected override byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) + { + // Argument checking: input must at the absolute minimum contain a key modifier, nonce, and tag + if (cbCiphertext < KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) + { + throw Error.CryptCommon_PayloadInvalid(); + } + + // Assumption: pbCipherText := { keyModifier || nonce || encryptedData || authenticationTag } + + uint cbPlaintext = checked(cbCiphertext - (KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES)); + + byte[] retVal = new byte[cbPlaintext]; + fixed (byte* pbRetVal = retVal) + { + // Calculate offsets + byte* pbKeyModifier = pbCiphertext; + byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES]; + byte* pbAuthTag = &pbEncryptedData[cbPlaintext]; + + // Use the KDF to recreate the symmetric block cipher key + // We'll need a temporary buffer to hold the symmetric encryption subkey + byte* pbSymmetricDecryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)]; + try + { + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: cbAdditionalAuthenticatedData, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbSymmetricDecryptionSubkey, + cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes); + + // Perform the decryption operation + + using (var decryptionSubkeyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes)) + { + byte dummy; + byte* pbPlaintext = (pbRetVal != null) ? pbRetVal : &dummy; // CLR doesn't like pinning empty buffers + + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo; + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authInfo); + authInfo.pbNonce = pbNonce; + authInfo.cbNonce = NONCE_SIZE_IN_BYTES; + authInfo.pbTag = pbAuthTag; + authInfo.cbTag = TAG_SIZE_IN_BYTES; + + // The call to BCryptDecrypt will also validate the authentication tag + uint cbDecryptedBytesWritten; + int ntstatus = UnsafeNativeMethods.BCryptDecrypt( + hKey: decryptionSubkeyHandle, + pbInput: pbEncryptedData, + cbInput: cbPlaintext, + pPaddingInfo: &authInfo, + pbIV: null, // IV not used; nonce provided in pPaddingInfo + cbIV: 0, + pbOutput: pbPlaintext, + cbOutput: cbPlaintext, + pcbResult: out cbDecryptedBytesWritten, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.Assert(cbDecryptedBytesWritten == cbPlaintext, "cbDecryptedBytesWritten == cbPlaintext"); + + // At this point, retVal := { decryptedPayload } + // And we're done! + return retVal; + } + } + finally + { + // The buffer contains key material, so nuke it. + UnsafeBufferUtil.SecureZeroMemory(pbSymmetricDecryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes); + } + } + } + + public override void Dispose() + { + _sp800_108_ctr_hmac_provider.Dispose(); + + // We don't want to dispose of the underlying algorithm instances because they + // might be reused. + } + + // 'pbNonce' must point to a 96-bit buffer. + // 'pbTag' must point to a 128-bit buffer. + // 'pbEncryptedData' must point to a buffer the same length as 'pbPlaintextData'. + private void DoGcmEncrypt(byte* pbKey, uint cbKey, byte* pbNonce, byte* pbPlaintextData, uint cbPlaintextData, byte* pbEncryptedData, byte* pbTag) + { + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authCipherInfo; + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Init(out authCipherInfo); + authCipherInfo.pbNonce = pbNonce; + authCipherInfo.cbNonce = NONCE_SIZE_IN_BYTES; + authCipherInfo.pbTag = pbTag; + authCipherInfo.cbTag = TAG_SIZE_IN_BYTES; + + using (var keyHandle = _symmetricAlgorithmHandle.GenerateSymmetricKey(pbKey, cbKey)) + { + uint cbResult; + int ntstatus = UnsafeNativeMethods.BCryptEncrypt( + hKey: keyHandle, + pbInput: pbPlaintextData, + cbInput: cbPlaintextData, + pPaddingInfo: &authCipherInfo, + pbIV: null, + cbIV: 0, + pbOutput: pbEncryptedData, + cbOutput: cbPlaintextData, + pcbResult: out cbResult, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.Assert(cbResult == cbPlaintextData, "cbResult == cbPlaintextData"); + } + } + + protected override byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer) + { + // Allocate a buffer to hold the key modifier, nonce, encrypted data, and tag. + // In GCM, the encrypted output will be the same length as the plaintext input. + byte[] retVal = new byte[checked(cbPreBuffer + KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES + cbPlaintext + TAG_SIZE_IN_BYTES + cbPostBuffer)]; + fixed (byte* pbRetVal = retVal) + { + // Calculate offsets + byte* pbKeyModifier = &pbRetVal[cbPreBuffer]; + byte* pbNonce = &pbKeyModifier[KEY_MODIFIER_SIZE_IN_BYTES]; + byte* pbEncryptedData = &pbNonce[NONCE_SIZE_IN_BYTES]; + byte* pbAuthTag = &pbEncryptedData[cbPlaintext]; + + // Randomly generate the key modifier and nonce + _genRandom.GenRandom(pbKeyModifier, KEY_MODIFIER_SIZE_IN_BYTES + NONCE_SIZE_IN_BYTES); + + // At this point, retVal := { preBuffer | keyModifier | nonce | _____ | _____ | postBuffer } + + // Use the KDF to generate a new symmetric block cipher key + // We'll need a temporary buffer to hold the symmetric encryption subkey + byte* pbSymmetricEncryptionSubkey = stackalloc byte[checked((int)_symmetricAlgorithmSubkeyLengthInBytes)]; + try + { + _sp800_108_ctr_hmac_provider.DeriveKeyWithContextHeader( + pbLabel: pbAdditionalAuthenticatedData, + cbLabel: cbAdditionalAuthenticatedData, + contextHeader: _contextHeader, + pbContext: pbKeyModifier, + cbContext: KEY_MODIFIER_SIZE_IN_BYTES, + pbDerivedKey: pbSymmetricEncryptionSubkey, + cbDerivedKey: _symmetricAlgorithmSubkeyLengthInBytes); + + // Perform the encryption operation + DoGcmEncrypt( + pbKey: pbSymmetricEncryptionSubkey, + cbKey: _symmetricAlgorithmSubkeyLengthInBytes, + pbNonce: pbNonce, + pbPlaintextData: pbPlaintext, + cbPlaintextData: cbPlaintext, + pbEncryptedData: pbEncryptedData, + pbTag: pbAuthTag); + + // At this point, retVal := { preBuffer | keyModifier | nonce | encryptedData | authenticationTag | postBuffer } + // And we're done! + return retVal; + } + finally + { + // The buffer contains key material, so nuke it. + UnsafeBufferUtil.SecureZeroMemory(pbSymmetricEncryptionSubkey, _symmetricAlgorithmSubkeyLengthInBytes); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/IBCryptGenRandom.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/IBCryptGenRandom.cs new file mode 100644 index 0000000000..72497de9cd --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/IBCryptGenRandom.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Cng +{ + internal unsafe interface IBCryptGenRandom + { + void GenRandom(byte* pbBuffer, uint cbBuffer); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/NCryptEncryptFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/NCryptEncryptFlags.cs new file mode 100644 index 0000000000..b45b21809b --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/NCryptEncryptFlags.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Cng +{ + [Flags] + internal enum NCryptEncryptFlags + { + NCRYPT_NO_PADDING_FLAG = 0x00000001, + NCRYPT_PAD_PKCS1_FLAG = 0x00000002, + NCRYPT_PAD_OAEP_FLAG = 0x00000004, + NCRYPT_PAD_PSS_FLAG = 0x00000008, + NCRYPT_SILENT_FLAG = 0x00000040, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/OSVersionUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/OSVersionUtil.cs new file mode 100644 index 0000000000..c42535428e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/OSVersionUtil.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft 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.Runtime.InteropServices; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + internal static class OSVersionUtil + { + private static readonly OSVersion _osVersion = GetOSVersion(); + + private static OSVersion GetOSVersion() + { + const string BCRYPT_LIB = "bcrypt.dll"; + SafeLibraryHandle bcryptLibHandle = null; + try + { + bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB); + } + catch + { + // we'll handle the exceptional case later + } + + if (bcryptLibHandle != null) + { + using (bcryptLibHandle) + { + if (bcryptLibHandle.DoesProcExist("BCryptKeyDerivation")) + { + // We're running on Win8+. + return OSVersion.Win8OrLater; + } + else + { + // We're running on Win7+. + return OSVersion.Win7OrLater; + } + } + } + else + { + // Not running on Win7+. + return OSVersion.NotWindows; + } + } + + public static bool IsBCryptOnWin7OrLaterAvailable() + { + return (_osVersion >= OSVersion.Win7OrLater); + } + + public static bool IsBCryptOnWin8OrLaterAvailable() + { + return (_osVersion >= OSVersion.Win8OrLater); + } + + private enum OSVersion + { + NotWindows = 0, + Win7OrLater = 1, + Win8OrLater = 2 + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Constants.cs b/src/Microsoft.AspNet.Security.DataProtection/Constants.cs index 0a681d188c..8d40b3b7f1 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Constants.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Constants.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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.Security.DataProtection { - // from bcrypt.h + // The majority of these are from bcrypt.h internal static class Constants { internal const int MAX_STACKALLOC_BYTES = 256; // greatest number of bytes that we'll ever allow to stackalloc in a single frame @@ -15,11 +15,11 @@ namespace Microsoft.AspNet.Security.DataProtection internal const string BCRYPT_KEY_DATA_BLOB = "KeyDataBlob"; internal const string BCRYPT_AES_WRAP_KEY_BLOB = "Rfc3565KeyWrapBlob"; - // Microsoft built-in providers. + // Microsoft built-in providers internal const string MS_PRIMITIVE_PROVIDER = "Microsoft Primitive Provider"; internal const string MS_PLATFORM_CRYPTO_PROVIDER = "Microsoft Platform Crypto Provider"; - // Common algorithm identifiers. + // Common algorithm identifiers internal const string BCRYPT_RSA_ALGORITHM = "RSA"; internal const string BCRYPT_RSA_SIGN_ALGORITHM = "RSA_SIGN"; internal const string BCRYPT_DH_ALGORITHM = "DH"; diff --git a/src/Microsoft.AspNet.Security.DataProtection/CryptRand.cs b/src/Microsoft.AspNet.Security.DataProtection/CryptRand.cs deleted file mode 100644 index 04bf2826c7..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/CryptRand.cs +++ /dev/null @@ -1,31 +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.Security.DataProtection -{ - /// - /// Helper class to populate buffers with cryptographically random data. - /// - public static class CryptRand - { - /// - /// Populates a buffer with cryptographically random data. - /// - /// The buffer to populate. - public static unsafe void FillBuffer(ArraySegment buffer) - { - // the ArraySegment<> ctor performs bounds checking - var unused = new ArraySegment(buffer.Array, buffer.Offset, buffer.Count); - - if (buffer.Count != 0) - { - fixed (byte* pBuffer = &buffer.Array[buffer.Offset]) - { - BCryptUtil.GenRandom(pBuffer, buffer.Count); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs index 29fabca02e..52e556fbcf 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs @@ -4,9 +4,14 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; -namespace Microsoft.AspNet.Security.DataProtection.Cng +#if !ASPNETCORE50 +using System.Runtime.ConstrainedExecution; +#endif + +namespace Microsoft.AspNet.Security.DataProtection { internal unsafe static class CryptoUtil { @@ -20,6 +25,13 @@ namespace Microsoft.AspNet.Security.DataProtection.Cng } } + // This isn't a typical Debug.Assert; the check is always performed, even in retail builds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertSafeHandleIsValid(SafeHandle safeHandle) + { + Assert(safeHandle != null && !safeHandle.IsInvalid, "Safe handle is invalid."); + } + // 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 @@ -31,5 +43,40 @@ namespace Microsoft.AspNet.Security.DataProtection.Cng Debug.Fail(message); throw new CryptographicException("Assertion failed: " + message); } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static T Fail(string message) where T : class + { + throw Fail(message); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static bool TimeConstantBuffersAreEqual(byte* bufA, byte* bufB, uint count) + { + bool areEqual = true; + for (uint i = 0; i < count; i++) + { + areEqual &= (bufA[i] == bufB[i]); + } + return areEqual; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static bool TimeConstantBuffersAreEqual(byte[] bufA, int offsetA, int countA, byte[] bufB, int offsetB, int countB) + { + // Technically this is an early exit scenario, but it means that the caller did something bizarre. + // An error at the call site isn't usable for timing attacks. + Assert(countA == countB, "countA == countB"); + + bool areEqual = true; + for (int i = 0; i < countA; i++) + { + areEqual &= (bufA[offsetA + i] == bufB[offsetB + i]); + } + return areEqual; + } } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs b/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs index ba198c6d8b..16589279ed 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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; diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionOptions.cs new file mode 100644 index 0000000000..9f2eefda56 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection +{ + public class DataProtectionOptions + { + public string ApplicationDiscriminator { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs deleted file mode 100644 index 3b612f6190..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs +++ /dev/null @@ -1,117 +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; -#if NET45 -using System.Security.Cryptography; -#endif -using System.Text; -using Microsoft.AspNet.Security.DataProtection; -using Microsoft.AspNet.Security.DataProtection.Util; - -namespace Microsoft.AspNet.Security.DataProtection -{ - /// - /// Provides methods for creating IDataProtectionProvider instances. - /// - public unsafe static class DataProtectionProvider - { - const int MASTER_KEY_REQUIRED_LENGTH = 512 / 8; - - private static readonly byte[] MASTER_SUBKEY_GENERATOR = Encoding.ASCII.GetBytes("Microsoft.AspNet.Security.DataProtection"); - - /// - /// Creates a new IDataProtectionProvider backed by DPAPI, where the protected - /// payload can only be decrypted by the current user. - /// - public static IDataProtectionProvider CreateFromDpapi() - { - return CreateFromDpapi(protectToLocalMachine: false); - } - -#if NET45 - // These are for mono - public static IDataProtectionProvider CreateFromLegacyDpapi() - { - return CreateFromLegacyDpapi(DataProtectionScope.CurrentUser); - } - - public static IDataProtectionProvider CreateFromLegacyDpapi(DataProtectionScope scope) - { - return new ProtectedDataProtectionProvider(scope); - } -#endif - - /// - /// Creates a new IDataProtectionProvider backed by DPAPI. - /// - /// True if protected payloads can be decrypted by any user - /// on the local machine, false if protected payloads should only be able to decrypted by the - /// current user account. - public static IDataProtectionProvider CreateFromDpapi(bool protectToLocalMachine) - { - return new DpapiDataProtectionProviderImpl(MASTER_SUBKEY_GENERATOR, protectToLocalMachine); - } - - /// - /// Creates a new IDataProtectionProvider with a randomly-generated master key. - /// - public static IDataProtectionProvider CreateNew() - { - byte* masterKey = stackalloc byte[MASTER_KEY_REQUIRED_LENGTH]; - try - { - BCryptUtil.GenRandom(masterKey, MASTER_KEY_REQUIRED_LENGTH); - return CreateImpl(masterKey, MASTER_KEY_REQUIRED_LENGTH); - } - finally - { - BufferUtil.SecureZeroMemory(masterKey, MASTER_KEY_REQUIRED_LENGTH); - } - } - - /// - /// Creates a new IDataProtectionProvider with the provided master key. - /// - public static IDataProtectionProvider CreateFromKey(byte[] masterKey) - { - if (masterKey == null) - { - throw new ArgumentNullException("masterKey"); - } - if (masterKey.Length < MASTER_KEY_REQUIRED_LENGTH) - { - string errorMessage = String.Format(CultureInfo.CurrentCulture, Res.DataProtectorFactory_MasterKeyTooShort, MASTER_KEY_REQUIRED_LENGTH); - throw new ArgumentOutOfRangeException("masterKey", errorMessage); - } - - fixed (byte* pMasterKey = masterKey) - { - return CreateImpl(pMasterKey, masterKey.Length); - } - } - - private static DataProtectionProviderImpl CreateImpl(byte* masterKey, int masterKeyLengthInBytes) - { - // We don't use the master key directly. We derive a master subkey via HMAC_{master_key}(MASTER_SUBKEY_GENERATOR). - byte* masterSubkey = stackalloc byte[MASTER_KEY_REQUIRED_LENGTH]; - try - { - using (var hashHandle = BCryptUtil.CreateHMACHandle(Algorithms.HMACSHA512AlgorithmHandle, masterKey, masterKeyLengthInBytes)) - { - fixed (byte* pMasterSubkeyGenerator = MASTER_SUBKEY_GENERATOR) - { - BCryptUtil.HashData(hashHandle, pMasterSubkeyGenerator, MASTER_SUBKEY_GENERATOR.Length, masterSubkey, MASTER_KEY_REQUIRED_LENGTH); - } - } - byte[] protectedKdk = BufferUtil.ToProtectedManagedByteArray(masterSubkey, MASTER_KEY_REQUIRED_LENGTH); - return new DataProtectionProviderImpl(protectedKdk); - } - finally - { - BufferUtil.SecureZeroMemory(masterSubkey, MASTER_KEY_REQUIRED_LENGTH); - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs deleted file mode 100644 index 45ffa2afd0..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs +++ /dev/null @@ -1,32 +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.Security.DataProtection -{ - internal unsafe sealed class DataProtectionProviderImpl : IDataProtectionProvider - { - private readonly byte[] _protectedKdk; - - public DataProtectionProviderImpl(byte[] protectedKdk) - { - _protectedKdk = protectedKdk; - } - - public IDataProtector CreateProtector(string purpose) - { - BCryptKeyHandle newAesKeyHandle; - BCryptHashHandle newHmacHashHandle; - byte[] newProtectedKdfSubkey; - - BCryptUtil.DeriveKeysSP800108(_protectedKdk, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newProtectedKdfSubkey); - return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newProtectedKdfSubkey); - } - - public void Dispose() - { - // no-op: we hold no protected resources - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs new file mode 100644 index 0000000000..f24ae68036 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft 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.IO; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.Dpapi; +using Microsoft.AspNet.Security.DataProtection.KeyManagement; +using Microsoft.AspNet.Security.DataProtection.Repositories; +using Microsoft.AspNet.Security.DataProtection.XmlEncryption; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Security.DataProtection +{ + public static class DataProtectionServices + { + public static IEnumerable GetDefaultServices() + { + return GetDefaultServices(new Configuration()); + } + + public static IEnumerable GetDefaultServices(IConfiguration configuration) + { + var describe = new ServiceDescriber(configuration); + + List descriptors = new List(); + descriptors.AddRange(OptionsServices.GetDefaultServices(configuration)); + descriptors.AddRange(OSVersionUtil.IsBCryptOnWin7OrLaterAvailable() + ? GetDefaultServicesWindows(describe) + : GetDefaultServicesNonWindows(describe)); + return descriptors; + } + + private static IEnumerable GetDefaultServicesNonWindows(ServiceDescriber describe) + { + // 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[] + { + describe.Instance(new DpapiDataProtectionProvider(DataProtectionScope.CurrentUser)) + }; + } + + private static IEnumerable GetDefaultServicesWindows(ServiceDescriber describe) + { + 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[] + { + describe.Singleton(), + describe.Instance(new FileSystemXmlRepository(azureWebSitesKeysFolder)) + }); + } + else + { + // Are we running with the user profile loaded? + DirectoryInfo localAppDataKeysFolder = TryGetLocalAppDataKeysFolderForUser(); + if (localAppDataKeysFolder != null) + { + descriptors.AddRange(new[] + { + describe.Singleton(), + describe.Instance(new FileSystemXmlRepository(localAppDataKeysFolder)) + }); + } + else + { + // Are we running with no user profile (e.g., IIS service)? + // Fall back to DPAPI for now. + // TODO: We should use the IIS auto-gen reg keys as our repository. + return new[] { + describe.Instance(new DpapiDataProtectionProvider(DataProtectionScope.LocalMachine)) + }; + } + } + + // We use CNG CBC + HMAC by default. + descriptors.AddRange(new[] + { + describe.Singleton(), + describe.Singleton(), + describe.Singleton(), + describe.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-BETA"); + return new DirectoryInfo(fullPathToKeys); + } + + private static DirectoryInfo TryGetLocalAppDataKeysFolderForUser() + { +#if !ASPNETCORE50 + // Environment.GetFolderPath returns null if the user profile isn't loaded. + string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (folderPath != null) + { + // TODO: Remove BETA moniker from below. + return new DirectoryInfo(Path.Combine(folderPath, "ASP.NET", "keys-BETA")); + } + 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-BETA")); + try + { + retVal.Create(); // throws if we don't have access, e.g., user profile not loaded + return retVal; + } catch + { + return null; + } +#endif + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs deleted file mode 100644 index 778eb089b7..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs +++ /dev/null @@ -1,190 +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; -using Microsoft.AspNet.Security.DataProtection.Util; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal unsafe sealed class DataProtectorImpl : IDataProtector - { - private const int AES_BLOCK_LENGTH_IN_BYTES = 128 / 8; - private const int AES_IV_LENGTH_IN_BYTES = AES_BLOCK_LENGTH_IN_BYTES; - private const int MAC_LENGTH_IN_BYTES = 256 / 8; - - private readonly BCryptKeyHandle _aesKeyHandle; - private readonly BCryptHashHandle _hmacHashHandle; - private readonly byte[] _protectedKdk; - - public DataProtectorImpl(BCryptKeyHandle aesKeyHandle, BCryptHashHandle hmacHashHandle, byte[] protectedKdk) - { - _aesKeyHandle = aesKeyHandle; - _hmacHashHandle = hmacHashHandle; - _protectedKdk = protectedKdk; - } - - private static int CalculateTotalProtectedDataSize(int unprotectedDataSizeInBytes) - { - Debug.Assert(unprotectedDataSizeInBytes >= 0); - - checked - { - // Padding always rounds the block count up, never down. - // If the input size is already a multiple of the block length, a block is added. - int numBlocks = 1 + unprotectedDataSizeInBytes / AES_BLOCK_LENGTH_IN_BYTES; - return - AES_IV_LENGTH_IN_BYTES /* IV */ - + numBlocks * AES_BLOCK_LENGTH_IN_BYTES /* ciphertext with padding */ - + MAC_LENGTH_IN_BYTES /* MAC */; - } - } - - private static CryptographicException CreateGenericCryptographicException() - { - return new CryptographicException(Res.DataProtectorImpl_BadEncryptedData); - } - - public IDataProtector CreateSubProtector(string purpose) - { - BCryptKeyHandle newAesKeyHandle; - BCryptHashHandle newHmacHashHandle; - byte[] newProtectedKdfSubkey; - - BCryptUtil.DeriveKeysSP800108(_protectedKdk, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newProtectedKdfSubkey); - return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newProtectedKdfSubkey); - } - - public void Dispose() - { - _aesKeyHandle.Dispose(); - _hmacHashHandle.Dispose(); - } - - public byte[] Protect(byte[] unprotectedData) - { - if (unprotectedData == null) - { - throw new ArgumentNullException("unprotectedData"); - } - - // When this method finishes, protectedData will contain { IV || ciphertext || HMAC(IV || ciphertext) } - byte[] protectedData = new byte[CalculateTotalProtectedDataSize(unprotectedData.Length)]; - - fixed (byte* pProtectedData = protectedData) - { - // first, generate a random IV for CBC mode encryption - byte* pIV = pProtectedData; - BCryptUtil.GenRandom(pIV, AES_IV_LENGTH_IN_BYTES); - - // then, encrypt the plaintext contents - byte* pCiphertext = &pIV[AES_IV_LENGTH_IN_BYTES]; - int expectedCiphertextLength = protectedData.Length - AES_IV_LENGTH_IN_BYTES - MAC_LENGTH_IN_BYTES; - fixed (byte* pPlaintext = unprotectedData.AsFixed()) - { - int actualCiphertextLength = BCryptUtil.EncryptWithPadding(_aesKeyHandle, pPlaintext, unprotectedData.Length, pIV, AES_IV_LENGTH_IN_BYTES, pCiphertext, expectedCiphertextLength); - if (actualCiphertextLength != expectedCiphertextLength) - { - throw new InvalidOperationException("Unexpected error while encrypting data."); - } - } - - // finally, calculate an HMAC over { IV || ciphertext } - byte* pMac = &pCiphertext[expectedCiphertextLength]; - using (var clonedHashHandle = BCryptUtil.DuplicateHash(_hmacHashHandle)) - { - // Use a cloned hash handle since IDataProtector instances could be singletons, but BCryptHashHandle instances contain - // state hence aren't thread-safe. Our own perf testing shows that duplicating existing hash handles is very fast. - BCryptUtil.HashData(clonedHashHandle, pProtectedData, AES_IV_LENGTH_IN_BYTES + expectedCiphertextLength, pMac, MAC_LENGTH_IN_BYTES); - } - } - - return protectedData; - } - - public byte[] Unprotect(byte[] protectedData) - { - if (protectedData == null) - { - throw new ArgumentNullException("protectedData"); - } - - byte[] retVal = null; - try - { - retVal = UnprotectImpl(protectedData); - } - catch - { - // swallow all exceptions; we'll homogenize - } - - if (retVal != null) - { - return retVal; - } - else - { - throw CreateGenericCryptographicException(); - } - } - - private byte[] UnprotectImpl(byte[] protectedData) - { - Debug.Assert(protectedData != null); - - // is the protected data even long enough to be valid? - if (protectedData.Length < AES_IV_LENGTH_IN_BYTES /* IV */ + AES_BLOCK_LENGTH_IN_BYTES /* min ciphertext size = 1 block */ + MAC_LENGTH_IN_BYTES) - { - return null; - } - - fixed (byte* pProtectedData = protectedData) - { - // calculate pointer offsets - byte* pIV = pProtectedData; - byte* pCiphertext = &pProtectedData[AES_IV_LENGTH_IN_BYTES]; - int ciphertextLength = protectedData.Length - AES_IV_LENGTH_IN_BYTES /* IV */ - MAC_LENGTH_IN_BYTES /* MAC */; - byte* pSuppliedMac = &pCiphertext[ciphertextLength]; - - // first, ensure that the MAC is valid - byte* pCalculatedMac = stackalloc byte[MAC_LENGTH_IN_BYTES]; - using (var clonedHashHandle = BCryptUtil.DuplicateHash(_hmacHashHandle)) - { - // see comments in Protect(byte[]) for why we duplicate the hash - BCryptUtil.HashData(clonedHashHandle, pProtectedData, AES_IV_LENGTH_IN_BYTES + ciphertextLength, pCalculatedMac, MAC_LENGTH_IN_BYTES); - } - if (!BCryptUtil.BuffersAreEqualSecure(pSuppliedMac, pCalculatedMac, MAC_LENGTH_IN_BYTES)) - { - return null; // MAC check failed - } - - // next, perform the actual decryption - // we don't know the actual plaintext length, but we know it must be strictly less than the ciphertext length - int plaintextBufferLength = ciphertextLength; - byte[] heapAllocatedPlaintext = null; - if (ciphertextLength > Constants.MAX_STACKALLOC_BYTES) - { - heapAllocatedPlaintext = new byte[plaintextBufferLength]; - } - - fixed (byte* pHeapAllocatedPlaintext = heapAllocatedPlaintext) - { - byte* pPlaintextBuffer = pHeapAllocatedPlaintext; - if (pPlaintextBuffer == null) - { - byte* temp = stackalloc byte[plaintextBufferLength]; // will be released when frame pops - pPlaintextBuffer = temp; - } - - int actualPlaintextLength = BCryptUtil.DecryptWithPadding(_aesKeyHandle, pCiphertext, ciphertextLength, pIV, AES_IV_LENGTH_IN_BYTES, pPlaintextBuffer, plaintextBufferLength); - Debug.Assert(actualPlaintextLength >= 0 && actualPlaintextLength < ciphertextLength); - - // truncate the return value to accomodate the plaintext size perfectly - return BufferUtil.ToManagedByteArray(pPlaintextBuffer, actualPlaintextLength); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DefaultDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/DefaultDataProtectionProvider.cs new file mode 100644 index 0000000000..d933097799 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DefaultDataProtectionProvider.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 Microsoft.AspNet.Security.DataProtection.KeyManagement; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Security.DataProtection +{ + public class DefaultDataProtectionProvider : IDataProtectionProvider + { + private readonly IDataProtectionProvider _innerProvider; + + public DefaultDataProtectionProvider() + { + // use DI defaults + var collection = new ServiceCollection(); + var defaultServices = DataProtectionServices.GetDefaultServices(); + collection.Add(defaultServices); + var serviceProvider = collection.BuildServiceProvider(); + + _innerProvider = (IDataProtectionProvider)serviceProvider.GetService(typeof(IDataProtectionProvider)); + CryptoUtil.Assert(_innerProvider != null, "_innerProvider != null"); + } + + public DefaultDataProtectionProvider( + [NotNull] IOptionsAccessor 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.Security.DataProtection/Dpapi/DataProtectionScope.cs b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/DataProtectionScope.cs new file mode 100644 index 0000000000..7cf629b023 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/DataProtectionScope.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. + +// We only define this type in core CLR since desktop CLR already contains it. +#if ASPNETCORE50 +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.Security.DataProtection/Dpapi/DpapiDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/DpapiDataProtectionProvider.cs new file mode 100644 index 0000000000..5082e385b3 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/DpapiDataProtectionProvider.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 System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.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.Security.DataProtection/Dpapi/DpapiDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/DpapiDataProtector.cs new file mode 100644 index 0000000000..0bc4cb073d --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/DpapiDataProtector.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft 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 System.Text; + +namespace Microsoft.AspNet.Security.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 static readonly UTF8Encoding _secureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + 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, _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) if (!(ex is CryptographicException)) + { + // 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) if (!(ex is CryptographicException)) + { + // Homogenize to CryptographicException + throw Error.CryptCommon_GenericError(ex); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Dpapi/IProtectedData.cs b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/IProtectedData.cs new file mode 100644 index 0000000000..3cba943f3d --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/IProtectedData.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft 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.Security.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.Security.DataProtection/Dpapi/ProtectedDataImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/ProtectedDataImpl.cs new file mode 100644 index 0000000000..ab6d8ac06f --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Dpapi/ProtectedDataImpl.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 System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.Cng; + +namespace Microsoft.AspNet.Security.DataProtection.Dpapi +{ + internal unsafe sealed class ProtectedDataImpl : IProtectedData + { + public byte[] Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope) + { +#if ASPNETCORE50 + 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 ASPNETCORE50 + ProtectedMemoryBlob 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.Security.DataProtection/DpapiDataProtectionProviderImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs deleted file mode 100644 index fa6df2f6ad..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs +++ /dev/null @@ -1,31 +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; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal sealed class DpapiDataProtectionProviderImpl : IDataProtectionProvider - { - private readonly byte[] _entropy; - private readonly bool _protectToLocalMachine; - - public DpapiDataProtectionProviderImpl(byte[] entropy, bool protectToLocalMachine) - { - Debug.Assert(entropy != null); - _entropy = entropy; - _protectToLocalMachine = protectToLocalMachine; - } - - public IDataProtector CreateProtector(string purpose) - { - return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose), _protectToLocalMachine); - } - - public void Dispose() - { - // no-op; no unmanaged resources to dispose - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs deleted file mode 100644 index 0d0ed33094..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs +++ /dev/null @@ -1,161 +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.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using Microsoft.AspNet.Security.DataProtection.Util; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal unsafe sealed class DpapiDataProtectorImpl : IDataProtector - { - // from dpapi.h - private const uint CRYPTPROTECT_LOCAL_MACHINE = 0x4; - private const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1; - - // Used as the 'purposes' parameter to DPAPI operations - private readonly byte[] _entropy; - - private readonly bool _protectToLocalMachine; - - public DpapiDataProtectorImpl(byte[] entropy, bool protectToLocalMachine) - { - Debug.Assert(entropy != null); - _entropy = entropy; - _protectToLocalMachine = protectToLocalMachine; - } - - private static CryptographicException CreateGenericCryptographicException(bool isErrorDueToProfileNotLoaded = false) - { - string message = (isErrorDueToProfileNotLoaded) ? Res.DpapiDataProtectorImpl_ProfileNotLoaded : Res.DataProtectorImpl_BadEncryptedData; - return new CryptographicException(message); - } - - public IDataProtector CreateSubProtector(string purpose) - { - return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose), _protectToLocalMachine); - } - - public void Dispose() - { - // no-op; no unmanaged resources to dispose - } - - private uint GetCryptProtectUnprotectFlags() - { - if (_protectToLocalMachine) - { - return CRYPTPROTECT_LOCAL_MACHINE | CRYPTPROTECT_UI_FORBIDDEN; - } - else - { - return CRYPTPROTECT_UI_FORBIDDEN; - } - } - - public byte[] Protect(byte[] unprotectedData) - { - if (unprotectedData == null) - { - throw new ArgumentNullException("unprotectedData"); - } - - DATA_BLOB dataOut = default(DATA_BLOB); - -#if NET45 - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bool success; - fixed (byte* pUnprotectedData = unprotectedData.AsFixed()) - { - fixed (byte* pEntropy = _entropy) - { - // no need for checked arithmetic here - DATA_BLOB dataIn = new DATA_BLOB() { cbData = (uint)unprotectedData.Length, pbData = pUnprotectedData }; - DATA_BLOB optionalEntropy = new DATA_BLOB() { cbData = (uint)_entropy.Length, pbData = pEntropy }; - success = UnsafeNativeMethods.CryptProtectData(&dataIn, IntPtr.Zero, &optionalEntropy, IntPtr.Zero, IntPtr.Zero, GetCryptProtectUnprotectFlags(), out dataOut); - } - } - - // Did a failure occur? - if (!success) - { - int errorCode = Marshal.GetLastWin32Error(); - bool isErrorDueToProfileNotLoaded = ((errorCode & 0xffff) == 2 /* ERROR_FILE_NOT_FOUND */); - throw CreateGenericCryptographicException(isErrorDueToProfileNotLoaded); - } - - // OOMs may be marked as success but won't return a valid pointer - if (dataOut.pbData == null) - { - throw new OutOfMemoryException(); - } - - return BufferUtil.ToManagedByteArray(dataOut.pbData, dataOut.cbData); - } - finally - { - // per MSDN, we need to use LocalFree (implemented by Marshal.FreeHGlobal) to clean up CAPI-allocated memory - if (dataOut.pbData != null) - { - Marshal.FreeHGlobal((IntPtr)dataOut.pbData); - } - } - } - - public byte[] Unprotect(byte[] protectedData) - { - if (protectedData == null) - { - throw new ArgumentNullException("protectedData"); - } - - DATA_BLOB dataOut = default(DATA_BLOB); - -#if NET45 - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bool success; - fixed (byte* pProtectedData = protectedData.AsFixed()) - { - fixed (byte* pEntropy = _entropy) - { - // no need for checked arithmetic here - DATA_BLOB dataIn = new DATA_BLOB() { cbData = (uint)protectedData.Length, pbData = pProtectedData }; - DATA_BLOB optionalEntropy = new DATA_BLOB() { cbData = (uint)_entropy.Length, pbData = pEntropy }; - success = UnsafeNativeMethods.CryptUnprotectData(&dataIn, IntPtr.Zero, &optionalEntropy, IntPtr.Zero, IntPtr.Zero, GetCryptProtectUnprotectFlags(), out dataOut); - } - } - - // Did a failure occur? - if (!success) - { - throw CreateGenericCryptographicException(); - } - - // OOMs may be marked as success but won't return a valid pointer - if (dataOut.pbData == null) - { - throw new OutOfMemoryException(); - } - - return BufferUtil.ToManagedByteArray(dataOut.pbData, dataOut.cbData); - } - finally - { - // per MSDN, we need to use LocalFree (implemented by Marshal.FreeHGlobal) to clean up CAPI-allocated memory - if (dataOut.pbData != null) - { - Marshal.FreeHGlobal((IntPtr)dataOut.pbData); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs new file mode 100644 index 0000000000..15e7ef1fbb --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.KeyManagement; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Security.DataProtection +{ + /// + /// An IDataProtectionProvider that is transient. + /// + /// + /// Payloads generated by a given EphemeralDataProtectionProvider instance can only + /// be deciphered by that same instance. Once the instance is lost, all ciphertexts + /// generated by that instance are permanently undecipherable. + /// + public sealed class EphemeralDataProtectionProvider : IDataProtectionProvider + { + private readonly KeyRingBasedDataProtectionProvider _dataProtectionProvider; + + public EphemeralDataProtectionProvider() + { + IKeyRingProvider keyringProvider; + + if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable()) + { + // Fastest implementation: AES-GCM + keyringProvider = new CngEphemeralKeyRing(); + } + else + { + // Slowest implementation: managed CBC + HMAC + keyringProvider = new ManagedEphemeralKeyRing(); + } + + _dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider); + } + + public IDataProtector CreateProtector([NotNull] string purpose) + { + // just forward to the underlying provider + return _dataProtectionProvider.CreateProtector(purpose); + } + + private sealed class DefaultOptionsAccessor : IOptionsAccessor where T : class, new() + { + public T Options { get; } = new T(); + + public T GetNamedOptions(string name) + { + return Options; + } + } + + // A special key ring that only understands one key id and which uses CNG. + private sealed class CngEphemeralKeyRing : IKeyRing, IKeyRingProvider + { + public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new CngGcmAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor()).CreateNewConfiguration().CreateEncryptorInstance(); + + public Guid DefaultKeyId { get; } = default(Guid); + + public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked) + { + isRevoked = false; + return (keyId == default(Guid)) ? DefaultAuthenticatedEncryptor : null; + } + + public IKeyRing GetCurrentKeyRing() + { + return this; + } + } + + // A special key ring that only understands one key id and which uses managed CBC + HMAC. + private sealed class ManagedEphemeralKeyRing : IKeyRing, IKeyRingProvider + { + public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new ManagedAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor()).CreateNewConfiguration().CreateEncryptorInstance(); + + public Guid DefaultKeyId { get; } = default(Guid); + + public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked) + { + isRevoked = false; + return (keyId == default(Guid)) ? DefaultAuthenticatedEncryptor : null; + } + + public IKeyRing GetCurrentKeyRing() + { + return this; + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Error.cs b/src/Microsoft.AspNet.Security.DataProtection/Error.cs new file mode 100644 index 0000000000..aa75abce2c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Error.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. + +using System; +using System.Globalization; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal static class Error + { + public static CryptographicException BCryptAlgorithmHandle_ProviderNotFound(string algorithmId) + { + string message = String.Format(CultureInfo.CurrentCulture, Resources.BCryptAlgorithmHandle_ProviderNotFound, algorithmId); + return new CryptographicException(message); + } + + public static ArgumentException Common_BufferIncorrectlySized(string parameterName, int actualSize, int expectedSize) + { + string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_BufferIncorrectlySized, actualSize, expectedSize); + return new ArgumentException(message, parameterName); + } + + public static CryptographicException CryptCommon_GenericError(Exception inner = null) + { + return new CryptographicException(Resources.CryptCommon_GenericError, inner); + } + + public static CryptographicException CryptCommon_PayloadInvalid() + { + string message = Resources.CryptCommon_PayloadInvalid; + return new CryptographicException(message); + } + + public static InvalidOperationException Common_PropertyCannotBeNullOrEmpty(string propertyName) + { + string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_PropertyCannotBeNullOrEmpty, propertyName); + throw new InvalidOperationException(message); + } + + public static CryptographicException Common_EncryptionFailed(Exception inner = null) + { + return new CryptographicException(Resources.Common_EncryptionFailed, inner); + } + + public static CryptographicException Common_KeyNotFound(Guid id) + { + string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_KeyNotFound, id); + return new CryptographicException(message); + } + + public static CryptographicException Common_KeyRevoked(Guid id) + { + string message = String.Format(CultureInfo.CurrentCulture, Resources.Common_KeyRevoked, id); + return new CryptographicException(message); + } + + public static CryptographicException Common_NotAValidProtectedPayload() + { + return new CryptographicException(Resources.Common_NotAValidProtectedPayload); + } + + public static CryptographicException Common_PayloadProducedByNewerVersion() + { + return new CryptographicException(Resources.Common_PayloadProducedByNewerVersion); + } + + public static CryptographicException DecryptionFailed(Exception inner) + { + return new CryptographicException(Resources.Common_DecryptionFailed, inner); + } + + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs index 2b39b475f1..3403240824 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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; @@ -6,15 +6,21 @@ using System; namespace Microsoft.AspNet.Security.DataProtection { /// - /// A factory that can provide IDataProtector instances. + /// An interface that can be used to create IDataProtector instances. /// - public interface IDataProtectionProvider : IDisposable + public interface IDataProtectionProvider { /// - /// Given a purpose, returns a new IDataProtector that has unique cryptographic keys tied to this purpose. + /// Creates an IDataProtector given a purpose. /// - /// The consumer of the IDataProtector. - /// An IDataProtector. + /// + /// 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.Security.DataProtection/IDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs index e873fbeed0..353a941710 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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; @@ -6,33 +6,26 @@ using System; namespace Microsoft.AspNet.Security.DataProtection { /// - /// Represents an object that can perform cryptographic operations. + /// An interface that can provide data protection services. /// - public interface IDataProtector : IDisposable + public interface IDataProtector : IDataProtectionProvider { /// - /// Given a subpurpose, returns a new IDataProtector that has unique cryptographic keys tied both the purpose - /// that was used to create this IDataProtector instance and the purpose that is provided as a parameter - /// to this method. + /// Cryptographically protects a piece of plaintext data. /// - /// The sub-consumer of the IDataProtector. - /// An IDataProtector. - IDataProtector CreateSubProtector(string purpose); - - /// - /// Cryptographically protects some input data. - /// - /// The data to be protected. - /// An array containing cryptographically protected data. - /// To retrieve the original data, call Unprotect on the protected data. + /// The plaintext data to protect. + /// The protected form of the plaintext data. byte[] Protect(byte[] unprotectedData); /// - /// Retrieves the original data that was protected by a call to Protect. + /// Cryptographically unprotects a piece of protected data. /// - /// The protected data to be decrypted. - /// The original data. - /// Throws CryptographicException if the protectedData parameter has been tampered with. + /// 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); } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/ISecret.cs b/src/Microsoft.AspNet.Security.DataProtection/ISecret.cs new file mode 100644 index 0000000000..8e73cc8cdd --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/ISecret.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; + +namespace Microsoft.AspNet.Security.DataProtection +{ + /// + /// Represents a secret value. + /// + public interface ISecret : IDisposable + { + /// + /// The length (in bytes) of the value. + /// + int Length { get; } + + /// + /// Writes the secret value to the specified buffer. + /// + /// The buffer which should receive the secret value. + /// + /// The buffer size must exactly match the length of the secret value. + /// + void WriteSecretIntoBuffer(ArraySegment buffer); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs index 548e0e7f65..79cb1e6370 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2; +using Microsoft.AspNet.Security.DataProtection.PBKDF2; -namespace Microsoft.AspNet.Security.DataProtection.Cng +namespace Microsoft.AspNet.Security.DataProtection { public static class KeyDerivation { diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs index 600383eb7a..196aed9523 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNet.Security.DataProtection.Cng +namespace Microsoft.AspNet.Security.DataProtection { /// /// Specifies the PRF which should be used for the key derivation algorithm. diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKey.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKey.cs new file mode 100644 index 0000000000..088ae89e09 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKey.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 Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + /// + /// The basic interface for representing an authenticated encryption key. + /// + public interface IKey + { + /// + /// The date at which encryptions with this key can begin taking place. + /// + DateTimeOffset ActivationDate { get; } + + /// + /// The date on which this key was created. + /// + DateTimeOffset CreationDate { get; } + + /// + /// The date after which encryptions with this key may no longer take place. + /// + /// + /// An expired key may still be used to decrypt existing payloads. + /// + DateTimeOffset ExpirationDate { get; } + + /// + /// Returns a value stating whether this key was revoked. + /// + /// + /// 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. + /// + bool IsRevoked { get; } + + /// + /// The id of the key. + /// + Guid KeyId { get; } + + /// + /// Creates an IAuthenticatedEncryptor instance that can be used to encrypt data + /// to and decrypt data from this key. + /// + /// An IAuthenticatedEncryptor. + IAuthenticatedEncryptor CreateEncryptorInstance(); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyManager.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyManager.cs new file mode 100644 index 0000000000..bbf9056e40 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyManager.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.Collections.Generic; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + /// + /// The basic interface for performing key management operations. + /// + public interface IKeyManager + { + /// + /// Creates a new key with the specified activation and expiration dates. + /// + /// 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); + + /// + /// Fetches all keys from the underlying repository. + /// + /// The collection of all keys. + IReadOnlyCollection GetAllKeys(); + + /// + /// Revokes a specific key. + /// + /// The id of the key to revoke. + /// An optional human-readable reason for revocation. + /// + /// This method will not mutate existing IKey instances. After calling this method, + /// all existing IKey instances should be discarded, and GetAllKeys should be called again. + /// + void RevokeKey(Guid keyId, string reason = null); + + /// + /// Revokes all keys created before a specified date. + /// + /// The revocation date. All keys with a creation date before + /// this value will be revoked. + /// An optional human-readable reason for revocation. + /// + /// This method will not mutate existing IKey instances. After calling this method, + /// all existing IKey instances should be discarded, and GetAllKeys should be called again. + /// + void RevokeAllKeys(DateTimeOffset revocationDate, string reason = null); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyRing.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyRing.cs new file mode 100644 index 0000000000..bae55be34e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyRing.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + internal interface IKeyRing + { + IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } + + Guid DefaultKeyId { get; } + + IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyRingProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyRingProvider.cs new file mode 100644 index 0000000000..da8115033d --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/IKeyRingProvider.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.KeyManagement +{ + internal interface IKeyRingProvider + { + IKeyRing GetCurrentKeyRing(); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/Key.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/Key.cs new file mode 100644 index 0000000000..a5ee6796a8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/Key.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 Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + internal sealed class Key : IKey + { + private readonly IAuthenticatedEncryptorConfiguration _encryptorConfiguration; + + public Key(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IAuthenticatedEncryptorConfiguration encryptorConfiguration) + { + KeyId = keyId; + CreationDate = creationDate; + ActivationDate = activationDate; + ExpirationDate = expirationDate; + + _encryptorConfiguration = encryptorConfiguration; + } + + public DateTimeOffset ActivationDate + { + get; + private set; + } + + public DateTimeOffset CreationDate + { + get; + private set; + } + + public DateTimeOffset ExpirationDate + { + get; + private set; + } + + public bool IsRevoked + { + get; + private set; + } + + public Guid KeyId + { + get; + private set; + } + + public IAuthenticatedEncryptor CreateEncryptorInstance() + { + return _encryptorConfiguration.CreateEncryptorInstance(); + } + + internal void SetRevoked() + { + IsRevoked = true; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyExtensions.cs new file mode 100644 index 0000000000..f1e9740c76 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.KeyManagement +{ + internal static class KeyExtensions + { + public static bool IsExpired(this IKey key, DateTime utcNow) + { + return (key.ExpirationDate.UtcDateTime <= utcNow); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRing.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRing.cs new file mode 100644 index 0000000000..5bd8773811 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRing.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft 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; +using Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + internal sealed class KeyRing : IKeyRing + { + private readonly AuthenticatedEncryptorHolder _defaultEncryptorHolder; + private readonly Dictionary _keyToEncryptorMap; + + public KeyRing(Guid defaultKeyId, IKey[] keys) + { + DefaultKeyId = defaultKeyId; + _keyToEncryptorMap = CreateEncryptorMap(defaultKeyId, keys, out _defaultEncryptorHolder); + } + + public KeyRing(Guid defaultKeyId, KeyRing other) + { + DefaultKeyId = defaultKeyId; + _keyToEncryptorMap = other._keyToEncryptorMap; + _defaultEncryptorHolder = _keyToEncryptorMap[defaultKeyId]; + } + + public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor + { + get + { + bool unused; + return _defaultEncryptorHolder.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 IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked) + { + isRevoked = false; + AuthenticatedEncryptorHolder holder; + _keyToEncryptorMap.TryGetValue(keyId, out holder); + return holder?.GetEncryptorInstance(out isRevoked); + } + + private sealed class AuthenticatedEncryptorHolder + { + private readonly IKey _key; + private IAuthenticatedEncryptor _encryptor; + + internal AuthenticatedEncryptorHolder(IKey key) + { + _key = key; + } + + internal IAuthenticatedEncryptor GetEncryptorInstance(out bool isRevoked) + { + // simple double-check lock pattern + // we can't use LazyInitializer because we don't have a simple value factory + IAuthenticatedEncryptor encryptor = Volatile.Read(ref _encryptor); + if (encryptor == null) + { + lock (this) + { + encryptor = Volatile.Read(ref _encryptor); + if (encryptor == null) + { + encryptor = _key.CreateEncryptorInstance(); + Volatile.Write(ref _encryptor, encryptor); + } + } + } + isRevoked = _key.IsRevoked; + return encryptor; + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.cs new file mode 100644 index 0000000000..daf0873218 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingBasedDataProtectionProvider.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; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + internal unsafe sealed class KeyRingBasedDataProtectionProvider : IDataProtectionProvider + { + private readonly IKeyRingProvider _keyringProvider; + + public KeyRingBasedDataProtectionProvider(IKeyRingProvider keyringProvider) + { + _keyringProvider = keyringProvider; + } + + public IDataProtector CreateProtector([NotNull] string purpose) + { + return new KeyRingBasedDataProtector(_keyringProvider, new[] { purpose }); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs new file mode 100644 index 0000000000..3b87e17147 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingBasedDataProtector.cs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft 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.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + internal unsafe sealed class KeyRingBasedDataProtector : IDataProtector + { + // This magic header identifies a v0 protected data blob. + // It's the high 28 bits of the SHA1 hash of "Microsoft.AspNet.Security.DataProtection.MultiplexingDataProtector" [US-ASCII]. + // The last 4 bits are reserved for version information. + private const uint MAGIC_HEADER_V0 = 0xE123CF30; + + private byte[] _additionalAuthenticatedDataTemplate; + private readonly IKeyRingProvider _keyringProvider; + private readonly string[] _purposes; + + public KeyRingBasedDataProtector(IKeyRingProvider keyringProvider, string[] purposes) + { + _additionalAuthenticatedDataTemplate = GenerateAdditionalAuthenticatedDataTemplateFromPurposes(purposes); + _keyringProvider = keyringProvider; + _purposes = purposes; + } + + private static byte[] ApplyEncryptorIdToAdditionalAuthenticatedDataTemplate(Guid encryptorId, byte[] additionalAuthenticatedDataTemplate) + { + 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 (Read32bitAlignedGuid(&pbOriginal[sizeof(uint)]) == encryptorId) + { + return additionalAuthenticatedDataTemplate; + } + } + + // 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) + { + Write32bitAlignedGuid(&pbCloned[sizeof(uint)], encryptorId); + } + 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); + } + + 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) + { + // argument & state checking + if (unprotectedData == null) + { + throw new ArgumentNullException("unprotectedData"); + } + + // 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"); + + // 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) + { + Volatile.Write(ref _additionalAuthenticatedDataTemplate, aadForInvocation); + } + + // 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), + 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)"); + } + catch (Exception ex) if (!(ex is CryptographicException)) + { + // 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 + // can result in weird behaviors at runtime. + private static Guid Read32bitAlignedGuid(void* ptr) + { + Debug.Assert((long)ptr % 4 == 0); + + Guid retVal; + ((int*)&retVal)[0] = ((int*)ptr)[0]; + ((int*)&retVal)[1] = ((int*)ptr)[1]; + ((int*)&retVal)[2] = ((int*)ptr)[2]; + ((int*)&retVal)[3] = ((int*)ptr)[3]; + return retVal; + } + + private static uint ReadBigEndian32BitInteger(byte* ptr) + { + return ((uint)ptr[0] << 24) + | ((uint)ptr[1] << 16) + | ((uint)ptr[2] << 8) + | ((uint)ptr[3]); + } + + private static bool TryGetVersionFromMagicHeader(uint magicHeader, out int version) + { + const uint MAGIC_HEADER_VERSION_MASK = 0xFU; + if ((magicHeader & ~MAGIC_HEADER_VERSION_MASK) == MAGIC_HEADER_V0) + { + version = (int)(magicHeader & MAGIC_HEADER_VERSION_MASK); + return true; + } + else + { + version = default(int); + return false; + } + } + + public byte[] Unprotect(byte[] protectedData) + { + // 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(); + } + + // 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))); + + try + { + // 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; + } + catch (Exception ex) if (!(ex is CryptographicException)) + { + // homogenize all failures to CryptographicException + throw Error.DecryptionFailed(ex); + } + } + + // Helper function to write a GUID to a 32-bit alignment; useful on ARM where unaligned reads + // can result in weird behaviors at runtime. + private static void Write32bitAlignedGuid(void* ptr, Guid value) + { + Debug.Assert((long)ptr % 4 == 0); + + ((int*)ptr)[0] = ((int*)&value)[0]; + ((int*)ptr)[1] = ((int*)&value)[1]; + ((int*)ptr)[2] = ((int*)&value)[2]; + ((int*)ptr)[3] = ((int*)&value)[3]; + } + + private static void WriteBigEndianInteger(byte* ptr, uint value) + { + ptr[0] = (byte)(value >> 24); + ptr[1] = (byte)(value >> 16); + ptr[2] = (byte)(value >> 8); + ptr[3] = (byte)(value); + } + + private sealed class PurposeBinaryWriter : BinaryWriter + { + // Strings should never contain invalid UTF16 chars, so we'll use a secure encoding. + private static readonly UTF8Encoding _secureEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + private static readonly byte[] _guidBuffer = new byte[sizeof(Guid)]; + + public PurposeBinaryWriter(MemoryStream stream) : base(stream, _secureEncoding, leaveOpen: true) { } + + public new void Write7BitEncodedInt(int value) + { + base.Write7BitEncodedInt(value); + } + + // 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)); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingProvider.cs new file mode 100644 index 0000000000..37d576c063 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/KeyRingProvider.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft 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.Linq; +using System.Threading; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + internal sealed class KeyRingProvider : 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 readonly IKeyManager _keyManager; + + public KeyRingProvider(IKeyManager keyManager) + { + _keyManager = keyManager; + } + + private CachedKeyRing CreateCachedKeyRingInstanceUnderLock(DateTime utcNow, CachedKeyRing existingCachedKeyRing) + { + 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) + { + existingCachedKeyRing = null; + } + + // 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; + } + + public IKeyRing GetCurrentKeyRing() + { + DateTime utcNow = DateTime.UtcNow; + + // Can we return the cached keyring to the caller? + var existingCachedKeyRing = Volatile.Read(ref _cachedKeyRing); + if (existingCachedKeyRing != null && existingCachedKeyRing.SoftRefreshTimeUtc > utcNow) + { + return existingCachedKeyRing.KeyRing; + } + + // The cached keyring hasn't been created or must be refreshed. + lock (_cachedKeyRingLockObj) + { + // Did somebody update the keyring while we were waiting for the lock? + existingCachedKeyRing = Volatile.Read(ref _cachedKeyRing); + if (existingCachedKeyRing != null && existingCachedKeyRing.SoftRefreshTimeUtc > utcNow) + { + return existingCachedKeyRing.KeyRing; + } + + // It's up to us to refresh the cached keyring. + var newCachedKeyRing = CreateCachedKeyRingInstanceUnderLock(utcNow, existingCachedKeyRing); + Volatile.Write(ref _cachedKeyRing, newCachedKeyRing); + return newCachedKeyRing.KeyRing; + } + } + + private static DateTime GetNextHardRefreshTime(DateTime utcNow) + { + // 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; + } + + private static DateTime MinDateTime(DateTime a, DateTime b) + { + Debug.Assert(a.Kind == DateTimeKind.Utc); + Debug.Assert(b.Kind == DateTimeKind.Utc); + return (a < b) ? a : b; + } + + private sealed class CachedKeyRing + { + internal DateTime HardRefreshTimeUtc; + internal KeyRing KeyRing; + internal IKey[] Keys; + internal DateTime SoftRefreshTimeUtc; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/XmlKeyManager.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/XmlKeyManager.cs new file mode 100644 index 0000000000..d472869b48 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyManagement/XmlKeyManager.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft 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.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.Security.DataProtection.Repositories; +using Microsoft.AspNet.Security.DataProtection.XmlEncryption; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Security.DataProtection.KeyManagement +{ + public sealed class XmlKeyManager : IKeyManager + { + 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); + + 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 readonly IAuthenticatedEncryptorConfigurationFactory _authenticatedEncryptorConfigurationFactory; + private readonly IServiceProvider _serviceProvider; + private readonly ITypeActivator _typeActivator; + private readonly IXmlRepository _xmlRepository; + private readonly IXmlEncryptor _xmlEncryptor; + + public XmlKeyManager( + [NotNull] IServiceProvider serviceProvider, + [NotNull] IAuthenticatedEncryptorConfigurationFactory authenticatedEncryptorConfigurationFactory, + [NotNull] ITypeActivator typeActivator, + [NotNull] IXmlRepository xmlRepository, + [NotNull] IXmlEncryptor xmlEncryptor) + { + _serviceProvider = serviceProvider; + _authenticatedEncryptorConfigurationFactory = authenticatedEncryptorConfigurationFactory; + _typeActivator = typeActivator; + _xmlRepository = xmlRepository; + _xmlEncryptor = xmlEncryptor; + } + + public IKey CreateNewKey(DateTimeOffset activationDate, DateTimeOffset expirationDate) + { + return CreateNewKey(Guid.NewGuid(), DateTimeOffset.UtcNow, activationDate, expirationDate); + } + + private IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate) + { + // + // ... + // ... + // ... + // + // <... 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); + } + + public IReadOnlyCollection GetAllKeys() + { + var allElements = _xmlRepository.GetAllElements(); + + Dictionary idToKeyMap = new Dictionary(); + HashSet revokedKeyIds = null; + DateTimeOffset? mostRecentMassRevocationDate = null; + + foreach (var element in allElements) + { + if (element.Name == KeyElementName) + { + var thisKey = ParseKeyElement(element); + if (idToKeyMap.ContainsKey(thisKey.KeyId)) + { + CryptoUtil.Fail("TODO: Duplicate key."); + } + idToKeyMap.Add(thisKey.KeyId, thisKey); + } + else if (element.Name == RevocationElementName) + { + object revocationInfo = ParseRevocationElement(element); + DateTimeOffset? revocationInfoAsDate = revocationInfo as DateTimeOffset?; + if (revocationInfoAsDate != null) + { + // 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 + if (revokedKeyIds == null) + { + revokedKeyIds = new HashSet(); + } + revokedKeyIds.Add((Guid)revocationInfo); + } + } + else + { + 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)) + { + key.SetRevoked(); + } + } + } + + // And we're done! + return idToKeyMap.Values.ToArray(); + } + + private Key ParseKeyElement(XElement keyElement) + { + Debug.Assert(keyElement.Name == KeyElementName); + + int version = (int)keyElement.Attribute(VersionAttributeName); + CryptoUtil.Assert(version == 1, "TODO: version == 1"); + + 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)"); + + var parser = (IAuthenticatedEncryptorConfigurationXmlReader)_typeActivator.CreateInstance(_serviceProvider, encryptorConfigurationParserType); + var encryptorConfiguration = parser.FromXml(encryptorConfigurationAsXml); + + Guid keyId = (Guid)keyElement.Attribute(IdAttributeName); + DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName); + DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName); + DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName); + + return new Key( + keyId: keyId, + creationDate: creationDate, + activationDate: activationDate, + expirationDate: expirationDate, + encryptorConfiguration: encryptorConfiguration); + } + + // returns a Guid (for specific keys) or a DateTimeOffset (for all keys created on or before a specific date) + private object ParseRevocationElement(XElement revocationElement) + { + Debug.Assert(revocationElement.Name == RevocationElementName); + + string keyIdAsString = revocationElement.Element(KeyElementName).Attribute(IdAttributeName).Value; + if (keyIdAsString == "*") + { + // all keys + return (DateTimeOffset)revocationElement.Element(RevocationDateElementName); + } + else + { + // only one key + return new Guid(keyIdAsString); + } + } + + public void RevokeAllKeys(DateTimeOffset revocationDate, string reason = null) + { + // + // ... + // + // ... + // + + 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 + string friendlyName = String.Format(CultureInfo.InvariantCulture, "revocation-{0:D}", keyId); + _xmlRepository.StoreElement(revocationElement, friendlyName); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Managed/HashAlgorithmExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/Managed/HashAlgorithmExtensions.cs new file mode 100644 index 0000000000..eec421cfd8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Managed/HashAlgorithmExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Managed +{ + internal static class HashAlgorithmExtensions + { + public static int GetDigestSizeInBytes(this HashAlgorithm hashAlgorithm) + { + var hashSizeInBits = hashAlgorithm.HashSize; + CryptoUtil.Assert(hashSizeInBits >= 0 && hashSizeInBits % 8 == 0, "hashSizeInBits >= 0 && hashSizeInBits % 8 == 0"); + return hashSizeInBits / 8; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Managed/IManagedGenRandom.cs b/src/Microsoft.AspNet.Security.DataProtection/Managed/IManagedGenRandom.cs new file mode 100644 index 0000000000..3028068dc7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Managed/IManagedGenRandom.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Managed +{ + internal interface IManagedGenRandom + { + byte[] GenRandom(int numBytes); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs new file mode 100644 index 0000000000..09f431dbdc --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Managed/ManagedAuthenticatedEncryptor.cs @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.Security.DataProtection.SP800_108; + +namespace Microsoft.AspNet.Security.DataProtection.Managed +{ + // An encryptor which does Encrypt(CBC) + HMAC using SymmetricAlgorithm and HashAlgorithm. + // The payloads produced by this encryptor should be compatible with the payloads + // produced by the CNG-based Encrypt(CBC) + HMAC authenticated encryptor. + internal unsafe sealed class ManagedAuthenticatedEncryptor : IAuthenticatedEncryptor, IDisposable + { + // Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single + // key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block + // encryption operations, which a high-traffic web server might perform in mere hours. + // AES and other 128-bit block ciphers are less susceptible to this due to the larger IV + // space, but unfortunately some organizations require older 64-bit block ciphers. To address + // the collision issue, we'll feed 128 bits of entropy to the KDF when performing subkey + // generation. This creates >= 192 bits total entropy for each operation, so we shouldn't + // expect a collision until >= 2^96 operations. Even 2^80 operations still maintains a <= 2^-32 + // probability of collision, and this is acceptable for the expected KDK lifetime. + private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8; + + // 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; + private readonly IManagedGenRandom _genRandom; + private readonly ProtectedMemoryBlob _keyDerivationKey; + private readonly Func _symmetricAlgorithmFactory; + private readonly int _symmetricAlgorithmBlockSizeInBytes; + private readonly int _symmetricAlgorithmSubkeyLengthInBytes; + private readonly int _validationAlgorithmDigestLengthInBytes; + private readonly int _validationAlgorithmSubkeyLengthInBytes; + private readonly Func _validationAlgorithmFactory; + + public ManagedAuthenticatedEncryptor(ProtectedMemoryBlob 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; + + // Validate that the symmetric algorithm has the properties we require + using (var symmetricAlgorithm = symmetricAlgorithmFactory()) + { + _symmetricAlgorithmFactory = symmetricAlgorithmFactory; + _symmetricAlgorithmBlockSizeInBytes = symmetricAlgorithm.GetBlockSizeInBytes(); + _symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes; + } + + // Validate that the MAC algorithm has the properties we require + using (var validationAlgorithm = validationAlgorithmFactory()) + { + _validationAlgorithmFactory = validationAlgorithmFactory; + _validationAlgorithmDigestLengthInBytes = validationAlgorithm.GetDigestSizeInBytes(); + _validationAlgorithmSubkeyLengthInBytes = _validationAlgorithmDigestLengthInBytes; // for simplicity we'll generate MAC subkeys with a length equal to the digest length + } + + 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"); + + _contextHeader = CreateContextHeader(); + } + + private byte[] CreateContextHeader() + { + var EMPTY_ARRAY = new byte[0]; + var EMPTY_ARRAY_SEGMENT = new ArraySegment(EMPTY_ARRAY); + + byte[] retVal = new byte[checked( + 1 /* KDF alg */ + + 1 /* chaining mode */ + + sizeof(uint) /* sym alg key size */ + + sizeof(uint) /* sym alg block size */ + + sizeof(uint) /* hmac alg key size */ + + sizeof(uint) /* hmac alg digest size */ + + _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */ + + _validationAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)]; + + int idx = 0; + + // First is the two-byte header + retVal[idx++] = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF + retVal[idx++] = 0; // 0x00 = CBC encryption + HMAC authentication + + // Next is information about the symmetric algorithm (key size followed by block size) + BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmBlockSizeInBytes); + + // Next is information about the keyed hash algorithm (key size followed by digest size) + BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmSubkeyLengthInBytes); + BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmDigestLengthInBytes); + + // See the design document for an explanation of the following code. + byte[] tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _validationAlgorithmSubkeyLengthInBytes]; + ManagedSP800_108_CTR_HMACSHA512.DeriveKeys( + kdk: EMPTY_ARRAY, + label: EMPTY_ARRAY_SEGMENT, + context: EMPTY_ARRAY_SEGMENT, + prfFactory: _kdkPrfFactory, + output: new ArraySegment(tempKeys)); + + // At this point, tempKeys := { K_E || K_H }. + + // Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer. + using (var symmetricAlg = CreateSymmetricAlgorithm()) + { + using (var cryptoTransform = symmetricAlg.CreateEncryptor( + rgbKey: new ArraySegment(tempKeys, 0, _symmetricAlgorithmSubkeyLengthInBytes).AsStandaloneArray(), + rgbIV: new byte[_symmetricAlgorithmBlockSizeInBytes])) + { + byte[] ciphertext = cryptoTransform.TransformFinalBlock(EMPTY_ARRAY, 0, 0); + CryptoUtil.Assert(ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes, "ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes"); + Buffer.BlockCopy(ciphertext, 0, retVal, idx, ciphertext.Length); + } + } + + idx += _symmetricAlgorithmBlockSizeInBytes; + + // MAC a zero-length input string and copy the digest to the return buffer. + using (var hashAlg = CreateValidationAlgorithm(new ArraySegment(tempKeys, _symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes).AsStandaloneArray())) + { + byte[] digest = hashAlg.ComputeHash(EMPTY_ARRAY); + CryptoUtil.Assert(digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes, "digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes"); + Buffer.BlockCopy(digest, 0, retVal, idx, digest.Length); + } + + idx += _validationAlgorithmDigestLengthInBytes; + CryptoUtil.Assert(idx == retVal.Length, "idx == retVal.Length"); + + // retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || macAlgKeySize || macAlgDigestSize || E("") || MAC("") }. + return retVal; + } + + private SymmetricAlgorithm CreateSymmetricAlgorithm() + { + var retVal = _symmetricAlgorithmFactory(); + CryptoUtil.Assert(retVal != null, "retVal != null"); + + retVal.Mode = CipherMode.CBC; + retVal.Padding = PaddingMode.PKCS7; + return retVal; + } + + private KeyedHashAlgorithm CreateValidationAlgorithm(byte[] key) + { + var retVal = _validationAlgorithmFactory(); + CryptoUtil.Assert(retVal != null, "retVal != null"); + + retVal.Key = key; + return retVal; + } + + public byte[] Decrypt(ArraySegment protectedPayload, ArraySegment additionalAuthenticatedData) + { + protectedPayload.Validate(); + additionalAuthenticatedData.Validate(); + + // Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC + if (protectedPayload.Count < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes)) + { + throw Error.CryptCommon_PayloadInvalid(); + } + + // Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) } + + try + { + // Step 1: Extract the key modifier and IV from the payload. + + int keyModifierOffset; // position in protectedPayload.Array where key modifier begins + int ivOffset; // position in protectedPayload.Array where key modifier ends / IV begins + int ciphertextOffset; // position in protectedPayload.Array where IV ends / ciphertext begins + int macOffset; // position in protectedPayload.Array where ciphertext ends / MAC begins + int eofOffset; // position in protectedPayload.Array where MAC ends + + checked + { + keyModifierOffset = protectedPayload.Offset; + ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES; + ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes; + } + + ArraySegment keyModifier = new ArraySegment(protectedPayload.Array, keyModifierOffset, ivOffset - keyModifierOffset); + byte[] iv = new byte[_symmetricAlgorithmBlockSizeInBytes]; + Buffer.BlockCopy(protectedPayload.Array, ivOffset, iv, 0, iv.Length); + + // Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys. + // We pin all unencrypted keys to limit their exposure via GC relocation. + + byte[] decryptedKdk = new byte[_keyDerivationKey.Length]; + byte[] decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; + byte[] validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes]; + byte[] derivedKeysBuffer = new byte[checked(decryptionSubkey.Length + validationSubkey.Length)]; + + fixed (byte* __unused__1 = decryptedKdk) + fixed (byte* __unused__2 = decryptionSubkey) + fixed (byte* __unused__3 = validationSubkey) + fixed (byte* __unused__4 = derivedKeysBuffer) + { + try + { + _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk)); + DeriveKeysWithContextHeader( + kdk: decryptedKdk, + label: additionalAuthenticatedData, + contextHeader: _contextHeader, + context: keyModifier, + prfFactory: _kdkPrfFactory, + output: new ArraySegment(derivedKeysBuffer)); + + Buffer.BlockCopy(derivedKeysBuffer, 0, decryptionSubkey, 0, decryptionSubkey.Length); + Buffer.BlockCopy(derivedKeysBuffer, decryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length); + + // Step 3: Calculate the correct MAC for this payload. + // correctHash := MAC(IV || ciphertext) + byte[] correctHash; + + using (var hashAlgorithm = CreateValidationAlgorithm(validationSubkey)) + { + checked + { + eofOffset = protectedPayload.Offset + protectedPayload.Count; + macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes; + } + + correctHash = hashAlgorithm.ComputeHash(protectedPayload.Array, ivOffset, macOffset - ivOffset); + } + + // Step 4: Validate the MAC provided as part of the payload. + + if (!CryptoUtil.TimeConstantBuffersAreEqual(correctHash, 0, correctHash.Length, protectedPayload.Array, macOffset, eofOffset - macOffset)) + { + throw Error.CryptCommon_PayloadInvalid(); // integrity check failure + } + + // Step 5: Decipher the ciphertext and return it to the caller. + + using (var symmetricAlgorithm = CreateSymmetricAlgorithm()) + using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv)) + { + var outputStream = new MemoryStream(); + using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write)) + { + cryptoStream.Write(protectedPayload.Array, ciphertextOffset, macOffset - ciphertextOffset); + cryptoStream.FlushFinalBlock(); + + // At this point, outputStream := { plaintext }, and we're done! + return outputStream.ToArray(); + } + } + } + finally + { + // nuke since these contain secret material + Array.Clear(decryptedKdk, 0, decryptedKdk.Length); + Array.Clear(decryptionSubkey, 0, decryptionSubkey.Length); + Array.Clear(validationSubkey, 0, validationSubkey.Length); + Array.Clear(derivedKeysBuffer, 0, derivedKeysBuffer.Length); + } + } + } + catch (Exception ex) if (!(ex is CryptographicException)) + { + // Homogenize all exceptions to CryptographicException. + throw Error.CryptCommon_GenericError(ex); + } + } + + private static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment label, byte[] contextHeader, ArraySegment context, Func prfFactory, ArraySegment output) + { + byte[] combinedContext = new byte[checked(contextHeader.Length + context.Count)]; + Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length); + Buffer.BlockCopy(context.Array, context.Offset, combinedContext, contextHeader.Length, context.Count); + ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(kdk, label, new ArraySegment(combinedContext), prfFactory, output); + } + + public void Dispose() + { + _keyDerivationKey.Dispose(); + } + + public byte[] Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) + { + plaintext.Validate(); + additionalAuthenticatedData.Validate(); + + try + { + var outputStream = new MemoryStream(); + + // Step 1: Generate a random key modifier and IV for this operation. + // Both will be equal to the block size of the block cipher algorithm. + + byte[] keyModifier = _genRandom.GenRandom(_symmetricAlgorithmSubkeyLengthInBytes); + byte[] iv = _genRandom.GenRandom(_symmetricAlgorithmBlockSizeInBytes); + + // Step 2: Copy the key modifier and the IV to the output stream since they'll act as a header. + + outputStream.Write(keyModifier, 0, keyModifier.Length); + outputStream.Write(iv, 0, iv.Length); + + // At this point, outputStream := { keyModifier || IV }. + + // Step 3: Decrypt the KDK, and use it to generate new encryption and HMAC keys. + // We pin all unencrypted keys to limit their exposure via GC relocation. + + byte[] decryptedKdk = new byte[_keyDerivationKey.Length]; + byte[] encryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes]; + byte[] validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes]; + byte[] derivedKeysBuffer = new byte[checked(encryptionSubkey.Length + validationSubkey.Length)]; + + fixed (byte* __unused__1 = decryptedKdk) + fixed (byte* __unused__2 = encryptionSubkey) + fixed (byte* __unused__3 = validationSubkey) + fixed (byte* __unused__4 = derivedKeysBuffer) + { + try + { + _keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment(decryptedKdk)); + DeriveKeysWithContextHeader( + kdk: decryptedKdk, + label: additionalAuthenticatedData, + contextHeader: _contextHeader, + context: new ArraySegment(keyModifier), + prfFactory: _kdkPrfFactory, + output: new ArraySegment(derivedKeysBuffer)); + + Buffer.BlockCopy(derivedKeysBuffer, 0, encryptionSubkey, 0, encryptionSubkey.Length); + Buffer.BlockCopy(derivedKeysBuffer, encryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length); + + // Step 4: Perform the encryption operation. + + using (var symmetricAlgorithm = CreateSymmetricAlgorithm()) + using (var cryptoTransform = symmetricAlgorithm.CreateEncryptor(encryptionSubkey, iv)) + using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write)) + { + cryptoStream.Write(plaintext.Array, plaintext.Offset, plaintext.Count); + cryptoStream.FlushFinalBlock(); + + // At this point, outputStream := { keyModifier || IV || ciphertext } + + // Step 5: Calculate the digest over the IV and ciphertext. + // We don't need to calculate the digest over the key modifier since that + // value has already been mixed into the KDF used to generate the MAC key. + + using (var validationAlgorithm = CreateValidationAlgorithm(validationSubkey)) + { +#if !ASPNETCORE50 + // As an optimization, avoid duplicating the underlying buffer if we're on desktop CLR. + byte[] underlyingBuffer = outputStream.GetBuffer(); +#else + byte[] underlyingBuffer = outputStream.ToArray(); +#endif + + byte[] mac = validationAlgorithm.ComputeHash(underlyingBuffer, KEY_MODIFIER_SIZE_IN_BYTES, checked((int)outputStream.Length - KEY_MODIFIER_SIZE_IN_BYTES)); + outputStream.Write(mac, 0, mac.Length); + + // At this point, outputStream := { keyModifier || IV || ciphertext || MAC(IV || ciphertext) } + // And we're done! + return outputStream.ToArray(); + } + } + } + finally + { + // nuke since these contain secret material + Array.Clear(decryptedKdk, 0, decryptedKdk.Length); + Array.Clear(encryptionSubkey, 0, encryptionSubkey.Length); + Array.Clear(validationSubkey, 0, validationSubkey.Length); + Array.Clear(derivedKeysBuffer, 0, derivedKeysBuffer.Length); + } + } + } + catch (Exception ex) if (!(ex is CryptographicException)) + { + // Homogenize all exceptions to CryptographicException. + throw Error.CryptCommon_GenericError(ex); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Managed/ManagedGenRandomImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/Managed/ManagedGenRandomImpl.cs new file mode 100644 index 0000000000..b89cc8e077 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Managed/ManagedGenRandomImpl.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 System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.DataProtection.Managed +{ + internal unsafe sealed class ManagedGenRandomImpl : IManagedGenRandom + { + private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + public static readonly ManagedGenRandomImpl Instance = new ManagedGenRandomImpl(); + + private ManagedGenRandomImpl() + { + } + + public byte[] GenRandom(int numBytes) + { + byte[] bytes = new byte[numBytes]; + _rng.GetBytes(bytes); + return bytes; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Managed/SymmetricAlgorithmExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/Managed/SymmetricAlgorithmExtensions.cs new file mode 100644 index 0000000000..48c8860ee1 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Managed/SymmetricAlgorithmExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Managed +{ + internal static class SymmetricAlgorithmExtensions + { + public static int GetBlockSizeInBytes(this SymmetricAlgorithm symmetricAlgorithm) + { + var blockSizeInBits = symmetricAlgorithm.BlockSize; + CryptoUtil.Assert(blockSizeInBits >= 0 && blockSizeInBits % 8 == 0, "blockSizeInBits >= 0 && blockSizeInBits % 8 == 0"); + return blockSizeInBits / 8; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/MemoryProtection.cs b/src/Microsoft.AspNet.Security.DataProtection/MemoryProtection.cs new file mode 100644 index 0000000000..0427ff6e62 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/MemoryProtection.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.Runtime.InteropServices; + +namespace Microsoft.AspNet.Security.DataProtection +{ + /// + /// Support for generating random data. + /// + internal unsafe static class MemoryProtection + { + // from dpapi.h + private const uint CRYPTPROTECTMEMORY_SAME_PROCESS = 0x00; + + public static void CryptProtectMemory(SafeHandle pBuffer, uint byteCount) + { + if (!UnsafeNativeMethods.CryptProtectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS)) + { + UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error(); + } + } + + public static void CryptUnprotectMemory(byte* pBuffer, uint byteCount) + { + if (!UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS)) + { + UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error(); + } + } + + public static void CryptUnprotectMemory(SafeHandle pBuffer, uint byteCount) + { + if (!UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS)) + { + UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.DataProtection/NotNullAttribute.cs new file mode 100644 index 0000000000..f65a70a85d --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/NotNullAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs index a9e499b80e..6e353d48c8 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 +namespace Microsoft.AspNet.Security.DataProtection.PBKDF2 { /// /// Internal interface used for abstracting away the PBKDF2 implementation since the implementation is OS-specific. diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs index 3fc75f67fd..527bdc5119 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs @@ -4,8 +4,9 @@ using System; using System.Diagnostics; using System.Security.Cryptography; +using System.Text; -namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 +namespace Microsoft.AspNet.Security.DataProtection.PBKDF2 { /// /// A PBKDF2 provider which utilizes the managed hash algorithm classes as PRFs. @@ -67,7 +68,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 private static KeyedHashAlgorithm PrfToManagedHmacAlgorithm(KeyDerivationPrf prf, string password) { - byte[] passwordBytes = Pbkdf2Util.SecureUtf8Encoding.GetBytes(password); + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); try { switch (prf) diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs index 1af12b4bdc..d33a3d71ca 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs @@ -2,9 +2,9 @@ // 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.Security.DataProtection.Cng; -namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 +namespace Microsoft.AspNet.Security.DataProtection.PBKDF2 { /// /// Internal base class used for abstracting away the PBKDF2 implementation since the implementation is OS-specific. @@ -12,14 +12,23 @@ namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 internal static class Pbkdf2Util { public static readonly IPbkdf2Provider Pbkdf2Provider = GetPbkdf2Provider(); - public static readonly UTF8Encoding SecureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); private static IPbkdf2Provider GetPbkdf2Provider() { // In priority order, our three implementations are Win8, Win7, and "other". - - // TODO: Provide Win7 & Win8 implementations when the new DataProtection stack is fully copied over. - return new ManagedPbkdf2Provider(); + if (OSVersionUtil.IsBCryptOnWin8OrLaterAvailable()) + { + // fastest implementation + return new Win8Pbkdf2Provider(); + } else if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable()) + { + // acceptable implementation + return new Win7Pbkdf2Provider(); + } else + { + // slowest implementation + return new ManagedPbkdf2Provider(); + } } } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Win7Pbkdf2Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Win7Pbkdf2Provider.cs new file mode 100644 index 0000000000..62d1cef6d4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Win7Pbkdf2Provider.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft 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.Text; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.PBKDF2 +{ + /// + /// A PBKDF2 provider which utilizes the Win7 API BCryptDeriveKeyPBKDF2. + /// + internal unsafe sealed class Win7Pbkdf2Provider : IPbkdf2Provider + { + public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested) + { + Debug.Assert(password != null); + Debug.Assert(salt != null); + Debug.Assert(iterationCount > 0); + Debug.Assert(numBytesRequested > 0); + + byte dummy; // CLR doesn't like pinning zero-length buffers, so this provides a valid memory address when working with zero-length buffers + + // Don't dispose of this algorithm instance; it is cached and reused! + var algHandle = PrfToCachedCngAlgorithmInstance(prf); + + // Convert password string to bytes. + // Allocate on the stack whenever we can to save allocations. + int cbPasswordBuffer = Encoding.UTF8.GetMaxByteCount(password.Length); + fixed (byte* pbHeapAllocatedPasswordBuffer = (cbPasswordBuffer > Constants.MAX_STACKALLOC_BYTES) ? new byte[cbPasswordBuffer] : null) + { + byte* pbPasswordBuffer = pbHeapAllocatedPasswordBuffer; + if (pbPasswordBuffer == null) + { + if (cbPasswordBuffer == 0) + { + pbPasswordBuffer = &dummy; + } + else + { + byte* pbStackAllocPasswordBuffer = stackalloc byte[cbPasswordBuffer]; // will be released when the frame unwinds + pbPasswordBuffer = pbStackAllocPasswordBuffer; + } + } + + try + { + int cbPasswordBufferUsed; // we're not filling the entire buffer, just a partial buffer + fixed (char* pszPassword = password) + { + cbPasswordBufferUsed = Encoding.UTF8.GetBytes(pszPassword, password.Length, pbPasswordBuffer, cbPasswordBuffer); + } + + fixed (byte* pbHeapAllocatedSalt = salt) + { + byte* pbSalt = (pbHeapAllocatedSalt != null) ? pbHeapAllocatedSalt : &dummy; + + byte[] retVal = new byte[numBytesRequested]; + fixed (byte* pbRetVal = retVal) + { + int ntstatus = UnsafeNativeMethods.BCryptDeriveKeyPBKDF2( + hPrf: algHandle, + pbPassword: pbPasswordBuffer, + cbPassword: (uint)cbPasswordBufferUsed, + pbSalt: pbSalt, + cbSalt: (uint)salt.Length, + cIterations: (ulong)iterationCount, + pbDerivedKey: pbRetVal, + cbDerivedKey: (uint)retVal.Length, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + } + return retVal; + } + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbPasswordBuffer, cbPasswordBuffer); + } + } + } + + private static BCryptAlgorithmHandle PrfToCachedCngAlgorithmInstance(KeyDerivationPrf prf) + { + switch (prf) + { + case KeyDerivationPrf.Sha1: + return CachedAlgorithmHandles.HMAC_SHA1; + case KeyDerivationPrf.Sha256: + return CachedAlgorithmHandles.HMAC_SHA256; + case KeyDerivationPrf.Sha512: + return CachedAlgorithmHandles.HMAC_SHA512; + default: + throw CryptoUtil.Fail("Unrecognized PRF."); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Win8Pbkdf2Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Win8Pbkdf2Provider.cs new file mode 100644 index 0000000000..02a33fb705 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Win8Pbkdf2Provider.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft 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.Text; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.PBKDF2 +{ + /// + /// A PBKDF2 provider which utilizes the Win8 API BCryptKeyDerivation. + /// + internal unsafe sealed class Win8Pbkdf2Provider : IPbkdf2Provider + { + public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested) + { + Debug.Assert(password != null); + Debug.Assert(salt != null); + Debug.Assert(iterationCount > 0); + Debug.Assert(numBytesRequested > 0); + + string algorithmName = PrfToCngAlgorithmId(prf); + fixed (byte* pbHeapAllocatedSalt = salt) + { + byte dummy; // CLR doesn't like pinning zero-length buffers, so this provides a valid memory address when working with zero-length buffers + byte* pbSalt = (pbHeapAllocatedSalt != null) ? pbHeapAllocatedSalt : &dummy; + + byte[] retVal = new byte[numBytesRequested]; + using (BCryptKeyHandle keyHandle = PasswordToPbkdfKeyHandle(password, CachedAlgorithmHandles.PBKDF2, prf)) + { + fixed (byte* pbRetVal = retVal) + { + Pbkdf2Win8ImplStep2(keyHandle, algorithmName, pbSalt, (uint)salt.Length, (ulong)iterationCount, pbRetVal, (uint)retVal.Length); + } + return retVal; + } + } + } + + private static BCryptKeyHandle PasswordToPbkdfKeyHandle(string password, BCryptAlgorithmHandle pbkdf2AlgHandle, KeyDerivationPrf prf) + { + byte dummy; // CLR doesn't like pinning zero-length buffers, so this provides a valid memory address when working with zero-length buffers + + // Convert password string to bytes. + // Allocate on the stack whenever we can to save allocations. + int cbPasswordBuffer = Encoding.UTF8.GetMaxByteCount(password.Length); + fixed (byte* pbHeapAllocatedPasswordBuffer = (cbPasswordBuffer > Constants.MAX_STACKALLOC_BYTES) ? new byte[cbPasswordBuffer] : null) + { + byte* pbPasswordBuffer = pbHeapAllocatedPasswordBuffer; + if (pbPasswordBuffer == null) + { + if (cbPasswordBuffer == 0) + { + pbPasswordBuffer = &dummy; + } + else + { + byte* pbStackAllocPasswordBuffer = stackalloc byte[cbPasswordBuffer]; // will be released when the frame unwinds + pbPasswordBuffer = pbStackAllocPasswordBuffer; + } + } + + try + { + int cbPasswordBufferUsed; // we're not filling the entire buffer, just a partial buffer + fixed (char* pszPassword = password) + { + cbPasswordBufferUsed = Encoding.UTF8.GetBytes(pszPassword, password.Length, pbPasswordBuffer, cbPasswordBuffer); + } + + return PasswordToPbkdfKeyHandleStep2(pbkdf2AlgHandle, pbPasswordBuffer, (uint)cbPasswordBufferUsed, prf); + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbPasswordBuffer, cbPasswordBuffer); + } + } + } + + private static BCryptKeyHandle PasswordToPbkdfKeyHandleStep2(BCryptAlgorithmHandle pbkdf2AlgHandle, byte* pbPassword, uint cbPassword, KeyDerivationPrf prf) + { + const uint PBKDF2_MAX_KEYLENGTH_IN_BYTES = 2048; // GetSupportedKeyLengths() on a Win8 box; value should never be lowered in any future version of Windows + if (cbPassword <= PBKDF2_MAX_KEYLENGTH_IN_BYTES) + { + // Common case: the password is small enough to be consumed directly by the PBKDF2 algorithm. + return pbkdf2AlgHandle.GenerateSymmetricKey(pbPassword, cbPassword); + } + else + { + // Rare case: password is very long; we must hash manually. + // PBKDF2 uses the PRFs in HMAC mode, and when the HMAC input key exceeds the hash function's + // block length the key is hashed and run back through the key initialization function. + + BCryptAlgorithmHandle prfAlgorithmHandle; // cached; don't dispose + switch (prf) + { + case KeyDerivationPrf.Sha1: + prfAlgorithmHandle = CachedAlgorithmHandles.SHA1; + break; + case KeyDerivationPrf.Sha256: + prfAlgorithmHandle = CachedAlgorithmHandles.SHA256; + break; + case KeyDerivationPrf.Sha512: + prfAlgorithmHandle = CachedAlgorithmHandles.SHA512; + break; + default: + throw CryptoUtil.Fail("Unrecognized PRF."); + } + + // Final sanity check: don't hash the password if the HMAC key initialization function wouldn't have done it for us. + if (cbPassword <= prfAlgorithmHandle.GetHashBlockLength() /* in bytes */) + { + return pbkdf2AlgHandle.GenerateSymmetricKey(pbPassword, cbPassword); + } + + // Hash the password and use the hash as input to PBKDF2. + uint cbPasswordDigest = prfAlgorithmHandle.GetHashDigestLength(); + CryptoUtil.Assert(cbPasswordDigest > 0, "cbPasswordDigest > 0"); + fixed (byte* pbPasswordDigest = new byte[cbPasswordDigest]) + { + try + { + using (var hashHandle = prfAlgorithmHandle.CreateHash()) + { + hashHandle.HashData(pbPassword, cbPassword, pbPasswordDigest, cbPasswordDigest); + } + return pbkdf2AlgHandle.GenerateSymmetricKey(pbPasswordDigest, cbPasswordDigest); + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbPasswordDigest, cbPasswordDigest); + } + } + } + } + + private static void Pbkdf2Win8ImplStep2(BCryptKeyHandle pbkdf2KeyHandle, string hashAlgorithm, byte* pbSalt, uint cbSalt, ulong iterCount, byte* pbDerivedBytes, uint cbDerivedBytes) + { + // First, build the buffers necessary to pass (hash alg, salt, iter count) into the KDF + BCryptBuffer* pBuffers = stackalloc BCryptBuffer[3]; + + pBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_ITERATION_COUNT; + pBuffers[0].pvBuffer = (IntPtr)(&iterCount); + pBuffers[0].cbBuffer = sizeof(ulong); + + pBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_SALT; + pBuffers[1].pvBuffer = (IntPtr)pbSalt; + pBuffers[1].cbBuffer = cbSalt; + + fixed (char* pszHashAlgorithm = hashAlgorithm) + { + pBuffers[2].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM; + pBuffers[2].pvBuffer = (IntPtr)pszHashAlgorithm; + pBuffers[2].cbBuffer = hashAlgorithm.GetTotalByteLengthIncludingNullTerminator(); + + // Add the header which points to the buffers + BCryptBufferDesc bufferDesc = default(BCryptBufferDesc); + BCryptBufferDesc.Initialize(ref bufferDesc); + bufferDesc.cBuffers = 3; + bufferDesc.pBuffers = pBuffers; + + // Finally, import the KDK into the KDF algorithm, then invoke the KDF + uint numBytesDerived; + int ntstatus = UnsafeNativeMethods.BCryptKeyDerivation( + hKey: pbkdf2KeyHandle, + pParameterList: &bufferDesc, + pbDerivedKey: pbDerivedBytes, + cbDerivedKey: cbDerivedBytes, + pcbResult: out numBytesDerived, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + // Final sanity checks before returning control to caller. + CryptoUtil.Assert(numBytesDerived == cbDerivedBytes, "numBytesDerived == cbDerivedBytes"); + } + } + + private static string PrfToCngAlgorithmId(KeyDerivationPrf prf) + { + switch (prf) + { + case KeyDerivationPrf.Sha1: + return Constants.BCRYPT_SHA1_ALGORITHM; + case KeyDerivationPrf.Sha256: + return Constants.BCRYPT_SHA256_ALGORITHM; + case KeyDerivationPrf.Sha512: + return Constants.BCRYPT_SHA512_ALGORITHM; + default: + throw CryptoUtil.Fail("Unrecognized PRF."); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c81d7655be --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft 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; + +// for unit testing +[assembly: InternalsVisibleTo("Microsoft.AspNet.Security.DataProtection.Test")] diff --git a/src/Microsoft.AspNet.Security.DataProtection/Properties/Res.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/Res.Designer.cs deleted file mode 100644 index bac3e9fcff..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/Properties/Res.Designer.cs +++ /dev/null @@ -1,94 +0,0 @@ -// -namespace Microsoft.AspNet.Security.DataProtection -{ - using System.Globalization; - using System.Reflection; - using System.Resources; - - internal static class Res - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.AspNet.Security.DataProtection.Res", typeof(Res).GetTypeInfo().Assembly); - - /// - /// Argument cannot be null or empty. - /// - internal static string Common_NullOrEmpty - { - get { return GetString("Common_NullOrEmpty"); } - } - - /// - /// Argument cannot be null or empty. - /// - internal static string FormatCommon_NullOrEmpty() - { - return GetString("Common_NullOrEmpty"); - } - - /// - /// The master key is too short. It must be at least {0} bytes in length. - /// - internal static string DataProtectorFactory_MasterKeyTooShort - { - get { return GetString("DataProtectorFactory_MasterKeyTooShort"); } - } - - /// - /// The master key is too short. It must be at least {0} bytes in length. - /// - internal static string FormatDataProtectorFactory_MasterKeyTooShort(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("DataProtectorFactory_MasterKeyTooShort"), p0); - } - - /// - /// The data to decrypt is invalid. - /// - internal static string DataProtectorImpl_BadEncryptedData - { - get { return GetString("DataProtectorImpl_BadEncryptedData"); } - } - - /// - /// The data to decrypt is invalid. - /// - internal static string FormatDataProtectorImpl_BadEncryptedData() - { - return GetString("DataProtectorImpl_BadEncryptedData"); - } - - /// - /// Couldn't protect data. Perhaps the user profile isn't loaded? - /// - internal static string DpapiDataProtectorImpl_ProfileNotLoaded - { - get { return GetString("DpapiDataProtectorImpl_ProfileNotLoaded"); } - } - - /// - /// Couldn't protect data. Perhaps the user profile isn't loaded? - /// - internal static string FormatDpapiDataProtectorImpl_ProfileNotLoaded() - { - return GetString("DpapiDataProtectorImpl_ProfileNotLoaded"); - } - - 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.Security.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..8d35437c5a --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs @@ -0,0 +1,222 @@ +// +namespace Microsoft.AspNet.Security.DataProtection +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Security.DataProtection.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// A provider could not be found for algorithm '{0}'. + /// + internal static string BCryptAlgorithmHandle_ProviderNotFound + { + get { return GetString("BCryptAlgorithmHandle_ProviderNotFound"); } + } + + /// + /// A provider could not be found for algorithm '{0}'. + /// + internal static string FormatBCryptAlgorithmHandle_ProviderNotFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("BCryptAlgorithmHandle_ProviderNotFound"), p0); + } + + /// + /// The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}). + /// + internal static string BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength + { + get { return GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"); } + } + + /// + /// The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}). + /// + internal static string FormatBCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("BCRYPT_KEY_LENGTHS_STRUCT_InvalidKeyLength"), p0, p1, p2, p3); + } + + /// + /// 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"); + } + + /// + /// The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length. + /// + internal static string Common_BufferIncorrectlySized + { + get { return GetString("Common_BufferIncorrectlySized"); } + } + + /// + /// The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length. + /// + internal static string FormatCommon_BufferIncorrectlySized(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Common_BufferIncorrectlySized"), p0, p1); + } + + /// + /// 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"); + } + + /// + /// Property {0} cannot be null or empty. + /// + internal static string Common_PropertyCannotBeNullOrEmpty + { + get { return GetString("Common_PropertyCannotBeNullOrEmpty"); } + } + + /// + /// Property {0} cannot be null or empty. + /// + internal static string FormatCommon_PropertyCannotBeNullOrEmpty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyCannotBeNullOrEmpty"), p0); + } + + /// + /// The provided payload could not be decrypted. Refer to the inner exception for more information. + /// + internal static string Common_DecryptionFailed + { + get { return GetString("Common_DecryptionFailed"); } + } + + /// + /// The provided payload could not be decrypted. Refer to the inner exception for more information. + /// + internal static string FormatCommon_DecryptionFailed() + { + return GetString("Common_DecryptionFailed"); + } + + /// + /// An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information. + /// + internal static string Common_EncryptionFailed + { + get { return GetString("Common_EncryptionFailed"); } + } + + /// + /// An error occurred while trying to encrypt the provided data. Refer to the inner exception for more information. + /// + internal static string FormatCommon_EncryptionFailed() + { + return GetString("Common_EncryptionFailed"); + } + + /// + /// The key {0:B} was not found in the keyring. + /// + internal static string Common_KeyNotFound + { + get { return GetString("Common_KeyNotFound"); } + } + + /// + /// The key {0:B} was not found in the keyring. + /// + internal static string FormatCommon_KeyNotFound() + { + return GetString("Common_KeyNotFound"); + } + + /// + /// The key {0:B} has been revoked. + /// + internal static string Common_KeyRevoked + { + get { return GetString("Common_KeyRevoked"); } + } + + /// + /// The key {0:B} has been revoked. + /// + internal static string FormatCommon_KeyRevoked() + { + return GetString("Common_KeyRevoked"); + } + + /// + /// The provided payload was not protected with this protection provider. + /// + internal static string Common_NotAValidProtectedPayload + { + get { return GetString("Common_NotAValidProtectedPayload"); } + } + + /// + /// The provided payload was not protected with this protection provider. + /// + internal static string FormatCommon_NotAValidProtectedPayload() + { + return GetString("Common_NotAValidProtectedPayload"); + } + + /// + /// The protected payload cannot be decrypted because it was protected with a newer version of the protection provider. + /// + internal static string Common_PayloadProducedByNewerVersion + { + get { return GetString("Common_PayloadProducedByNewerVersion"); } + } + + /// + /// The protected payload cannot be decrypted because it was protected with a newer version of the protection provider. + /// + internal static string FormatCommon_PayloadProducedByNewerVersion() + { + return GetString("Common_PayloadProducedByNewerVersion"); + } + + 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.Security.DataProtection/ProtectedDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/ProtectedDataProtectionProvider.cs deleted file mode 100644 index bf34a8dcd8..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/ProtectedDataProtectionProvider.cs +++ /dev/null @@ -1,75 +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. - -#if NET45 -using System; -using System.Security.Cryptography; -using System.Text; - -namespace Microsoft.AspNet.Security.DataProtection -{ - internal class ProtectedDataProtectionProvider : IDataProtectionProvider - { - private readonly DataProtectionScope _scope; - - public ProtectedDataProtectionProvider(DataProtectionScope scope) - { - _scope = scope; - } - - public IDataProtector CreateProtector(string purpose) - { - return new ProtectedDataProtector(_scope, purpose); - } - - public void Dispose() - { - - } - - private class ProtectedDataProtector : IDataProtector - { - private readonly DataProtectionScope _scope; - private readonly byte[] _entropy; - - public ProtectedDataProtector(DataProtectionScope scope, string purpose) - { - _scope = scope; - _entropy = Encoding.UTF8.GetBytes(purpose); - } - - private ProtectedDataProtector(DataProtectionScope scope, byte[] entropy) - { - _scope = scope; - _entropy = entropy; - } - - public IDataProtector CreateSubProtector(string purpose) - { - var purposeBytes = Encoding.UTF8.GetBytes(purpose); - var subProtectorEntropy = new byte[_entropy.Length + purposeBytes.Length]; - - Buffer.BlockCopy(_entropy, 0, subProtectorEntropy, 0, _entropy.Length); - Buffer.BlockCopy(purposeBytes, 0, subProtectorEntropy, _entropy.Length, purposeBytes.Length); - - return new ProtectedDataProtector(_scope, subProtectorEntropy); - } - - public byte[] Protect(byte[] unprotectedData) - { - return ProtectedData.Protect(unprotectedData, _entropy, _scope); - } - - public byte[] Unprotect(byte[] protectedData) - { - return ProtectedData.Unprotect(protectedData, _entropy, _scope); - } - - public void Dispose() - { - - } - } - } -} -#endif diff --git a/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs b/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs new file mode 100644 index 0000000000..ce3b6dae7f --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection +{ + public unsafe sealed class ProtectedMemoryBlob : IDisposable, ISecret + { + // from wincrypt.h + private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16; + + private readonly SecureLocalAllocHandle _encryptedMemoryHandle; + private readonly uint _plaintextLength; + + public ProtectedMemoryBlob(ArraySegment plaintext) + { + plaintext.Validate(); + + _encryptedMemoryHandle = Protect(plaintext); + _plaintextLength = (uint)plaintext.Count; + } + + public ProtectedMemoryBlob(byte[] plaintext) + : this(new ArraySegment(plaintext)) + { + } + + public ProtectedMemoryBlob(byte* plaintext, int plaintextLength) + { + if (plaintext == null) + { + throw new ArgumentNullException("plaintext"); + } + if (plaintextLength < 0) + { + throw new ArgumentOutOfRangeException("plaintextLength"); + } + + _encryptedMemoryHandle = Protect(plaintext, (uint)plaintextLength); + _plaintextLength = (uint)plaintextLength; + } + + public ProtectedMemoryBlob(ISecret secret) + { + if (secret == null) + { + throw new ArgumentNullException("secret"); + } + + ProtectedMemoryBlob other = secret as ProtectedMemoryBlob; + if (other != null) + { + // Fast-track: simple deep copy scenario. + this._encryptedMemoryHandle = other._encryptedMemoryHandle.Duplicate(); + this._plaintextLength = other._plaintextLength; + } + else + { + // Copy the secret to a temporary managed buffer, then protect the buffer. + // We pin the temp buffer and zero it out when we're finished to limit exposure of the secret. + byte[] tempPlaintextBuffer = new byte[secret.Length]; + fixed (byte* pbTempPlaintextBuffer = tempPlaintextBuffer) + { + try + { + secret.WriteSecretIntoBuffer(new ArraySegment(tempPlaintextBuffer)); + _encryptedMemoryHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length); + _plaintextLength = (uint)tempPlaintextBuffer.Length; + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbTempPlaintextBuffer, tempPlaintextBuffer.Length); + } + } + } + } + + public int Length + { + get + { + return (int)_plaintextLength; // ctor guarantees the length fits into a signed int + } + } + + public void Dispose() + { + _encryptedMemoryHandle.Dispose(); + } + + private static SecureLocalAllocHandle Protect(ArraySegment plaintext) + { + fixed (byte* pbPlaintextArray = plaintext.Array) + { + return Protect(&pbPlaintextArray[plaintext.Offset], (uint)plaintext.Count); + } + } + + private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext) + { + // We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE. + uint numTotalBytesToAllocate = cbPlaintext; + uint numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE); + if (numBytesPaddingRequired == CRYPTPROTECTMEMORY_BLOCK_SIZE) + { + numBytesPaddingRequired = 0; // we're already a proper multiple of the block size + } + checked { numTotalBytesToAllocate += numBytesPaddingRequired; } + CryptoUtil.Assert(numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0"); + + // Allocate and copy plaintext data; padding is uninitialized / undefined. + SecureLocalAllocHandle encryptedMemoryHandle = SecureLocalAllocHandle.Allocate((IntPtr)numTotalBytesToAllocate); + UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: encryptedMemoryHandle, byteCount: cbPlaintext); + + // Finally, CryptProtectMemory the whole mess. + if (numTotalBytesToAllocate != 0) + { + MemoryProtection.CryptProtectMemory(encryptedMemoryHandle, byteCount: numTotalBytesToAllocate); + } + return encryptedMemoryHandle; + } + + public static ProtectedMemoryBlob Random(int numBytes) + { + CryptoUtil.Assert(numBytes >= 0, "numBytes >= 0"); + + if (numBytes == 0) + { + byte dummy; + return new ProtectedMemoryBlob(&dummy, 0); + } + else + { + byte[] bytes = new byte[numBytes]; + fixed (byte* pbBytes = bytes) + { + try + { + BCryptUtil.GenRandom(pbBytes, (uint)numBytes); + return new ProtectedMemoryBlob(pbBytes, numBytes); + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbBytes, numBytes); + } + } + } + } + + private void UnprotectInto(byte* pbBuffer) + { + 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: _encryptedMemoryHandle, to: pbBuffer, byteCount: _plaintextLength); + MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength); + } + else + { + // Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and + // perform the decryption in the duplicate buffer, then copy the plaintext data over. + using (var duplicateHandle = _encryptedMemoryHandle.Duplicate()) + { + MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length)); + UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength); + } + } + } + + public void WriteSecretIntoBuffer(ArraySegment buffer) + { + // Parameter checking + buffer.Validate(); + if (buffer.Count != Length) + { + throw Error.Common_BufferIncorrectlySized("buffer", actualSize: buffer.Count, expectedSize: Length); + } + + // only unprotect if the secret is zero-length, as CLR doesn't like pinning zero-length buffers + if (Length != 0) + { + fixed (byte* pbBufferArray = buffer.Array) + { + UnprotectInto(&pbBufferArray[buffer.Offset]); + } + } + } + + public void WriteSecretIntoBuffer(byte* buffer, int bufferLength) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + if (bufferLength < 0) + { + throw new ArgumentOutOfRangeException("bufferLength"); + } + if (bufferLength != Length) + { + throw Error.Common_BufferIncorrectlySized("bufferLength", actualSize: bufferLength, expectedSize: Length); + } + + UnprotectInto(buffer); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Repositories/FileSystemXmlRepository.cs b/src/Microsoft.AspNet.Security.DataProtection/Repositories/FileSystemXmlRepository.cs new file mode 100644 index 0000000000..c09c085587 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Repositories/FileSystemXmlRepository.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft 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.IO; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.DataProtection.Repositories +{ + /// + /// An XML repository backed by a file system. + /// + public class FileSystemXmlRepository : IXmlRepository + { + public FileSystemXmlRepository([NotNull] DirectoryInfo directory) + { + Directory = directory; + } + + protected DirectoryInfo Directory + { + get; + private set; + } + + public virtual IReadOnlyCollection GetAllElements() + { + // forces complete enumeration + return GetAllElementsImpl().ToArray(); + } + + private IEnumerable GetAllElementsImpl() + { + Directory.Create(); // won't throw if the directory already exists + + // Find all files matching the pattern "{guid}.xml" + 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); + } + + Guid unused; + if (Guid.TryParseExact(simpleFilename, "D" /* registry format */, out unused)) + { + XDocument document; + using (var fileStream = File.OpenRead(fileSystemInfo.FullName)) + { + document = XDocument.Load(fileStream); + } + + // 'yield return' outside the preceding 'using' block so we don't hold files open longer than necessary + yield return document.Root; + } + } + } + + 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()); + } + + private void StoreElement(XElement element, Guid id) + { + // 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)); + + try + { + using (var tempFileStream = File.OpenWrite(tempFilename)) + { + new XDocument(element).Save(tempFileStream); + } + + // Once the file has been fully written, perform the rename. + // Renames are atomic operations on the file systems we support. + File.Move(tempFilename, finalFilename); + } + finally + { + File.Delete(tempFilename); // won't throw if the file doesn't exist + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Repositories/IXmlRepository.cs b/src/Microsoft.AspNet.Security.DataProtection/Repositories/IXmlRepository.cs new file mode 100644 index 0000000000..572701d922 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Repositories/IXmlRepository.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.Collections.Generic; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.DataProtection.Repositories +{ + /// + /// The basic interface for storing and retrieving XML elements. + /// + public interface IXmlRepository + { + /// + /// Gets all top-level XML elements in the repository. + /// + /// + /// All top-level elements in the repository. + /// + IReadOnlyCollection GetAllElements(); + + /// + /// Adds a top-level XML element to the repository. + /// + /// The element to add. + /// An optional name to be associated with the XML element. + /// For instance, if this repository stores XML files on disk, the friendly name may + /// be used as part of the file name. Repository implementations are not required to + /// observe this parameter even if it has been provided by the caller. + void StoreElement(XElement element, string friendlyName); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Res.resx b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx similarity index 75% rename from src/Microsoft.AspNet.Security.DataProtection/Res.resx rename to src/Microsoft.AspNet.Security.DataProtection/Resources.resx index 0a01c8908d..b03285c38d 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Res.resx +++ b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx @@ -117,16 +117,40 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Argument cannot be null or empty. + + A provider could not be found for algorithm '{0}'. - - The master key is too short. It must be at least {0} bytes in length. + + The key length {0} is invalid. Valid key lengths are {1} to {2} bits (step size {3}). - - The data to decrypt is invalid. + + An error occurred during a cryptographic operation. - - Couldn't protect data. Perhaps the user profile isn't loaded? + + The provided buffer is of length {0} byte(s). It must instead be exactly {1} byte(s) in length. + + + The payload was invalid. + + + Property {0} cannot be null or empty. + + + The provided payload could not be decrypted. Refer to the inner exception for more information. + + + 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:B} has been revoked. + + + The provided payload 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. \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs new file mode 100644 index 0000000000..432549207e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/ISP800_108_CTR_HMACSHA512Provider.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.SP800_108 +{ + internal unsafe interface ISP800_108_CTR_HMACSHA512Provider : IDisposable + { + void DeriveKey(byte* pbLabel, uint cbLabel, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs new file mode 100644 index 0000000000..54c2891ad7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/ManagedSP800_108_CTR_HMACSHA512.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Managed; + +namespace Microsoft.AspNet.Security.DataProtection.SP800_108 +{ + internal static class ManagedSP800_108_CTR_HMACSHA512 + { + public static void DeriveKeys(byte[] kdk, ArraySegment label, ArraySegment context, Func prfFactory, ArraySegment output) + { + // make copies so we can mutate these local vars + int outputOffset = output.Offset; + int outputCount = output.Count; + + using (HashAlgorithm prf = prfFactory(kdk)) + { + // See SP800-108, Sec. 5.1 for the format of the input to the PRF routine. + byte[] prfInput = new byte[checked(sizeof(uint) /* [i]_2 */ + label.Count + 1 /* 0x00 */ + context.Count + sizeof(uint) /* [K]_2 */)]; + + // Copy [L]_2 to prfInput since it's stable over all iterations + uint outputSizeInBits = (uint)checked((int)outputCount * 8); + prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24); + prfInput[prfInput.Length - 3] = (byte)(outputSizeInBits >> 16); + prfInput[prfInput.Length - 2] = (byte)(outputSizeInBits >> 8); + prfInput[prfInput.Length - 1] = (byte)(outputSizeInBits); + + // Copy label and context to prfInput since they're stable over all iterations + Buffer.BlockCopy(label.Array, label.Offset, prfInput, sizeof(uint), label.Count); + Buffer.BlockCopy(context.Array, context.Offset, prfInput, sizeof(int) + label.Count + 1, context.Count); + + int prfOutputSizeInBytes = prf.GetDigestSizeInBytes(); + for (uint i = 1; outputCount > 0; i++) + { + // Copy [i]_2 to prfInput since it mutates with each iteration + prfInput[0] = (byte)(i >> 24); + prfInput[1] = (byte)(i >> 16); + prfInput[2] = (byte)(i >> 8); + prfInput[3] = (byte)(i); + + // Run the PRF and copy the results to the output buffer + byte[] prfOutput = prf.ComputeHash(prfInput); + CryptoUtil.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length"); + int numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount); + Buffer.BlockCopy(prfOutput, 0, output.Array, outputOffset, numBytesToCopyThisIteration); + Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so nuke it + + // adjust offsets + outputOffset += numBytesToCopyThisIteration; + outputCount -= numBytesToCopyThisIteration; + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.cs new file mode 100644 index 0000000000..11750100c5 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Extensions.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.Security.DataProtection.SP800_108 +{ + internal unsafe static class SP800_108_CTR_HMACSHA512Extensions + { + public static void DeriveKeyWithContextHeader(this ISP800_108_CTR_HMACSHA512Provider provider, byte* pbLabel, uint cbLabel, byte[] contextHeader, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey) + { + uint cbCombinedContext = checked((uint)contextHeader.Length + cbContext); + + // Try allocating the combined context on the stack to avoid temporary managed objects; only fall back to heap if buffers are too large. + byte[] heapAllocatedCombinedContext = (cbCombinedContext > Constants.MAX_STACKALLOC_BYTES) ? new byte[cbCombinedContext] : null; + fixed (byte* pbHeapAllocatedCombinedContext = heapAllocatedCombinedContext) + { + byte* pbCombinedContext = pbHeapAllocatedCombinedContext; + if (pbCombinedContext == null) + { + byte* pbStackAllocatedCombinedContext = stackalloc byte[(int)cbCombinedContext]; // will be released when frame pops + pbCombinedContext = pbStackAllocatedCombinedContext; + } + + fixed (byte* pbContextHeader = contextHeader) + { + UnsafeBufferUtil.BlockCopy(from: pbContextHeader, to: pbCombinedContext, byteCount: contextHeader.Length); + } + UnsafeBufferUtil.BlockCopy(from: pbContext, to: &pbCombinedContext[contextHeader.Length], byteCount: cbContext); + + // At this point, combinedContext := { contextHeader || context } + provider.DeriveKey(pbLabel, cbLabel, pbCombinedContext, cbCombinedContext, pbDerivedKey, cbDerivedKey); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs new file mode 100644 index 0000000000..e87017a8f1 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/SP800_108_CTR_HMACSHA512Util.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.SafeHandles; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SP800_108 +{ + /// + /// Provides an implementation of the SP800-108-CTR-HMACSHA512 key derivation function. + /// This class assumes at least Windows 7 / Server 2008 R2. + /// + /// + /// More info at http://csrc.nist.gov/publications/nistpubs/800-108/sp800-108.pdf, Sec. 5.1. + /// + internal unsafe static class SP800_108_CTR_HMACSHA512Util + { + private static readonly bool _isWin8OrLater = GetIsRunningWin8OrLater(); + + // Creates a provider with an empty key. + public static ISP800_108_CTR_HMACSHA512Provider CreateEmptyProvider() + { + byte dummy; + return CreateProvider(pbKdk: &dummy, cbKdk: 0); + } + + // Creates a provider from the given key. + public static ISP800_108_CTR_HMACSHA512Provider CreateProvider(byte* pbKdk, uint cbKdk) + { + return (_isWin8OrLater) + ? (ISP800_108_CTR_HMACSHA512Provider)new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk) + : (ISP800_108_CTR_HMACSHA512Provider)new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk); + } + + // Creates a provider from the given secret. + public static ISP800_108_CTR_HMACSHA512Provider CreateProvider(ProtectedMemoryBlob kdk) + { + uint secretLengthInBytes = checked((uint)kdk.Length); + if (secretLengthInBytes == 0) + { + return CreateEmptyProvider(); + } + else + { + fixed (byte* pbPlaintextSecret = new byte[secretLengthInBytes]) + { + try + { + kdk.WriteSecretIntoBuffer(pbPlaintextSecret, checked((int)secretLengthInBytes)); + return CreateProvider(pbPlaintextSecret, secretLengthInBytes); + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbPlaintextSecret, secretLengthInBytes); + } + } + } + } + + private static bool GetIsRunningWin8OrLater() + { + // In priority order, our three implementations are Win8, Win7, and "other". + + const string BCRYPT_LIB = "bcrypt.dll"; + + SafeLibraryHandle bcryptLibHandle = null; + try + { + bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB); + } + catch + { + // BCrypt not available? We'll fall back to managed code paths. + } + + if (bcryptLibHandle != null) + { + using (bcryptLibHandle) + { + if (bcryptLibHandle.DoesProcExist("BCryptKeyDerivation")) + { + // We're running on Win8+. + return true; + } + } + } + + // Not running on Win8+ + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs new file mode 100644 index 0000000000..29157aeefc --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/Win7SP800_108_CTR_HMACSHA512Provider.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SP800_108 +{ + internal unsafe sealed class Win7SP800_108_CTR_HMACSHA512Provider : ISP800_108_CTR_HMACSHA512Provider + { + private readonly BCryptHashHandle _hashHandle; + + public Win7SP800_108_CTR_HMACSHA512Provider(byte* pbKdk, uint cbKdk) + { + _hashHandle = CachedAlgorithmHandles.HMAC_SHA512.CreateHmac(pbKdk, cbKdk); + } + + public void DeriveKey(byte* pbLabel, uint cbLabel, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey) + { + const uint SHA512_DIGEST_SIZE_IN_BYTES = 512 / 8; + byte* pbHashDigest = stackalloc byte[(int)SHA512_DIGEST_SIZE_IN_BYTES]; + + // NOTE: pbDerivedKey and cbDerivedKey are modified as data is copied to the output buffer. + + // this will be zero-inited + byte[] tempInputBuffer = new byte[checked( + sizeof(int) /* [i] */ + + cbLabel /* Label */ + + 1 /* 0x00 */ + + cbContext /* Context */ + + sizeof(int) /* [L] */)]; + + fixed (byte* pbTempInputBuffer = tempInputBuffer) + { + // Step 1: Calculate all necessary offsets into the temp input & output buffer. + byte* pbTempInputCounter = pbTempInputBuffer; + byte* pbTempInputLabel = &pbTempInputCounter[sizeof(int)]; + byte* pbTempInputContext = &pbTempInputLabel[cbLabel + 1 /* 0x00 */]; + byte* pbTempInputBitlengthIndicator = &pbTempInputContext[cbContext]; + + // Step 2: Copy Label and Context into the temp input buffer. + UnsafeBufferUtil.BlockCopy(from: pbLabel, to: pbTempInputLabel, byteCount: cbLabel); + UnsafeBufferUtil.BlockCopy(from: pbContext, to: pbTempInputContext, byteCount: cbContext); + + // Step 3: copy [L] into last part of data to be hashed, big-endian + BitHelpers.WriteTo(pbTempInputBitlengthIndicator, checked(cbDerivedKey * 8)); + + // Step 4: iterate until all desired bytes have been generated + for (uint i = 1; cbDerivedKey > 0; i++) + { + // Step 4a: Copy [i] into the first part of data to be hashed, big-endian + BitHelpers.WriteTo(pbTempInputCounter, i); + + // Step 4b: Hash. Win7 doesn't allow reusing hash algorithm objects after the final hash + // has been computed, so we'll just keep calling DuplicateHash on the original virgin + // hash handle. This offers a slight performance increase over allocating a new hash + // handle for each iteration. We don't need to mess with any of this on Win8 since on + // that platform we use BCryptKeyDerivation directly, which offers superior performance. + using (var hashHandle = _hashHandle.DuplicateHash()) + { + hashHandle.HashData(pbTempInputBuffer, (uint)tempInputBuffer.Length, pbHashDigest, SHA512_DIGEST_SIZE_IN_BYTES); + } + + // Step 4c: Copy bytes from the temporary buffer to the output buffer. + uint numBytesToCopy = Math.Min(cbDerivedKey, SHA512_DIGEST_SIZE_IN_BYTES); + UnsafeBufferUtil.BlockCopy(from: pbHashDigest, to: pbDerivedKey, byteCount: numBytesToCopy); + pbDerivedKey += numBytesToCopy; + cbDerivedKey -= numBytesToCopy; + } + } + } + + public void Dispose() + { + _hashHandle.Dispose(); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs new file mode 100644 index 0000000000..2aa5d58b6b --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108/Win8SP800_108_CTR_HMACSHA512Provider.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SP800_108 +{ + internal unsafe sealed class Win8SP800_108_CTR_HMACSHA512Provider : ISP800_108_CTR_HMACSHA512Provider + { + private readonly BCryptKeyHandle _keyHandle; + + public Win8SP800_108_CTR_HMACSHA512Provider(byte* pbKdk, uint cbKdk) + { + _keyHandle = ImportKey(pbKdk, cbKdk); + } + + public void DeriveKey(byte* pbLabel, uint cbLabel, byte* pbContext, uint cbContext, byte* pbDerivedKey, uint cbDerivedKey) + { + const int SHA512_ALG_CHAR_COUNT = 7; + char* pszHashAlgorithm = stackalloc char[SHA512_ALG_CHAR_COUNT /* includes terminating null */]; + pszHashAlgorithm[0] = 'S'; + pszHashAlgorithm[1] = 'H'; + pszHashAlgorithm[2] = 'A'; + pszHashAlgorithm[3] = '5'; + pszHashAlgorithm[4] = '1'; + pszHashAlgorithm[5] = '2'; + pszHashAlgorithm[6] = (char)0; + + // First, build the buffers necessary to pass (label, context, PRF algorithm) into the KDF + BCryptBuffer* pBuffers = stackalloc BCryptBuffer[3]; + + pBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_LABEL; + pBuffers[0].pvBuffer = (IntPtr)pbLabel; + pBuffers[0].cbBuffer = cbLabel; + + pBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_CONTEXT; + pBuffers[1].pvBuffer = (IntPtr)pbContext; + pBuffers[1].cbBuffer = cbContext; + + pBuffers[2].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM; + pBuffers[2].pvBuffer = (IntPtr)pszHashAlgorithm; + pBuffers[2].cbBuffer = checked(SHA512_ALG_CHAR_COUNT * sizeof(char)); + + // Add the header which points to the buffers + BCryptBufferDesc bufferDesc = default(BCryptBufferDesc); + BCryptBufferDesc.Initialize(ref bufferDesc); + bufferDesc.cBuffers = 3; + bufferDesc.pBuffers = pBuffers; + + // Finally, invoke the KDF + uint numBytesDerived; + int ntstatus = UnsafeNativeMethods.BCryptKeyDerivation( + hKey: _keyHandle, + pParameterList: &bufferDesc, + pbDerivedKey: pbDerivedKey, + cbDerivedKey: cbDerivedKey, + pcbResult: out numBytesDerived, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + + // Final sanity checks before returning control to caller. + CryptoUtil.Assert(numBytesDerived == cbDerivedKey, "numBytesDerived == cbDerivedKey"); + } + + public void Dispose() + { + _keyHandle.Dispose(); + } + + private static BCryptKeyHandle ImportKey(byte* pbKdk, uint cbKdk) + { + // The MS implementation of SP800_108_CTR_HMAC has a limit on the size of the key it can accept. + // If the incoming key is too long, we'll hash it using SHA512 to bring it back to a manageable + // length. This transform is appropriate since SP800_108_CTR_HMAC is just a glorified HMAC under + // the covers, and the HMAC algorithm allows hashing the key using the underlying PRF if the key + // is greater than the PRF's block length. + + const uint SHA512_BLOCK_SIZE_IN_BYTES = 1024 / 8; + const uint SHA512_DIGEST_SIZE_IN_BYTES = 512 / 8; + + if (cbKdk > SHA512_BLOCK_SIZE_IN_BYTES) + { + // Hash key. + byte* pbHashedKey = stackalloc byte[(int)SHA512_DIGEST_SIZE_IN_BYTES]; + try + { + using (var hashHandle = CachedAlgorithmHandles.SHA512.CreateHash()) + { + hashHandle.HashData(pbKdk, cbKdk, pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES); + } + return CachedAlgorithmHandles.SP800_108_CTR_HMAC.GenerateSymmetricKey(pbKdk, cbKdk); + } + finally + { + UnsafeBufferUtil.SecureZeroMemory(pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES); + } + } + else + { + // Use key directly. + return CachedAlgorithmHandles.SP800_108_CTR_HMAC.GenerateSymmetricKey(pbKdk, cbKdk); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs deleted file mode 100644 index 95ba77614c..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs +++ /dev/null @@ -1,193 +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.Net; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Cryptography; -using Microsoft.AspNet.Security.DataProtection.Util; -using Microsoft.Win32.SafeHandles; - -namespace Microsoft.AspNet.Security.DataProtection -{ - /// - /// Provides an implementation of the SP800-108-CTR-HMACSHA512 key derivation function. - /// This class assumes at least Windows 7 / Server 2008 R2. - /// - /// - /// More info at http://csrc.nist.gov/publications/nistpubs/800-108/sp800-108.pdf, Sec. 5.1. - /// - internal unsafe static class SP800_108Helper - { - private const string BCRYPT_LIB = "bcrypt.dll"; - - [SuppressUnmanagedCodeSecurity] - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - // http://msdn.microsoft.com/en-us/library/hh448506(v=vs.85).aspx - private delegate int BCryptKeyDerivation( - [In] BCryptKeyHandle hKey, - [In] BCryptBufferDesc* pParameterList, - [In] byte* pbDerivedKey, - [In] uint cbDerivedKey, - [Out] out uint pcbResult, - [In] uint dwFlags); - - private static readonly BCryptAlgorithmHandle SP800108AlgorithmHandle; - private delegate void DeriveKeysDelegate(byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength); - private static DeriveKeysDelegate _thunk = CreateThunk(out SP800108AlgorithmHandle); - - private static BCryptAlgorithmHandle CreateSP800108AlgorithmHandle() - { - // create the SP800-108 instance - BCryptAlgorithmHandle algHandle; - int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_SP800108_CTR_HMAC_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: 0); - if (status != 0 || algHandle == null || algHandle.IsInvalid) - { - throw new CryptographicException(status); - } - - return algHandle; - } - - private static DeriveKeysDelegate CreateThunk(out BCryptAlgorithmHandle sp800108AlgorithmHandle) - { - SafeLibraryHandle bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB); - var win8Thunk = bcryptLibHandle.GetProcAddress("BCryptKeyDerivation", throwIfNotFound: false); - if (win8Thunk != null) - { - // Permanently reference bcrypt.dll for the lifetime of the AppDomain. - // When the AD goes away the SafeLibraryHandle will automatically be released. - GCHandle.Alloc(bcryptLibHandle); - sp800108AlgorithmHandle = CreateSP800108AlgorithmHandle(); - return win8Thunk.DeriveKeysWin8; - } - else - { - sp800108AlgorithmHandle = null; - return DeriveKeysWin7; - } - } - - /// - /// Performs a key derivation using SP800-108-CTR-HMACSHA512. - /// - /// Pointer to the key derivation key. - /// Length (in bytes) of the key derivation key. - /// Purpose to attach to the generated subkey. Corresponds to the 'Label' parameter - /// in the KDF. May be null. - /// Pointer to a buffer which will receive the subkey. - /// Length (in bytes) of the output buffer. - public static void DeriveKeys(byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength) - { - _thunk(pKdk, kdkByteLength, purpose, pOutputBuffer, outputBufferByteLength); - } - - // Wraps our own SP800-108 implementation around bcrypt.dll primitives. - private static void DeriveKeysWin7(byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength) - { - const int TEMP_RESULT_OUTPUT_BYTES = 512 / 8; // hardcoded to HMACSHA512 - - // NOTE: pOutputBuffer and outputBufferByteLength are modified as data is copied from temporary buffers - // to the final output buffer. - - // used to hold the output of the HMACSHA512 routine - byte* pTempResultBuffer = stackalloc byte[TEMP_RESULT_OUTPUT_BYTES]; - int purposeLength = (purpose != null) ? purpose.Length : 0; - - // this will be zero-inited - byte[] dataToBeHashed = new byte[checked( - sizeof(int) /* [i] */ - + purposeLength /* Label */ - + 1 /* 0x00 */ - + 0 /* Context */ - + sizeof(int) /* [L] */)]; - - fixed (byte* pDataToBeHashed = dataToBeHashed) - { - // Step 1: copy purpose into Label part of data to be hashed - if (purposeLength > 0) - { - fixed (byte* pPurpose = purpose) - { - BufferUtil.BlockCopy(from: pPurpose, to: &pDataToBeHashed[sizeof(int)], byteCount: purposeLength); - } - } - - // Step 2: copy [L] into last part of data to be hashed, big-endian - uint numBitsToGenerate = checked(outputBufferByteLength * 8); - MemoryUtil.UnalignedWriteBigEndian(&pDataToBeHashed[dataToBeHashed.Length - sizeof(int)], numBitsToGenerate); - - // Step 3: iterate until all desired bytes have been generated - for (int i = 1; outputBufferByteLength > 0; i++) - { - // Step 3a: Copy [i] into the first part of data to be hashed, big-endian - MemoryUtil.UnalignedWriteBigEndian(pDataToBeHashed, (uint)i); - - // Step 3b: Hash. Win7 doesn't allow reusing hash algorithm objects after the final hash - // has been computed, so we need to create a new instance of the hash object for each - // iteration. We don't bother with this optimization on Win8 since we call BCryptKeyDerivation - // instead when on that OS. - using (var hashHandle = BCryptUtil.CreateHMACHandle(Algorithms.HMACSHA512AlgorithmHandle, pKdk, kdkByteLength)) - { - BCryptUtil.HashData(hashHandle, pDataToBeHashed, dataToBeHashed.Length, pTempResultBuffer, TEMP_RESULT_OUTPUT_BYTES); - } - - // Step 3c: Copy bytes from the temporary buffer to the output buffer. - uint numBytesToCopy = Math.Min(outputBufferByteLength, (uint)TEMP_RESULT_OUTPUT_BYTES); - BufferUtil.BlockCopy(from: pTempResultBuffer, to: pOutputBuffer, byteCount: numBytesToCopy); - pOutputBuffer += numBytesToCopy; - outputBufferByteLength -= numBytesToCopy; - } - } - } - - // Calls into the Win8 implementation (bcrypt.dll) for the SP800-108 KDF - private static void DeriveKeysWin8(this BCryptKeyDerivation fnKeyDerivation, byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength) - { - // Create a buffer to hold the hash algorithm name - fixed (char* pszPrfAlgorithmName = Constants.BCRYPT_SHA512_ALGORITHM) - { - BCryptBuffer* pBCryptBuffers = stackalloc BCryptBuffer[2]; - - // The first buffer should contain the PRF algorithm name (hardcoded to HMACSHA512). - // Per http://msdn.microsoft.com/en-us/library/aa375368(v=vs.85).aspx, cbBuffer must include the terminating null char. - pBCryptBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM; - pBCryptBuffers[0].pvBuffer = (IntPtr)pszPrfAlgorithmName; - pBCryptBuffers[0].cbBuffer = (uint)((Constants.BCRYPT_SHA512_ALGORITHM.Length + 1) * sizeof(char)); - uint numBuffers = 1; - - fixed (byte* pPurpose = ((purpose != null && purpose.Length != 0) ? purpose : null)) - { - if (pPurpose != null) - { - // The second buffer will hold the purpose bytes if they're specified. - pBCryptBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_LABEL; - pBCryptBuffers[1].pvBuffer = (IntPtr)pPurpose; - pBCryptBuffers[1].cbBuffer = (uint)purpose.Length; - numBuffers = 2; - } - - // Add the header - BCryptBufferDesc bufferDesc = default(BCryptBufferDesc); - BCryptBufferDesc.Initialize(ref bufferDesc); - bufferDesc.cBuffers = numBuffers; - bufferDesc.pBuffers = pBCryptBuffers; - - // Finally, perform the calculation and validate that the actual number of bytes derived matches - // the number that the caller requested. - uint numBytesDerived; - int status; - using (BCryptKeyHandle kdkHandle = BCryptUtil.ImportKey(SP800108AlgorithmHandle, pKdk, kdkByteLength)) - { - status = fnKeyDerivation(kdkHandle, &bufferDesc, pOutputBuffer, outputBufferByteLength, out numBytesDerived, dwFlags: 0); - } - if (status != 0 || numBytesDerived != outputBufferByteLength) - { - throw new CryptographicException(status); - } - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptAlgorithmHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptAlgorithmHandle.cs new file mode 100644 index 0000000000..2b72ae08d9 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptAlgorithmHandle.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.Diagnostics; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles +{ + internal unsafe sealed class BCryptAlgorithmHandle : BCryptHandle + { + // Called by P/Invoke when returning SafeHandles + private BCryptAlgorithmHandle() { } + + /// + /// Creates an unkeyed hash handle from this hash algorithm. + /// + public BCryptHashHandle CreateHash() + { + return CreateHashImpl(null, 0); + } + + private BCryptHashHandle CreateHashImpl(byte* pbKey, uint cbKey) + { + BCryptHashHandle retVal; + int ntstatus = UnsafeNativeMethods.BCryptCreateHash(this, out retVal, IntPtr.Zero, 0, pbKey, cbKey, dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.AssertSafeHandleIsValid(retVal); + + retVal.SetAlgorithmProviderHandle(this); + return retVal; + } + + /// + /// Creates an HMAC hash handle from this hash algorithm. + /// + public BCryptHashHandle CreateHmac(byte* pbKey, uint cbKey) + { + Debug.Assert(pbKey != null); + Debug.Assert(cbKey != 0); + + return CreateHashImpl(pbKey, cbKey); + } + + /// + /// Imports a key into a symmetric encryption or KDF algorithm. + /// + public BCryptKeyHandle GenerateSymmetricKey(byte* pbSecret, uint cbSecret) + { + BCryptKeyHandle retVal; + int ntstatus = UnsafeNativeMethods.BCryptGenerateSymmetricKey(this, out retVal, IntPtr.Zero, 0, pbSecret, cbSecret, 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.AssertSafeHandleIsValid(retVal); + + retVal.SetAlgorithmProviderHandle(this); + return retVal; + } + + /// + /// Gets the name of this BCrypt algorithm. + /// + public string GetAlgorithmName() + { + // First, calculate how many characters are in the name. + uint byteLengthOfNameWithTerminatingNull = GetProperty(Constants.BCRYPT_ALGORITHM_NAME, null, 0); + CryptoUtil.Assert(byteLengthOfNameWithTerminatingNull % sizeof(char) == 0 && byteLengthOfNameWithTerminatingNull > sizeof(char), "byteLengthOfNameWithTerminatingNull % sizeof(char) == 0 && byteLengthOfNameWithTerminatingNull > sizeof(char)"); + uint numCharsWithoutNull = (byteLengthOfNameWithTerminatingNull - 1) / sizeof(char); + + if (numCharsWithoutNull == 0) + { + return String.Empty; // degenerate case + } + + // Allocate a string object and write directly into it (CLR team approves of this mechanism). + string retVal = new String((char)0, checked((int)numCharsWithoutNull)); + uint numBytesCopied; + fixed (char* pRetVal = retVal) + { + numBytesCopied = GetProperty(Constants.BCRYPT_ALGORITHM_NAME, pRetVal, byteLengthOfNameWithTerminatingNull); + } + CryptoUtil.Assert(numBytesCopied == byteLengthOfNameWithTerminatingNull, "numBytesCopied == byteLengthOfNameWithTerminatingNull"); + return retVal; + } + + /// + /// Gets the cipher block length (in bytes) of this block cipher algorithm. + /// + public uint GetCipherBlockLength() + { + uint cipherBlockLength; + uint numBytesCopied = GetProperty(Constants.BCRYPT_BLOCK_LENGTH, &cipherBlockLength, sizeof(uint)); + CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)"); + return cipherBlockLength; + } + + /// + /// Gets the hash block length (in bytes) of this hash algorithm. + /// + public uint GetHashBlockLength() + { + uint hashBlockLength; + uint numBytesCopied = GetProperty(Constants.BCRYPT_HASH_BLOCK_LENGTH, &hashBlockLength, sizeof(uint)); + CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)"); + return hashBlockLength; + } + + /// + /// Gets the key lengths (in bits) supported by this algorithm. + /// + public BCRYPT_KEY_LENGTHS_STRUCT GetSupportedKeyLengths() + { + BCRYPT_KEY_LENGTHS_STRUCT supportedKeyLengths; + uint numBytesCopied = GetProperty(Constants.BCRYPT_KEY_LENGTHS, &supportedKeyLengths, (uint)sizeof(BCRYPT_KEY_LENGTHS_STRUCT)); + CryptoUtil.Assert(numBytesCopied == sizeof(BCRYPT_KEY_LENGTHS_STRUCT), "numBytesCopied == sizeof(BCRYPT_KEY_LENGTHS_STRUCT)"); + return supportedKeyLengths; + } + + /// + /// Gets the digest length (in bytes) of this hash algorithm provider. + /// + public uint GetHashDigestLength() + { + uint digestLength; + uint numBytesCopied = GetProperty(Constants.BCRYPT_HASH_LENGTH, &digestLength, sizeof(uint)); + CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)"); + return digestLength; + } + + public static BCryptAlgorithmHandle OpenAlgorithmHandle(string algorithmId, string implementation = null, bool hmac = false) + { + // from bcrypt.h + const uint BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008; + + // from ntstatus.h + const int STATUS_NOT_FOUND = unchecked((int)0xC0000225); + + BCryptAlgorithmHandle algHandle; + int ntstatus = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, algorithmId, implementation, dwFlags: (hmac) ? BCRYPT_ALG_HANDLE_HMAC_FLAG : 0); + + // error checking + if (ntstatus == STATUS_NOT_FOUND) + { + throw Error.BCryptAlgorithmHandle_ProviderNotFound(algorithmId); + } + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.AssertSafeHandleIsValid(algHandle); + + return algHandle; + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + return (UnsafeNativeMethods.BCryptCloseAlgorithmProvider(handle, dwFlags: 0) == 0); + } + + public void SetChainingMode(string chainingMode) + { + fixed (char* pszChainingMode = chainingMode ?? String.Empty) + { + SetProperty(Constants.BCRYPT_CHAINING_MODE, pszChainingMode, checked((uint)(chainingMode.Length + 1 /* null terminator */) * sizeof(char))); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptHandle.cs new file mode 100644 index 0000000000..a5001cb26f --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptHandle.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 Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles +{ + internal unsafe abstract class BCryptHandle : SafeHandleZeroOrMinusOneIsInvalid + { + protected BCryptHandle() + : base(ownsHandle: true) + { + } + + protected uint GetProperty(string pszProperty, void* pbOutput, uint cbOutput) + { + uint retVal; + int ntstatus = UnsafeNativeMethods.BCryptGetProperty(this, pszProperty, pbOutput, cbOutput, out retVal, dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + return retVal; + } + + protected void SetProperty(string pszProperty, void* pbInput, uint cbInput) + { + int ntstatus = UnsafeNativeMethods.BCryptSetProperty(this, pszProperty, pbInput, cbInput, dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptHashHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptHashHandle.cs new file mode 100644 index 0000000000..af30a1b3a0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptHashHandle.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft 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.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles +{ + internal unsafe sealed class BCryptHashHandle : BCryptHandle + { + private BCryptAlgorithmHandle _algProviderHandle; + + // Called by P/Invoke when returning SafeHandles + private BCryptHashHandle() { } + + /// + /// Duplicates this hash handle, including any existing hashed state. + /// + public BCryptHashHandle DuplicateHash() + { + BCryptHashHandle duplicateHandle; + int ntstatus = UnsafeNativeMethods.BCryptDuplicateHash(this, out duplicateHandle, IntPtr.Zero, 0, 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + CryptoUtil.AssertSafeHandleIsValid(duplicateHandle); + + duplicateHandle._algProviderHandle = this._algProviderHandle; + return duplicateHandle; + } + + /// + /// Calculates the cryptographic hash over a set of input data. + /// + public void HashData(byte* pbInput, uint cbInput, byte* pbHashDigest, uint cbHashDigest) + { + int ntstatus; + if (cbInput > 0) + { + ntstatus = UnsafeNativeMethods.BCryptHashData( + hHash: this, + pbInput: pbInput, + cbInput: cbInput, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + } + + ntstatus = UnsafeNativeMethods.BCryptFinishHash( + hHash: this, + pbOutput: pbHashDigest, + cbOutput: cbHashDigest, + dwFlags: 0); + UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus); + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + return (UnsafeNativeMethods.BCryptDestroyHash(handle) == 0); + } + + // We don't actually need to hold a reference to the algorithm handle, as the native CNG library + // already holds the reference for us. But once we create a hash from an algorithm provider, odds + // are good that we'll create another hash from the same algorithm provider at some point in the + // future. And since algorithm providers are expensive to create, we'll hold a strong reference + // to all known in-use providers. This way the cached algorithm provider handles utility class + // doesn't keep creating providers over and over. + internal void SetAlgorithmProviderHandle(BCryptAlgorithmHandle algProviderHandle) + { + _algProviderHandle = algProviderHandle; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptKeyHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptKeyHandle.cs new file mode 100644 index 0000000000..d03777d5da --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/BCryptKeyHandle.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; + +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles +{ + internal sealed class BCryptKeyHandle : BCryptHandle + { + private BCryptAlgorithmHandle _algProviderHandle; + + // Called by P/Invoke when returning SafeHandles + private BCryptKeyHandle() { } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + _algProviderHandle = null; + return (UnsafeNativeMethods.BCryptDestroyKey(handle) == 0); + } + + // We don't actually need to hold a reference to the algorithm handle, as the native CNG library + // already holds the reference for us. But once we create a key from an algorithm provider, odds + // are good that we'll create another key from the same algorithm provider at some point in the + // future. And since algorithm providers are expensive to create, we'll hold a strong reference + // to all known in-use providers. This way the cached algorithm provider handles utility class + // doesn't keep creating providers over and over. + internal void SetAlgorithmProviderHandle(BCryptAlgorithmHandle algProviderHandle) + { + _algProviderHandle = algProviderHandle; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/LocalAllocHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/LocalAllocHandle.cs new file mode 100644 index 0000000000..a7add3bb9a --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/LocalAllocHandle.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.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles +{ + /// + /// Represents a handle returned by LocalAlloc. + /// + internal class LocalAllocHandle : SafeHandleZeroOrMinusOneIsInvalid + { + // Called by P/Invoke when returning SafeHandles + protected LocalAllocHandle() + : base(ownsHandle: true) { } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); // actually calls LocalFree + return true; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/NCryptDescriptorHandle.cs similarity index 52% rename from src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs rename to src/Microsoft.AspNet.Security.DataProtection/SafeHandles/NCryptDescriptorHandle.cs index 55275b556a..fff0f360f4 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/NCryptDescriptorHandle.cs @@ -1,15 +1,14 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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.Win32.SafeHandles; -namespace Microsoft.AspNet.Security.DataProtection +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles { - internal sealed class BCryptKeyHandle : SafeHandleZeroOrMinusOneIsInvalid + internal sealed class NCryptDescriptorHandle : SafeHandleZeroOrMinusOneIsInvalid { - // Called by P/Invoke when returning SafeHandles - private BCryptKeyHandle() + private NCryptDescriptorHandle() : base(ownsHandle: true) { } @@ -17,7 +16,7 @@ namespace Microsoft.AspNet.Security.DataProtection // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. protected override bool ReleaseHandle() { - return (UnsafeNativeMethods.BCryptDestroyKey(handle) == 0); + return (UnsafeNativeMethods.NCryptCloseProtectionDescriptor(handle) == 0); } } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeCertContextHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeCertContextHandle.cs new file mode 100644 index 0000000000..c36caa7cdc --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeCertContextHandle.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.Runtime.CompilerServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection.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.Security.DataProtection/SafeHandleZeroOrMinusOneIsInvalid.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeHandleZeroOrMinusOneIsInvalid.cs similarity index 62% rename from src/Microsoft.AspNet.Security.DataProtection/SafeHandleZeroOrMinusOneIsInvalid.cs rename to src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeHandleZeroOrMinusOneIsInvalid.cs index 1b8411a3a4..fe725ea4d2 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/SafeHandleZeroOrMinusOneIsInvalid.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeHandleZeroOrMinusOneIsInvalid.cs @@ -1,22 +1,27 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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; -#if !NET45 -namespace Microsoft.Win32.SafeHandles { - internal abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle { +#if ASPNETCORE50 +namespace Microsoft.Win32.SafeHandles +{ + internal abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle + { // Called by P/Invoke when returning SafeHandles protected SafeHandleZeroOrMinusOneIsInvalid(bool ownsHandle) - : base(IntPtr.Zero, ownsHandle) { + : base(IntPtr.Zero, ownsHandle) + { } - public override bool IsInvalid { - get { + public override bool IsInvalid + { + get + { return (handle == IntPtr.Zero || handle == (IntPtr)(-1)); } } } } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeLibraryHandle.cs similarity index 61% rename from src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs rename to src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeLibraryHandle.cs index b1b7d6e0af..789edd4686 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeLibraryHandle.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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; @@ -6,28 +6,38 @@ using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using System.Security; +using Microsoft.Win32.SafeHandles; -#if NET45 +#if !ASPNETCORE50 using System.Runtime.ConstrainedExecution; #endif -namespace Microsoft.Win32.SafeHandles +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles { /// /// Represents a handle to a Windows module (DLL). /// - internal sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid + internal unsafe sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid { // Called by P/Invoke when returning SafeHandles private SafeLibraryHandle() : base(ownsHandle: true) { } + /// + /// Returns a value stating whether the library exports a given proc. + /// + public bool DoesProcExist(string lpProcName) + { + IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName); + return (pfnProc != IntPtr.Zero); + } + /// /// Gets a delegate pointing to a given export from this library. /// public TDelegate GetProcAddress(string lpProcName, bool throwIfNotFound = true) where TDelegate : class { - Debug.Assert(typeof(TDelegate).GetTypeInfo().IsSubclassOf(typeof(Delegate)), "TDelegate must be a delegate type!"); + Debug.Assert(typeof(Delegate).IsAssignableFrom(typeof(TDelegate)), "TDelegate must be a delegate type!"); IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName); if (pfnProc == IntPtr.Zero) @@ -42,7 +52,11 @@ namespace Microsoft.Win32.SafeHandles } } +#if ASPNETCORE50 + return Marshal.GetDelegateForFunctionPointer(pfnProc); +#else return (TDelegate)(object)Marshal.GetDelegateForFunctionPointer(pfnProc, typeof(TDelegate)); +#endif } /// @@ -63,13 +77,48 @@ namespace Microsoft.Win32.SafeHandles } } + /// + /// Formats a message string using the resource table in the specified library. + /// + public string FormatMessage(int messageId) + { + // from winbase.h + const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100; + const uint FORMAT_MESSAGE_FROM_HMODULE = 0x00000800; + const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; + + LocalAllocHandle messageHandle; + int numCharsOutput = UnsafeNativeMethods.FormatMessage( + dwFlags: FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + lpSource: this, + dwMessageId: (uint)messageId, + dwLanguageId: 0 /* ignore current culture */, + lpBuffer: out messageHandle, + nSize: 0 /* unused */, + Arguments: IntPtr.Zero /* unused */); + + if (numCharsOutput != 0 && messageHandle != null && !messageHandle.IsInvalid) + { + // Successfully retrieved the message. + using (messageHandle) + { + return new String((char*)messageHandle.DangerousGetHandle(), 0, numCharsOutput).Trim(); + } + } + else + { + // Message not found - that's fine. + return null; + } + } + /// /// Opens a library. If 'filename' is not a fully-qualified path, the default search path is used. /// public static SafeLibraryHandle Open(string filename) { SafeLibraryHandle handle = UnsafeNativeMethods.LoadLibrary(filename); - if (handle == null || handle.IsInvalid) { UnsafeNativeMethods.ThrowExceptionForLastWin32Error(); @@ -83,62 +132,51 @@ namespace Microsoft.Win32.SafeHandles return UnsafeNativeMethods.FreeLibrary(handle); } +#if !ASPNETCORE50 [SuppressUnmanagedCodeSecurity] +#endif private static class UnsafeNativeMethods { -#if ASPNETCORE50 - private const string api_ms_win_core_libraryloader_LIB = "api-ms-win-core-libraryloader-l1-1-0.dll"; -#else private const string KERNEL32_LIB = "kernel32.dll"; -#endif + + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms679351(v=vs.85).aspx + [DllImport(KERNEL32_LIB, EntryPoint = "FormatMessageW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int FormatMessage( + [In] uint dwFlags, + [In] SafeLibraryHandle lpSource, + [In] uint dwMessageId, + [In] uint dwLanguageId, + [Out] out LocalAllocHandle lpBuffer, + [In] uint nSize, + [In] IntPtr Arguments + ); + // http://msdn.microsoft.com/en-us/library/ms683152(v=vs.85).aspx [return: MarshalAs(UnmanagedType.Bool)] -#if ASPNETCORE50 - [DllImport(api_ms_win_core_libraryloader_LIB, ExactSpelling = true, SetLastError = true)] -#else - [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] +#if !ASPNETCORE50 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] #endif - public static extern bool FreeLibrary(IntPtr hModule); - + [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] + internal static extern bool FreeLibrary(IntPtr hModule); // http://msdn.microsoft.com/en-us/library/ms683200(v=vs.85).aspx [return: MarshalAs(UnmanagedType.Bool)] -#if ASPNETCORE50 - [DllImport(api_ms_win_core_libraryloader_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] -#else [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] -#endif internal static extern bool GetModuleHandleEx( [In] uint dwFlags, [In] SafeLibraryHandle lpModuleName, // can point to a location within the module if GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS is set [Out] out IntPtr phModule); -#if ASPNETCORE50 - [DllImport(api_ms_win_core_libraryloader_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] -#else // http://msdn.microsoft.com/en-us/library/ms683212(v=vs.85).aspx [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] -#endif - internal static extern IntPtr GetProcAddress( [In] SafeLibraryHandle hModule, [In, MarshalAs(UnmanagedType.LPStr)] string lpProcName); -#if ASPNETCORE50 - [DllImport(api_ms_win_core_libraryloader_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern SafeLibraryHandle LoadLibraryExW([In,MarshalAs(UnmanagedType.LPWStr)] string lpFileName, IntPtr hFile, uint dwFlags); - - internal static SafeLibraryHandle LoadLibrary(string lpFileName) - { - return LoadLibraryExW(lpFileName, IntPtr.Zero, 0); - } -#else // http://msdn.microsoft.com/en-us/library/ms684175(v=vs.85).aspx [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, SetLastError = true)] internal static extern SafeLibraryHandle LoadLibrary( - [In, MarshalAs(UnmanagedType.LPWStr)]string lpFileName); -#endif + [In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName); internal static void ThrowExceptionForLastWin32Error() { diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeNCryptKeyHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeNCryptKeyHandle.cs new file mode 100644 index 0000000000..6b2bacaf6e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SafeNCryptKeyHandle.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft 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 ASPNETCORE50 +namespace Microsoft.AspNet.Security.DataProtection.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.Security.DataProtection/SafeHandles/SecureLocalAllocHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SecureLocalAllocHandle.cs new file mode 100644 index 0000000000..34cca9d1e4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandles/SecureLocalAllocHandle.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft 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 System.Runtime.InteropServices; +using System.Security; + +#if !ASPNETCORE50 +using System.Runtime.ConstrainedExecution; +#endif + +namespace Microsoft.AspNet.Security.DataProtection.SafeHandles +{ + /// + /// Represents a handle returned by LocalAlloc. + /// The memory will be zeroed out before it's freed. + /// + internal unsafe sealed class SecureLocalAllocHandle : LocalAllocHandle + { + private readonly IntPtr _cb; + + private SecureLocalAllocHandle(IntPtr cb) + { + _cb = cb; + } + + public IntPtr Length + { + get + { + return _cb; + } + } + + /// + /// Allocates some amount of memory using LocalAlloc. + /// + public static SecureLocalAllocHandle Allocate(IntPtr cb) + { + SecureLocalAllocHandle newHandle = new SecureLocalAllocHandle(cb); + newHandle.AllocateImpl(cb); + return newHandle; + } + +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] +#endif + private void AllocateImpl(IntPtr cb) + { + handle = Marshal.AllocHGlobal(cb); // actually calls LocalAlloc + } + + public SecureLocalAllocHandle Duplicate() + { + SecureLocalAllocHandle duplicateHandle = Allocate(_cb); + UnsafeBufferUtil.BlockCopy(from: this, to: duplicateHandle, length: _cb); + return duplicateHandle; + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + UnsafeBufferUtil.SecureZeroMemory((byte*)handle, _cb); // compiler won't optimize this away + return base.ReleaseHandle(); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/StringExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/StringExtensions.cs new file mode 100644 index 0000000000..f081611b3f --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/StringExtensions.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.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal static class StringExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetTotalByteLengthIncludingNullTerminator(this string input) + { + if (input == null) + { + // degenerate case + return 0; + } + else + { + uint numChars = (uint)input.Length + 1U; // no overflow check necessary since Length is signed + return checked(numChars * sizeof(char)); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs b/src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs deleted file mode 100644 index 44d277e244..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs +++ /dev/null @@ -1,13 +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; - -#if !NET45 -namespace System.Security -{ - [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] - internal sealed class SuppressUnmanagedCodeSecurityAttribute : Attribute { } -} -#endif diff --git a/src/Microsoft.AspNet.Security.DataProtection/UnsafeBufferUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/UnsafeBufferUtil.cs new file mode 100644 index 0000000000..ef6a69bdbc --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/UnsafeBufferUtil.cs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft 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 System.Threading; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +#if !ASPNETCORE50 +using System.Runtime.ConstrainedExecution; +#endif + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal unsafe static class UnsafeBufferUtil + { + private static readonly byte[] _emptyArray = new byte[0]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static void BlockCopy(void* from, void* to, int byteCount) + { + BlockCopy(from, to, checked((uint)byteCount)); // will be checked before invoking the delegate + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static void BlockCopy(void* from, void* to, uint byteCount) + { + if (byteCount != 0) + { + BlockCopyImpl((byte*)from, (byte*)to, byteCount); + } + } + +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] +#endif + public static void BlockCopy(LocalAllocHandle from, void* to, uint byteCount) + { + bool refAdded = false; + try + { + from.DangerousAddRef(ref refAdded); + BlockCopy((void*)from.DangerousGetHandle(), to, byteCount); + } + finally + { + if (refAdded) + { + from.DangerousRelease(); + } + } + } + +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] +#endif + public static void BlockCopy(byte* from, LocalAllocHandle to, uint byteCount) + { + bool refAdded = false; + try + { + to.DangerousAddRef(ref refAdded); + BlockCopy(from, (void*)to.DangerousGetHandle(), byteCount); + } + finally + { + if (refAdded) + { + to.DangerousRelease(); + } + } + } + +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] +#endif + public static void BlockCopy(LocalAllocHandle from, LocalAllocHandle to, IntPtr length) + { + if (length == IntPtr.Zero) + { + return; + } + + bool fromRefAdded = false; + bool toRefAdded = false; + try + { + from.DangerousAddRef(ref fromRefAdded); + to.DangerousAddRef(ref toRefAdded); + if (sizeof(IntPtr) == 4) + { + BlockCopyImpl(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()); + } + } + finally + { + if (fromRefAdded) + { + from.DangerousRelease(); + } + if (toRefAdded) + { + to.DangerousRelease(); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BlockCopyImpl(byte* from, byte* to, uint byteCount) + { +#if ASPNETCORE50 + Buffer.MemoryCopy(from, to, (ulong)byteCount, (ulong)byteCount); +#else + while (byteCount-- != 0) { + to[byteCount] = from[byteCount]; + } +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BlockCopyImpl(byte* from, byte* to, ulong byteCount) + { +#if ASPNETCORE50 + Buffer.MemoryCopy(from, to, byteCount, byteCount); +#else + while (byteCount-- != 0) { + to[byteCount] = from[byteCount]; + } +#endif + } + + /// + /// Securely clears a memory buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static void SecureZeroMemory(byte* buffer, int byteCount) + { + SecureZeroMemory(buffer, checked((uint)byteCount)); + } + + /// + /// Securely clears a memory buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static void SecureZeroMemory(byte* buffer, uint byteCount) + { + if (byteCount != 0) + { + do + { + buffer[--byteCount] = 0; + } while (byteCount != 0); + + // Volatile to make sure the zero-writes don't get optimized away + Volatile.Write(ref *buffer, 0); + } + } + + /// + /// Securely clears a memory buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static void SecureZeroMemory(byte* buffer, ulong byteCount) + { + if (byteCount != 0) + { + do + { + buffer[--byteCount] = 0; + } while (byteCount != 0); + + // Volatile to make sure the zero-writes don't get optimized away + Volatile.Write(ref *buffer, 0); + } + } + + /// + /// Securely clears a memory buffer. + /// +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + public static void SecureZeroMemory(byte* buffer, IntPtr length) + { + if (sizeof(IntPtr) == 4) + { + SecureZeroMemory(buffer, (uint)length.ToInt32()); + } + else + { + 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.Security.DataProtection/UnsafeNativeMethods.cs b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs index 7b8081dc23..c3721ed328 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs @@ -1,22 +1,35 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Copyright (c) Microsoft 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.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; +using Microsoft.Win32.SafeHandles; + +#if !ASPNETCORE50 +using System.Runtime.ConstrainedExecution; +#endif namespace Microsoft.AspNet.Security.DataProtection { +#if !ASPNETCORE50 [SuppressUnmanagedCodeSecurity] +#endif internal unsafe static class UnsafeNativeMethods { private const string BCRYPT_LIB = "bcrypt.dll"; + private static readonly SafeLibraryHandle _bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB); + private const string CRYPT32_LIB = "crypt32.dll"; - private const string NTDLL_LIB = "ntdll.dll"; - -#if !ASPNETCORE50 - private const string KERNEL32_LIB = "kernel32.dll"; -#endif + private static readonly SafeLibraryHandle _crypt32LibHandle = SafeLibraryHandle.Open(CRYPT32_LIB); + + private const string NCRYPT_LIB = "ncrypt.dll"; + private static readonly SafeLibraryHandle _ncryptLibHandle = SafeLibraryHandle.Open(NCRYPT_LIB); /* * BCRYPT.DLL @@ -45,7 +58,7 @@ namespace Microsoft.AspNet.Security.DataProtection [In] BCryptKeyHandle hKey, [In] byte* pbInput, [In] uint cbInput, - [In] IntPtr pPaddingInfo, + [In] void* pPaddingInfo, [In] byte* pbIV, [In] uint cbIV, [In] byte* pbOutput, @@ -67,11 +80,17 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint dwFlags); [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375399(v=vs.85).aspx internal static extern int BCryptDestroyHash( [In] IntPtr hHash); [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375404(v=vs.85).aspx internal static extern int BCryptDestroyKey( [In] IntPtr hKey); @@ -91,7 +110,7 @@ namespace Microsoft.AspNet.Security.DataProtection [In] BCryptKeyHandle hKey, [In] byte* pbInput, [In] uint cbInput, - [In] IntPtr pPaddingInfo, + [In] void* pPaddingInfo, [In] byte* pbIV, [In] uint cbIV, [In] byte* pbOutput, @@ -107,6 +126,17 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint cbOutput, [In] uint dwFlags); + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375453(v=vs.85).aspx + internal static extern int BCryptGenerateSymmetricKey( + [In] BCryptAlgorithmHandle hAlgorithm, + [Out] out BCryptKeyHandle phKey, + [In] IntPtr pbKeyObject, + [In] uint cbKeyObject, + [In] byte* pbSecret, + [In] uint cbSecret, + [In] uint dwFlags); + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375458(v=vs.85).aspx internal static extern int BCryptGenRandom( @@ -116,22 +146,19 @@ namespace Microsoft.AspNet.Security.DataProtection [In] BCryptGenRandomFlags dwFlags); [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] - // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375468(v=vs.85).aspx - internal static extern int BCryptHashData( - [In] BCryptHashHandle hHash, - [In] byte* pbInput, - [In] uint cbInput, + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375464(v=vs.85).aspx + internal static extern int BCryptGetProperty( + [In] BCryptHandle hObject, + [In, MarshalAs(UnmanagedType.LPWStr)] string pszProperty, + [In] void* pbOutput, + [In] uint cbOutput, + [Out] out uint pcbResult, [In] uint dwFlags); [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] - // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375475(v=vs.85).aspx - internal static extern int BCryptImportKey( - [In] BCryptAlgorithmHandle hAlgorithm, - [In] IntPtr hImportKey, // unused - [In, MarshalAs(UnmanagedType.LPWStr)] string pszBlobType, - [Out] out BCryptKeyHandle phKey, - [In] IntPtr pbKeyObject, // unused - [In] uint cbKeyObject, + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375468(v=vs.85).aspx + internal static extern int BCryptHashData( + [In] BCryptHashHandle hHash, [In] byte* pbInput, [In] uint cbInput, [In] uint dwFlags); @@ -152,14 +179,14 @@ namespace Microsoft.AspNet.Security.DataProtection [Out] out BCryptAlgorithmHandle phAlgorithm, [In, MarshalAs(UnmanagedType.LPWStr)] string pszAlgId, [In, MarshalAs(UnmanagedType.LPWStr)] string pszImplementation, - [In] BCryptAlgorithmFlags dwFlags); + [In] uint dwFlags); [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375504(v=vs.85).aspx internal static extern int BCryptSetProperty( - [In] SafeHandle hObject, + [In] BCryptHandle hObject, [In, MarshalAs(UnmanagedType.LPWStr)] string pszProperty, - [In] IntPtr pbInput, + [In] void* pbInput, [In] uint cbInput, [In] uint dwFlags); @@ -167,6 +194,43 @@ namespace Microsoft.AspNet.Security.DataProtection * CRYPT32.DLL */ + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi)] +#if !ASPNETCORE50 + [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 !ASPNETCORE50 + [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 !ASPNETCORE50 + [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( @@ -178,13 +242,6 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint dwFlags, [Out] out DATA_BLOB pDataOut); - [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] - // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380262(v=vs.85).aspx - internal static extern bool CryptProtectMemory( - [In] byte* pData, - [In] uint cbData, - [In] uint dwFlags); - [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380882(v=vs.85).aspx internal static extern bool CryptUnprotectData( @@ -196,23 +253,131 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint dwFlags, [Out] out DATA_BLOB pDataOut); + /* + * CRYPT32.DLL + */ + + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380262(v=vs.85).aspx [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern bool CryptProtectMemory( + [In] SafeHandle pData, + [In] uint cbData, + [In] uint dwFlags); + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380890(v=vs.85).aspx - internal static extern bool CryptUnprotectMemory( + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern bool CryptUnprotectMemory( [In] byte* pData, [In] uint cbData, [In] uint dwFlags); -#if ASPNETCORE50 - [DllImport(NTDLL_LIB)] - internal static extern void RtlZeroMemory( - [In] IntPtr Destination, - [In] UIntPtr /* SIZE_T */ Length); -#else - [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi)] - internal static extern void RtlZeroMemory( - [In] IntPtr Destination, - [In] UIntPtr /* SIZE_T */ Length); + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380890(v=vs.85).aspx + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + public static extern bool CryptUnprotectMemory( + [In] SafeHandle pData, + [In] uint cbData, + [In] uint dwFlags); + + /* + * NCRYPT.DLL + */ + + [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] +#if !ASPNETCORE50 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] #endif + // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706799(v=vs.85).aspx + internal static extern int NCryptCloseProtectionDescriptor( + [In] IntPtr hDescriptor); + + [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706800(v=vs.85).aspx + internal static extern int NCryptCreateProtectionDescriptor( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwszDescriptorString, + [In] uint dwFlags, + [Out] out NCryptDescriptorHandle phDescriptor); + + [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // 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); + + [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706802(v=vs.85).aspx + internal static extern int NCryptProtectSecret( + [In] NCryptDescriptorHandle hDescriptor, + [In] uint dwFlags, + [In] byte* pbData, + [In] uint cbData, + [In] IntPtr pMemPara, + [In] IntPtr hWnd, + [Out] out LocalAllocHandle ppbProtectedBlob, + [Out] out uint pcbProtectedBlob); + + [DllImport(NCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706811(v=vs.85).aspx + internal static extern int NCryptUnprotectSecret( + [In] IntPtr phDescriptor, + [In] uint dwFlags, + [In] byte* pbProtectedBlob, + [In] uint cbProtectedBlob, + [In] IntPtr pMemPara, + [In] IntPtr hWnd, + [Out] out LocalAllocHandle ppbData, + [Out] out uint pcbData); + + /* + * HELPER FUNCTIONS + */ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowExceptionForBCryptStatus(int ntstatus) + { + // This wrapper method exists because 'throw' statements won't always be inlined. + if (ntstatus != 0) + { + ThrowExceptionForBCryptStatusImpl(ntstatus); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowExceptionForBCryptStatusImpl(int ntstatus) + { + string message = _bcryptLibHandle.FormatMessage(ntstatus); + throw new CryptographicException(message); + } + + public static void ThrowExceptionForLastCrypt32Error() + { + int lastError = Marshal.GetLastWin32Error(); + Debug.Assert(lastError != 0, "This method should only be called if there was an error."); + + string message = _crypt32LibHandle.FormatMessage(lastError); + throw new CryptographicException(message); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowExceptionForNCryptStatus(int ntstatus) + { + // This wrapper method exists because 'throw' statements won't always be inlined. + if (ntstatus != 0) + { + ThrowExceptionForNCryptStatusImpl(ntstatus); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowExceptionForNCryptStatusImpl(int ntstatus) + { + string message = _ncryptLibHandle.FormatMessage(ntstatus); + throw new CryptographicException(message); + } } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs deleted file mode 100644 index bc56d1a15e..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs +++ /dev/null @@ -1,113 +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 System.Security.Cryptography; - -namespace Microsoft.AspNet.Security.DataProtection.Util -{ - internal unsafe static class BufferUtil - { - private static readonly byte[] _emptyArray = new byte[0]; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BlockCopy(void* from, void* to, int byteCount) - { - BlockCopy(from, to, checked((uint)byteCount)); // will be checked before invoking the delegate - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BlockCopy(void* from, void* to, uint byteCount) - { - if (byteCount != 0) - { -#if NET45 - BlockCopySlow((byte*)from, (byte*)to, byteCount); -#else - Buffer.MemoryCopy(source: from, destination: to, destinationSizeInBytes: byteCount, sourceBytesToCopy: byteCount); -#endif - } - } - -#if NET45 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BlockCopySlow(byte* from, byte* to, uint byteCount) - { - while (byteCount-- != 0) - { - *(to++) = *(from++); - } - } -#endif - - /// - /// Securely clears a memory buffer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SecureZeroMemory(byte* buffer, int byteCount) - { - SecureZeroMemory(buffer, checked((uint)byteCount)); - } - - /// - /// Securely clears a memory buffer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SecureZeroMemory(byte* buffer, uint byteCount) - { - UnsafeNativeMethods.RtlZeroMemory((IntPtr)buffer, (UIntPtr)byteCount); - } - - /// - /// 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; - } - } - - /// - /// Creates a new managed byte[] from unmanaged memory. The returned value will be protected - /// by CryptProtectMemory. - /// - public static byte[] ToProtectedManagedByteArray(byte* ptr, int byteCount) - { - byte[] bytes = new byte[byteCount]; - fixed (byte* pBytes = bytes) - { - try - { - BlockCopy(from: ptr, to: pBytes, byteCount: byteCount); - BCryptUtil.ProtectMemoryWithinThisProcess(pBytes, (uint)byteCount); - } - catch - { - SecureZeroMemory(pBytes, byteCount); - throw; - } - } - return bytes; - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.cs deleted file mode 100644 index ebf1aa2462..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.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; -using System.Diagnostics; - -namespace Microsoft.AspNet.Security.DataProtection.Util -{ - /// - /// Defines helper methods for working with fixed expression blocks. - /// - internal static class ByteArrayExtensions - { - private static readonly byte[] _dummyBuffer = new byte[1]; - - // Since the 'fixed' keyword turns a zero-length array into a pointer, we need - // to make sure we're always providing a buffer of length >= 1 so that the - // p/invoke methods we pass the pointers to don't see a null pointer. Callers - // are still responsible for passing a proper length to the p/invoke routines. - public static byte[] AsFixed(this byte[] buffer) - { - Debug.Assert(buffer != null); - return (buffer.Length != 0) ? buffer : _dummyBuffer; - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs deleted file mode 100644 index cd2e672c73..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs +++ /dev/null @@ -1,23 +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; - -namespace Microsoft.AspNet.Security.DataProtection.Util -{ - internal unsafe static class MemoryUtil - { - /// - /// Writes an Int32 to a potentially unaligned memory address, big-endian. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnalignedWriteBigEndian(byte* address, uint value) - { - *(address++) = (byte)(value >> 24); - *(address++) = (byte)(value >> 16); - *(address++) = (byte)(value >> 8); - *(address) = (byte)value; - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/WeakReferenceHelpers.cs b/src/Microsoft.AspNet.Security.DataProtection/WeakReferenceHelpers.cs new file mode 100644 index 0000000000..638fdc6231 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/WeakReferenceHelpers.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal static class WeakReferenceHelpers + { + public static T GetSharedInstance(ref WeakReference weakReference, Func factory) + where T : class, IDisposable + { + // First, see if the WR already exists and points to a live object. + WeakReference existingWeakRef = Volatile.Read(ref weakReference); + T newTarget = null; + WeakReference newWeakRef = null; + + while (true) + { + if (existingWeakRef != null) + { + T existingTarget; + if (weakReference.TryGetTarget(out existingTarget)) + { + // If we created a new target on a previous iteration of the loop but we + // weren't able to store the target into the desired location, dispose of it now. + newTarget?.Dispose(); + return existingTarget; + } + } + + // If the existing WR didn't point anywhere useful and this is our + // first iteration through the loop, create the new target and WR now. + if (newTarget == null) + { + newTarget = factory(); + Debug.Assert(newTarget != null); + newWeakRef = new WeakReference(newTarget); + } + Debug.Assert(newWeakRef != null); + + // Try replacing the existing WR with our newly-created one. + WeakReference currentWeakRef = Interlocked.CompareExchange(ref weakReference, newWeakRef, existingWeakRef); + if (ReferenceEquals(currentWeakRef, existingWeakRef)) + { + // success, 'weakReference' now points to our newly-created WR + return newTarget; + } + + // If we got to this point, somebody beat us to creating a new WR. + // We'll loop around and check it for validity. + Debug.Assert(currentWeakRef != null); + existingWeakRef = currentWeakRef; + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/CertificateXmlEncryptor.cs new file mode 100644 index 0000000000..e9a4388de3 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/CertificateXmlEncryptor.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.X509Certificates; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that performs XML encryption using an X.509 certificate. + /// + /// + /// This type currently requires Windows 8.1 (Windows Server 2012 R2) or higher. + /// + public sealed class CertificateXmlEncryptor : IXmlEncryptor + { + private readonly DpapiNGXmlEncryptor _dpapiEncryptor; + + public CertificateXmlEncryptor([NotNull] X509Certificate2 cert) + { + 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. + /// + /// 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) + { + return _dpapiEncryptor.Encrypt(plaintextElement); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.cs new file mode 100644 index 0000000000..410ce331c2 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGProtectionDescriptorFlags.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; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + // from ncrypt.h and ncryptprotect.h + [Flags] + public enum DpapiNGProtectionDescriptorFlags + { + None = 0, + NamedDescriptor = 0x00000001, + MachineKey = 0x00000020, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.cs new file mode 100644 index 0000000000..d0c2f8bade --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGXmlDecryptor.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.IO; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.Cng; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that can decrypt XML elements which were encrypted using Windows DPAPI:NG. + /// + internal unsafe sealed class DpapiNGXmlDecryptor : IXmlDecryptor + { + /// + /// Decrypts the specified XML element using Windows DPAPI:NG. + /// + /// The encrypted XML element to decrypt. This element is unchanged by the method. + /// The decrypted form of the XML element. + 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)) + { + byte[] plaintextXmlBytes = new byte[secret.Length]; + try + { + secret.WriteSecretIntoBuffer(new ArraySegment(plaintextXmlBytes)); + using (var memoryStream = new MemoryStream(plaintextXmlBytes, writable: false)) + { + return XElement.Load(memoryStream); + } + } + finally + { + Array.Clear(plaintextXmlBytes, 0, plaintextXmlBytes.Length); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs new file mode 100644 index 0000000000..bb123d73b3 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiNGXmlEncryptor.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft 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.IO; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.KeyManagement; +using Microsoft.AspNet.Security.DataProtection.SafeHandles; + +#if !ASPNETCORE50 +using System.Security.Principal; +#endif + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that can encrypt XML elements using Windows DPAPI:NG. + /// + public sealed class DpapiNGXmlEncryptor : IXmlEncryptor + { + internal static readonly XName DpapiNGEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("dpapiNGEncryptedSecret"); + + private readonly NCryptDescriptorHandle _protectionDescriptorHandle; + + public DpapiNGXmlEncryptor() + : this(GetDefaultProtectionDescriptorString(), DpapiNGProtectionDescriptorFlags.None) + { + } + + 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. + /// + /// 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) + { + // First, convert the XML element to a byte[] so that it can be encrypted. + ProtectedMemoryBlob secret; + using (var memoryStream = new MemoryStream()) + { + plaintextElement.Save(memoryStream); + +#if !ASPNETCORE50 + // If we're on full desktop CLR, utilize the underlying buffer directly as an optimization. + byte[] underlyingBuffer = memoryStream.GetBuffer(); + secret = new ProtectedMemoryBlob(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 ProtectedMemoryBlob(clonedBuffer); + Array.Clear(clonedBuffer, 0, clonedBuffer.Length); +#endif + } + + // + // ... 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)); + } + + private static string GetDefaultProtectionDescriptorString() + { +#if !ASPNETCORE50 + // Creates a SID=... protection descriptor string for the current user. + // Reminder: DPAPI:NG provides only encryption, not authentication. + using (WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent()) + { + // use the SID to create an SDDL string + return String.Format(CultureInfo.InvariantCulture, "SID={0}", currentIdentity.User.Value); + } +#else + throw new NotImplementedException("TODO: Doesn't yet work on Core CLR."); +#endif + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlDecryptor.cs new file mode 100644 index 0000000000..e6376dbec0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlDecryptor.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.IO; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.Cng; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that can decrypt XML elements which were encrypted using Windows DPAPI. + /// + internal unsafe sealed class DpapiXmlDecryptor : IXmlDecryptor + { + /// + /// Decrypts the specified XML element using Windows DPAPI. + /// + /// The encrypted XML element to decrypt. This element is unchanged by the method. + /// The decrypted form of the XML element. + 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)) + { + byte[] plaintextXmlBytes = new byte[secret.Length]; + try + { + secret.WriteSecretIntoBuffer(new ArraySegment(plaintextXmlBytes)); + using (var memoryStream = new MemoryStream(plaintextXmlBytes, writable: false)) + { + return XElement.Load(memoryStream); + } + } + finally + { + Array.Clear(plaintextXmlBytes, 0, plaintextXmlBytes.Length); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs new file mode 100644 index 0000000000..718758673f --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.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.IO; +using System.Xml.Linq; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Microsoft.AspNet.Security.DataProtection.KeyManagement; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that can encrypt XML elements using Windows DPAPI. + /// + public sealed class DpapiXmlEncryptor : IXmlEncryptor + { + internal static readonly XName DpapiEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("dpapiEncryptedSecret"); + + /// + /// Encrypts the specified XML element using Windows DPAPI. + /// + /// 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) + { + // First, convert the XML element to a byte[] so that it can be encrypted. + ProtectedMemoryBlob secret; + using (var memoryStream = new MemoryStream()) + { + plaintextElement.Save(memoryStream); + +#if !ASPNETCORE50 + // If we're on full desktop CLR, utilize the underlying buffer directly as an optimization. + byte[] underlyingBuffer = memoryStream.GetBuffer(); + secret = new ProtectedMemoryBlob(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 ProtectedMemoryBlob(clonedBuffer); + Array.Clear(clonedBuffer, 0, clonedBuffer.Length); +#endif + } + + // + // ... base64 data ... + // + byte[] encryptedBytes = DpapiSecretSerializerHelper.ProtectWithDpapi(secret); + return new XElement(DpapiEncryptedSecretElementName, + new XAttribute("decryptor", typeof(DpapiXmlDecryptor).AssemblyQualifiedName), + new XAttribute("version", 1), + Convert.ToBase64String(encryptedBytes)); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/IXmlDecryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/IXmlDecryptor.cs new file mode 100644 index 0000000000..7002cff30c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/IXmlDecryptor.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; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// The basic interface for decrypting an XML element. + /// + public interface IXmlDecryptor + { + /// + /// 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); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/IXmlEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/IXmlEncryptor.cs new file mode 100644 index 0000000000..733f60739b --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/IXmlEncryptor.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; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// The basic interface for encrypting an XML element. + /// + public interface IXmlEncryptor + { + /// + /// Encrypts the specified XML element. + /// + /// The plaintext XML element to encrypt. This element is unchanged by the method. + /// The encrypted form of the XML element. + XElement Encrypt(XElement plaintextElement); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/NullXmlDecryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/NullXmlDecryptor.cs new file mode 100644 index 0000000000..f2dae82986 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/NullXmlDecryptor.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.Linq; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that can decrypt XML elements which were encrypted using a null encryptor. + /// + internal unsafe sealed class NullXmlDecryptor : IXmlDecryptor + { + public XElement Decrypt([NotNull] XElement encryptedElement) + { + CryptoUtil.Assert(encryptedElement.Name == NullXmlEncryptor.NullEncryptedSecretElementName, + "TODO: Incorrect element."); + + return encryptedElement.Elements().Single(); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/NullXmlEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/NullXmlEncryptor.cs new file mode 100644 index 0000000000..3a0c1f09ae --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/NullXmlEncryptor.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft 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.Security.DataProtection.KeyManagement; + +namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption +{ + /// + /// A class that performs null XML encryption (just returns the plaintext). + /// + public sealed class NullXmlEncryptor : IXmlEncryptor + { + internal static readonly XName NullEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("nullEncryptedSecret"); + + /// + /// Encrypts the specified XML element using a null encryptor. + /// + /// 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) + { + // + // + // + return new XElement(NullEncryptedSecretElementName, + new XAttribute("decryptor", typeof(NullXmlDecryptor).AssemblyQualifiedName), + plaintextElement); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/project.json b/src/Microsoft.AspNet.Security.DataProtection/project.json index 0c20fb3aee..b7a1aad940 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/project.json +++ b/src/Microsoft.AspNet.Security.DataProtection/project.json @@ -1,29 +1,58 @@ { - "version": "1.0.0-*", - "frameworks": { - "net45": { - "frameworkAssemblies": { - "System.Security": "" + "version": "1.0.0-*", + "frameworks": { + "net451": { + "dependencies": { + "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Microsoft.Framework.OptionsModel": "1.0.0-*" + }, + "frameworkAssemblies": { + "System.Security": "4.0.0.0", + "System.Xml": "4.0.0.0", + "System.Xml.Linq": "4.0.0.0" + } + }, + "aspnet50": { + "dependencies": { + "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Microsoft.Framework.OptionsModel": "1.0.0-*" + }, + "frameworkAssemblies": { + "System.Security": "4.0.0.0", + "System.Xml": "4.0.0.0", + "System.Xml.Linq": "4.0.0.0" + } + }, + "aspnetcore50": { + "dependencies": { + "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Microsoft.Framework.OptionsModel": "1.0.0-*", + "System.Diagnostics.Debug": "4.0.10-beta-*", + "System.Diagnostics.Tools": "4.0.0-beta-*", + "System.Globalization": "4.0.10-beta-*", + "System.IO.FileSystem": "4.0.0-beta-*", + "System.Linq": "4.0.0-beta-*", + "System.Reflection": "4.0.10-beta-*", + "System.Reflection.TypeExtensions": "4.0.0-beta-*", + "System.Resources.ResourceManager": "4.0.0-beta-*", + "System.Runtime": "4.0.20-beta-*", + "System.Runtime.Extensions": "4.0.10-beta-*", + "System.Runtime.Handles": "4.0.0-beta-*", + "System.Runtime.InteropServices": "4.0.20-beta-*", + "System.Security.Cryptography.X509Certificates": "4.0.0-beta-*", + "System.Security.Cryptography.Encryption": "4.0.0-beta-*", + "System.Security.Cryptography.Encryption.Aes": "4.0.0-beta-*", + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*", + "System.Security.Cryptography.RandomNumberGenerator": "4.0.0-beta-*", + "System.Text.Encoding.Extensions": "4.0.10-beta-*", + "System.Threading": "4.0.0-beta-*", + "System.Xml.XDocument": "4.0.0-beta-*" + } } }, - "aspnetcore50": { - "dependencies": { - "System.Diagnostics.Debug": "4.0.10-beta-*", - "System.Diagnostics.Tools": "4.0.0-beta-*", - "System.Globalization": "4.0.10-beta-*", - "System.Linq": "4.0.0-beta-*", - "System.Reflection": "4.0.10-beta-*", - "System.Resources.ResourceManager": "4.0.0-beta-*", - "System.Runtime": "4.0.20-beta-*", - "System.Runtime.Extensions": "4.0.10-beta-*", - "System.Runtime.InteropServices": "4.0.20-beta-*", - "System.Security.Cryptography.Encryption": "4.0.0-beta-*", - "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*", - "System.Text.Encoding.Extensions": "4.0.10-beta-*" - } + "compilationOptions": { + "allowUnsafe": true, + "warningsAsErrors": true, + "languageVersion": "experimental" } - }, - "compilationOptions": { - "allowUnsafe": true - } } diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs new file mode 100644 index 0000000000..bc2265436b --- /dev/null +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.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.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Xunit; + +namespace Microsoft.AspNet.Security.DataProtection.Test.Cng +{ + public unsafe class CbcAuthenticatedEncryptorTests + { + [Fact] + public void Encrypt_Decrypt_RoundTrips() + { + // Arrange + ProtectedMemoryBlob kdk = new ProtectedMemoryBlob(new byte[512 / 8]); + CbcAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk, + symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC, + symmetricAlgorithmKeySizeInBytes: 256 / 8, + hmacAlgorithmHandle: CachedAlgorithmHandles.HMAC_SHA256); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + + // Act + byte[] ciphertext = encryptor.Encrypt(plaintext, aad); + byte[] decipheredtext = encryptor.Decrypt(new ArraySegment(ciphertext), aad); + + // Assert + Assert.Equal(plaintext, decipheredtext); + } + + [Fact] + public void Encrypt_Decrypt_Tampering_Fails() + { + // Arrange + ProtectedMemoryBlob kdk = new ProtectedMemoryBlob(new byte[512 / 8]); + CbcAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk, + symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC, + symmetricAlgorithmKeySizeInBytes: 256 / 8, + hmacAlgorithmHandle: CachedAlgorithmHandles.HMAC_SHA256); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + byte[] validCiphertext = encryptor.Encrypt(plaintext, aad); + + // Act & assert - 1 + // Ciphertext is too short to be a valid payload + byte[] invalidCiphertext_tooShort = new byte[10]; + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(invalidCiphertext_tooShort), aad); + }); + + // Act & assert - 2 + // Ciphertext has been manipulated + byte[] invalidCiphertext_manipulated = (byte[])validCiphertext.Clone(); + invalidCiphertext_manipulated[0] ^= 0x01; + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(invalidCiphertext_manipulated), aad); + }); + + // Act & assert - 3 + // Ciphertext is too long + byte[] invalidCiphertext_tooLong = validCiphertext.Concat(new byte[] { 0 }).ToArray(); + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(invalidCiphertext_tooLong), aad); + }); + + // Act & assert - 4 + // AAD is incorrect + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(validCiphertext), new ArraySegment(Encoding.UTF8.GetBytes("different aad"))); + }); + } + + [Fact] + public void Encrypt_KnownKey() + { + // Arrange + ProtectedMemoryBlob kdk = new ProtectedMemoryBlob(Encoding.UTF8.GetBytes("master key")); + CbcAuthenticatedEncryptor encryptor = new CbcAuthenticatedEncryptor(kdk, + symmetricAlgorithmHandle: CachedAlgorithmHandles.AES_CBC, + symmetricAlgorithmKeySizeInBytes: 256 / 8, + hmacAlgorithmHandle: CachedAlgorithmHandles.HMAC_SHA256, + genRandom: new SequentialGenRandom()); + ArraySegment plaintext = new ArraySegment(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, 2, 3); + ArraySegment aad = new ArraySegment(new byte[] { 7, 6, 5, 4, 3, 2, 1, 0 }, 1, 4); + + // Act + byte[] retVal = encryptor.Encrypt( + plaintext: plaintext, + additionalAuthenticatedData: aad, + preBufferSize: 3, + postBufferSize: 4); + + // Assert + + // retVal := 00 00 00 (preBuffer) + // | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (keyModifier) + // | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F (IV) + // | B7 EA 3E 32 58 93 A3 06 03 89 C6 66 03 63 08 4B (encryptedData) + // | 9D 8A 85 C7 0F BD 98 D8 7F 72 E7 72 3E B5 A6 26 (HMAC) + // | 6C 38 77 F7 66 19 A2 C9 2C BB AD DA E7 62 00 00 + // | 00 00 00 00 (postBuffer) + + string retValAsString = Convert.ToBase64String(retVal); + Assert.Equal("AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh+36j4yWJOjBgOJxmYDYwhLnYqFxw+9mNh/cudyPrWmJmw4d/dmGaLJLLut2udiAAAAAAAA", retValAsString); + } + } +} diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs new file mode 100644 index 0000000000..5663edd0e1 --- /dev/null +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft 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.Security.Cryptography; +using System.Text; +using Microsoft.AspNet.Security.DataProtection.Cng; +using Xunit; + +namespace Microsoft.AspNet.Security.DataProtection.Test.Cng +{ + public unsafe class GcmAuthenticatedEncryptorTests + { + [Fact] + public void Encrypt_Decrypt_RoundTrips() + { + // Arrange + ProtectedMemoryBlob kdk = new ProtectedMemoryBlob(new byte[512 / 8]); + GcmAuthenticatedEncryptor encryptor = new GcmAuthenticatedEncryptor(kdk, CachedAlgorithmHandles.AES_GCM, symmetricAlgorithmKeySizeInBytes: 256 / 8); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + + // Act + byte[] ciphertext = encryptor.Encrypt(plaintext, aad); + byte[] decipheredtext = encryptor.Decrypt(new ArraySegment(ciphertext), aad); + + // Assert + Assert.Equal(plaintext, decipheredtext); + } + + [Fact] + public void Encrypt_Decrypt_Tampering_Fails() + { + // Arrange + ProtectedMemoryBlob kdk = new ProtectedMemoryBlob(new byte[512 / 8]); + GcmAuthenticatedEncryptor encryptor = new GcmAuthenticatedEncryptor(kdk, CachedAlgorithmHandles.AES_GCM, symmetricAlgorithmKeySizeInBytes: 256 / 8); + ArraySegment plaintext = new ArraySegment(Encoding.UTF8.GetBytes("plaintext")); + ArraySegment aad = new ArraySegment(Encoding.UTF8.GetBytes("aad")); + byte[] validCiphertext = encryptor.Encrypt(plaintext, aad); + + // Act & assert - 1 + // Ciphertext is too short to be a valid payload + byte[] invalidCiphertext_tooShort = new byte[10]; + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(invalidCiphertext_tooShort), aad); + }); + + // Act & assert - 2 + // Ciphertext has been manipulated + byte[] invalidCiphertext_manipulated = (byte[])validCiphertext.Clone(); + invalidCiphertext_manipulated[0] ^= 0x01; + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(invalidCiphertext_manipulated), aad); + }); + + // Act & assert - 3 + // Ciphertext is too long + byte[] invalidCiphertext_tooLong = validCiphertext.Concat(new byte[] { 0 }).ToArray(); + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(invalidCiphertext_tooLong), aad); + }); + + // Act & assert - 4 + // AAD is incorrect + Assert.Throws(() => + { + encryptor.Decrypt(new ArraySegment(validCiphertext), new ArraySegment(Encoding.UTF8.GetBytes("different aad"))); + }); + } + + [Fact] + public void Encrypt_KnownKey() + { + // Arrange + ProtectedMemoryBlob kdk = new ProtectedMemoryBlob(Encoding.UTF8.GetBytes("master key")); + GcmAuthenticatedEncryptor encryptor = new GcmAuthenticatedEncryptor(kdk, CachedAlgorithmHandles.AES_GCM, symmetricAlgorithmKeySizeInBytes: 128 / 8, genRandom: new SequentialGenRandom()); + ArraySegment plaintext = new ArraySegment(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, 2, 3); + ArraySegment aad = new ArraySegment(new byte[] { 7, 6, 5, 4, 3, 2, 1, 0 }, 1, 4); + + // Act + byte[] retVal = encryptor.Encrypt( + plaintext: plaintext, + additionalAuthenticatedData: aad, + preBufferSize: 3, + postBufferSize: 4); + + // Assert + + // retVal := 00 00 00 (preBuffer) + // | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (keyModifier) + // | 10 11 12 13 14 15 16 17 18 19 1A 1B (nonce) + // | 43 B6 91 (encryptedData) + // | 8D 0D 66 D9 A1 D9 44 2D 5D 8E 41 DA 39 60 9C E8 (authTag) + // | 00 00 00 00 (postBuffer) + + string retValAsString = Convert.ToBase64String(retVal); + Assert.Equal("AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaG0O2kY0NZtmh2UQtXY5B2jlgnOgAAAAA", retValAsString); + } + } +} diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/SequentialGenRandom.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/SequentialGenRandom.cs new file mode 100644 index 0000000000..f995199dbb --- /dev/null +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/SequentialGenRandom.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 Microsoft.AspNet.Security.DataProtection.Cng; + +namespace Microsoft.AspNet.Security.DataProtection.Test.Cng +{ + internal unsafe class SequentialGenRandom : IBCryptGenRandom + { + public void GenRandom(byte* pbBuffer, uint cbBuffer) + { + for (uint i = 0; i < cbBuffer; i++) + { + pbBuffer[i] = (byte)i; + } + } + } +} diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/Microsoft.AspNet.Security.DataProtection.Test.kproj b/test/Microsoft.AspNet.Security.DataProtection.Test/Microsoft.AspNet.Security.DataProtection.Test.kproj new file mode 100644 index 0000000000..34cf58a991 --- /dev/null +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/Microsoft.AspNet.Security.DataProtection.Test.kproj @@ -0,0 +1,29 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 7a637185-2ba1-437d-9d4c-7cc4f94cf7bf + Library + + + ConsoleDebugger + + + WebDebugger + + + + + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/project.json b/test/Microsoft.AspNet.Security.DataProtection.Test/project.json new file mode 100644 index 0000000000..bad79b6949 --- /dev/null +++ b/test/Microsoft.AspNet.Security.DataProtection.Test/project.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", + "Moq": "4.2.1312.1622", + "Xunit.KRunner": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { } + }, + "commands": { + "test": "Xunit.KRunner" + }, + "compilationOptions": { + "allowUnsafe": true + } +}