Implement new DataProtection pipeline.
This commit is contained in:
parent
542d87d9d2
commit
769f21783a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"sources": [ "src" ]
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An XML repository backed by Azure blob storage.
|
||||
/// </summary>
|
||||
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<BlobStorageXmlRepositoryOptions> 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<XElement> 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 <keyRing> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies options for configuring an Azure blob storage-based repository.
|
||||
/// </summary>
|
||||
public class BlobStorageXmlRepositoryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The blob storage directory where the key ring will be stored.
|
||||
/// </summary>
|
||||
public CloudBlobDirectory Directory { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>DF3671D7-A9B1-45F1-A195-0AD596001735</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T> : 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<T>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>C2FD9D02-AA0E-45FA-8561-EE357A94B73D</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> 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<T>(this ArraySegment<T> arraySegment)
|
||||
{
|
||||
// Since ArraySegment<T> is a struct, it can be improperly initialized or torn.
|
||||
// We call the ctor again to make sure the instance data is valid.
|
||||
var unused = new ArraySegment<T>(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> plaintext, ArraySegment<byte> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <secret> element.
|
||||
XElement secretElement;
|
||||
byte[] plaintextSecret = new byte[_secret.Length];
|
||||
try
|
||||
{
|
||||
_secret.WriteSecretIntoBuffer(new ArraySegment<byte>(plaintextSecret));
|
||||
secretElement = new XElement(SecretElementName, Convert.ToBase64String(plaintextSecret));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
|
||||
}
|
||||
|
||||
// Then encrypt it and wrap it in another <secret> element.
|
||||
var encryptedSecretElement = encryptor.Encrypt(secretElement);
|
||||
CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptedSecretElement.Attribute("decryptor")),
|
||||
@"TODO: <secret> encryption was invalid.");
|
||||
|
||||
return new XElement(SecretElementName, encryptedSecretElement);
|
||||
}
|
||||
|
||||
public XElement ToXml([NotNull] IXmlEncryptor xmlEncryptor)
|
||||
{
|
||||
// <cbcEncryptor reader="{TYPE}">
|
||||
// <encryption algorithm="{STRING}" provider="{STRING}" keyLength="{INT}" />
|
||||
// <validation algorithm="{STRING}" provider="{STRING}" />
|
||||
// <secret>...</secret>
|
||||
// </cbcEncryptor>
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A factory that is able to create a CNG-based IAuthenticatedEncryptor
|
||||
/// using CBC encryption + HMAC validation.
|
||||
/// </summary>
|
||||
public unsafe sealed class CngCbcAuthenticatedEncryptorConfigurationFactory : IAuthenticatedEncryptorConfigurationFactory
|
||||
{
|
||||
private readonly CngCbcAuthenticatedEncryptorConfigurationOptions _options;
|
||||
|
||||
public CngCbcAuthenticatedEncryptorConfigurationFactory([NotNull] IOptionsAccessor<CngCbcAuthenticatedEncryptorConfigurationOptions> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for configuring an authenticated encryption mechanism which uses
|
||||
/// Windows CNG algorithms in CBC encryption + HMAC validation modes.
|
||||
/// </summary>
|
||||
public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the algorithm to use for symmetric encryption.
|
||||
/// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The algorithm must support CBC-style encryption and must have a block size of 64 bits or greater.
|
||||
/// The default value is 'AES'.
|
||||
/// </remarks>
|
||||
public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the provider which contains the implementation of the symmetric encryption algorithm.
|
||||
/// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider.
|
||||
/// This property is optional.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value is null.
|
||||
/// </remarks>
|
||||
public string EncryptionAlgorithmProvider { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The length (in bits) of the key that will be used for symmetric encryption.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key length must be 128 bits or greater.
|
||||
/// The default value is 256.
|
||||
/// </remarks>
|
||||
public int EncryptionAlgorithmKeySize { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the algorithm to use for hashing data.
|
||||
/// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The algorithm must support being opened in HMAC mode and must have a digest length
|
||||
/// of 128 bits or greater.
|
||||
/// The default value is 'SHA256'.
|
||||
/// </remarks>
|
||||
public string HashAlgorithm { get; set; } = Constants.BCRYPT_SHA256_ALGORITHM;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the provider which contains the implementation of the hash algorithm.
|
||||
/// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider.
|
||||
/// This property is optional.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value is null.
|
||||
/// </remarks>
|
||||
public string HashAlgorithmProvider { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a duplicate of this object, which allows the original object to remain mutable.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
// <cbcEncryptor reader="{TYPE}">
|
||||
// <encryption algorithm="{STRING}" provider="{STRING}" keyLength="{INT}" />
|
||||
// <validation algorithm="{STRING}" provider="{STRING}" />
|
||||
// <secret>...</secret>
|
||||
// </cbcEncryptor>
|
||||
|
||||
CryptoUtil.Assert(element.Name == CngCbcAuthenticatedEncryptorConfiguration.CbcEncryptorElementName,
|
||||
@"TODO: Bad element.");
|
||||
|
||||
var options = new CngCbcAuthenticatedEncryptorConfigurationOptions();
|
||||
|
||||
// read <encryption> 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 <validation> element
|
||||
var validationElement = element.Element(CngCbcAuthenticatedEncryptorConfiguration.ValidationElementName);
|
||||
options.HashAlgorithm = (string)validationElement.Attribute("algorithm");
|
||||
options.HashAlgorithmProvider = (string)validationElement.Attribute("provider");
|
||||
|
||||
// read the child of the <secret> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <secret> element.
|
||||
XElement secretElement;
|
||||
byte[] plaintextSecret = new byte[_secret.Length];
|
||||
try
|
||||
{
|
||||
_secret.WriteSecretIntoBuffer(new ArraySegment<byte>(plaintextSecret));
|
||||
secretElement = new XElement(SecretElementName, Convert.ToBase64String(plaintextSecret));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
|
||||
}
|
||||
|
||||
// Then encrypt it and wrap it in another <secret> element.
|
||||
var encryptedSecretElement = encryptor.Encrypt(secretElement);
|
||||
CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptedSecretElement.Attribute("decryptor")),
|
||||
@"TODO: <secret> encryption was invalid.");
|
||||
|
||||
return new XElement(SecretElementName, encryptedSecretElement);
|
||||
}
|
||||
|
||||
public XElement ToXml([NotNull] IXmlEncryptor xmlEncryptor)
|
||||
{
|
||||
// <cbcEncryptor reader="{TYPE}">
|
||||
// <encryption algorithm="{STRING}" provider="{STRING}" keyLength="{INT}" />
|
||||
// <secret>...</secret>
|
||||
// </cbcEncryptor>
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A factory that is able to create a CNG-based IAuthenticatedEncryptor
|
||||
/// using CBC encryption + HMAC validation.
|
||||
/// </summary>
|
||||
public unsafe sealed class CngGcmAuthenticatedEncryptorConfigurationFactory : IAuthenticatedEncryptorConfigurationFactory
|
||||
{
|
||||
private readonly CngGcmAuthenticatedEncryptorConfigurationOptions _options;
|
||||
|
||||
public CngGcmAuthenticatedEncryptorConfigurationFactory([NotNull] IOptionsAccessor<CngGcmAuthenticatedEncryptorConfigurationOptions> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for configuring an authenticated encryption mechanism which uses
|
||||
/// Windows CNG encryption algorithms in Galois/Counter Mode.
|
||||
/// </summary>
|
||||
public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the algorithm to use for symmetric encryption.
|
||||
/// This property corresponds to the 'pszAlgId' parameter of BCryptOpenAlgorithmProvider.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The algorithm must support GCM-style encryption and must have a block size of exactly 128 bits.
|
||||
/// The default value is 'AES'.
|
||||
/// </remarks>
|
||||
public string EncryptionAlgorithm { get; set; } = Constants.BCRYPT_AES_ALGORITHM;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the provider which contains the implementation of the symmetric encryption algorithm.
|
||||
/// This property corresponds to the 'pszImplementation' parameter of BCryptOpenAlgorithmProvider.
|
||||
/// This property is optional.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value is null.
|
||||
/// </remarks>
|
||||
public string EncryptionAlgorithmProvider { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The length (in bits) of the key that will be used for symmetric encryption.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key length must be 128 bits or greater.
|
||||
/// The default value is 256.
|
||||
/// </remarks>
|
||||
public int EncryptionAlgorithmKeySize { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a duplicate of this object, which allows the original object to remain mutable.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
// <cbcEncryptor reader="{TYPE}">
|
||||
// <encryption algorithm="{STRING}" provider="{STRING}" keyLength="{INT}" />
|
||||
// <secret>...</secret>
|
||||
// </cbcEncryptor>
|
||||
|
||||
CryptoUtil.Assert(element.Name == CngGcmAuthenticatedEncryptorConfiguration.GcmEncryptorElementName,
|
||||
@"TODO: Bad element.");
|
||||
|
||||
var options = new CngGcmAuthenticatedEncryptorConfigurationOptions();
|
||||
|
||||
// read <encryption> 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 <secret> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The basic interface for providing an authenticated encryption and decryption routine.
|
||||
/// </summary>
|
||||
public interface IAuthenticatedEncryptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the authentication tag of and decrypts a blob of encrypted data.
|
||||
/// </summary>
|
||||
/// <param name="ciphertext">The ciphertext (including authentication tag) to decrypt.</param>
|
||||
/// <param name="additionalAuthenticatedData">Any ancillary data which was used during computation
|
||||
/// of the authentication tag. The same AAD must have been specified in the corresponding
|
||||
/// call to 'Encrypt'.</param>
|
||||
/// <returns>The original plaintext data (if the authentication tag was validated and decryption succeeded).</returns>
|
||||
/// <remarks>All cryptography-related exceptions should be homogenized to CryptographicException.</remarks>
|
||||
byte[] Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData);
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts and tamper-proofs a piece of data.
|
||||
/// </summary>
|
||||
/// <param name="plaintext">The plaintext to encrypt. This input may be zero bytes in length.</param>
|
||||
/// <param name="additionalAuthenticatedData">A piece of data which will not be included in
|
||||
/// the returned ciphertext but which will still be covered by the authentication tag.
|
||||
/// This input may be zero bytes in length. The same AAD must be specified in the corresponding
|
||||
/// call to Decrypt.</param>
|
||||
/// <returns>The ciphertext blob, including authentication tag.</returns>
|
||||
/// <remarks>All cryptography-related exceptions should be homogenized to CryptographicException.</remarks>
|
||||
byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a type that contains configuration information about an IAuthenticatedEncryptor
|
||||
/// instance, including how to serialize it to XML.
|
||||
/// </summary>
|
||||
public interface IAuthenticatedEncryptorConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new IAuthenticatedEncryptor instance based on the current configuration.
|
||||
/// </summary>
|
||||
/// <returns>An IAuthenticatedEncryptor instance.</returns>
|
||||
IAuthenticatedEncryptor CreateEncryptorInstance();
|
||||
|
||||
/// <summary>
|
||||
/// Exports the current configuration to XML, optionally encrypting secret key material.
|
||||
/// </summary>
|
||||
/// <param name="xmlEncryptor">The XML encryptor used to encrypt secret material.</param>
|
||||
/// <returns>An XElement representing the current configuration object.</returns>
|
||||
XElement ToXml(IXmlEncryptor xmlEncryptor);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a type that can create new authenticated encryption configuration objects.
|
||||
/// </summary>
|
||||
public interface IAuthenticatedEncryptorConfigurationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new configuration object with fresh secret key material.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An IAuthenticatedEncryptorConfiguration instance.
|
||||
/// </returns>
|
||||
IAuthenticatedEncryptorConfiguration CreateNewConfiguration();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a type that can deserialize an XML-serialized IAuthenticatedEncryptorConfiguration.
|
||||
/// </summary>
|
||||
public interface IAuthenticatedEncryptorConfigurationXmlReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserializes an XML-serialized IAuthenticatedEncryptorConfiguration.
|
||||
/// </summary>
|
||||
/// <param name="element">The XML element to deserialize.</param>
|
||||
/// <returns>The deserialized IAuthenticatedEncryptorConfiguration.</returns>
|
||||
IAuthenticatedEncryptorConfiguration FromXml(XElement element);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <secret> element.
|
||||
XElement secretElement;
|
||||
byte[] plaintextSecret = new byte[_secret.Length];
|
||||
try
|
||||
{
|
||||
_secret.WriteSecretIntoBuffer(new ArraySegment<byte>(plaintextSecret));
|
||||
secretElement = new XElement(SecretElementName, Convert.ToBase64String(plaintextSecret));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(plaintextSecret, 0, plaintextSecret.Length);
|
||||
}
|
||||
|
||||
// Then encrypt it and wrap it in another <secret> element.
|
||||
var encryptedSecretElement = encryptor.Encrypt(secretElement);
|
||||
CryptoUtil.Assert(!String.IsNullOrEmpty((string)encryptedSecretElement.Attribute("decryptor")),
|
||||
@"TODO: <secret> encryption was invalid.");
|
||||
|
||||
return new XElement(SecretElementName, encryptedSecretElement);
|
||||
}
|
||||
|
||||
public XElement ToXml([NotNull] IXmlEncryptor xmlEncryptor)
|
||||
{
|
||||
// <managedEncryptor reader="{TYPE}">
|
||||
// <encryption type="{TYPE}" keyLength="{INT}" />
|
||||
// <validation type="{TYPE}" />
|
||||
// <secret>...</secret>
|
||||
// </managedEncryptor>
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ManagedAuthenticatedEncryptorConfigurationOptions> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for configuring an authenticated encryption mechanism which uses
|
||||
/// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
|
||||
/// </summary>
|
||||
public sealed class ManagedAuthenticatedEncryptorConfigurationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the algorithm to use for symmetric encryption.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The algorithm must support CBC-style encryption and PKCS#7 padding and must have a block size of 64 bits or greater.
|
||||
/// The default algorithm is AES.
|
||||
/// </remarks>
|
||||
public Type EncryptionAlgorithmType { get; set; } = typeof(Aes);
|
||||
|
||||
/// <summary>
|
||||
/// The length (in bits) of the key that will be used for symmetric encryption.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The key length must be 128 bits or greater.
|
||||
/// The default value is 256.
|
||||
/// </remarks>
|
||||
public int EncryptionAlgorithmKeySize { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// A factory for the algorithm to use for validation.
|
||||
/// This property is required to have a value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The algorithm must have a digest length of 128 bits or greater.
|
||||
/// The default algorithm is HMACSHA256.
|
||||
/// </remarks>
|
||||
public Type ValidationAlgorithmType { get; set; } = typeof(HMACSHA256);
|
||||
|
||||
/// <summary>
|
||||
/// Makes a duplicate of this object, which allows the original object to remain mutable.
|
||||
/// </summary>
|
||||
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<SymmetricAlgorithm> encryptorFactory = GetEncryptionAlgorithmFactory();
|
||||
Func<KeyedHashAlgorithm> 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<SymmetricAlgorithm> 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<SymmetricAlgorithm>)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(EncryptionAlgorithmType))).Creator;
|
||||
}
|
||||
}
|
||||
|
||||
private Func<KeyedHashAlgorithm> 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<KeyedHashAlgorithm>)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator;
|
||||
}
|
||||
|
||||
private interface IActivator<out T>
|
||||
{
|
||||
Func<T> Creator { get; }
|
||||
}
|
||||
|
||||
private class AlgorithmActivator<T> : IActivator<T> where T : new()
|
||||
{
|
||||
public Func<T> Creator { get; } = Activator.CreateInstance<T>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
// <managedEncryptor reader="{TYPE}">
|
||||
// <encryption type="{STRING}" keyLength="{INT}" />
|
||||
// <validation type="{STRING}" />
|
||||
// <secret>...</secret>
|
||||
// </managedEncryptor>
|
||||
|
||||
CryptoUtil.Assert(element.Name == ManagedAuthenticatedEncryptorConfiguration.EncryptionElementName,
|
||||
@"TODO: Bad element.");
|
||||
|
||||
var options = new ManagedAuthenticatedEncryptorConfigurationOptions();
|
||||
|
||||
// read <encryption> element
|
||||
var encryptionElement = element.Element(ManagedAuthenticatedEncryptorConfiguration.EncryptionElementName);
|
||||
options.EncryptionAlgorithmType = Type.GetType((string)encryptionElement.Attribute("type"), throwOnError: true);
|
||||
options.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength");
|
||||
|
||||
// read <validation> element
|
||||
var validationElement = element.Element(ManagedAuthenticatedEncryptorConfiguration.ValidationElementName);
|
||||
options.ValidationAlgorithmType = Type.GetType((string)validationElement.Attribute("type"), throwOnError: true);
|
||||
|
||||
// read the child of the <secret> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes an unsigned 32-bit value to a memory address, big-endian.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteTo(void* ptr, uint value)
|
||||
{
|
||||
byte* bytePtr = (byte*)ptr;
|
||||
bytePtr[0] = (byte)(value >> 24);
|
||||
bytePtr[1] = (byte)(value >> 16);
|
||||
bytePtr[2] = (byte)(value >> 8);
|
||||
bytePtr[3] = (byte)(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a signed 32-bit value to a memory address, big-endian.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteTo(byte[] buffer, ref int idx, int value)
|
||||
{
|
||||
WriteTo(buffer, ref idx, (uint)value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a signed 32-bit value to a memory address, big-endian.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteTo(byte[] buffer, ref int idx, uint value)
|
||||
{
|
||||
buffer[idx++] = (byte)(value >> 24);
|
||||
buffer[idx++] = (byte)(value >> 16);
|
||||
buffer[idx++] = (byte)(value >> 8);
|
||||
buffer[idx++] = (byte)(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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]
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides cached CNG algorithm provider instances, as calling BCryptOpenAlgorithmProvider is expensive.
|
||||
/// Callers should use caution never to dispose of the algorithm provider instances returned by this type.
|
||||
/// </summary>
|
||||
internal static class CachedAlgorithmHandles
|
||||
{
|
||||
private static CachedAlgorithmInfo _aesCbc = new CachedAlgorithmInfo(() => GetAesAlgorithm(chainingMode: Constants.BCRYPT_CHAIN_MODE_CBC));
|
||||
private static CachedAlgorithmInfo _aesGcm = new CachedAlgorithmInfo(() => GetAesAlgorithm(chainingMode: Constants.BCRYPT_CHAIN_MODE_GCM));
|
||||
private static CachedAlgorithmInfo _hmacSha1 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA1_ALGORITHM));
|
||||
private static CachedAlgorithmInfo _hmacSha256 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA256_ALGORITHM));
|
||||
private static CachedAlgorithmInfo _hmacSha512 = new CachedAlgorithmInfo(() => GetHmacAlgorithm(algorithm: Constants.BCRYPT_SHA512_ALGORITHM));
|
||||
private static CachedAlgorithmInfo _pbkdf2 = new CachedAlgorithmInfo(GetPbkdf2Algorithm);
|
||||
private static CachedAlgorithmInfo _sha1 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA1_ALGORITHM));
|
||||
private static CachedAlgorithmInfo _sha256 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA256_ALGORITHM));
|
||||
private static CachedAlgorithmInfo _sha512 = new CachedAlgorithmInfo(() => GetHashAlgorithm(algorithm: Constants.BCRYPT_SHA512_ALGORITHM));
|
||||
private static CachedAlgorithmInfo _sp800_108_ctr_hmac = new CachedAlgorithmInfo(GetSP800_108_CTR_HMACAlgorithm);
|
||||
|
||||
public static BCryptAlgorithmHandle AES_CBC
|
||||
{
|
||||
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<BCryptAlgorithmHandle> _algorithmHandle;
|
||||
private readonly Func<BCryptAlgorithmHandle> _factory;
|
||||
|
||||
public CachedAlgorithmInfo(Func<BCryptAlgorithmHandle> factory)
|
||||
{
|
||||
_algorithmHandle = null;
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
public static BCryptAlgorithmHandle GetAlgorithmHandle(ref CachedAlgorithmInfo cachedAlgorithmInfo)
|
||||
{
|
||||
return WeakReferenceHelpers.GetSharedInstance(ref cachedAlgorithmInfo._algorithmHandle, cachedAlgorithmInfo._factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
|
||||
{
|
||||
// This wrapper simply converts ArraySegment<byte> to byte* and calls the impl method.
|
||||
|
||||
// Input validation
|
||||
ciphertext.Validate();
|
||||
additionalAuthenticatedData.Validate();
|
||||
|
||||
byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer
|
||||
fixed (byte* pbCiphertextArray = ciphertext.Array)
|
||||
{
|
||||
fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DecryptImpl(
|
||||
pbCiphertext: (pbCiphertextArray != null) ? &pbCiphertextArray[ciphertext.Offset] : &dummy,
|
||||
cbCiphertext: (uint)ciphertext.Count,
|
||||
pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy,
|
||||
cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count);
|
||||
}
|
||||
catch (Exception ex) 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<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
|
||||
{
|
||||
return Encrypt(plaintext, additionalAuthenticatedData, 0, 0);
|
||||
}
|
||||
|
||||
public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData, uint preBufferSize, uint postBufferSize)
|
||||
{
|
||||
// This wrapper simply converts ArraySegment<byte> to byte* and calls the impl method.
|
||||
|
||||
// Input validation
|
||||
plaintext.Validate();
|
||||
additionalAuthenticatedData.Validate();
|
||||
|
||||
byte dummy; // used only if plaintext or AAD is empty, since otherwise 'fixed' returns null pointer
|
||||
fixed (byte* pbPlaintextArray = plaintext.Array)
|
||||
{
|
||||
fixed (byte* pbAdditionalAuthenticatedDataArray = additionalAuthenticatedData.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return EncryptImpl(
|
||||
pbPlaintext: (pbPlaintextArray != null) ? &pbPlaintextArray[plaintext.Offset] : &dummy,
|
||||
cbPlaintext: (uint)plaintext.Count,
|
||||
pbAdditionalAuthenticatedData: (pbAdditionalAuthenticatedDataArray != null) ? &pbAdditionalAuthenticatedDataArray[additionalAuthenticatedData.Offset] : &dummy,
|
||||
cbAdditionalAuthenticatedData: (uint)additionalAuthenticatedData.Count,
|
||||
cbPreBuffer: preBufferSize,
|
||||
cbPostBuffer: postBufferSize);
|
||||
}
|
||||
catch (Exception ex) 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte>(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<byte>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to populate buffers with cryptographically random data.
|
||||
/// </summary>
|
||||
public static class CryptRand
|
||||
{
|
||||
/// <summary>
|
||||
/// Populates a buffer with cryptographically random data.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to populate.</param>
|
||||
public static unsafe void FillBuffer(ArraySegment<byte> buffer)
|
||||
{
|
||||
// the ArraySegment<> ctor performs bounds checking
|
||||
var unused = new ArraySegment<byte>(buffer.Array, buffer.Offset, buffer.Count);
|
||||
|
||||
if (buffer.Count != 0)
|
||||
{
|
||||
fixed (byte* pBuffer = &buffer.Array[buffer.Offset])
|
||||
{
|
||||
BCryptUtil.GenRandom(pBuffer, buffer.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for creating IDataProtectionProvider instances.
|
||||
/// </summary>
|
||||
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");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IDataProtectionProvider backed by DPAPI, where the protected
|
||||
/// payload can only be decrypted by the current user.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IDataProtectionProvider backed by DPAPI.
|
||||
/// </summary>
|
||||
/// <param name="protectToLocalMachine">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.</param>
|
||||
public static IDataProtectionProvider CreateFromDpapi(bool protectToLocalMachine)
|
||||
{
|
||||
return new DpapiDataProtectionProviderImpl(MASTER_SUBKEY_GENERATOR, protectToLocalMachine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IDataProtectionProvider with a randomly-generated master key.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IDataProtectionProvider with the provided master key.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IServiceDescriptor> GetDefaultServices()
|
||||
{
|
||||
return GetDefaultServices(new Configuration());
|
||||
}
|
||||
|
||||
public static IEnumerable<IServiceDescriptor> GetDefaultServices(IConfiguration configuration)
|
||||
{
|
||||
var describe = new ServiceDescriber(configuration);
|
||||
|
||||
List<IServiceDescriptor> descriptors = new List<IServiceDescriptor>();
|
||||
descriptors.AddRange(OptionsServices.GetDefaultServices(configuration));
|
||||
descriptors.AddRange(OSVersionUtil.IsBCryptOnWin7OrLaterAvailable()
|
||||
? GetDefaultServicesWindows(describe)
|
||||
: GetDefaultServicesNonWindows(describe));
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
private static IEnumerable<IServiceDescriptor> 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<IDataProtectionProvider>(new DpapiDataProtectionProvider(DataProtectionScope.CurrentUser))
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<IServiceDescriptor> GetDefaultServicesWindows(ServiceDescriber describe)
|
||||
{
|
||||
List<ServiceDescriptor> descriptors = new List<ServiceDescriptor>();
|
||||
|
||||
// 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<IXmlEncryptor,NullXmlEncryptor>(),
|
||||
describe.Instance<IXmlRepository>(new FileSystemXmlRepository(azureWebSitesKeysFolder))
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Are we running with the user profile loaded?
|
||||
DirectoryInfo localAppDataKeysFolder = TryGetLocalAppDataKeysFolderForUser();
|
||||
if (localAppDataKeysFolder != null)
|
||||
{
|
||||
descriptors.AddRange(new[]
|
||||
{
|
||||
describe.Singleton<IXmlEncryptor, DpapiXmlEncryptor>(),
|
||||
describe.Instance<IXmlRepository>(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<IDataProtectionProvider>(new DpapiDataProtectionProvider(DataProtectionScope.LocalMachine))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// We use CNG CBC + HMAC by default.
|
||||
descriptors.AddRange(new[]
|
||||
{
|
||||
describe.Singleton<IAuthenticatedEncryptorConfigurationFactory, CngCbcAuthenticatedEncryptorConfigurationFactory>(),
|
||||
describe.Singleton<ITypeActivator, TypeActivator>(),
|
||||
describe.Singleton<IKeyManager, XmlKeyManager>(),
|
||||
describe.Singleton<IDataProtectionProvider, DefaultDataProtectionProvider>()
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DataProtectionOptions> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte[]>("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<byte[]>("Null return value.");
|
||||
}
|
||||
catch (Exception ex) if (!(ex is CryptographicException))
|
||||
{
|
||||
// Homogenize to CryptographicException
|
||||
throw Error.CryptCommon_GenericError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte>(retVal));
|
||||
return retVal;
|
||||
}
|
||||
#else
|
||||
return ProtectedData.Unprotect(encryptedData, optionalEntropy, scope);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An IDataProtectionProvider that is transient.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<T> : IOptionsAccessor<T> 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<CngGcmAuthenticatedEncryptorConfigurationOptions>()).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<ManagedAuthenticatedEncryptorConfigurationOptions>()).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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A factory that can provide IDataProtector instances.
|
||||
/// An interface that can be used to create IDataProtector instances.
|
||||
/// </summary>
|
||||
public interface IDataProtectionProvider : IDisposable
|
||||
public interface IDataProtectionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a purpose, returns a new IDataProtector that has unique cryptographic keys tied to this purpose.
|
||||
/// Creates an IDataProtector given a purpose.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The consumer of the IDataProtector.</param>
|
||||
/// <returns>An IDataProtector.</returns>
|
||||
/// <param name="purposes">
|
||||
/// 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.
|
||||
/// </param>
|
||||
/// <returns>An IDataProtector tied to the provided purpose.</returns>
|
||||
IDataProtector CreateProtector(string purpose);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an object that can perform cryptographic operations.
|
||||
/// An interface that can provide data protection services.
|
||||
/// </summary>
|
||||
public interface IDataProtector : IDisposable
|
||||
public interface IDataProtector : IDataProtectionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a subpurpose, returns a new IDataProtector that has unique cryptographic keys tied <em>both</em> the purpose
|
||||
/// that was used to create this IDataProtector instance <em>and</em> the purpose that is provided as a parameter
|
||||
/// to this method.
|
||||
/// Cryptographically protects a piece of plaintext data.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The sub-consumer of the IDataProtector.</param>
|
||||
/// <returns>An IDataProtector.</returns>
|
||||
IDataProtector CreateSubProtector(string purpose);
|
||||
|
||||
/// <summary>
|
||||
/// Cryptographically protects some input data.
|
||||
/// </summary>
|
||||
/// <param name="unprotectedData">The data to be protected.</param>
|
||||
/// <returns>An array containing cryptographically protected data.</returns>
|
||||
/// <remarks>To retrieve the original data, call Unprotect on the protected data.</remarks>
|
||||
/// <param name="unprotectedData">The plaintext data to protect.</param>
|
||||
/// <returns>The protected form of the plaintext data.</returns>
|
||||
byte[] Protect(byte[] unprotectedData);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the original data that was protected by a call to Protect.
|
||||
/// Cryptographically unprotects a piece of protected data.
|
||||
/// </summary>
|
||||
/// <param name="protectedData">The protected data to be decrypted.</param>
|
||||
/// <returns>The original data.</returns>
|
||||
/// <remarks>Throws CryptographicException if the <em>protectedData</em> parameter has been tampered with.</remarks>
|
||||
/// <param name="protectedData">The protected data to unprotect.</param>
|
||||
/// <returns>The plaintext form of the protected data.</returns>
|
||||
/// <remarks>
|
||||
/// Implementations should throw CryptographicException if the protected data is
|
||||
/// invalid or malformed.
|
||||
/// </remarks>
|
||||
byte[] Unprotect(byte[] protectedData);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a secret value.
|
||||
/// </summary>
|
||||
public interface ISecret : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The length (in bytes) of the value.
|
||||
/// </summary>
|
||||
int Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Writes the secret value to the specified buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer which should receive the secret value.</param>
|
||||
/// <remarks>
|
||||
/// The buffer size must exactly match the length of the secret value.
|
||||
/// </remarks>
|
||||
void WriteSecretIntoBuffer(ArraySegment<byte> buffer);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Security.DataProtection.Cng
|
||||
namespace Microsoft.AspNet.Security.DataProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the PRF which should be used for the key derivation algorithm.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The basic interface for representing an authenticated encryption key.
|
||||
/// </summary>
|
||||
public interface IKey
|
||||
{
|
||||
/// <summary>
|
||||
/// The date at which encryptions with this key can begin taking place.
|
||||
/// </summary>
|
||||
DateTimeOffset ActivationDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The date on which this key was created.
|
||||
/// </summary>
|
||||
DateTimeOffset CreationDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The date after which encryptions with this key may no longer take place.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An expired key may still be used to decrypt existing payloads.
|
||||
/// </remarks>
|
||||
DateTimeOffset ExpirationDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value stating whether this key was revoked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A revoked key may still be used to decrypt existing payloads, but the payloads
|
||||
/// must be treated as potentially unauthentic unless the application has some
|
||||
/// other assurance that the payloads are authentic.
|
||||
/// </remarks>
|
||||
bool IsRevoked { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the key.
|
||||
/// </summary>
|
||||
Guid KeyId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an IAuthenticatedEncryptor instance that can be used to encrypt data
|
||||
/// to and decrypt data from this key.
|
||||
/// </summary>
|
||||
/// <returns>An IAuthenticatedEncryptor.</returns>
|
||||
IAuthenticatedEncryptor CreateEncryptorInstance();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The basic interface for performing key management operations.
|
||||
/// </summary>
|
||||
public interface IKeyManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new key with the specified activation and expiration dates.
|
||||
/// </summary>
|
||||
/// <param name="activationDate">The date on which encryptions to this key may begin.</param>
|
||||
/// <param name="expirationDate">The date after which encryptions to this key may no longer take place.</param>
|
||||
/// <returns>The newly-created IKey instance.</returns>
|
||||
/// <remarks>
|
||||
/// This method also persists the newly-created IKey instance to the underlying repository.
|
||||
/// </remarks>
|
||||
IKey CreateNewKey(DateTimeOffset activationDate, DateTimeOffset expirationDate);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all keys from the underlying repository.
|
||||
/// </summary>
|
||||
/// <returns>The collection of all keys.</returns>
|
||||
IReadOnlyCollection<IKey> GetAllKeys();
|
||||
|
||||
/// <summary>
|
||||
/// Revokes a specific key.
|
||||
/// </summary>
|
||||
/// <param name="keyId">The id of the key to revoke.</param>
|
||||
/// <param name="reason">An optional human-readable reason for revocation.</param>
|
||||
/// <remarks>
|
||||
/// This method will not mutate existing IKey instances. After calling this method,
|
||||
/// all existing IKey instances should be discarded, and GetAllKeys should be called again.
|
||||
/// </remarks>
|
||||
void RevokeKey(Guid keyId, string reason = null);
|
||||
|
||||
/// <summary>
|
||||
/// Revokes all keys created before a specified date.
|
||||
/// </summary>
|
||||
/// <param name="revocationDate">The revocation date. All keys with a creation date before
|
||||
/// this value will be revoked.</param>
|
||||
/// <param name="reason">An optional human-readable reason for revocation.</param>
|
||||
/// <remarks>
|
||||
/// This method will not mutate existing IKey instances. After calling this method,
|
||||
/// all existing IKey instances should be discarded, and GetAllKeys should be called again.
|
||||
/// </remarks>
|
||||
void RevokeAllKeys(DateTimeOffset revocationDate, string reason = null);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Guid, AuthenticatedEncryptorHolder> _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<Guid, AuthenticatedEncryptorHolder> CreateEncryptorMap(Guid defaultKeyId, IKey[] keys, out AuthenticatedEncryptorHolder defaultEncryptorHolder)
|
||||
{
|
||||
defaultEncryptorHolder = null;
|
||||
|
||||
var encryptorMap = new Dictionary<Guid, AuthenticatedEncryptorHolder>(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<T> because we don't have a simple value factory
|
||||
IAuthenticatedEncryptor encryptor = Volatile.Read(ref _encryptor);
|
||||
if (encryptor == null)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
encryptor = Volatile.Read(ref _encryptor);
|
||||
if (encryptor == null)
|
||||
{
|
||||
encryptor = _key.CreateEncryptorInstance();
|
||||
Volatile.Write(ref _encryptor, encryptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
isRevoked = _key.IsRevoked;
|
||||
return encryptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte>(unprotectedData),
|
||||
additionalAuthenticatedData: new ArraySegment<byte>(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<byte> ciphertext = new ArraySegment<byte>(protectedData, sizeof(uint) + sizeof(Guid), protectedData.Length - (sizeof(uint) + sizeof(Guid))); // chop off magic header + encryptor id
|
||||
ArraySegment<byte> additionalAuthenticatedData = new ArraySegment<byte>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
// <key id="{GUID}" version="1" xmlns="{XMLNS}">
|
||||
// <creationDate>...</creationDate>
|
||||
// <activationDate>...</activationDate>
|
||||
// <expirationDate>...</expirationDate>
|
||||
// <authenticatedEncryptor>
|
||||
// <... parser="{TYPE}" />
|
||||
// </authenticatedEncryptor>
|
||||
// </xxx:key>
|
||||
|
||||
// Create the <xxx:authenticatedEncryptor /> 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 <xxx:key /> element.
|
||||
var keyElement = new XElement(KeyElementName,
|
||||
new XAttribute(IdAttributeName, keyId),
|
||||
new XAttribute(VersionAttributeName, 1),
|
||||
new XElement(CreationDateElementName, creationDate),
|
||||
new XElement(ActivationDateElementName, activationDate),
|
||||
new XElement(ExpirationDateElementName, expirationDate),
|
||||
new XElement(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<IKey> GetAllKeys()
|
||||
{
|
||||
var allElements = _xmlRepository.GetAllElements();
|
||||
|
||||
Dictionary<Guid, Key> idToKeyMap = new Dictionary<Guid, Key>();
|
||||
HashSet<Guid> 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<Guid>();
|
||||
}
|
||||
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)
|
||||
{
|
||||
// <revocation version="1" xmlns="{XMLNS}">
|
||||
// <revocationDate>...</revocationDate>
|
||||
// <key id="*" />
|
||||
// <reason>...</reason>
|
||||
// </revocation>
|
||||
|
||||
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)
|
||||
{
|
||||
// <revocation version="1" xmlns="{XMLNS}">
|
||||
// <revocationDate>...</revocationDate>
|
||||
// <key id="{GUID}" />
|
||||
// <reason>...</reason>
|
||||
// </revocation>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte[], HashAlgorithm> _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
|
||||
|
||||
private readonly byte[] _contextHeader;
|
||||
private readonly IManagedGenRandom _genRandom;
|
||||
private readonly ProtectedMemoryBlob _keyDerivationKey;
|
||||
private readonly Func<SymmetricAlgorithm> _symmetricAlgorithmFactory;
|
||||
private readonly int _symmetricAlgorithmBlockSizeInBytes;
|
||||
private readonly int _symmetricAlgorithmSubkeyLengthInBytes;
|
||||
private readonly int _validationAlgorithmDigestLengthInBytes;
|
||||
private readonly int _validationAlgorithmSubkeyLengthInBytes;
|
||||
private readonly Func<KeyedHashAlgorithm> _validationAlgorithmFactory;
|
||||
|
||||
public ManagedAuthenticatedEncryptor(ProtectedMemoryBlob keyDerivationKey, Func<SymmetricAlgorithm> symmetricAlgorithmFactory, int symmetricAlgorithmKeySizeInBytes, Func<KeyedHashAlgorithm> 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<byte>(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<byte>(tempKeys));
|
||||
|
||||
// At this point, tempKeys := { K_E || K_H }.
|
||||
|
||||
// Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer.
|
||||
using (var symmetricAlg = CreateSymmetricAlgorithm())
|
||||
{
|
||||
using (var cryptoTransform = symmetricAlg.CreateEncryptor(
|
||||
rgbKey: new ArraySegment<byte>(tempKeys, 0, _symmetricAlgorithmSubkeyLengthInBytes).AsStandaloneArray(),
|
||||
rgbIV: new byte[_symmetricAlgorithmBlockSizeInBytes]))
|
||||
{
|
||||
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<byte>(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<byte> protectedPayload, ArraySegment<byte> additionalAuthenticatedData)
|
||||
{
|
||||
protectedPayload.Validate();
|
||||
additionalAuthenticatedData.Validate();
|
||||
|
||||
// Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC
|
||||
if (protectedPayload.Count < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes))
|
||||
{
|
||||
throw Error.CryptCommon_PayloadInvalid();
|
||||
}
|
||||
|
||||
// Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: Extract the key modifier and IV from the payload.
|
||||
|
||||
int keyModifierOffset; // position in protectedPayload.Array where key modifier begins
|
||||
int ivOffset; // position in protectedPayload.Array where key modifier ends / IV begins
|
||||
int ciphertextOffset; // position in protectedPayload.Array where IV ends / ciphertext begins
|
||||
int macOffset; // position in protectedPayload.Array where ciphertext ends / MAC begins
|
||||
int eofOffset; // position in protectedPayload.Array where MAC ends
|
||||
|
||||
checked
|
||||
{
|
||||
keyModifierOffset = protectedPayload.Offset;
|
||||
ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
|
||||
ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes;
|
||||
}
|
||||
|
||||
ArraySegment<byte> keyModifier = new ArraySegment<byte>(protectedPayload.Array, keyModifierOffset, ivOffset - keyModifierOffset);
|
||||
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<byte>(decryptedKdk));
|
||||
DeriveKeysWithContextHeader(
|
||||
kdk: decryptedKdk,
|
||||
label: additionalAuthenticatedData,
|
||||
contextHeader: _contextHeader,
|
||||
context: keyModifier,
|
||||
prfFactory: _kdkPrfFactory,
|
||||
output: new ArraySegment<byte>(derivedKeysBuffer));
|
||||
|
||||
Buffer.BlockCopy(derivedKeysBuffer, 0, decryptionSubkey, 0, decryptionSubkey.Length);
|
||||
Buffer.BlockCopy(derivedKeysBuffer, decryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
|
||||
|
||||
// Step 3: Calculate the correct MAC for this payload.
|
||||
// correctHash := MAC(IV || ciphertext)
|
||||
byte[] correctHash;
|
||||
|
||||
using (var hashAlgorithm = CreateValidationAlgorithm(validationSubkey))
|
||||
{
|
||||
checked
|
||||
{
|
||||
eofOffset = protectedPayload.Offset + protectedPayload.Count;
|
||||
macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes;
|
||||
}
|
||||
|
||||
correctHash = hashAlgorithm.ComputeHash(protectedPayload.Array, ivOffset, macOffset - ivOffset);
|
||||
}
|
||||
|
||||
// Step 4: Validate the MAC provided as part of the payload.
|
||||
|
||||
if (!CryptoUtil.TimeConstantBuffersAreEqual(correctHash, 0, correctHash.Length, protectedPayload.Array, macOffset, eofOffset - macOffset))
|
||||
{
|
||||
throw Error.CryptCommon_PayloadInvalid(); // integrity check failure
|
||||
}
|
||||
|
||||
// Step 5: Decipher the ciphertext and return it to the caller.
|
||||
|
||||
using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
|
||||
using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv))
|
||||
{
|
||||
var outputStream = new MemoryStream();
|
||||
using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write))
|
||||
{
|
||||
cryptoStream.Write(protectedPayload.Array, ciphertextOffset, macOffset - ciphertextOffset);
|
||||
cryptoStream.FlushFinalBlock();
|
||||
|
||||
// At this point, outputStream := { plaintext }, and we're done!
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 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<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> 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<byte>(combinedContext), prfFactory, output);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_keyDerivationKey.Dispose();
|
||||
}
|
||||
|
||||
public byte[] Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData)
|
||||
{
|
||||
plaintext.Validate();
|
||||
additionalAuthenticatedData.Validate();
|
||||
|
||||
try
|
||||
{
|
||||
var outputStream = new MemoryStream();
|
||||
|
||||
// Step 1: Generate a random key modifier and IV for this operation.
|
||||
// Both will be equal to the block size of the block cipher algorithm.
|
||||
|
||||
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<byte>(decryptedKdk));
|
||||
DeriveKeysWithContextHeader(
|
||||
kdk: decryptedKdk,
|
||||
label: additionalAuthenticatedData,
|
||||
contextHeader: _contextHeader,
|
||||
context: new ArraySegment<byte>(keyModifier),
|
||||
prfFactory: _kdkPrfFactory,
|
||||
output: new ArraySegment<byte>(derivedKeysBuffer));
|
||||
|
||||
Buffer.BlockCopy(derivedKeysBuffer, 0, encryptionSubkey, 0, encryptionSubkey.Length);
|
||||
Buffer.BlockCopy(derivedKeysBuffer, encryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
|
||||
|
||||
// Step 4: Perform the encryption operation.
|
||||
|
||||
using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
|
||||
using (var cryptoTransform = symmetricAlgorithm.CreateEncryptor(encryptionSubkey, iv))
|
||||
using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write))
|
||||
{
|
||||
cryptoStream.Write(plaintext.Array, plaintext.Offset, plaintext.Count);
|
||||
cryptoStream.FlushFinalBlock();
|
||||
|
||||
// At this point, outputStream := { keyModifier || IV || ciphertext }
|
||||
|
||||
// Step 5: Calculate the digest over the IV and ciphertext.
|
||||
// We don't need to calculate the digest over the key modifier since that
|
||||
// value has already been mixed into the KDF used to generate the MAC key.
|
||||
|
||||
using (var validationAlgorithm = CreateValidationAlgorithm(validationSubkey))
|
||||
{
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Support for generating random data.
|
||||
/// </summary>
|
||||
internal unsafe static class MemoryProtection
|
||||
{
|
||||
// from dpapi.h
|
||||
private const uint CRYPTPROTECTMEMORY_SAME_PROCESS = 0x00;
|
||||
|
||||
public static void CryptProtectMemory(SafeHandle pBuffer, uint byteCount)
|
||||
{
|
||||
if (!UnsafeNativeMethods.CryptProtectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS))
|
||||
{
|
||||
UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error();
|
||||
}
|
||||
}
|
||||
|
||||
public static void CryptUnprotectMemory(byte* pBuffer, uint byteCount)
|
||||
{
|
||||
if (!UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS))
|
||||
{
|
||||
UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error();
|
||||
}
|
||||
}
|
||||
|
||||
public static void CryptUnprotectMemory(SafeHandle pBuffer, uint byteCount)
|
||||
{
|
||||
if (!UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, byteCount, CRYPTPROTECTMEMORY_SAME_PROCESS))
|
||||
{
|
||||
UnsafeNativeMethods.ThrowExceptionForLastCrypt32Error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue