Skip to content

Instantly share code, notes, and snippets.

@dazinator
Last active December 27, 2023 09:46
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save dazinator/0cdb8e1fbf81d3ed5d44 to your computer and use it in GitHub Desktop.
Save dazinator/0cdb8e1fbf81d3ed5d44 to your computer and use it in GitHub Desktop.
Decrypt a Legacy ASP.NET Forms Authentication Cookie (that uses SHA1 validation, and AES encryption) - without horrendous dependencies on system.web.. This allows you to decrypt a forms authentication cookie that was created in ASP.NET 3.5, from an ASP.NET 5 application.
internal static class FormsAuthenticationTicketHelper
{
private const byte CURRENT_TICKET_SERIALIZED_VERSION = 0x01;
private const int MAX_TICKET_LENGTH = 4096;
// Resurrects a FormsAuthenticationTicket from its serialized blob representation.
// The input blob must be unsigned and unencrypted. This function returns null if
// the serialized ticket format is invalid. The caller must also verify that the
// ticket is still valid, as this method doesn't check expiration.
public static FormsAuthenticationTicket Deserialize(byte[] serializedTicket, int serializedTicketLength)
{
try
{
using (MemoryStream ticketBlobStream = new MemoryStream(serializedTicket))
{
using (SerializingBinaryReader ticketReader = new SerializingBinaryReader(ticketBlobStream))
{
// Step 1: Read the serialized format version number from the stream.
// Currently the only supported format is 0x01.
// LENGTH: 1 byte
byte serializedFormatVersion = ticketReader.ReadByte();
if (serializedFormatVersion != CURRENT_TICKET_SERIALIZED_VERSION)
{
return null; // unexpected value
}
// Step 2: Read the ticket version number from the stream.
// LENGTH: 1 byte
int ticketVersion = ticketReader.ReadByte();
// Step 3: Read the ticket issue date from the stream.
// LENGTH: 8 bytes
long ticketIssueDateUtcTicks = ticketReader.ReadInt64();
DateTime ticketIssueDateUtc = new DateTime(ticketIssueDateUtcTicks, DateTimeKind.Utc);
DateTime ticketIssueDateLocal = ticketIssueDateUtc.ToLocalTime();
// Step 4: Read the spacer from the stream.
// LENGTH: 1 byte
byte spacer = ticketReader.ReadByte();
if (spacer != 0xfe)
{
return null; // unexpected value
}
// Step 5: Read the ticket expiration date from the stream.
// LENGTH: 8 bytes
long ticketExpirationDateUtcTicks = ticketReader.ReadInt64();
DateTime ticketExpirationDateUtc = new DateTime(ticketExpirationDateUtcTicks, DateTimeKind.Utc);
DateTime ticketExpirationDateLocal = ticketExpirationDateUtc.ToLocalTime();
// Step 6: Read the ticket persistence field from the stream.
// LENGTH: 1 byte
byte ticketPersistenceFieldValue = ticketReader.ReadByte();
bool ticketIsPersistent;
switch (ticketPersistenceFieldValue)
{
case 0:
ticketIsPersistent = false;
break;
case 1:
ticketIsPersistent = true;
break;
default:
return null; // unexpected value
}
// Step 7: Read the ticket username from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketName = ticketReader.ReadBinaryString();
// Step 8: Read the ticket custom data from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketUserData = ticketReader.ReadBinaryString();
// Step 9: Read the ticket cookie path from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketCookiePath = ticketReader.ReadBinaryString();
// Step 10: Read the footer from the stream.
// LENGTH: 1 byte
byte footer = ticketReader.ReadByte();
if (footer != 0xff)
{
return null; // unexpected value
}
// Step 11: Verify that we have consumed the entire payload.
// We don't expect there to be any more information after the footer.
// The caller is responsible for telling us when the actual payload
// is finished, as he may have handed us a byte array that contains
// the payload plus signature as an optimization, and we don't want
// to misinterpet the signature as a continuation of the payload.
if (ticketBlobStream.Position != serializedTicketLength)
{
return null;
}
// Success.
return FromUtc(
ticketVersion /* version */,
ticketName /* name */,
ticketIssueDateUtc /* issueDateUtc */,
ticketExpirationDateUtc /* expirationUtc */,
ticketIsPersistent /* isPersistent */,
ticketUserData /* userData */,
ticketCookiePath /* cookiePath */);
}
}
}
catch
{
// If anything goes wrong while parsing the token, just treat the token as invalid.
return null;
}
}
internal static FormsAuthenticationTicket FromUtc(int version, String name, DateTime issueDateUtc, DateTime expirationUtc, bool isPersistent, String userData, String cookiePath)
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(version, name, issueDateUtc.ToLocalTime(), expirationUtc.ToLocalTime(), isPersistent, userData, cookiePath);
//ticket._IssueDateUtcHasValue = true;
//ticket._IssueDateUtc = issueDateUtc;
//ticket._ExpirationUtcHasValue = true;
//ticket._ExpirationUtc = expirationUtc;
return ticket;
}
// Turns a FormsAuthenticationTicket into a serialized blob.
// The resulting blob is not encrypted or signed.
public static byte[] Serialize(FormsAuthenticationTicket ticket)
{
using (MemoryStream ticketBlobStream = new MemoryStream())
{
using (SerializingBinaryWriter ticketWriter = new SerializingBinaryWriter(ticketBlobStream))
{
// SECURITY NOTE:
// Earlier versions of the serializer (Framework20 / Framework40) wrote out a
// random 8-byte header as the first part of the payload. This random header
// was used as an IV when the ticket was encrypted, since the early encryption
// routines didn't automatically append an IV when encrypting data. However,
// the MSRC 10405 (Pythia) patch causes all of our crypto routines to use an
// IV automatically, so there's no need for us to include a random IV in the
// serialized stream any longer. We can just write out only the data, and the
// crypto routines will do the right thing.
// Step 1: Write the ticket serialized format version number (currently 0x01) to the stream.
// LENGTH: 1 byte
ticketWriter.Write(CURRENT_TICKET_SERIALIZED_VERSION);
// Step 2: Write the ticket version number to the stream.
// This is the developer-specified FormsAuthenticationTicket.Version property,
// which is just ticket metadata. Technically it should be stored as a 32-bit
// integer instead of just a byte, but we have historically been storing it
// as just a single byte forever and nobody has complained.
// LENGTH: 1 byte
ticketWriter.Write((byte)ticket.Version);
// Step 3: Write the ticket issue date to the stream.
// We store this value as UTC ticks. We can't use DateTime.ToBinary() since it
// isn't compatible with .NET v1.1.
// LENGTH: 8 bytes (64-bit little-endian in payload)
ticketWriter.Write(ticket.IssueDate.ToUniversalTime().Ticks);
// Step 4: Write a one-byte spacer (0xfe) to the stream.
// One of the old ticket formats (Framework40) expects the unencrypted payload
// to contain 0x000000 (3 null bytes) beginning at position 9 in the stream.
// Since we're currently at offset 10 in the serialized stream, we can take
// this opportunity to purposely inject a non-null byte at this offset, which
// intentionally breaks compatibility with Framework40 mode.
// LENGTH: 1 byte
Debug.Assert(ticketBlobStream.Position == 10, "Critical that we be at position 10 in the stream at this point.");
ticketWriter.Write((byte)0xfe);
// Step 5: Write the ticket expiration date to the stream.
// We store this value as UTC ticks.
// LENGTH: 8 bytes (64-bit little endian in payload)
ticketWriter.Write(ticket.Expiration.ToUniversalTime().Ticks);
// Step 6: Write the ticket persistence field to the stream.
// LENGTH: 1 byte
ticketWriter.Write(ticket.IsPersistent);
// Step 7: Write the ticket username to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.Name);
// Step 8: Write the ticket custom data to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.UserData);
// Step 9: Write the ticket cookie path to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.CookiePath);
// Step 10: Write a one-byte footer (0xff) to the stream.
// One of the old FormsAuthenticationTicket formats (Framework20) requires
// that the payload end in 0x0000 (U+0000). By making the very last byte
// of this format non-null, we can guarantee a compatiblity break between
// this format and Framework20.
// LENGTH: 1 byte
ticketWriter.Write((byte)0xff);
// Finished.
return ticketBlobStream.ToArray();
}
}
}
public static byte[] SerialiseLegacy(FormsAuthenticationTicket ticket, bool encrypt, bool legacyPadding)
{
byte[] bData = new byte[4096]; // will store the ticket data.
byte[] pBin = new byte[4];
long[] pDates = new long[2];
byte[] pNull = { 0, 0, 0 };
// DevDiv Bugs 137864: 8 bytes may not be enough random bits as the length should be equal to the
// key size. In CompatMode > Framework20SP1, use the IVType.Random feature instead of these 8 bytes,
// but still include empty 8 bytes for compat with webengine.dll, where CookieAuthConstructTicket is.
// Note that even in CompatMode = Framework20SP2 we fill 8 bytes with random data if the ticket
// is not going to be encrypted.
bool willEncrypt = encrypt;
// bool legacyPadding = !willEncrypt || (MachineKeySection.CompatMode == MachineKeyCompatibilityMode.Framework20SP1);
if (legacyPadding)
{
// Fill the first 8 bytes of the blob with random bits
byte[] bRandom = new byte[8];
RNGCryptoServiceProvider randgen = new RNGCryptoServiceProvider();
randgen.GetBytes(bRandom);
Buffer.BlockCopy(bRandom, 0, bData, 0, 8);
}
else {
// use blank 8 bytes for compatibility with CookieAuthConstructTicket (do nothing)
}
pBin[0] = (byte)ticket.Version;
pBin[1] = (byte)(ticket.IsPersistent ? 1 : 0);
pDates[0] = ticket.IssueDate.ToFileTime();
pDates[1] = ticket.Expiration.ToFileTime();
int iRet = UnsafeNativeMethods.CookieAuthConstructTicket(
bData, bData.Length,
ticket.Name, ticket.UserData, ticket.CookiePath,
pBin, pDates);
if (iRet < 0)
return null;
byte[] ciphertext = new byte[iRet];
Buffer.BlockCopy(bData, 0, ciphertext, 0, iRet);
return ciphertext;
}
//public static FormsAuthenticationTicket DeserialiseLegacy(byte[] serializedTicket, int serializedTicketLength)
//{
// int iSize = ((serializedTicketLength > MAX_TICKET_LENGTH) ? MAX_TICKET_LENGTH : serializedTicketLength);
// StringBuilder name = new StringBuilder(iSize);
// StringBuilder data = new StringBuilder(iSize);
// StringBuilder path = new StringBuilder(iSize);
// byte[] pBin = new byte[4];
// long[] pDates = new long[2];
// int iRet = UnsafeNativeMethods.CookieAuthParseTicket(serializedTicket, serializedTicketLength,
// name, iSize,
// data, iSize,
// path, iSize,
// pBin, pDates);
// if (iRet != 0)
// return null;
// DateTime dt1 = DateTime.FromFileTime(pDates[0]);
// DateTime dt2 = DateTime.FromFileTime(pDates[1]);
// FormsAuthenticationTicket ticket = new FormsAuthenticationTicket((int)pBin[0],
// name.ToString(),
// dt1,
// dt2,
// (bool)(pBin[1] != 0),
// data.ToString(),
// path.ToString());
// return ticket;
//}
// see comments on SerializingBinaryWriter
private sealed class SerializingBinaryReader : BinaryReader
{
public SerializingBinaryReader(Stream input)
: base(input)
{
}
public string ReadBinaryString()
{
int charCount = Read7BitEncodedInt();
byte[] bytes = ReadBytes(charCount * 2);
char[] chars = new char[charCount];
for (int i = 0; i < chars.Length; i++)
{
chars[i] = (char)(bytes[2 * i] | (bytes[2 * i + 1] << 8));
}
return new String(chars);
}
public override string ReadString()
{
// should never call this method since it will produce wrong results
throw new NotImplementedException();
}
}
// This is a special BinaryWriter which serializes strings in a way that is
// entirely round-trippable. For example, the string "\ud800" is a valid .NET
// Framework string, but since U+D800 is an unpaired Unicode surrogate the
// built-in Encoding types will not round-trip it. Strings are serialized as a
// 7-bit character count (not byte count!) followed by a UTF-16LE payload.
private sealed class SerializingBinaryWriter : BinaryWriter
{
public SerializingBinaryWriter(Stream output)
: base(output)
{
}
public override void Write(string value)
{
// should never call this method since it will produce wrong results
throw new NotImplementedException();
}
public void WriteBinaryString(string value)
{
byte[] bytes = new byte[value.Length * 2];
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
bytes[2 * i] = (byte)c;
bytes[2 * i + 1] = (byte)(c >> 8);
}
Write7BitEncodedInt(value.Length);
Write(bytes);
}
}
}
public static class HexUtils
{
static byte[] s_ahexval;
static internal byte[] HexStringToByteArray(String str)
{
if (((uint)str.Length & 0x1) == 0x1) // must be 2 nibbles per byte
{
return null;
}
byte[] ahexval = s_ahexval; // initialize a table for faster lookups
if (ahexval == null)
{
ahexval = new byte['f' + 1];
for (int i = ahexval.Length; --i >= 0;)
{
if ('0' <= i && i <= '9')
{
ahexval[i] = (byte)(i - '0');
}
else if ('a' <= i && i <= 'f')
{
ahexval[i] = (byte)(i - 'a' + 10);
}
else if ('A' <= i && i <= 'F')
{
ahexval[i] = (byte)(i - 'A' + 10);
}
}
s_ahexval = ahexval;
}
byte[] result = new byte[str.Length / 2];
int istr = 0, ir = 0;
int n = result.Length;
while (--n >= 0)
{
int c1, c2;
try
{
c1 = ahexval[str[istr++]];
}
catch (ArgumentNullException)
{
c1 = 0;
return null;// Inavlid char
}
catch (ArgumentException)
{
c1 = 0;
return null;// Inavlid char
}
catch (IndexOutOfRangeException)
{
c1 = 0;
return null;// Inavlid char
}
try
{
c2 = ahexval[str[istr++]];
}
catch (ArgumentNullException)
{
c2 = 0;
return null;// Inavlid char
}
catch (ArgumentException)
{
c2 = 0;
return null;// Inavlid char
}
catch (IndexOutOfRangeException)
{
c2 = 0;
return null;// Inavlid char
}
result[ir++] = (byte)((c1 << 4) + c2);
}
return result;
}
public static string BinaryToHex(byte[] data)
{
if (data == null)
{
return null;
}
char[] hex = new char[checked(data.Length * 2)];
for (int i = 0; i < data.Length; i++)
{
byte thisByte = data[i];
hex[2 * i] = NibbleToHex((byte)(thisByte >> 4)); // high nibble
hex[2 * i + 1] = NibbleToHex((byte)(thisByte & 0xf)); // low nibble
}
return new string(hex);
}
/// <summary>
/// Converts a hexadecimal string into its binary representation.
/// </summary>
/// <param name="data">The hex string.</param>
/// <returns>The byte array corresponding to the contents of the hex string,
/// or null if the input string is not a valid hex string.</returns>
public static byte[] HexToBinary(string data)
{
if (data == null || data.Length % 2 != 0)
{
// input string length is not evenly divisible by 2
return null;
}
byte[] binary = new byte[data.Length / 2];
for (int i = 0; i < binary.Length; i++)
{
int highNibble = HexToInt(data[2 * i]);
int lowNibble = HexToInt(data[2 * i + 1]);
if (highNibble == -1 || lowNibble == -1)
{
return null; // bad hex data
}
binary[i] = (byte)((highNibble << 4) | lowNibble);
}
return binary;
}
public static int HexToInt(char h)
{
return (h >= '0' && h <= '9') ? h - '0' :
(h >= 'a' && h <= 'f') ? h - 'a' + 10 :
(h >= 'A' && h <= 'F') ? h - 'A' + 10 :
-1;
}
// converts a nibble (4 bits) to its uppercase hexadecimal character representation [0-9, A-F]
private static char NibbleToHex(byte nibble)
{
return (char)((nibble < 10) ? (nibble + '0') : (nibble - 10 + 'A'));
}
}
public class LegacyFormsAuthenticationTicketEncryptor
{
private string _DecryptionKeyText = string.Empty;
private static RNGCryptoServiceProvider _randomNumberGenerator;
private static RNGCryptoServiceProvider RandomNumberGenerator
{
get
{
if (_randomNumberGenerator == null)
{
_randomNumberGenerator = new RNGCryptoServiceProvider();
}
return _randomNumberGenerator;
}
}
private byte[] _DecryptionKeyBlob = null;
public LegacyFormsAuthenticationTicketEncryptor(string decryptionKey)
{
_DecryptionKeyText = decryptionKey;
_DecryptionKeyBlob = HexUtils.HexStringToByteArray(decryptionKey);
}
public FormsAuthenticationTicket DecryptCookie(string cookieString, Sha1HashProvider hasher)
{
byte[] cookieBlob = null;
// 1. Convert from hex to binary.
if ((cookieString.Length % 2) == 0)
{ // Could be a hex string
try
{
cookieBlob = HexUtils.HexToBinary(cookieString);
}
catch { }
}
if (cookieBlob == null)
{
return null;
}
// decrypt
// hasher = new Sha1HashProvider(_ValidationKeyText);
byte[] decryptedCookie = Decrypt(cookieBlob, hasher, true);
int ticketLength = decryptedCookie.Length - hasher.HashSize;
bool validHash = hasher.CheckHash(decryptedCookie, ticketLength);
var newTicket = FormsAuthenticationTicketHelper.Deserialize(decryptedCookie, ticketLength);
// var ticket = FormsAuthenticationTicketHelper.DeserialiseLegacy(decryptedCookie, ticketLength);
return newTicket;
}
private byte[] EncryptCookieData(byte[] cookieBlob, int length, Sha1HashProvider hasher = null)
{
var aesProvider = new AesCryptoServiceProvider();
aesProvider.Key = _DecryptionKeyBlob;
aesProvider.BlockSize = 128;
aesProvider.GenerateIV();
aesProvider.IV = new byte[aesProvider.IV.Length];
aesProvider.Mode = CipherMode.CBC;
var decryptor = aesProvider.CreateEncryptor();
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
{
bool createIv = true;
bool useRandomIv = true;
bool sign = false;
if (createIv)
{
int ivLength = RoundupNumBitsToNumBytes(aesProvider.KeySize);
byte[] iv = null;
if (hasher != null)
{
iv = hasher.GetIVHash(cookieBlob, ivLength);
}
else if (useRandomIv)
{
iv = new byte[ivLength];
RandomNumberGenerator.GetBytes(iv);
}
// first write the iv.
cs.Write(iv, 0, iv.Length);
}
// then write ticket data.
cs.Write(cookieBlob, 0, cookieBlob.Length);
cs.FlushFinalBlock();
byte[] paddedData = ms.ToArray();
if (sign)
{
throw new NotImplementedException();
// append signature to encrypted bytes.
}
return paddedData;
}
}
}
private byte[] Decrypt(byte[] cookieBlob, Sha1HashProvider hasher, bool isHashAppended)
{
if (hasher == null)
{
throw new ArgumentNullException("hasher");
}
if (isHashAppended)
{
// need to check the hash signature, and strip it off the end of the byte array.
cookieBlob = hasher.CheckHashAndRemove(cookieBlob);
if (cookieBlob == null)
{
// signature verification failed
throw new Exception();
}
}
// Now decrypt the encrypted cookie data.
using (var aesProvider = new AesCryptoServiceProvider())
{
aesProvider.Key = _DecryptionKeyBlob;
aesProvider.BlockSize = 128;
aesProvider.GenerateIV();
aesProvider.IV = new byte[aesProvider.IV.Length];
aesProvider.Mode = CipherMode.CBC;
using (var ms = new MemoryStream())
{
using (var decryptor = aesProvider.CreateDecryptor())
{
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
{
cs.Write(cookieBlob, 0, cookieBlob.Length);
cs.FlushFinalBlock();
byte[] paddedData = ms.ToArray();
// The data contains some random bytes prepended at the start. Remove them.
int ivLength = RoundupNumBitsToNumBytes(aesProvider.KeySize);
int dataLength = paddedData.Length - ivLength;
if (dataLength < 0)
{
throw new Exception();
}
byte[] decryptedData = new byte[dataLength];
Buffer.BlockCopy(paddedData, ivLength, decryptedData, 0, dataLength);
return decryptedData;
}
}
}
}
}
internal static int RoundupNumBitsToNumBytes(int numBits)
{
if (numBits < 0)
return 0;
return (numBits / 8) + (((numBits & 7) != 0) ? 1 : 0);
}
/// <summary>
/// Encrypts the ticket, and if a hasher is provided, will also include a signature in the encrypted data.
/// </summary>
/// <param name="ticket"></param>
/// <param name="hasher">If hasher it not null, it will be used to generate hash which is used to sign the encrypted data by adding it to the end. If it is null, no signature will be added.</param>
/// <param name="randomiseUsingHash">If true, the hash of the encrypted data will be prepended to the beginning, otherwise random bytes will be generated and prepended.</param>
/// <returns></returns>
public string Encrypt(FormsAuthenticationTicket ticket, Sha1HashProvider hasher, bool randomiseUsingHash = false)
{
bool encrypt = true;
bool padding = true;
// make ticked into binary blob.
byte[] ticketBlob = FormsAuthenticationTicketHelper.Serialize(ticket);
if (ticketBlob == null)
{
throw new Exception();
}
byte[] cookieBlob = ticketBlob;
// Compute a hash and add to the blob.
if (hasher != null)
{
byte[] hashBlob = hasher.GetHMACSHA1Hash(ticketBlob, null, 0, ticketBlob.Length);
if (hashBlob == null)
{
throw new Exception();
}
// create a new byte array big enough to store the ticket data, and the hash data which is appended to the end.
cookieBlob = new byte[hashBlob.Length + ticketBlob.Length];
Buffer.BlockCopy(ticketBlob, 0, cookieBlob, 0, ticketBlob.Length);
Buffer.BlockCopy(hashBlob, 0, cookieBlob, ticketBlob.Length, hashBlob.Length);
}
// now encrypt the cookie data.
byte[] encryptedCookieBlob = EncryptCookieData(cookieBlob, cookieBlob.Length, randomiseUsingHash ? hasher : null);
// now convert the binary encrypted cookie data to a hex value.
if (encryptedCookieBlob == null)
{
throw new Exception();
}
var cookieData = HexUtils.BinaryToHex(encryptedCookieBlob);
return cookieData;
}
}
public class Sha1HashProvider
{
public const int SHA1_HASH_SIZE = 20;
public const int SHA1_KEY_SIZE = 64;
private static int _HashSize;
private static int _KeySize;
private byte[] _validationKeyBlob;
private byte[] _inner = null;
private byte[] _outer = null;
public Sha1HashProvider(string validationKey, int hashSize = SHA1_HASH_SIZE, int keySize = SHA1_KEY_SIZE)
{
_HashSize = hashSize;
_KeySize = keySize;
_validationKeyBlob = HexUtils.HexStringToByteArray(validationKey);
SetInnerOuterKeys(_validationKeyBlob, ref _inner, ref _outer);
}
public byte[] GetHMACSHA1Hash(byte[] buf, byte[] modifier, int start, int length)
{
if (start < 0 || start > buf.Length)
throw new ArgumentException("start");
if (length < 0 || buf == null || (start + length) > buf.Length)
throw new ArgumentException("length");
byte[] hash = new byte[_HashSize];
int hr = UnsafeNativeMethods.GetHMACSHA1Hash(buf, start, length,
modifier, (modifier == null) ? 0 : modifier.Length,
_inner, _inner.Length, _outer, _outer.Length,
hash, hash.Length);
if (hr == 0)
return hash;
//_UseHMACSHA = false;
return null;
}
public byte[] CheckHashAndRemove(byte[] bufHashed)
{
if (!CheckHash(bufHashed, bufHashed.Length - _HashSize))
return null;
byte[] buf2 = new byte[bufHashed.Length - _HashSize];
Buffer.BlockCopy(bufHashed, 0, buf2, 0, buf2.Length);
return buf2;
}
public bool CheckHash(byte[] decryptedCookie, int hashIndex)
{
// 2. SHA1 Hash is appended to the end.
// Verify the hash matches by re-computing the hash for this message, and comparing.
byte[] hashCheckBlob = GetHMACSHA1Hash(decryptedCookie, null, 0, hashIndex);
if (hashCheckBlob == null)
{
throw new Exception();
}
//////////////////////////////////////////////////////////////////////
// Step 2: Make sure the MAC has expected length
if (hashCheckBlob == null || hashCheckBlob.Length != _HashSize)
throw new Exception();
// To prevent a timing attack, we should verify the entire hash instead of failing
// early the first time we see a mismatched byte.
bool hashCheckFailed = false;
for (int i = 0; i < _HashSize; i++)
{
if (hashCheckBlob[i] != decryptedCookie[hashIndex + i])
{
hashCheckFailed = true;
}
}
return !hashCheckFailed;
}
private static void SetInnerOuterKeys(byte[] validationKey, ref byte[] inner, ref byte[] outer)
{
byte[] key = null;
if (validationKey.Length > _KeySize)
{
key = new byte[_HashSize];
int hr = UnsafeNativeMethods.GetSHA1Hash(validationKey, validationKey.Length, key, key.Length);
Marshal.ThrowExceptionForHR(hr);
}
if (inner == null)
inner = new byte[_KeySize];
if (outer == null)
outer = new byte[_KeySize];
int i;
for (i = 0; i < _KeySize; i++)
{
inner[i] = 0x36;
outer[i] = 0x5C;
}
for (i = 0; i < validationKey.Length; i++)
{
inner[i] ^= validationKey[i];
outer[i] ^= validationKey[i];
}
}
public int HashSize { get { return _HashSize; } }
public byte[] GetIVHash(byte[] buf, int ivLength)
{
// return an IV that is computed as a hash of the buffer
int bytesToWrite = ivLength;
int bytesWritten = 0;
byte[] iv = new byte[ivLength];
// get SHA1 hash of the buffer and copy to the IV.
// if hash length is less than IV length, re-hash the hash and
// append until IV is full.
byte[] hash = buf;
while (bytesWritten < ivLength)
{
byte[] newHash = new byte[_HashSize];
int hr = UnsafeNativeMethods.GetSHA1Hash(hash, hash.Length, newHash, newHash.Length);
Marshal.ThrowExceptionForHR(hr);
hash = newHash;
int bytesToCopy = Math.Min(_HashSize, bytesToWrite);
Buffer.BlockCopy(hash, 0, iv, bytesWritten, bytesToCopy);
bytesWritten += bytesToCopy;
bytesToWrite -= bytesToCopy;
}
return iv;
}
}
[TestClass]
public class FormsAuthenitcationTests
{
private string _ValidationKeyText = "your validation key typically would be in machinekey section in config file.";
private string _DecryptionKeyText = "your decryptionkey typically would be in machinekey section in config file";
[TestMethod]
public void DecryptSkyConnectCookie()
{
// cookieString is the contents of a forms authentication cookie that you wish to decrypt and deserialise back into a FormsAuthenticationTicket
var cookieString = "D07F829FB636B...shortened for brevity";
var sut = new LegacyFormsAuthenticationTicketEncryptor(_DecryptionKeyText);
FormsAuthenticationTicket ticket = sut.DecryptCookie(cookieString, new Sha1HashProvider(_ValidationKeyText));
Assert.IsNotNull(ticket);
Console.Write(ticket.Name);
}
}
[ComVisible(false)]
internal static class UnsafeNativeMethods
{
[DllImport("C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\webengine4.dll")]
internal static extern int GetHMACSHA1Hash(byte[] data1, int dataOffset1, int dataSize1, byte[] data2, int dataSize2, byte[] innerKey, int innerKeySize, byte[] outerKey, int outerKeySize, byte[] hash, int hashSize);
[DllImport("C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\webengine4.dll", CharSet = CharSet.Unicode)]
internal static extern int CookieAuthConstructTicket(byte[] pData,
int iDataLen,
string szName,
string szData,
string szPath,
byte[] pBytes,
long[] pDates);
[DllImport("webengine4.dll")]
internal static extern int GetSHA1Hash(byte[] data, int dataSize, byte[] hash, int hashSize);
}
@anil-kk
Copy link

anil-kk commented Jun 13, 2016

@dazinator, Hi Thank you so much for your solution. It made my day easy. Can I use your solution in my projects? Thanks in advance.

@dazinator
Copy link
Author

dazinator commented Jun 15, 2016

Hi @anil2 - I had to look at the reference source code for .net (specifically .net 3.5 system.web) in order to work out how FormsAuthentication decrypts the cookie, so I could then write an equivalent decryption routine without all of the dependencies. I am not a licencing guru, so I am unsure if this code is legally ok to use in production applications, you'd have to consult someone with necessary legal expertise in such matters. However I personally don;t have any problems with you taking the code that I have written and using it, but whether Microsoft would mind is not something I can comment on authoratatively - however I believe that it is ok to use based on my understanding of the microsoft reference licence:

Reference License

The .NET Framework source is being released under a read-only reference license. When we announced that we were releasing the source back in October, some people had concerns about the potential impact of their viewing the source. To help clarify and address these concerns, we made a small change to the license to specifically call out that the license does not apply to users developing software for a non-Windows platform that has “the same or substantially the same features or functionality” as the .NET Framework. If the software you are developing is for Windows platforms, you can look at the code, even if that software has "the same or substantially the same features or functionality" as the .NET Framework.

@anil-kk
Copy link

anil-kk commented Jul 18, 2016

@dazinator Thank you.
I see there is code for encrypting a cookie, I tried to use that part, there is some problem validating hash. Did you check it if the encrypting part is working well on your end?

@tuzimaster
Copy link

@anil2 @dazinator I've also encounter an error when decrypting a cookie that has been encrypted with these methods. Is there a sample showing how to encrypt the cookie?

@dazinator
Copy link
Author

If anyone is having issues with this, please see: https://github.com/dazinator/AspNetCore.LegacyAuthCookieCompat

@dazinator
Copy link
Author

I have a feeling it might be possible to replace webengine4.dll - some promising stuff here: https://github.com/mono/mono/blob/0bcbe39b148bb498742fc68416f8293ccd350fb6/mcs/class/System.Web/System.Web.Util/MachineKeySectionUtils.cs

I won't have time to investigate any further though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment