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.
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.