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
- Download JDK 1.4 http://java.sun.com/j2se/1.4/download.html.
- Security features in JDK 1.4 http://java.sun.com/j2se/1.4/docs/guide/security/.
- The Java Cryptography Architecture http://java.sun.com/products/jce/.
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].