Encrypted Preferences in Java

If you're using the Preferences API in Java, this encryption strategy lets you hide your preferences data in plain sight.


October 01, 2002
URL:http://www.drdobbs.com/database/ternary-search-trees/database/encrypted-preferences-in-java/184416587

The Preferences API is now included in the core Java release, as of Version 1.4. It provides a simple, fully cross-platform mechanism for storing small amounts of data, and uses a simple hierarchical "name/value" structure for organizing data. It is intended to be used for configuration and preference data.

Preference data is stored differently on each platform. In fact, is entirely up to each Java implementation how it will store the actual data that is, what backing store it will use. In general, the backing store is not intended to be secure. For example, it may be implemented on top of the Registry, or some other storage facility that does not provide a way to hide sensitive data.

This article considers the technique of automatically encrypting data before storing it in the preferences database. This permits applications to use the Preferences API even for sensitive data, such as passwords and personal information.

What You'll Learn

This article is not intended to provide a tutorial on encryption. It is assumed that you already understand how encryption in Java works, or that you are willing to learn about it elsewhere.

This article focuses on integrating encryption techniques with the Preferences API. We won't focus on the many encryption algorithm options — we'll use a simple DES key to perform encryption and decryption, with the understanding that you may well want to replace this approach with another Java-based encryption method.

The most important aspect of this technique is making the encryption transparent. We want the encryption to happen behind-the-scenes, with as little intervention as possible. As you'll see, we'll be creating an EncryptedPreferences object, which acts just like a regular Preferences object except that it transparently takes care of encryption for us.

If you haven't ever used the Preferences API, don't worry. You'll pick up what you need to know along the way.

A Simple Test Program

Before we get into the details of how it all works, let's take a look at a simple test program. This program (pkg.Test) stores a couple of values in the preferences database.

Preferences root =
  Preferences.userNodeForPackage( Test.class );

root.put( "not", "encrypted" );

Preferences subnode = root.node( "subnode" );
subnode.put( "also not", "encrypted" );

root.exportSubtree( System.out );

You can find the full source to pkg.Test in Listing One.

The first two lines acquire a Preferences object for this program, "Test.class." Or rather, for the package it's contained in, "pkg." Remember, each package gets its own private area within the preferences database. The userNodeForPackage() method gets the Preferences object for our private area. This is the root node of the area in which we will store data.

Listing One: A simple test program. It stores a value in the preferences database in the root node for its package ("pkg"), and another value in a subnode of the root node.
// $Id$

package pkg;

import java.util.prefs.*;

public class Test
{
  static public void main( String args[] ) throws Exception {
    Preferences root = Preferences.userNodeForPackage( Test.class );

    root.put( "not", "encrypted" );

    Preferences subnode = root.node( "subnode" );
    subnode.put( "also not", "encrypted" );

    root.exportSubtree( System.out );
  }
}

The next line stores a value— or rather, a key/value pair. The key is "not," and the value is "encrypted." Later on, you can ask for the value corresponding to the key "not," and you'll get back the value "encrypted."

The next two lines create a subnode of our main node. Into this subnode, we put another key/value pair. The key is "also not," and the value is "encrypted."

Finally, we take a look at what we've done by exporting the entire database— that is, the entire database for our program. While the backing store might store data in any format, the exported data always uses the same format, which you can see in Listing Two.

Listing Two: The preference data for our sample program pkg.Test, exported in XML format.
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE preferences SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>

<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="user">
    <map />
    <node name="pkg">
      <map>
        <entry key="not" value="encrypted" />
      </map>
      <node name="encrypted">
        <map>
          <entry key="mjdajaddcioehjeljmahmpdbfhomifhp" value="hknhbmkdphkpbjipdijphpniboiecadn" />
        </map>
        <node name="subnode">
          <map>
            <entry key="eempdaneimckpiod" value="bkoaejfbcjpkckmflkijoomngbopblco" />
          </map>
        </node>
      </node>
      <node name="subnode">
        <map>
          <entry key="also not" value="encrypted" />
        </map>
      </node>
    </node>
  </root>
</preferences>

If the data is being stored in the Registry, you can see it by using regedit. In my system, the preferences data is stored in \HKEY_CURRENT_USER\Software\ JavaSoft\Prefs\pkg, as you can see in Figure 1.

Trying It with Encryption

Figure 1: The results of running pkg.Test

Using encrypted preferences is easy. Here's the encrypted version, pkg.encrypted.EncryptedTest, which does the same thing as pkg.Test, except that it uses encryption:

Preferences root =
  EncryptedPreferences.userNodeForPackage(
    EncryptedTest.class, secretKey );

root.put( "transparent", "encryption" );

Preferences subnode = root.node( "subnode" );
subnode.put( "also", "encrypted" );

root.exportSubtree( System.out );

You can find the full source to pkg.encrypted.EncryptedTest in Listing Three.

Listing Three: A simple test program, this time using encryption. It does more or less the same thing as the program in Listing One, except that this variant uses an Encrypted Preferences object, which transparently encrypts the data before storing it, and decrypts it before retrieving it.
// $Id$

package pkg.encrypted;

import java.security.*;
import java.util.prefs.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import ep.*;
import pkg.Util;

public class EncryptedTest
{
  static private final String algorithm = "DES";

  static public void main( String args[] ) throws Exception {
    byte rawKey[] = Util.readFile( "key" );
    DESKeySpec dks = new DESKeySpec( rawKey );
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( algorithm );
    SecretKey secretKey = keyFactory.generateSecret( dks );

    Preferences root =
      EncryptedPreferences.userNodeForPackage(
        EncryptedTest.class, secretKey );

    root.put( "transparent", "encryption" );

    Preferences subnode = root.node( "subnode" );
    subnode.put( "also", "encrypted" );

    root.exportSubtree( System.out );
  }
}

The most important thing to see here is that instead of using the Preferences.userNodeForPackage() method, we're using the EncryptedPreferences.userNodeForPackage() method. And this method returns an EncryptedPreferences, rather than a regular Preferences object.

Our EncryptedPreferences object, which lives in the package "ep," is a subclass of AbstractPreferences. This means it can serve in the same capacity as the object you get from Preferences.userNodeForPackage()— it's a drop-in replacement. And this is clear from the listing above— it does just what the unencrypted version did, except for storing different strings. It also exports the subtree in the same way, producing a similar dump (see Listing Four).

Listing Four: The transparently encrypted preference data for our sample program pkg.encrypted.EncryptedTest, exported in XML format.
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE preferences SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>

<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="user">
    <map />
    <node name="pkg">
      <map>
        <entry key="not" value="encrypted" />
      </map>
      <node name="encrypted">
        <map>
          <entry key="mjdajaddcioehjeljmahmpdbfhomifhp" value="hknhbmkdphkpbjipdijphpniboiecadn" />
        </map>
        <node name="subnode">
          <map>
            <entry key="eempdaneimckpiod" value="bkoaejfbcjpkckmflkijoomngbopblco" />
          </map>
        </node>
      </node>
      <node name="subnode">
        <map>
          <entry key="also not" value="encrypted" />
        </map>
      </node>
    </node>
  </root>
</preferences>

Another important thing to notice is that this variant of userNodeForPreferences() takes an additional argument: a DES key.

Our Encryption Model

As mentioned, we're not going to go into much detail about encryption, since there are many different algorithms and no single algorithm is right for everything. To get our example working, we'll be using the DES algorithm and generate a DES key, which will just be stored in a file. Although this works, it isn't recommended in practice unless you can be sure that the file can be kept secret from prying eyes.

You can use pkg.GenerateKey to generate the key file, supplying the name of the destination file on the command line. For convenience, just run generatekey.bat to generate the key file.

EncryptedTest reads the key from the file. It converts the raw data from the file into a SecretKey object, as follows:

byte rawKey[] = Util.readFile( "key" );
DESKeySpec dks = new DESKeySpec( rawKey );
SecretKeyFactory keyFactory =
  SecretKeyFactory.getInstance( algorithm );
SecretKey secretKey =
  keyFactory.generateSecret( dks );

You can find the full source to pkg.encrypted.EncryptedTest in Listing Three.

This SecretKey is required by the EncryptedPreferences.userNodeForPackage() method. Thus, once you've acquired a SecretKey and passed it to that method, you don't have to worry about encryption again— it happens automatically and transparently.

The Encrypted Data

If you run EncryptedTest instead of Test and then look at the registry in regedit, you'll see something different; see Figure 2. Instead of the key "transparent," you'll see the key "mjdajaddcioehjeljmahmpdbfhomifhp." And instead of the value "encrypted," you'll see "hknhbmkdphkpbjipdijphpniboiecadn." These gibberish strings are the encrypted forms of the strings that our test program stored. Well, actually they aren't just encrypted— they're also encoded. Since the Preferences API is fundamentally string based, we need to encode our encrypted data as Strings. But bugs can result if the default charset doesn't encode all our bytes faithfully, so to be as sure as possible, we encode the raw encrypted data using characters that exist in all charsets. Each nybble (4-bit sequence) in our data is mapped to a different character from 'a' to 'p.'

Figure 2: The results of running pkg.encrypted.EncryptedTest.

It's important to recognize that you can't mix encrypted data and unencrypted data in the same node of the preferences database. This means that each package will have to decide independently whether it wants to use encryption for its data, and then call Preferences.userNodeForPreferences() or EncryptedPreferences.userNodeForPreferences() accordingly. As you'll see, an encrypted preferences Node transparently encrypts its own data, as well as data in any of its subnodes. Keep this in mind when planning out your application.

The Preferences Architecture

The java.util.prefs package is structured around two crucial classes: Preferences and AbstractPreferences. Despite the names, Preferences is actually more abstract than AbstractPreferences— that is, AbstractPreferences is a subclass of Preferences, and implements some of the required methods.

Generally speaking, you don't need to subclass either of these classes. If you're just using the Preferences API to store and retrieve data, you call Preferences.userNodeForPackage() (or a similar method), you get a Preferences object, and you use it. Actually, your Preferences object is really something else. Under Windows, it's a WindowsPreferences object. Under Linux, on the other hand, it's a FileSystemPreferences object because the preferences data is stored in the filesystem in an XML-formatted file. These classes are subclasses of AbstractPreferences. See Figure 3 for the relationships between these classes.

Figure 3: Inheritance relationships between classes in the java.util.prefs package.

Normally, you don't need to subclass these classes, but our program isn't normal. We're seeking to modify the underlying implementation, so we need to create our own subclass of AbstractPreferences. AbstractPreferences provides you the option to override only nine methods, the so-called Service Provider Interface (SPI) methods. It implements all other necessary methods in terms of these methods, so you only have to deal with these nine. They are as follows:

Thus, any subclass that provides these nine also winds up implementing everything else necessary for being a full-fledged Preferences node. All other methods use these nine to actually read and write data. This is represented schematically in Figure 4.

Figure 4: Subclassing AbstractPreferences, and the role of the nine SPI methods.

We override these methods to implement encryption. We'll see exactly how in the next section.

How EncryptedPreferences Works

In fact, we don't just have one subclass of AbstractPreferences, we have four of them. More precisely, we have a four-layer inheritance hierarchy— or five, if you include Abstract Preferences. This hierarchy is shown in Figure 5. This is a pretty complicated hierarchy, so we'll have to spend some time justifying it.

Figure 5: Our inheritance hierarchy.

Let's consider our main requirements. First, we want to use a regular Preferences object to store our encrypted data. That is, when writing values, we want a special, custom Preferences object to take care of the encryption, and then pass the encrypted data on to a regular Preferences object. Likewise, when reading values, we want to read them from a regular Preferences object, decrypt them, and return them to the caller. In short, we want to use delegation. This way, the encryption acts like a filter on the key/value data.

Once we've created an EncryptedPreferences object for a particular node, we want all children of that node to be encrypted as well. These requirements provide a natural division of labor into four classes:

  1. Delegation— modifying requests to read or write and passing them to another Preferences object.
  2. Wrapping-'we want any subnode of a custom node to also be custom.
  3. Obfuscation— this implements a filter, whereby each key and value is first modified ("obfuscated") before being stored.
  4. Encryption— the encryption process is implemented as a special case of obfuscation.

Of course, it would be possible to implement all four of these pieces in a single class, with all the logic merged together. But it should be considered good programming practice to divide it into pieces because the pieces are useful on their own.

The next four sections will consider each of these pieces, both as a part of the encryption process and as a useful class that could fit well into other programs.

Delegation

DelegatedPreferences is a class that implements each of the nine SPI methods by calling that same method on another AbstractPreferences object. That is, it delegates the implementation to another object. This is necessary to our encryption system because we want our encryption system to make use of a regular AbstractPreferences object. The advantage of this is that it only has to worry about the encryption— it delegates the rest of the work to another object. This greatly reduces the complexity of the EncryptionPreferences object. The DelegationPreferences object can be used in any other system that required this kind of limited modification to the preferences functionality. You'll find the source code to DelegatedPreferences in Listing Five.

Listing Five: The source for DelegatedPreferences, which is responsible for delegating SPI method calls to another Preferences object.
// $Id$

package ep;

import java.util.prefs.*;

public class DelegatedPreferences extends AbstractPreferences
{
  private AbstractPreferences target;
  static private final boolean verbose = false;

  protected DelegatedPreferences( AbstractPreferences parent, String name,
      AbstractPreferences target ) {
    super( parent, name );
    this.target = target;
  }

  protected String getSpi( String key ) {
    if (verbose) {
      System.out.println( "DP["+target+"]:getSpi( "+key+" )" );
    }

    return target.get( key, null );
  }

  protected void putSpi( String key, String value ) {
    if (verbose) {
      System.out.println( "DP["+target+"]:putSpi( "+key+", "+value+" )" );
    }

    target.put( key, value );
  }

  protected void removeSpi( String key ) {
    if (verbose) {
      System.out.println( "DP["+target+"]:removeSpi( "+key+" )" );
    }

    target.remove( key );
  }

  protected AbstractPreferences childSpi( String name ) {
    if (verbose) {
      System.out.println( "DP["+target+"]:chlidSpi( "+name+" )" );
    }

    return (AbstractPreferences)target.node( name );
  }

  protected void removeNodeSpi() throws BackingStoreException {
    if (verbose) {
      System.out.println( "DP["+target+"]:removeNode()" );
    }

    target.removeNode();
  }

  protected String[] keysSpi() throws BackingStoreException {
    if (verbose) {
      System.out.println( "DP["+target+"]:keysSpi()" );
    }

    return target.keys();
  }

  protected String[] childrenNamesSpi() throws BackingStoreException {
    if (verbose) {
      System.out.println( "DP["+target+"]:childrenNamesSpi()" );
    }

    return target.childrenNames();
  }

  protected void syncSpi() throws BackingStoreException {
    if (verbose) {
      System.out.println( "DP["+target+"]:sync()" );
    }

    target.sync();
  }


  protected void flushSpi() throws BackingStoreException {
    if (verbose) {
      System.out.println( "DP["+target+"]:flush()" );
    }

    target.flush();
  }
}
Wrapping

Wrapping is what ensures that a subnode of a DelegatingPreferences object is also a DelegatingPreferences object. For our purposes, this means that a subnode of an EncryptedPreferences object will also be an EncryptedPreferences object. "Wrapping" a preferences node really means creating a new node that delegates to it.

When the childSpi() method is called, it is responsible for finding and returning the child, or subnode, of the given node. In WrappedPreferences, childSpi() gets the new node, but before returning it, wraps it in another node. It does this by calling wrapChild().

If you look ahead to the source for EncryptedPreferences in Listing Eight, you'll see that it has the required implementation of wrapChild(), excerpted here:

public WrappedPreferences wrapChild(
    WrappedPreferences parent,
    String name,
    AbstractPreferences child ) {

  EncryptedPreferences ep =
    new EncryptedPreferences(
      parent,
      name,
      child );

  ep.setStuff( stuff );

  return ep;
}

Wrapping, of course, isn't just useful for the approaches used in this article— it can be used for any specialized Prefefences object that wants to be applied uniformly both to its own data and to the data of its subnodes. You'll find the source code to WrappedPreferences in Listing Six.

Listing Six: The source for WrappedPreferences, which is responsible for delegating SPI method calls to another Preferences object.
// $Id$

package ep;

import java.util.prefs.*;

abstract public class WrappedPreferences extends DelegatedPreferences
{
  protected WrappedPreferences( AbstractPreferences parent, String name,
      AbstractPreferences target ) {
    super( parent, name, target );
  }

  protected AbstractPreferences childSpi( String name ) {
    return wrapChild( this, name,
                      (AbstractPreferences)super.childSpi( name ) );
  }

  public WrappedPreferences wrapChild( WrappedPreferences parent,
                                              String name,
                                              AbstractPreferences child ) {
    throw new UnsupportedOperationException(
      "You must override WrappedPreferences.wrapChild()" );
  }
}
Obfuscation

Obfuscation is defined here as the process of modifying keys and values before storing them to the preferences database, and of unmodifying them when reading them back. To create an obfuscating preferences object, simply subclass ObfuscatedPreferences, and implement the following two methods:

public String obfuscateString( String string );
public String deObfuscateString( String string );

obfuscateString() alters a string so that its meaning is in some way hidden, and deObfuscatedString() reverses this process. It's crucial that deObfuscateString() knows how to reverse the obfuscation process on its own.

(Note that we don't obfuscate the names of subnodes. This is because node names are usually based on package names, and we want the preferences system to be able to find the nodes for our packages properly.)

Obfuscation doesn't have to be encryption— it could replace the string with some other value that may or may not be secret; it can translate to another language; reverse the characters; or anything, as long as its reversible. You'll find the source code to ObfuscatedPreferences in Listing Seven.

Listing Seven: The source for ObfuscatedPreferences, which modifies keys and values before writing them to the preferences database.
// $Id$

package ep;

import java.util.prefs.*;

public abstract class ObfuscatedPreferences extends WrappedPreferences
{
  protected ObfuscatedPreferences( AbstractPreferences parent, String name,
      AbstractPreferences target ) {
    super( parent, name, target );
  }

  protected String getSpi( String key ) {
    return deObfuscateString( super.getSpi( obfuscateString( key ) ) );
  }

  protected void putSpi( String key, String value ) {
    super.putSpi( obfuscateString( key ), obfuscateString( value ) );
  }

  protected void removeSpi( String key ) {
    super.removeSpi( obfuscateString( key ) );
  }

  protected String[] keysSpi() throws BackingStoreException {
    String keys[] = super.keysSpi();
    String dkeys[] = (String[])keys.clone();
    for (int i=0; i<dkeys.length; ++i) {
      dkeys[i] = deObfuscateString( dkeys[i] );
    }
    return dkeys;
  }

  abstract public String obfuscateString( String string );
  abstract public String deObfuscateString( String string );
}

Encryption

Finally, the encryption. We've done most of the work in the three previous classes. First and foremost, EncryptedPreferences has to do encryption. As we saw in the previous section, this functionality is placed inside obfuscateString() and deObfuscateString(). If you look at the source in Listing Eight, you'll see that the actual encryption/decryption work is done in a class called "EncryptionStuff," which can be found in Listing Nine.

Listing Eight: The source for EncryptedPreferences, which encrypts keys and values before writing them to the preferences database, and then decrypts them on the way back out.
// $Id$

package ep;

import java.security.*;
import java.util.prefs.*;
import javax.crypto.*;
import javax.crypto.spec.*;

public class EncryptedPreferences extends ObfuscatedPreferences
{
  private EncryptionStuff stuff;

  protected EncryptedPreferences( AbstractPreferences parent, String name,
      AbstractPreferences target ) {
    super( parent, name, target );
  }

  private void setStuff( EncryptionStuff stuff ) {
    this.stuff = stuff;
  }

  private EncryptionStuff getStuff() {
    return stuff;
  }

  public String obfuscateString( String string ) {
    try {
      return getStuff().obfuscateString( string );
    } catch( GeneralSecurityException gse ) {
      gse.printStackTrace();
    }
    return null;
  }

  public String deObfuscateString( String string ) {
    try {
      return getStuff().deObfuscateString( string );
    } catch( GeneralSecurityException gse ) {
      gse.printStackTrace();
    }
    return null;
  }

  public WrappedPreferences wrapChild( WrappedPreferences parent,
                                              String name,
                                              AbstractPreferences child ) {
    EncryptedPreferences ep = new EncryptedPreferences( parent, name, child );
    ep.setStuff( stuff );
    return ep;
  }

  static public Preferences userNodeForPackage( Class clasz,
                                                SecretKey secretKey ) {
    AbstractPreferences ap =
      (AbstractPreferences)Preferences.userNodeForPackage( clasz );
    EncryptedPreferences ep = new EncryptedPreferences( null, "", ap );
    try {
      ep.setStuff( new EncryptionStuff( secretKey ) );
      return ep;
    } catch( GeneralSecurityException gse ) {
      gse.printStackTrace();
    }
    return null;
  }

  static public Preferences systemNodeForPackage( Class clasz,
                                                SecretKey secretKey ) {
    AbstractPreferences ap =
      (AbstractPreferences)Preferences.systemNodeForPackage( clasz );
    EncryptedPreferences ep = new EncryptedPreferences( null, "", ap );
    try {
      ep.setStuff( new EncryptionStuff( secretKey ) );
      return ep;
    } catch( GeneralSecurityException gse ) {
      gse.printStackTrace();
    }
    return null;
  }
}
Listing Nine: The source for EncryptedStuff, which contains the objects required for encryption and decryption of preferences data.

// $Id$

package ep;

import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;

public class EncryptionStuff
{
  static private final String algorithm = "DES";
  static private SecureRandom sr = new SecureRandom();
  private SecretKey secretKey;
  private Cipher cipher;

  public EncryptionStuff( SecretKey secretKey ) throws GeneralSecurityException {
    this.secretKey = secretKey;

    cipher = Cipher.getInstance( algorithm );
  }

  public String arrayToString( byte raw[] ) {
    StringBuffer sb = new StringBuffer();
    for (int i=0; i<raw.length; ++i) {
      short s = (short)raw[i];
      if (s>0)
        s += 256;
      int hi = s>>4;
      int lo = s&0xf;
      sb.append( (char)('a'+hi) );
      sb.append( (char)('a'+lo) );
    }
    return sb.toString();
  }

  public byte[] stringToArray( String string ) {
    StringBuffer sb = new StringBuffer( string );
    int len = sb.length();

    if ((len&1)==1)
      throw new RuntimeException( "String must be of even length! "+string );

    byte raw[] = new byte[len/2];
    int ii=0;
    for (int i=0; i<len; i+=2) {
      int hic = sb.charAt( i ) - 'a';
      int loc = sb.charAt( i+1 ) - 'a';
      byte b = (byte)( (hic><4) | loc );
      raw[ii++] = b;
    }
    return raw;
  }

  synchronized public String obfuscateString( String string )
      throws GeneralSecurityException {
    cipher.init( Cipher.ENCRYPT_MODE, secretKey, sr );
    byte raw[] = string.getBytes();
    byte oraw[] = cipher.doFinal( raw );
    String ostring = arrayToString( oraw );
    return ostring;
  }

  synchronized public String deObfuscateString( String string )
      throws GeneralSecurityException {
    cipher.init( Cipher.DECRYPT_MODE, secretKey, sr );
    byte raw[] = stringToArray( string );
    byte draw[] = cipher.doFinal( raw );
    String dstring = new String( draw );
    return dstring;
  }
}

Also, if you'll recall, EncryptedPreferences has to implement wrapChild(), which is used to ensure that subnodes are wrapped in EncryptedPreference objects as well.

Trying It Out

Testing this stuff is easy. As mentioned previously, run generatekey.bat to generate the key file, and then run pkg.encrypted.EncryptedTest to see the EncryptedPreferences in action, as follows:


C:\> generatekey.bat
Generating key....
C:\> java pkg.encrypted.EncryptionTest
[XML output not shown]

Remember, look in the registry to see the encrypted and nonencrypted values. If you don't know where to find them, do a search for the string "pkg."

Finally, a utility class (pkg.Util), which is called by the other classes, is included in Listing Ten. It provides functionality for reading and writing byte arrays.

Listing Ten: The source for Util.java, a utility class used by other classes in the packaged "pkg."
// $Id$

package pkg;

import java.io.*;

public class Util
{
  // Read a file into a byte array
  static public byte[] readFile( String filename ) throws IOException {
    File file = new File( filename );
    long len = file.length();
    byte data[] = new byte[(int)len];
    FileInputStream fin = new FileInputStream( file );
    int r = fin.read( data );
    if (r != len)
      throw new IOException( "Only read "+r+" of "+len+" for "+file );
    fin.close();
    return data;
  }

  // Write byte array to a file
  static public void writeFile( String filename, byte data[] )
      throws IOException {
    FileOutputStream fout = new FileOutputStream( filename );
    fout.write( data );
    fout.close();
  }
}
Conclusion

This implementation is fairly complicated. While I was writing the code, I found the number of classes growing naturally as I realized that many aspects of this architecture could be reused for other purposes.

The nice thing about the four-way separation of labor is that it gave itself over to a hierarchical relationship— each of the four Preferences subclasses relied on the one before it. This structure wasn't clear from the beginning-'as I began dividing the functionality into multiple pieces, I found that I had to think a while before I was able to make everything fit into a nice hierarchical ordering.

The Preferences API is well structured, but something like this was needed. The nine SPI methods help the process of extending, modifying, and/or filtering any interactions with the Preferences API.

The result is that we're able to modify the functionality behind the scenes. Once we've created our EncryptedPreferences object, we can use it everywhere we use a regular Preferences object. This makes it easy to convert an existing application to use encrypted preferences, if necessary, without having to make extensive changes to the code.

Resources


Greg Travis is a freelance Java programmer and technology writer living in New York City. His interests include algorithm optimization, programming language design, signal processing (with emphasis on music), and real-time 3D graphics. Other articles he's written can be found at http://www.panix.com/~mito/articles/. He can be reached at [email protected].

October 2002/Encrypted Preferences in Java

Figure 4: Subclassing AbstractPreferences, and the role of the nine SPI methods.

October 2002/Encrypted Preferences in Java

Figure 5: Our inheritance hierarchy.

October 2002/Encrypted Preferences in Java

Listing 1: A simple test program. It stores a value in the preferences database in the root node for its package ("pkg"), and another value in a subnode of the root node.

// $Id$

package pkg;

import java.util.prefs.*;

public class Test
{
  static public void main( String args[] ) throws Exception {
    Preferences root = Preferences.userNodeForPackage( Test.class );

    root.put( "not", "encrypted" );

    Preferences subnode = root.node( "subnode" );
    subnode.put( "also not", "encrypted" );

    root.exportSubtree( System.out );
  }
}
October 2002/Encrypted Preferences in Java

Listing 10: The source for Util.java, a utility class used by other classes in the packaged "pkg."

// $Id$

package pkg;

import java.io.*;

public class Util
{
  // Read a file into a byte array
  static public byte[] readFile( String filename ) throws IOException {
    File file = new File( filename );
    long len = file.length();
    byte data[] = new byte[(int)len];
    FileInputStream fin = new FileInputStream( file );
    int r = fin.read( data );
    if (r != len)
      throw new IOException( "Only read "+r+" of "+len+" for "+file );
    fin.close();
    return data;
  }

  // Write byte array to a file
  static public void writeFile( String filename, byte data[] )
      throws IOException {
    FileOutputStream fout = new FileOutputStream( filename );
    fout.write( data );
    fout.close();
  }
}
October 2002/Encrypted Preferences in Java

Listing 2: The preference data for our sample program pkg.Test, exported in XML format.

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE preferences SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>

<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="user">
    <map />
    <node name="pkg">
      <map>
        <entry key="not" value="encrypted" />
      </map>
      <node name="encrypted">
        <map>
          <entry key="mjdajaddcioehjeljmahmpdbfhomifhp" value="hknhbmkdphkpbjipdijphpniboiecadn" />
        </map>
        <node name="subnode">
          <map>
            <entry key="eempdaneimckpiod" value="bkoaejfbcjpkckmflkijoomngbopblco" />
          </map>
        </node>
      </node>
      <node name="subnode">
        <map>
          <entry key="also not" value="encrypted" />
        </map>
      </node>
    </node>
  </root>
</preferences>

October 2002/Encrypted Preferences in Java

Listing 3: A simple test program, this time using encryption. It does more or less the same thing as the program in Listing 1, except that this variant uses an Encrypted Preferences object, which transparently encrypts the data before storing it, and decrypts it before retrieving it.

// $Id$

package pkg.encrypted;

import java.security.*;
import java.util.prefs.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import ep.*;
import pkg.Util;

public class EncryptedTest
{
  static private final String algorithm = "DES";

  static public void main( String args[] ) throws Exception {
    byte rawKey[] = Util.readFile( "key" );
    DESKeySpec dks = new DESKeySpec( rawKey );
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( algorithm );
    SecretKey secretKey = keyFactory.generateSecret( dks );

    Preferences root =
      EncryptedPreferences.userNodeForPackage(
        EncryptedTest.class, secretKey );

    root.put( "transparent", "encryption" );

    Preferences subnode = root.node( "subnode" );
    subnode.put( "also", "encrypted" );

    root.exportSubtree( System.out );
  }
}

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.