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:
- getSpi()
- putSpi()
- removeSpi()
- childSpi()
- removeNodeSpi()
- keysSpi()
- childrenNamesSpi()
- syncSpi()
- flushSpi()
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:
- Delegation modifying requests to read or write and passing them to another Preferences object.
- Wrapping-'we want any subnode of a custom node to also be custom.
- Obfuscation this implements a filter, whereby each key and value is first modified ("obfuscated") before being stored.
- 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.