Version 2

    This article discusses using an Interceptor to automatically generate MAC digests for saved and updated records. This provides a mechanism to identify database records created or modified by unauthorized and presumably malicious parties.

     

    In an ideal world, we would all have correctly configured and bug-free databases and servers and no unauthorized user could ever modify the contents of our database. In the real world, this is a significant problem that must sometimes be explicitly considered due to legal requirements, etc.

    One of the best ways to detect modification of our database is to store a MAC message digest as part of the record. Unlike simple digests such as CRC-32, MD5 or SHA1 which can be easily computed by any knowledgeable but malicious user, MAC digests can only be calculated by somebody who knows a secret key.

     

    We begin by defining the interface Digestable.  While all of the work could be done in the Interceptor, putting the computeDigest method in the interface allows us to work around situations where an object retrieved from the database is not exactly the same as the object saved.

    /**
     * Interface that indicates that MAC digest should be computed for
     * implementing classes.  These classes must also define a String field
     * named 'digest' that will hold the digest.
     */
    public interface Digestable {
    
       /**
        *  Compute the MAC digest of this object.
        */
       String computeDigest(java.security.Key key);
    }
    

     

    Alternately, a base class may be defined that provides a reasonable default implementation of the digest function:

    import java.io.ByteArrayOutputStream;
    import java.io.ObjectOutputStream;
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    
    import java.security.Key;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    
    import javax.crypto.Mac;
    import javax.crypto.SecretKey;
    
    import sun.misc.BASE64Encoder;
    
    /**
     * Base class for all classes that will have MAC digest computed by
     * hibernate interceptor.
     * <p>
     * The default digest function uses Java serialization - easiest for
     * us to implement, but it's harder to generate MAC digest from database
     * entries alone (e.g., when modifying an existing database to include
     * digests).  Some people may prefer to compute a digest on the 'toString()'
     * method, or an explicit concatenation of the critical fields.
     * <p>
     * Finally, the digest function must return the same value if an object
     * will be changed in the process of being saved to the database and
     * restored.  This can happen when floating point numbers are truncated,
     * empty strings are replaced with null strings, etc.
     */
    public class Digestable extends java.io.Serializable {
    
       private transient String digest;
    
       /**
        * Compute MAC digest of this object using standard
        * Java serialization tools.  This requires the digest
        * field to be transient so it is not included in the
        * serialization.
        *
        * @param key secret key previously generated
        *
        * @throws InvalidKeyException
        * @throws NoSuchAlgorithmException
        * @throws UnsupportedEncodingException
        * @throws IOException
        */
       public String computeDigest(Key key) throws Exception {
          Mac mac = Mac.getInstance(key.getAlgorithm()).init(key);
          ByteArrayOutputStream bs = new ByteArrayOutputStream();
          ObjectOutputStream os = new ObjectOutputStream(bs);
          os.writeObject(this);
          os.flush();
          return new BASE64Encoder().encode(mac.doFinal(bs.toByteArray());
       }
    }
    

     

    Our Interceptor is now straightforward:

    import net.sf.hibernate.*;
    import net.sf.hibernate.type.Type;
    import java.security.Key;
    
    /**
     * Hibernate interceptor that transparently computes MAC digest 
     * of the Digestable objects as they are written
     * to the database.
     */
    public class MacInterceptor implements Interceptor {
    
       /** name of field containing digest */
       private final static String DIGEST_NAME = "digest";
    
       /** MAC key */
       private final transient Key key;
    
       /**
        * Constructor
        */
       public MacInterceptor(Key key) {
          this.key = key;
       }
    
       /**
        * Method called when an existing record is updated.
        */
       public boolean onFlushDirty (
             Object entity,
             Serializable id,
             Object[] currentState,
             Object[] previousState,
             Object[] propertyNames,
             Type[] types)
          throws CallbackException {
    
          boolean result = false;
          if (entity instanceof Digestable) {
             Digestable d = (Digestable) entity;
             for (int i = 0; i < propertyNames.length; i++) {
                if (propertyNames[i].equals(DIGEST_NAME)) {
                   try {
                      currentState[i] = d.computeDigest(key);
                   } catch (Exception e) {
                      throw new CallbackException("error computing digest", e);
                   }
                   result = true;
                }
             }
          }
    
          return result;
       }
    
    
       /**
        * Method called when a new record is saved.
        */
       public boolean onSave(
             Object entity,
             Serializable id,
             Object[] state,
             String[] propertyNames,
             Type[] types)
          throws CallbackException {
    
          boolean result = false;
          if (entity instanceof Digestable) {
             Digestable d = (Digestable) entity;
             for (int i = 0; i < propertyNames.length; i++) {
                if (propertyNames[i].equals(DIGEST_NAME)) {
                   try {
                      state[i] = d.computeDigest(key);
                   } catch (Exception e) {
                      throw new CallbackException("error computing digest", e);
                   }
                   result = true;
                }
             }
          }
    
          return result;
       }
    
       // rest of methods elided...
    }
    

     

    One missing feature is the ability to check the MAC digest of objects as they are loaded. The only solutions I have identified are to either instantiate the object within onLoad so that the digest can be computed and compared, or to implement Validatable so that it computes and compares the digests. The former duplicates effort, the latter complicates the application since it forces each setter to update the digest.


    Sidebar: how do we generate MAC keys?  A new key can be generated with

    import javax.crypto.KeyGenerator;
    import javax.crypto.SecretKey;
    
    ...
    SecretKey key = KeyGenerator.getInstance("HmacMD5").generateKey();
    

     

    This key should be stored in a KeyStore with a password.  Applications can then retrieve the Key with a JNDI-provided key name and password -- for obvious reasons the unencrypted key cannot be stored in the hibernate database!