/// <summary> /// Parses a string containing a token and returns the token it contains, /// if that token is valid. /// </summary> /// <param name="key">The shared secret key with which the token was originally signed.</param> /// <param name="token">The original token string representation.</param> public static SecurityToken FromString(string key, string token) { // Decode the incoming string and do a basic sanity check. byte[] RawData = token.Base64WebSafeDecode(); if (RawData.Length < (HMAC_SIZE + 1)) { throw new SecurityTokenInvalidException("String is too small to be a valid " + typeof(SecurityToken).Name); } // Recompute the HMAC using the provided key (as the key is a secret and // cannot be encoded into the token itself), and make sure it matches. If // not, then the token is corrupt and cannot be trusted. byte[] RawHMAC = RawData.Skip(RawData.Length - HMAC_SIZE).ToArray(); RawData = RawData.Take(RawData.Length - HMAC_SIZE).ToArray(); if (RawData.GetHMACSHA1Bytes(key).Base64Encode() != RawHMAC.Base64Encode()) { throw new SecurityTokenInvalidException("HMAC validation failed."); } // Extract the flags from the payload. SecurityTokenFlags Flags = (SecurityTokenFlags)RawData.First(); RawData = RawData.Skip(1).ToArray(); // If data was encrypted, decrypt it. SymmetricAlgorithm Alg = CreateCipher(key); if ((Flags & SecurityTokenFlags.EncryptedCTS) != 0) { RawData = Alg.CTSDecrypt(Alg.CTSDecrypt(RawData).Reverse().ToArray()); } else if ((Flags & SecurityTokenFlags.EncryptedCBC) != 0) { RawData = Alg.CBCDecrypt(Alg.CTSDecrypt(RawData).Reverse().ToArray()); } // If the data was originally deflated, decompress it. if ((Flags & SecurityTokenFlags.Deflated) != 0) { RawData = RawData.DeflateDecompress().ToArray(); } // If the data contains an expiration date, then decode the expiration date // and make sure the token has not expired. DateTime Exp = DateTime.MaxValue; if ((Flags & SecurityTokenFlags.ExpireDate) != 0) { Exp = Epoch.AddSeconds(BitConverter.ToUInt32(RawData.Skip(RawData.Length - sizeof(UInt32)).EndianFlip().ToArray(), 0)); if (Exp < DateTime.UtcNow) { throw new SecurityTokenExpiredException("Token has expired."); } RawData = RawData.Take(RawData.Length - sizeof(UInt32)).ToArray(); } // The remaining data is the key/value pair data, packed as an even number of // strings, each prefixed with a big-endian uint16 length specifier, followed // by that many bytes of UTF-8-encoded string data. After unpacking strings, // rebuild the original dictionary. Queue <string> Values = new Queue <string>(); while (RawData.Length > 0) { ushort StrLen = BitConverter.ToUInt16(RawData.Take(2).EndianFlip().ToArray(), 0); Values.Enqueue(Encoding.UTF8.GetString(RawData.Skip(2).Take(StrLen).ToArray())); RawData = RawData.Skip(StrLen + 2).ToArray(); } Dictionary <string, string> NewData = new Dictionary <string, string>(); while (Values.Count > 0) { NewData[Values.Dequeue()] = Values.Dequeue(); } // Return a security token containing the original expiration, key, and // payload data. Note that if any of the checks above fails (payload validation, // matching key, expiration) then an exception will be thrown instead of // returning any token. return(new SecurityToken { Expires = Exp, Key = key, Data = NewData }); }
/// <summary> /// Converts the token to its string representation, including packing, compressing, encrypting, /// and digitally signing the token's contents and encoding to a web-safe Base64 variant. /// </summary> public override string ToString() { SecurityTokenFlags Flags = SecurityTokenFlags.None; // Pack the key/value pairs in the dictionary into a single byte array, each // value being encoded in UTF-8 (full Unicode support, with efficient storage // for US-ASCII data). Note that each string is limited to 65535 chars, to // save the extra 4 bytes per pair for encoding the extra zeros for a 32-bit // value, considering that any value that large is excessive for the intended // application. List <byte> InnerPayload = new List <byte>(); foreach (string S in Data.Keys.OrderBy(K => K).SelectMany(K => new string[] { K, Data[K] })) { byte[] StrBytes = Encoding.UTF8.GetBytes(S); if (StrBytes.Length > UInt16.MaxValue) { throw new SecurityTokenException("Each key/value in a " + typeof(SecurityToken).Name + " cannot " + "exceed " + UInt16.MaxValue.ToString() + " bytes in UTF-8 representation."); } InnerPayload.AddRange(BitConverter.GetBytes((UInt16)StrBytes.Length).EndianFlip()); InnerPayload.AddRange(StrBytes); } // If an expiration date is set and the value is valid, set the expiration flag and // encode an expiration date at the end of the payload. Expiration date is encoded // as a 32-bit unsigned Unix date with second precision, for simplicity. if ((Expires < DateTime.MaxValue) && (Expires >= Epoch)) { Flags |= SecurityTokenFlags.ExpireDate; InnerPayload.AddRange(BitConverter.GetBytes((UInt32)(Expires - Epoch).TotalSeconds).EndianFlip()); } // Automatically attempt to compress the payload. If the payload successfully compresses to a // smaller size than the original, set the compression flag and use the compressed payload. // This may make the encoded token at least look a little smaller, though compression ratios // will be unimpressive on data this small. Note that compression is done before adding the // validation HMAC, as (1) HMAC data doesn't compress well, and (2) we want to be able to validate // the compressed payload before attempting to engage the decompressor on the other side, as piping // untrusted data into a decompressor may be a security or DoS risk. List <byte> PackedPayload = InnerPayload.DeflateCompress(Ionic.Zlib.CompressionLevel.BestCompression).ToList(); if (PackedPayload.Count < InnerPayload.Count) { InnerPayload = PackedPayload; Flags |= SecurityTokenFlags.Deflated; } // Encrypt the inner payload with the same key as used for signing. Try to use CTS, if the data // is long enough, as it doesn't add any overhead, but fall back on CBC w/ PKCS7 padding for very // short messages. SymmetricAlgorithm Alg = CreateCipher(Key); if (InnerPayload.Count >= (Alg.BlockSize / 8)) { InnerPayload = Alg.CTSEncrypt(InnerPayload.ToArray()).ToList(); Flags |= SecurityTokenFlags.EncryptedCTS; } else { InnerPayload = Alg.CBCEncrypt(InnerPayload.ToArray()).ToList(); Flags |= SecurityTokenFlags.EncryptedCBC; } // Reverse the encrypted content and re-encrypt it, to cause any plaintext change to propagate // to every bit of output. This should theoretically provide at least a little hardening against // a chosen-plaintext attack. We can always use CTS mode here, as the content at this point is // guaranteed to be long enough for CTS mode (a previous CBC encryption would have expanded it). InnerPayload.Reverse(); InnerPayload = Alg.CTSEncrypt(InnerPayload.ToArray()).ToList(); // Concatenate the message flags, content, and validation HMAC and return the result encoded // in a format appropriate for use in a URL QueryString. List <byte> Final = new List <byte>(); Final.Add((byte)Flags); Final.AddRange(InnerPayload); Final.AddRange(Final.ToArray().GetHMACSHA1Bytes(Key)); return(Final.ToArray().Base64WebSafeEncode()); }