Storing Session State on the Client: Design Details

The custom tag library looks in the request scope for Java objects that implement java.io.Serializable. The Java objects are serialized using the Java serialization APIs and encoded into base64 encoding. We chose to use base64 encoding because it is HTTP friendly and well understood. This strategy is not limited to base64 encodings or encoding only the Java objects in the request scope. The strategy titled Securing Client-Side State discusses how to prevent a malicious client from manipulating this state.

<cs:client-state action="word" beanName="sampleBean" secure="true">
<table>
<c:forEach var="word" varStatus="status" begin="0" items="${sampleBean.words}">
<tr>
<td>
<input type="checkbox" name="word_${status.index}" value="${word}">
${word}
</input>
</td>
</tr>
</c:forEach>
<tr>
<td>
New Word: <input type="text" name="newWord" size="25"/>
</td>
</tr>
<tr>
<td>
<input type="submit" value="Update List"/>
</td>
</tr>
</table>
</cs:client-state>

Code Example 1: Using Tag for Client-Side State

Code Example 1 above shows a custom tag library "client-state" mapped to the prefix cs, which is responsible for the conversion of hidden form variables in the request scope and URL parameters to the base64 representation above. Code Example 2 illustrates the HTML generated by this tag:

<form method="POST" action="word">
<input type="hidden" name="beanName" value="sampleBean" />
<input type="hidden" name="sampleBean" value="rO0ABXNyAC5jb20uc3VuLmoyZWUuYmx1ZXByaW50cy5jbGllbnRzdGF0ZS5TYW1wbGVCZWFuhCyFzCR588MCAAJMAAhuZXh0UGFnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABXdvcmRzdAAVTGphdmEvdXRpbC9BcnJheUxpc3Q7eHBwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAJ3BAAAAAp0AARncmVndAAFaW5kZXJ4" />
<table>
<tr>
<td>
<input type="checkbox" name="word_0" value="greg">
greg
</input>
</td>
</tr>
<tr>
<td>
<input type="checkbox" name="word_1" value="inder">
inder
</input>
</td>
</tr>
<tr>
<td>
New Word: <input type="text" name="newWord" size="25"/>
</td>
</tr>
<tr>
<td>
<input type="submit" value="Update List"/>
</td>
</tr>
</table>

</form>

Code Example 2: Generated HTML for the Client-Side State Tag

The form above is mapped to the URI word as was specified in the action attribute by the client-state tag. The URI word is mapped to a servlet that calls a utility class that looks at the form value of the parameter beanName, which is sampleBean. The value of the hidden form field with the name attribute sampleBean contains the base64 encoded serialized content. If the client-state tag secure attribute is set to true, the content is also encrypted.

This design has some limitations that include the following:

Storing state on the client has many advantages. Using the custom tag strategy to encode data and a servlet to decode state is a good a re-usable means of providing state management and utilizing the client to store state. Storing state on the client takes the burden off the server, allowing the server to support more clients.

Securing Client-Side State

The client-side state is secured by encrypting and attaching a message authentication code (MAC) to it. Code Example 3 illustrates how the encryption is done for the data stored in the hidden form fields:
 // generate a key that can be used for encryption from the supplied password
byte[] rawKey = convertPasswordToKey(password);
// choose block encryption algorithm using a utility method
Cipher cipher = getBlockCipherForEncryption(rawKey);
// encrypt the plaintext
byte[] encdata = cipher.doFinal(plaindata);
// choose mac algorithm
Mac mac = getMac(rawKey);
// generate MAC for the initialization vector of the cipher
byte[] iv = cipher.getIV();
mac.update(iv);
// generate MAC for the encrypted data
mac.update(encdata);
// generate MAC
byte[] macBytes = mac.doFinal();
// concat byte arrays for MAC, IV, and encrypted data
// Note that the order is important here. MAC and IV are
// of fixed length and need to appear before the encrypted data
// for easy extraction while decrypting.
byte[] tmp = concatBytes(macBytes, iv);
byte[] securedata = concatBytes(tmp, encdata);
byte[] outputdata = Base64.encode(securedata);

Code Example 3: Encrypting Data to Store in Hidden Form Fields

The encryption process initializes the cipher (encryption algorithm) with a key that is generated from the password. During this initialization process, the cipher generates an initialization vector (IV). This initialization vector is a random number that is used by the cipher to ensure that different outputs are generated for the same input during different runs of the encryption algorithm. The IV is not a secret, and is kept iwith the encrypted contents. To make the encrypted content and the IV tamper-resistant, a MAC is generated from MAC and IV. Note that the MAC also uses the initialization vector to ensure that a different MAC is generated for the same data during different runs of the MAC algorithm. The output (securedata in the code example 3) consists of MAC, followed by IV, and the encrypted data. MAC and IV are kept before the encrypted data because they are of fixed length, and hence can be extracted easily during the decryption process. 

The output of the encryption process is converted into text by using base64 encoding. This text is then stored in the hidden form fields in the HTML that is sent to the client. Sequence Diagram 1 below shows how server-side state of the SampleBean is encrypted at the server-side and then stored on the client.

Sequence Diagram 1: Encrypting State to Store on the Client

When the client submits the form, the hidden fields are extracted and are first converted to byte arrays by decoding the base64 encoding. The contents are then decrypted after ensuring that no tempering was done by verifying the MAC. Code Example 4 illustrates how this is done.

 byte[] hiddenfield = // data obtained from the hidden form field
byte[] securedata = Base64.decode(hiddenfield);
// Extract MAC
byte[] macBytes = new byte[MAC_LENGTH];
System.arraycopy(securedata, 0, macBytes, 0, macBytes.length);
// Extract initialization vector used for encryption
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(securedata, macBytes.length, iv, 0, iv.length);
// Extract encrypted data
byte[] encdata = new byte[securedata.length - macBytes.length - iv.length];
System.arraycopy(securedata, macBytes.length + iv.length, encdata, 0, encdata.length);
// verify MAC by regenerating it and comparing it with the received value
byte[] rawKey = convertPasswordToKey(password);
Mac mac = getMac(rawKey);
mac.update(iv);
mac.update(encdata);
byte[] macBytesCalculated = mac.doFinal();
if (Arrays.equals(macBytes, macBytesCalculated)) {
// decrypt data only if the MAC was valid
Cipher cipher = getBlockCipherForDecryption(rawKey, iv);
byte[] plaindata = cipher.doFinal(encdata);
return plaindata;
}

Code Example 4: Decrypting the Data Received From the Hidden Form Fields in a POST

Sequence Diagram 2 below shows how the client-side state is extracted at the server side. The data is updated and re-encrypted and sent back to the client.

Sequence Diagram 2: Decrypting State Stored on the Client

The client-state tag action attribute in index.jsp is mapped to the URI word (see Code Example 2 for more details). The URI word is mapped to a servlet WordServlet which calls the utility class ClientStateDeserializer to decode and decrypt the value of hidden form field containing the sampleBean. The value of the parameter sampleBean is reconsititued by first decoding the base64 encoded content to a byte array and then decrypting the content by calling the decrypt method in ByteArrayGuard. The reconsititued SampleBean object is placed in a request scope attribute with the name sampleBean. The WordServlet then updates the state of the bean and forwards the request back to the index.jsp page. The index.jsp page includes a client-state tag that is mapped to ClientStateTag which encrypts the data using ByteArrayGuard, encodes it in base64, and returns it back to the page as a hidden form field to the client.

The encryption and the decryption routines require the use of a password that is known and used only by the server. In this solution, we use a different password for each web user. This password should not be generated based on any input from the web user because that can compromise its security. Because typical web applications allow self-registration for users, it is impractical to involve the web site administrators in choosing the passwords. Therefore, we autogenerate the passwords using a password generation strategy that is configurable. The solution ships with a SessionPasswordStrategy that generates the passwords randomly and stores them in the HttpSession.

The disadvantage of this strategy is that the client-side state is lost when the session expires. This problem can be addressed by implementing an alternate strategy that stores the passwords in a database.

References

For more information about this topic, refer to the following:

© Sun Microsystems 2005. All of the material in The Java BluePrints Solutions Catalog is copyright-protected and may not be published in other works without express written permission from Sun Microsystems.