13.6. Cipher StreamsIn the Cipher class we just examined, we had to provide the data to be encrypted or decrypted as multiple blocks of data. This is not necessarily the best interface for programmers: what if we want to send and receive arbitrary streams of data over the network? It would often be inconvenient to get all the data into buffers before it can be encrypted or decrypted. The solution to this problem is the ability to associate a cipher object with an input or output stream. When data is written to such an output stream, it is automatically encrypted, and when data is read from such an input stream, it is automatically decrypted. This allows a developer to use Java's normal semantics of nested filter streams to send and receive encrypted data. 13.6.1. The CipherOutputStream ClassThe class that encrypts data on output to a stream is the CipherOutputStream class (javax.crypto.CipherOutputStream):
Like all classes that extend the FilterOutputStream class, constructing a cipher output stream requires that an existing output stream has already been created. This allows us to use the existing output stream from a socket or a file as the destination stream for the encrypted data:
The output stream may be operated on with any of the methods from the FilterOutputStream class--the write() methods, the flush() method, and the close() method, which all provide the semantics you would expect. Often, of course, these methods are never used directly--for example, if you're sending text data over a socket, you'll wrap a cipher output stream around the socket's output stream, but then you'll wrap a print writer around that; the programming interface then becomes a series of calls to the print() and println() methods. You can use any similar output stream to get a different interface. It does not matter if the cipher object that was passed to the constructor does automatic padding or not--the CipherOutputStream class itself does not make that restriction. As a practical matter, however, you'll want to use a padding cipher object, since otherwise you'll be responsible for keeping track of the amount of data passed to the output stream and tacking on your own padding. Usually, the better alternative is to use a byte-oriented mode such as CFB8. This is particularly true in streams that are going to be used conversationally: a message is sent, a response received, and then another message is sent, etc. In this case, you want to make sure that the entire message is sent; you cannot allow the cipher to buffer any data internally while it waits for a full block to arrive. And, for reasons we're just about to describe, you cannot call the flush() method in this case either. Hence, you need to use a streaming cipher (or, technically, a block cipher in streaming mode) in this case. When the flush() method is called on a CipherOutputStream (either directly, or because the stream is being closed), the padding of the stream comes into play. If the cipher is automatically padding, the padding bytes are generated in the flush() method. If the cipher is not automatically padding and the number of bytes that have been sent through the stream is not a multiple of the cipher's block size, then the flush() method (or indirectly the close() method) throws an IllegalBlockSizeException (note that this requires that the IllegalBlockSizeException be a runtime exception). If the cipher is performing padding, it is very important not to call the flush() method unless it is immediately followed by a call to the close() method. If the flush() method is called in the middle of processing data, padding is added in the middle of the data. This means the data does not decrypt correctly. Remember that certain output streams (especially some types of PrintWriter streams) flush automatically; if you're using a padding cipher, don't use one of those output streams. We can use this class to write some encrypted data to a file like this: Class Definitionpublic class Send { public static void main(String args[]) { try { KeyGenerator kg = KeyGenerator.getInstance("DES"); kg.init(new SecureRandom()); SecretKey key = kg.generateKey(); SecretKeyFactory skf = SecretKeyFactory.getInstance("DES"); Class spec = Class.forName("javax.crypto.spec.DESKeySpec"); DESKeySpec ks = (DESKeySpec) skf.getKeySpec(key, spec); ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("keyfile")); oos.writeObject(ks.getKey()); Cipher c = Cipher.getInstance("DES/CFB8/NoPadding"); c.init(Cipher.ENCRYPT_MODE, key); CipherOutputStream cos = new CipherOutputStream( new FileOutputStream("ciphertext"), c); PrintWriter pw = new PrintWriter( new OutputStreamWriter(cos)); pw.println("Stand and unfold yourself"); pw.close(); oos.writeObject(c.getIV()); oos.close(); } catch (Exception e) { System.out.println(e); } } } There are two steps involved here. First, we must create the cipher object, which means that we must have a secret key available. The problem of secret key management is a hard one to solve; we'll discuss it a little farther along. For now, we're just going to save the key object to a file that can later be read by whomever needs the key. Note that we've gone through the usual steps of writing the data produced by the secret key factory so that the recipient of the key need not use the same provider we use. After we generate the key, we must create the cipher object, initialize it with that key, and then use that cipher object to construct our output stream. Once the data is sent to the stream, we close the stream, which flushes the cipher object, performs any necessary padding, and completes the encryption. In this case, we've chosen to use CFB8 mode, so there is no need for padding. But in general, this last step is important: if we don't explicitly close the PrintWriter stream, when the program exits, data that is buffered in the cipher object itself will not get flushed to the file. The resulting encrypted file will be unreadable, as it won't have the correct amount of data in its last block.[2]
13.6.2. The CipherInputStream ClassThe output stream is only half the battle; in order to read that data, we must use the CipherInputStream class (javax.crypto.CipherInputStream):
A cipher input stream is constructed with this method:
All the points we made about the CipherOutputStream class are equally valid for the CipherInputStream class. You can operate on it with any of the methods in its superclass, although you'll typically want to wrap it in something like a buffered reader, and the cipher object that is associated with the input stream needs to perform automatic padding or use a mode that does not require padding (in fact, it must use the same padding scheme and mode that the output stream that is sending it data used). The CipherInputStream class does not directly support the notion of a mark. The markSupported() method returns false unless you've wrapped the cipher input stream around another class that supports a mark. Here's how we could read the data file that we created above: Class Definitionpublic class Receive { public static void main(String args[]) { try { ObjectInputStream ois = new ObjectInputStream( new FileInputStream("keyfile")); DESKeySpec ks = new DESKeySpec((byte[]) ois.readObject()); SecretKeyFactory skf = SecretKeyFactory.getInstance("DES"); SecretKey key = skf.generateSecret(ks); Cipher c = Cipher.getInstance("DES/CFB8/NoPadding"); c.init(Cipher.DECRYPT_MODE, key, newIvParameterSpec((byte[]) ois.readObject()); CipherInputStream cis = new CipherInputStream( new FileInputStream("ciphertext"), c); cis.read(new byte[8]); BufferedReader br = new BufferedReader( new InputStreamReader(cis)); System.out.println("Got message"); System.out.println(br.readLine()); } catch (Exception e) { System.out.println(e); } } } In this case, we must first read the secret key from the file where it was saved, and then create the cipher object initialized with that key. Then we can create our input stream and read the data from the stream, automatically decrypting it as it goes. 13.6.3. SSL EncryptionIn the world of the Internet, data encryption is often achieved with SSL--the Secure Socket Layer protocol. These sockets use encryption to encrypt data as it is written to the socket and to decrypt that data as it is read from the socket. SSL encryption is built into many popular web browsers and web servers; these programs depend on SSL to provide the necessary encryption to implement the https protocol. For Java applet developers who want to use SSL, there are three options:
For now, none of these solutions is completely attractive. The technique of using URLs is well known and demonstrated in any book on Java network programming, but suffers from the limitations we discussed above. The SSL-based Socket classes have a known interface and are simple to use, but suffer from availability questions (although no more than the JCE itself). Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|