home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 5 - Maintaining State / Protecting Client-Side Information
Encrypting Client-Side State Information

Message authentication checks implement a "look but don't touch" policy. Users can't modify the state information, but they can still see what's there. In many web applications, there's no harm in this, but with the hangman game it has the unwanted consequence that the user can peek at the unknown word, either by viewing the page source in the fill-out form version or by quitting the browser and viewing the cookie database file.

To prevent this from happening without abandoning client-side storage entirely, you can encrypt the state information. Your application will have the secret key necessary to decrypt the information, but without launching an expensive cryptanalysis project (and maybe not even then) the user won't be able to get at the data. Encryption can be combined with a MAC in order to obtain truly bulletproof client-side authentication.

Example 5-4 shows the hangman game code modified to save its state using encrypted cookies. It takes advantage of a recently introduced Perl module called Crypt::CBC. This implements the Cipher Block Chaining encryption mode and allows the encryption of messages of arbitrary length (previously only block-mode ciphers were available for Perl, which force you to encrypt the message in rigid 8-byte units). Crypt::CBC must be used in conjunction with a block-mode cipher, either Crypt::DES or Crypt::IDEA. The former implements the popular U.S. Data Encryption Standard encryption algorithm, while the latter implements the newer and stronger International Data Encryption Algorithm. You can download these modules from CPAN.4

To save space, we again show just the changes to the basic hangman script that are needed to encrypted state information.

use MD5 ();
use Crypt::CBC ();
# retrieve the state
$CIPHER ||= Crypt::CBC->new(SECRET, 'IDEA');
my $state = get_state() unless param('clear');

At the top of the script we now bring in the Crypt::CBC module, as well as MD5. We then create a Crypt::CBC object and store it in a new global variable called $CIPHER. The Crypt::CBC::new() method takes two arguments: our secret key and the name of the block algorithm to use for encryption. We use the IDEA algorithm here because it is much harder to crack than the older DES. When we initialize the $CIPHER variable we take advantage of the persistence of Apache API scripts. The ||= assignment guarantees that a new Crypt::CBC object will be created only if $CIPHER does not previously exist. This reduces the amount of computation the script has to do at startup time.

The actual encryption and decryption is performed in save_state() and get_state().

# Save the current state
sub save_state {
   my $state = shift;
   MAC($state, 'generate');  # add MAC to the state
   # encrypt the cookie
   my $encrypted = $CIPHER->encrypt_hex(join ':', %{$state});
   return CGI::Cookie->new(-name => COOKIE_NAME,
                           -value => $encrypted,
                           -expires => '+1M');
}

In save_state(), we generate the MAC as before but add an additional step. We first serialize the state hash reference by joining its keys and values with the : character (we chose this character because we know that it never occurs in the state information). We next call the $CIPHER object's encrypt_hex() method to encrypt the serialized information and store it in a variable named $encrypted. encrypt_hex() first performs the encryption and then converts the encrypted information into a printable hexadecimal string. This encrypted string is then turned into an HTTP cookie named Hangman4.

# Retrieve an existing state
sub get_state {
   my $cookie = cookie(COOKIE_NAME);
   return undef unless $cookie;
   # decrypt the cookie
   my %state = split ':', $CIPHER->decrypt_hex($cookie);
   authentication_error() unless MAC(\%state, 'check');
   return \%state;
}

The get_state() subroutine performs the corresponding decryption of the data. It retrieves the cookie, decrypts it by calling the $CIPHER object's decrypt_hex() method, and turns it back into a hash by splitting on the : character. We then check the MAC as before and return the state information to the caller.

If the user were to peek at his cookies file, he'd see something like this (some of the fields have been removed for the sake of simplicity):

www.modperl.com /perl/state/ Hangman4 5e650600dc0fac462d0d86adf3c 5d7e5fc46a5b2991b10093b548fafacc7d50c48923cdcb375a703f1e3224dfa98455 360f2423a0e6a95ccf791731e2946faef347c0b1f4ef6e5893cab190a2b0772c40bf ce32d7a5ce8a74e2fc65cdc7d5b5a

The long hexadecimal string following the cookie's name is the encrypted information. The user cannot access the data contained in this string, nor can he make any changes to it. Any change to the string will cause a section of the data to decrypt incorrectly, making the MAC check fail.

Note that you can use this technique to encrypt the contents of fill-out fields as well, allowing you to store client-side information securely even when the user has set the browser to refuse cookies.

The amount of state information stored by the hangman script is relatively modest. Therefore, there isn't significant overhead either from the encryption/decryption process or from the transmission of the encrypted information across the network. The Hangman4 script has the same subjective response rate as the unencrypted scripts. If the amount of state information were to grow quite large, however, the encryption overhead might become noticeable. Another thing to watch out for is the size of the cookie; the maximum size a browser can store is about 4 KB. With large amounts of state information, you might consider compressing the state data before encrypting it. The Compress::Zlib module, which we used in the previous chapter, makes this convenient. Be sure to compress the data before you encrypt it. Encrypted data is notoriously uncompressable.

Example 5-4. The Hangman Game with Encryption of Client-Side Data

#!/usr/local/bin/perl
# file: hangman4.pl
# hangman game using encrypted cookies to save state
use IO::File ();
use CGI qw(:standard);
use CGI::Cookie ();
use MD5 ();
use Crypt::CBC ();
use strict;
use vars '$CIPHER';
use constant WORDS => '/usr/games/lib/hangman-words';
use constant ICONS => '/icons/hangman';
use constant TRIES => 6;
use constant COOKIE_NAME => 'Hangman4';
use constant SECRET => '0mn1um ex 0vum';
# retrieve the state
$CIPHER ||= CBC->new(SECRET,'IDEA');
my $state = get_state() unless param('clear');
. . . everything in the middle remains the same . . .
# Save the current state
sub save_state {
   my $state = shift;
   MAC($state,'generate');  # add MAC to the state
   # encrypt the cookie
   my $encrypted = $CIPHER->encrypt_hex(join(':',%{$state}));
return CGI::Cookie->new(-name=>COOKIE_NAME, -value=>$encrypted, -expires=>'+1M'); }
# Retrieve an existing state
sub get_state {
   my $cookie = cookie(COOKIE_NAME);
   return undef unless $cookie;
   # decrypt the cookie
   my %state = split ':', $CIPHER->decrypt_hex($cookie);
   authentication_error() unless MAC(\%state, 'check');
   return \%state;
}

Footnotes

3 As this book was going to press, Gisle Aas had released a Digest::HMAC module which implements a more sophisticated version of this algorithm. You should consider using it for highly sensitive applications.

4 Be aware that some countries regulate the use of cryptography. For example, cryptography is illegal in France, while the United States forbids the export of cryptographic software beyond its territorial borders. If you are living outside the U.S., don't download Crypt::DES or Crypt::IDEA from an American CPAN site. Use one of the European or Asian mirrors instead ;-). At the time this was written, the Crypt::DES and Crypt::IDEA modules required the gcc compiler to build correctly. Hopefully, this will have changed by the time you read this.    Show Contents   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.