/// <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()); }