Handling a particular form of encryption in the FileDumper program is not hard. Handling the general case is not. It's not that decryption is difficult. In fact, it's quite easy. However, most encryption schemes require more than simply providing a key. You also need to know an assortment of algorithm parameters, like initialization vector, salt, iteration count, and more. Higher level protocols usually pass this information between the encryption program and the decryption program. The simplest protocol is to store the information unencrypted at the beginning of the encrypted file. You saw an example of this in the FileDecryptor and FileEncryptor programs. The FileEncryptor chose a random initialization vector and placed its length and the vector itself at the beginning of the encrypted file so the decryptor could easily find it.
For the next iteration of the FileDumper program, I am going to use the simplest available encryption scheme, DES in ECB mode with PKCS5Padding. Furthermore, the key is simply the first eight bytes of the password. This is probably the least secure algorithm discussed in this chapter. However, it doesn't require an initialization vector, salt, or other metainformation to be passed between the encryptor and the decryptor. Because of the nature of filter streams, it is relatively straightforward to add decryption services to the FileDumper program, assuming you know the format in which the encrypted data is stored. Generally, you'll want to decrypt a file before dumping it. This does not require a new dump filter. Instead, I simply pass the file through a cipher input stream before passing it to one of the dump filters.
When a file is both compressed and encrypted, compression is usually performed first. Therefore, we'll always decompress after decrypting. The reason is twofold. Since encryption schemes make data appear random, and compression works by taking advantage of redundancy in nonrandom data, it is difficult, if not impossible, to compress encrypted files. In fact, one quick test of how good an encryption scheme is to compress an encrypted file. If the file is compressible, it's virtually certain the encryption scheme is flawed and can be broken. Conversely, compressing files before encrypting them removes redundancy from the data that a code breaker can exploit and thereby may shore up some weaker algorithms. On the other hand, some algorithms have been broken by taking advantage of magic numbers and other known plaintext sequences that compression programs insert into the encrypted data. Thus, there's no guarantee that compressing files before encrypting them makes them harder to penetrate. The best option is simply to use the strongest encryption available.
We'll let the user set the password with the -password command-line switch. The next argument after -password is assumed to be the password. Example 12-8, FileDumper5, demonstrates.
Example 12-8. FileDumpers
import java.io.*; import java.util.zip.*; import java.security.*; import javax.crypto.*; import javax.crypto.spec.*; import com.elharo.io.*; public class FileDumper5 { public static final int ASC = 0; public static final int DEC = 1; public static final int HEX = 2; public static final int SHORT = 3; public static final int INT = 4; public static final int LONG = 5; public static final int FLOAT = 6; public static final int DOUBLE = 7; public static void main(String[] args) { if (args.length < 1) { System.err.println( "Usage: java FileDumper5 [-ahdsilfx] [-little] [-gzip|-deflated] " + "[-password password] file1..."); } boolean bigEndian = true; int firstFile = 0; int mode = ASC; boolean deflated = false; boolean gzipped = false; String password = null; // Process command-line switches. for (firstFile = 0; firstFile < args.length; firstFile++) { if (!args[firstFile].startsWith("-")) break; if (args[firstFile].equals("-h")) mode = HEX; else if (args[firstFile].equals("-d")) mode = DEC; else if (args[firstFile].equals("-s")) mode = SHORT; else if (args[firstFile].equals("-i")) mode = INT; else if (args[firstFile].equals("-l")) mode = LONG; else if (args[firstFile].equals("-f")) mode = FLOAT; else if (args[firstFile].equals("-x")) mode = DOUBLE; else if (args[firstFile].equals("-little")) bigEndian = false; else if (args[firstFile].equals("-deflated") && !gzipped) deflated = true; else if (args[firstFile].equals("-gzip") && !deflated) gzipped = true; else if (args[firstFile].equals("-password")) { password = args[firstFile+1]; firstFile++; } } for (int i = firstFile; i < args.length; i++) { try { InputStream in = new FileInputStream(args[i]); dump(in, System.out, mode, bigEndian, deflated, gzipped, password); if (i < args.length-1) { // more files to dump System.out.println( ); System.out.println("--------------------------------------"); System.out.println( ); } } catch (IOException ex) { System.err.println(ex); ex.printStackTrace( ); } } } public static void dump(InputStream in, OutputStream out, int mode, boolean bigEndian, boolean deflated, boolean gzipped, String password) throws IOException { // The reference variable in may point to several different objects // within the space of the next few lines. if (password != null && password.length( ) > 0) { // Create a key. try { byte[] desKeyData = password.getBytes( ); DESKeySpec desKeySpec = new DESKeySpec(desKeyData); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey desKey = keyFactory.generateSecret(desKeySpec); // Use Data Encryption Standard. Cipher des = Cipher.getInstance("DES/ECB/PKCS5Padding"); des.init(Cipher.DECRYPT_MODE, desKey); in = new CipherInputStream(in, des); } catch (GeneralSecurityException ex) { throw new IOException(ex.getMessage( )); } } if (deflated) { in = new InflaterInputStream(in); } else if (gzipped) { in = new GZIPInputStream(in); } // could really pass to FileDumper3 at this point if (bigEndian) { DataInputStream din = new DataInputStream(in); switch (mode) { case HEX: in = new HexFilter(in); break; case DEC: in = new DecimalFilter(in); break; case INT: in = new IntFilter(din); break; case SHORT: in = new ShortFilter(din); break; case LONG: in = new LongFilter(din); break; case DOUBLE: in = new DoubleFilter(din); break; case FLOAT: in = new FloatFilter(din); break; default: } } else { LittleEndianInputStream lin = new LittleEndianInputStream(in); switch (mode) { case HEX: in = new HexFilter(in); break; case DEC: in = new DecimalFilter(in); break; case INT: in = new LEIntFilter(lin); break; case SHORT: in = new LEShortFilter(lin); break; case LONG: in = new LELongFilter(lin); break; case DOUBLE: in = new LEDoubleFilter(lin); break; case FLOAT: in = new LEFloatFilter(lin); break; default: } } for (int c = in.read(); c != -1; c = in.read( )) { out.write(c); } in.close( ); } } |
Note how little needed to change. I simply imported two more packages and added a command-line switch and about a dozen lines of code (which could easily have been half that) to build a Cipher object and add a cipher input stream to the chain. Other encryption schemes, like password-based encryption, would not be hard to support either. The main difficulty lies in deciding exactly how the key would be entered since not all algorithms have keys that map to passwords in a straightforward way. I leave that as an exercise for the reader.
Basic I/O
Introducing I/O
Output Streams
Input Streams
Data Sources
File Streams
Network Streams
Filter Streams
Filter Streams
Print Streams
Data Streams
Streams in Memory
Compressing Streams
JAR Archives
Cryptographic Streams
Object Serialization
New I/O
Buffers
Channels
Nonblocking I/O
The File System
Working with Files
File Dialogs and Choosers
Text
Character Sets and Unicode
Readers and Writers
Formatted I/O with java.text
Devices
The Java Communications API
USB
The J2ME Generic Connection Framework
Bluetooth
Character Sets