Beispiel #1
0
        /// <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
            });
        }
Beispiel #2
0
        /// <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());
        }