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
+ }
+}