Sunday, 28 February 2010

An issue with HttpCookie using HasKeys property and encoded single value string

Leave a Comment
In this post I would like to share my experiance with HttpCookie object used in some e-commerce application.
A web application containts the cookie implementation, which allows customers to persists thier contact details on the client's side using cookies. That code was implemented by third-party web-developers using a well-known HttpCookie class from the "System.Web" assembly. The provided functionality seemed working as expected untill some customers have sent us complains about unability to persist thier contact details for the future shopping needs. The problem was quite hard to identify. In many cases the coookie was persisted without any problem. Eventually, after digging into HttpCookie code implementation, we have found that the HasKeys property was responsible for all these troubles. The HttpCookie object, in our code, is created and stored with single value which is encoded using Base64 scheme as a standard for transmitting data on the Web. Below is the sample code to demonstrate the case:

First we create the cookie and store it on the customer's machine.

// Build a string from the Form values
            string valuesToPersist = "FirstNameVal=" + TextBoxFirstName.Text + "&SecondNameVal=" + TextBoxSecondName.Text;
            // Encode the plain text using Base64 scheme
            byte[] dBytes = Encoding.UTF8.GetBytes(valuesToPersist);
            string strEncoded = Convert.ToBase64String(dBytes);
            // Create sample single-value cookie
            HttpCookie sampleCookie = new HttpCookie("CookieExample");
            sampleCookie.Value = strEncoded;
            sampleCookie.Expires = DateTime.Now.AddDays(7);

Then on the customers's return the cookie should be retrieved and its value dislayed on the page.

// Retrieve available cookies
            HttpCookie cookie = Request.Cookies.Get("CookieExample");
            // Check whether a cookie has subkeys and populate the form with retrieved values
            if (cookie.HasKeys)
                string storedValue = cookie.Value;
                // Decript the value (decription ommited here)  and split into the array
                string[] cookieValues = cookie.Value.Split('&');
                // Populate Form with 
                TextBoxFirstName.Text = cookieValues[0];
                TextBoxSecondName.Text = cookieValues[1];

I know that the property HasKeys is supposed to be used when the cookie stores multipe values (read more on this on MSDN).
If we look at the implementation of HttpCookie, then we can see that HasKeys property allows us to check and single value as well. So the above statement is correct but it is producing our problem.

If we look at the implementation code of the HttpCookie in Reflector, for this statement, then we can see that cookie object checks its Values collection which is of type NaveValueCollection:

               public bool HasKeys
                        return this.Values.HasKeys();

And to follow the implementation of the Values member, we can see that it populates the multiValue collection from the stringValue field, which holds our encoded string:

public NameValueCollection Values
                        if (this._multiValue == null)
                            this._multiValue = new HttpValueCollection();
                            if (this._stringValue != null)
                                if ((this._stringValue.IndexOf('&') >= 0) || (this._stringValue.IndexOf('=') >= 0))
                                    this._multiValue.Add(null, this._stringValue);
                                this._stringValue = null;
                        this._changed = true;
                        return this._multiValue;

Now it becomes obvious that the _multiValue collection populated from the _stringValue (single value) if our encoded string contains eigher ‘&’ or “=”. Then the HasKeys value set to true. Otherwise it returns false. Does our encoded string contain any “&” or “=”? Yes, the “=” sometimes presented at the end of the string. And that’s why the cookies are persisted ok sometimes. This is where some explanation about Base64 encoding scheme is required.

Base64 encoding scheme encodes arbitrary binary data as a string composed from a set of 64 characters (see the Base-64 Alphabet image).
The character "=" is used for the padding. This character is the main problem in our cookies issue.

The Base64 encoding schema is used on the Web because of efficiency. It only uses 64 characters. Thus any of the characters can be represented with just 6 bits. (for more explanation on this here:)

The Base64 refers to a specific MIME content transfer encoding(more details here). The transformation takes place as a sequence of octets to get a format that can be expressed in short lines of 6-bit characters, as required by transfer protocols such as SMTP.
So input data used in the sequence of octets which represented as 3x8 (24bits) buffer that can be used 6-bits at a time (4 chars). The first byte is placed in the most significant eight bits of the 24-bit buffer, the next in the middle eight, and the third in the least significant eight bits. The remaining buffer bits (if any left) will be zero. The process is repeated on the remaining data until fewer than four octets remain. If three octets remain, they are processed normally. If fewer than three octets (24 bits) are remaining to encode, the input data is right-padded with zero bits to form an integral multiple of six bits. if two octets of the 24-bit buffer are padded-zeros, two "=" characters are appended to the output; if one octet of the 24-bit buffer is filled with padded-zeros, one "=" character is appended. This guarantees that the decoder will correctly reconstructed data and encoded output length is a multiple of 4 bytes.

The output, in some cases will be like one below with padded chars at the end:
string strEncoded = "FZpZVzcz1zZXJnZXklNDByeWFuYWlyLmNvbQ==";

Or like that:
string strEncoded = "c1ZpZXdfQ29udGFjdElucHU0MHJ5YW5haXIuY29t";                

So, in order to make our code works in all cases we need to remove the check for subkeys and use cookies.value field.

That is it. I hope you found my article useful and it saved you some of debugging time.