//-----------------------------------------------------------------------
//
// Copyright (c) Microsoft. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Security;
///
/// Provides helpers that mimic the ASP.NET 4.5 MachineKey.Protect / Unprotect APIs,
/// even when running on ASP.NET 4.0. Consumers are expected to follow the same
/// conventions used by the MachineKey.Protect / Unprotect APIs (consult MSDN docs
/// for how these are meant to be used). Additionally, since this helper class
/// dynamically switches between the two based on whether the current application is
/// .NET 4.0 or 4.5, consumers should never persist output from the Protect method
/// since the implementation will change when upgrading 4.0 -> 4.5. This should be
/// used for transient data only.
///
internal static class MachineKeyUtil {
///
/// MachineKey implementation depending on the target .NET framework version
///
private static readonly IMachineKey MachineKeyImpl = GetMachineKeyImpl();
///
/// ProtectUnprotect delegate.
///
/// The data.
/// The purposes.
/// Result of either Protect or Unprotect methods.
private delegate byte[] ProtectUnprotect(byte[] data, string[] purposes);
///
/// Abstract the MachineKey implementation in .NET 4.0 and 4.5
///
private interface IMachineKey {
///
/// Protects the specified user data.
///
/// The user data.
/// The purposes.
/// The protected data.
byte[] Protect(byte[] userData, string[] purposes);
///
/// Unprotects the specified protected data.
///
/// The protected data.
/// The purposes.
/// The unprotected data.
byte[] Unprotect(byte[] protectedData, string[] purposes);
}
///
/// Protects the specified user data.
///
/// The user data.
/// The purposes.
/// The encrypted data
public static byte[] Protect(byte[] userData, params string[] purposes) {
return MachineKeyImpl.Protect(userData, purposes);
}
///
/// Unprotects the specified protected data.
///
/// The protected data.
/// The purposes.
/// The unencrypted data
public static byte[] Unprotect(byte[] protectedData, params string[] purposes) {
return MachineKeyImpl.Unprotect(protectedData, purposes);
}
///
/// Gets the machine key implementation based on the runtime framework version.
///
/// The machine key implementation
private static IMachineKey GetMachineKeyImpl() {
// Late bind to the MachineKey.Protect / Unprotect methods only if .
// This helps ensure that round-tripping the payloads continues to work even if the application is
// deployed to a mixed 4.0 / 4.5 farm environment.
PropertyInfo targetFrameworkProperty = typeof(HttpRuntime).GetProperty("TargetFramework", typeof(Version));
Version targetFramework = (targetFrameworkProperty != null) ? targetFrameworkProperty.GetValue(null, null) as Version : null;
if (targetFramework != null && targetFramework >= new Version(4, 5)) {
ProtectUnprotect protectThunk = (ProtectUnprotect)Delegate.CreateDelegate(typeof(ProtectUnprotect), typeof(MachineKey), "Protect", ignoreCase: false, throwOnBindFailure: false);
ProtectUnprotect unprotectThunk = (ProtectUnprotect)Delegate.CreateDelegate(typeof(ProtectUnprotect), typeof(MachineKey), "Unprotect", ignoreCase: false, throwOnBindFailure: false);
if (protectThunk != null && unprotectThunk != null) {
return new MachineKey45(protectThunk, unprotectThunk); // ASP.NET 4.5
}
}
return new MachineKey40(); // ASP.NET 4.0
}
///
/// On ASP.NET 4.0, we perform some transforms which mimic the behaviors of MachineKey.Protect
/// and Unprotect.
///
private sealed class MachineKey40 : IMachineKey {
///
/// This is the magic header that identifies a MachineKey40 payload.
/// It helps differentiate this from other encrypted payloads.
private const uint MagicHeader = 0x8519140c;
///
/// The SHA-256 factory to be used.
///
private static readonly Func sha256Factory = GetSHA256Factory();
///
/// Protects the specified user data.
///
/// The user data.
/// The purposes.
/// The protected data
public byte[] Protect(byte[] userData, string[] purposes) {
if (userData == null) {
throw new ArgumentNullException("userData");
}
// dataWithHeader = {magic header} .. {purposes} .. {userData}
byte[] dataWithHeader = new byte[checked(4 /* magic header */ + (256 / 8) /* purposes */ + userData.Length)];
unchecked {
dataWithHeader[0] = (byte)(MagicHeader >> 24);
dataWithHeader[1] = (byte)(MagicHeader >> 16);
dataWithHeader[2] = (byte)(MagicHeader >> 8);
dataWithHeader[3] = (byte)MagicHeader;
}
byte[] purposeHash = ComputeSHA256(purposes);
Buffer.BlockCopy(purposeHash, 0, dataWithHeader, 4, purposeHash.Length);
Buffer.BlockCopy(userData, 0, dataWithHeader, 4 + (256 / 8), userData.Length);
// encrypt + sign
string hexValue = MachineKey.Encode(dataWithHeader, MachineKeyProtection.All);
// convert hex -> binary
byte[] binary = HexToBinary(hexValue);
return binary;
}
///
/// Unprotects the specified protected data.
///
/// The protected data.
/// The purposes.
/// The unprotected data
public byte[] Unprotect(byte[] protectedData, string[] purposes) {
if (protectedData == null) {
throw new ArgumentNullException("protectedData");
}
// convert binary -> hex and calculate what the purpose should read
string hexEncodedData = BinaryToHex(protectedData);
byte[] purposeHash = ComputeSHA256(purposes);
try {
// decrypt / verify signature
byte[] dataWithHeader = MachineKey.Decode(hexEncodedData, MachineKeyProtection.All);
// validate magic header and purpose string
if (dataWithHeader != null
&& dataWithHeader.Length >= (4 + (256 / 8))
&& (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(dataWithHeader, 0)) == MagicHeader
&& AreByteArraysEqual(new ArraySegment(purposeHash), new ArraySegment(dataWithHeader, 4, 256 / 8))) {
// validation succeeded
byte[] userData = new byte[dataWithHeader.Length - 4 - (256 / 8)];
Buffer.BlockCopy(dataWithHeader, 4 + (256 / 8), userData, 0, userData.Length);
return userData;
}
}
catch {
// swallow since will be rethrown immediately below
}
// if we reached this point, some cryptographic operation failed
throw new CryptographicException(Strings.Generic_CryptoFailure);
}
///
/// Convert bytes to hex string.
///
/// The input array.
/// Hex string
internal static string BinaryToHex(byte[] binary) {
StringBuilder builder = new StringBuilder(checked(binary.Length * 2));
for (int i = 0; i < binary.Length; i++) {
byte b = binary[i];
builder.Append(HexDigit(b >> 4));
builder.Append(HexDigit(b & 0x0F));
}
string result = builder.ToString();
return result;
}
///
/// This method is specially written to take the same amount of time
/// regardless of where 'a' and 'b' differ. Please do not optimize it.
/// first array.
/// second array.
/// if equal, others
private static bool AreByteArraysEqual(ArraySegment a, ArraySegment b) {
if (a.Count != b.Count) {
return false;
}
bool areEqual = true;
for (int i = 0; i < a.Count; i++) {
areEqual &= a.Array[a.Offset + i] == b.Array[b.Offset + i];
}
return areEqual;
}
///
/// Computes a SHA256 hash over all of the input parameters.
/// Each parameter is UTF8 encoded and preceded by a 7-bit encoded
/// integer describing the encoded byte length of the string.
/// The parameters.
/// The output hash
[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")]
private static byte[] ComputeSHA256(IList parameters) {
using (MemoryStream ms = new MemoryStream()) {
using (BinaryWriter bw = new BinaryWriter(ms)) {
if (parameters != null) {
foreach (string parameter in parameters) {
bw.Write(parameter); // also writes the length as a prefix; unambiguous
}
bw.Flush();
}
using (SHA256 sha256 = sha256Factory()) {
byte[] retVal = sha256.ComputeHash(ms.GetBuffer(), 0, checked((int)ms.Length));
return retVal;
}
}
}
}
///
/// Gets the SHA-256 factory.
///
/// SHA256 factory
private static Func GetSHA256Factory() {
// Note: ASP.NET 4.5 always prefers CNG, but the CNG algorithms are not that
// performant on 4.0 and below. The following list is optimized for speed
// given our scenarios.
if (!CryptoConfig.AllowOnlyFipsAlgorithms) {
// This provider is not FIPS-compliant, so we can't use it if FIPS compliance
// is mandatory.
return () => new SHA256Managed();
}
try {
using (SHA256Cng sha256 = new SHA256Cng()) {
return () => new SHA256Cng();
}
}
catch (PlatformNotSupportedException) {
// CNG not supported (perhaps because we're not on Windows Vista or above); move on
}
// If all else fails, fall back to CAPI.
return () => new SHA256CryptoServiceProvider();
}
///
/// Convert to hex character
///
/// The value to be converted.
/// Hex character
private static char HexDigit(int value) {
return (char)(value > 9 ? value + '7' : value + '0');
}
///
/// Convert hdex string to bytes.
///
/// Input hex string.
/// The bytes
private static byte[] HexToBinary(string hex) {
int size = hex.Length / 2;
byte[] bytes = new byte[size];
for (int idx = 0; idx < size; idx++) {
bytes[idx] = (byte)((HexValue(hex[idx * 2]) << 4) + HexValue(hex[(idx * 2) + 1]));
}
return bytes;
}
///
/// Convert hex digit to byte.
///
/// The hex digit.
/// The byte
private static int HexValue(char digit) {
return digit > '9' ? digit - '7' : digit - '0';
}
}
///
/// On ASP.NET 4.5, we can just delegate to MachineKey.Protect and MachineKey.Unprotect directly,
/// which contain optimized code paths.
///
private sealed class MachineKey45 : IMachineKey {
///
/// Protect thunk
///
private readonly ProtectUnprotect protectThunk;
///
/// Unprotect thunk
///
private readonly ProtectUnprotect unprotectThunk;
///
/// Initializes a new instance of the class.
///
/// The protect thunk.
/// The unprotect thunk.
public MachineKey45(ProtectUnprotect protectThunk, ProtectUnprotect unprotectThunk) {
this.protectThunk = protectThunk;
this.unprotectThunk = unprotectThunk;
}
///
/// Protects the specified user data.
///
/// The user data.
/// The purposes.
/// The protected data
public byte[] Protect(byte[] userData, string[] purposes) {
return this.protectThunk(userData, purposes);
}
///
/// Unprotects the specified protected data.
///
/// The protected data.
/// The purposes.
/// The unprotected data
public byte[] Unprotect(byte[] protectedData, string[] purposes) {
return this.unprotectThunk(protectedData, purposes);
}
}
}
}