Forked from dazinator/FormsAuthenticationTicketHelper.cs
Created
July 19, 2022 09:29
-
-
Save hoangitk/53a592cb616f55c555f262b145d5cc9f 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment