Book Home Java Security Search this book

13.6. Cipher Streams

In 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 Class

The class that encrypts data on output to a stream is the CipherOutputStream class (javax.crypto.CipherOutputStream):

public class CipherOutputStream extends FilterOutputStream

Provide a class that will encrypt data as it is written to the underlying stream.

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:

public CipherOutputStream(OutputStream outputStream, Cipher cipher)

Create a cipher output stream, associating the given cipher object with the existing output stream. The given cipher must already have been initialized, or an IllegalStateException will be thrown.

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 Definition

public 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]

[2]Closing the output stream is necessary whenever the stream performs buffering, but it is particularly important to remember in this context.

13.6.2. The CipherInputStream Class

The output stream is only half the battle; in order to read that data, we must use the CipherInputStream class (javax.crypto.CipherInputStream):

public class CipherInputStream extends FilterInputStream

Create a filter stream capable of decrypting data as it is read from the underlying input stream.

A cipher input stream is constructed with this method:

public CipherInputStream(InputStream is, Cipher c)

Create a cipher input stream that associates the existing input stream with the given cipher. The cipher must previously have been initialized.

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 Definition

public 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 Encryption

In 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:

  1. Use the URL class.

    The URL class can be used to open a URL that the applet can read data from. If the URL is a POST URL, the applet can send some initial data before it reads the data. On browsers that will support it, you can specify an https protocol when the URL is constructed, in which case the data exchanged by the applet and the remote web server will be encrypted. Note that this is not supported by the JDK itself.

    There are a few limitations with this method. First, the data exchange is limited to the web server and the applet using the single request-response protocol of HTTP. Data cannot be streamed in this way, and you must write an appropriate back-end cgi-bin script, servlet, or other program to process the data. Second, not all browsers support the https protocol, and those that do support https may not support a Java applet opening an https URL. On the other hand, this will tunnel data through a firewall, which is one of the main reasons why it is used.

  2. Use an SSLSocket class.

    There are a number of vendors who supply SSLSocket and SSLServerSocket classes that extend the Socket and ServerSocket classes; these classes provide all the semantics of their java.net counterparts with the additional feature that the data they exchange is encrypted with the SSL algorithm.

    These classes are generally subject to import and export restrictions; in particular, Sun's SSLSocket and SSLServerSocket classes (which come with the Java Server product) cannot be exported, and certain countries will not allow these implementations to be imported. There are SSL implementations that have been written outside the United States, so they have fewer restrictions (but they may contain implementations of RSA that may not be used within the United States).

  3. Use an RSA-based security provider.

    The Cipher class that we examined above has the ability to support RSA encryption. Many third-party security providers will have RSA implementations; some of these are listed in Appendix C, "Security Resources".

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).



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.