Professional Java Security : Symmetric Encryption Part 3

Key Storage

One of the primary difficulties in establishing good security through cryptography is key storage. In order for a key to be useful, it must be persistent. But by keeping the key around, we risk it being compromised. Let’s imagine that we want to encrypt some personal files on our computer so that no one can read them. We create a 448-bit Blowfish key and encrypt our files with it, thinking that our files will now be secure. But if someone were to get into our machine, they’d be able to read the key and decrypt the files. In order for the files to be safe, we have to protect the key that we used to encrypt those files.

One way to protect the key is to keep it on a floppy disk and only use it when a file needs to be encrypted or decrypted. Another possibility is to use a smart card, which is a tiny computer, the size of a credit card. Its purpose is to store keys and perform cryptographic operations directly on the card, without ever exposing the key. Smart cards and floppy disks are somewhat inconvenient, however, because they require some external physical medium to store the key.

Let’s return to the idea of keeping the key on our computer in the filesystem. Anyone with read access could decrypt our files. One way to work around this is to encrypt the keys using password-based encryption, using a good strong password that no one is likely to guess. This is what Netscape Communicator does when storing private certificates for encrypted e-mail.

Storing the keys in the filesystem reduces their security, even if they’re encrypted: they’re now protected by PBE, which is almost always going to be less strong than the keys the key store contains. It’s a good idea to protect any key storage mechanism in additional ways, like setting file permissions and physically securing the box the keys are on. Let’s discuss encrypting keys for storage and transmission.

Key Wrapping and Unwrapping

JCE 1.2.1, which some providers implement, provides an easy means of encrypting a key. As we mentioned earlier, javax.crypto.Cipher has a method wrap(), which takes a key as an argument and returns a byte array, which is the encrypted value of the key. We need to initialize the cipher in WRAP_MODE instead of ENCRYPT_MODE. If we had a PBE cipher, just like in the previous example, we could wrap a secret key like so:

To decrypt the key, we initialize it in UNWRAP_MODE, and then call cipher.unwrap() with the algorithm of the wrapped key and the type SECRET_KEY, because that’s the type of key that was wrapped:

Key Encryption without Wrapping

If we don’t have wrap() and unwrap() in our provider’s cipher implementation, then we’ll need to perform the encryption and decryption using java.security.Key's getEncoded() method and javax.crypto.SecretKeySpec.

Padding

Block ciphers, like most of the ciphers we’ve been discussing so far, operate on distinct chunks of data – usually 64 bits. Some newer block ciphers like AES operate on 128 bits or more at a time. But the plaintext data to be encrypted won’t always be a multiple of the block size. So before encrypting, padding needs to be added to the data. There are a number of different ways padding can be added, but most symmetric algorithms use one of two types of padding:

  • No padding
  • PKCS#5 padding

No padding is exactly that, no padding. It requires that the data we are encrypting end on a block exactly, with no extra data. PKCS#5 is more commonly used. PKCS stands for Public Key Cryptography Standard, and there are a number of PKCS standards created for use in various cryptographic functions, like key exchange and certificate requests. Along with those broad protocol definitions, they also define some padding methods. Of these methods, PKCS#5 is the most commonly used for symmetric encryption.

PKCS#5 padding works as follows: the bytes remaining to fill a block are assigned a number, which is the number of bytes that were added to fill the block. For instance, if we have an 8-byte block, and only 3 bytes are filled, then we have 5 bytes to pad. Those 5 bytes are all assigned the value “5”, for the 5 bytes of padding. The illustration opposite should help clarify this:

If we have data that ends on an even multiple of 8 bytes, we need to add an entire block of padding. This way, we know that there is always padding that must be removed after decryption.

Mode

In addition to specifying the padding, we also need to specify the mode for a block cipher before using it. The mode defines how a cipher should apply an encryption algorithm. Changing the mode can allow a block cipher to function as a stream cipher. Block ciphers operate on data a block at a time, where a block can be any number of bits, usually 64 or 128. A stream cipher, on the other hand, can encrypt or decrypt data a byte at a time, making it much more useful for streaming applications, like network communication.

We will discuss some of the more common modes below.

ECB (Electronic Code Book)

ECB is the simplest mode: the same plaintext block will always encrypt to the exact same ciphertext block. This is fine for sending single chunks of data, like a key, but not good for implementing an encrypted stream of information. This is because if the same plaintext is sent multiple times, the same ciphertext will also be sent.

Let’s say we’re sending the following message in a chat application, “meet me later”, and we’re sending it one character at a time. The following might be our ciphertext if we were using DES:

Notice that ‘e’ always encrypt to the exact same ciphertext. If an attacker knew that we were sending text messages, then a frequency analysis would quickly indicate that e’s were encrypted to “TRw+doCp3EQ=”. With some time and a complete encrypted session transcript, it wouldn’t be difficult to crack the code, as the use of ECB in this application has reduced the strength of the encryption from 256 bits to less than 100 characters. This is only as secure as a simple character-by-character replacement.

The real weakness here is that each block is encrypted the same way. If our key or data keeps changing, ECB is perfectly safe. But if similar blocks keep getting sent with the same key, it is possible to gain some information from those blocks that we might not want broadcast.

CBC (Cipher Block Chaining)

CBC mode changes the behavior of the cipher so that the same plaintext block no longer necessarily encrypts to the same ciphertext block, thus solving the main problem with ECB. CBC uses information from the previous block to encrypt the current block, thus changing it from ECB. A problem with this method is that identical messages will still encrypt identically, because all of the blocks that would alter future blocks are the same. To fix this, we need to use an initialization vector or IV. The IV is just a block of random data used to initialize the cipher. It need not be kept secret, but it should be different for every message. That way, even if we send two identical messages, as long as they have different IVs, they will encrypt differently. In that sense, an initialization vector is a lot like salt used in password-based encryption.

CBC is suitable for transmitting text, but it requires transmitting a full block of data at a time –  usually 8 characters. This is fine for a complete message, but not for a talk application, which needs to send a single character at a time.

CFB (Cipher FeedBack)

CFB works similarly to CBC, except that it can operate on smaller chunks of data –  typically 8 bits. This is perfect for encrypting something like a chat session, where single byte chunks of data need to be sent.

CFB also requires an IV that must be unique for each message sent with the same key.

OFB (Output FeedBack)

OFB is similar to CFB, except that it provides better protection against data being lost in transit. A single bit error in the ciphertext produces a single bit of error in the plaintext. Other modes cause the entire block to get lost.

Like CFB and CBC, OFB also requires an IV.

CipherStreams

Some of the most useful classes that the JCE provides are the two CipherStream classes in the javax.crypto package: CipherInputStream and CipherOutputStream. They provide convenient wrappers around standard input and output streams that automatically encrypt and decrypt. You can use them anywhere you use a normal InputStream or OutputStream, like network programming or file IO.

CipherInputStream and CipherOutputStream are both constructed by a call to new() with the stream to wrap and the cipher to use. If we have a cipher prepared, we could encrypt a file with:

Decrypting is similar to this — we simply place the cipher in DECRYPT_MODE, and use the cipher streams in the same fashion.

When using cipher streams, it is important to set the mode properly. ECB mode, as we mentioned, isn’t a good idea for large chunks of data that are non-random, like a text file. Instead, we’ll need to use a mode like CBC, which requires an initialization vector. We’ll show setting the mode in the code example coming up.

Initializing a Cipher with an IV

javax.crypto.spec.IVParameterSpec encapsulates an initialization vector in the JCE. One is constructed with a byte array and a call to new() for IVParameterSpec.

To create the bytes for the IV, we should use java.security.SecureRandom to generate a byte array equivalent to the block size for the cipher we are using. Most block ciphers have a block size of 64 bits (8 bytes). AES has a variable block size, either 128, 192, or 256 bits, but is typically set to 128-bit (16 bytes).

SecureRandom

SecureRandom is easy to use. We can construct one normally, with the default constructor, and then pass byte arrays to it with nextBytes() and receive them back filled with random data. For instance, to create an 8-byte array using secure random we could do the following:

As an aside, you may have noticed a delay during the running of the examples we have seen so far and wondered what causes it. Well, it’s not the encryption that’s so slow; it’s the initialization of java.security.SecureRandom. Sun’s implementation of SecureRandom creates a great number of threads and checks their interaction over a period of time. It then uses that to initialize a Pseudo-Random Number Generator (PRNG). It’s the thread creation and interaction that takes so long.

To combat this, a couple of providers have created implementations of SecureRandom that are at least partially native, giving a huge speed increase in the seeding of the random number generator. Cryptix (http://www.cryptix.org) provides an implementation that runs on Linux and the various BSDs, and Virtual Unlimited (http://www.virtualunlimited.com) provides one for Windows.

Also, the initialization of SecureRandom is only expensive the first time it’s done in a single Java VM instance. If you have a long-running program or a server, you can initialize SecureRandom on startup and all later operations will run quite quickly. You may also want to try to architect your programs so that they run for a longer period of time rather than starting and stopping often. This would allow you to amortize the cost of initialization.

Creating and Using an IV

To create the IV with those random bytes, just construct a new instance of IVParameterSpec:

Now we can use the IV to initialize a cipher, like so:

And we’re ready to use that cipher in a cipher output stream.

Keeping Track of the IV

It’s important to realize that that same IV needs to be used to initialize a cipher for decrypting as well. It’s a lot like the salt used with password-based encryption. Just like the salt, it doesn’t need to be hidden like the key does. We can safely expose it along with the ciphertext that we’ve encrypted.

A common way to handle this is to place the initialization vector at the beginning of the ciphertext. That’s exactly what we’re going to do in the next example, which will encrypt and decrypt a file, and also keep a symmetric key stored in the filesystem encrypted with a password.

CipherStream Example – FileEncryptor

We’re going to write a program that encrypts and decrypts files using cipher streams. We’ll use AES (sometimes known as Rijndael) as the algorithm to encrypt the file. In order to decrypt it, we’ll need to use the same key, so we’ll store the key encrypted in a file with password-based encryption.

We’ll offer three options when running our little application: key creation, file encryption, and file decryption. Each of those options will cause a different method to get called: createKey(), encrypt(), or decrypt().

Now let’s write the createKey() method. The first thing we need to do is generate an AES key. Since AES is also known as Rijndael, we’ll use that as an algorithm name to create a key generator.

Now we want to encrypt the key with a password. We’ll create an 8-byte salt and create a PBE cipher with the password:

Now we can use the cipher to encrypt the encoded form of the key:

In order to be able to decrypt the key, we need to have the salt. We’ll write the salt that we generated to the first 8 bytes of the file, and then we’ll write the encrypted key and close the file.

Before we can do any file encryption or decryption, we’ll need to have access to the key. We’re going to write a method called loadKey() that will load a key, with a password specified as an argument. This is essentially the reverse of the createKey() method: we read in the salt, and the encrypted key bytes, and then decrypt the key with a PBE cipher. We’ll use javax.crypto.spec.SecretKeySpec to create a key from the decrypted key bytes.

Now we can write the encrypt method. We start by loading the key with the loadKey() method we just wrote. Next we need to create an initialization vector that is 16 bytes long, equal to the block size of Rijndael.

Now we’ll open the files for reading and writing. We’ll write the IV bytes to the output file unencrypted, as we’ll need to use it later to decrypt the file. Then we’ll create an IVParameterSpec object that we will use to create a cipher.

Now we want to wrap a CipherOutputStream around the FileOutputStream using the cipher we just created:

Now we simply read the bytes from the input stream and write them to the cipher stream. This will encrypt the entire file. When we’re done, we close the input and output.

Decrypting the file is just the opposite. We read in the IV, initialize a cipher, and create a CipherInputStream and use it to decrypt the file.

Running the Application

Now let’s run the application. We’ll begin by creating a key, protected with the password “sasquatch”.

This will print out a few messages like this:

Next we can encrypt a file. Create a file called test.txt in your current directory, and place some text in it. We can then encrypt it to testEncrypted.txt with the following command:

This will give the following output:

You can now view the testEncrypted.txt file to check that it has been encrypted. If we wish, we can decrypt this file with the following command:

The output we get from this is:

We can now view the file testDecrypted.txt to verify that it has been properly decrypted.

CipherStreams with Other Algorithms

There’s no requirement that we use Rijndael when encrypting files in the example above. It could easily be done with any other block cipher without changing anything besides the algorithm name and the bit size of the key. To use DESede for instance, just change “Rijndael” to “DESede” and the bit size from 256 to 168.

Sometimes we’ll want to use a stream cipher, like RC4. RC4 is very fast, and would probably be the best choice for encrypting extremely large files, like audio or video files. RC4 is not quite as strong as AES, but as we’ve mentioned, 128 bits is enough strength for almost any application. Also, if we feel it’s necessary, we can change the keysize we’re using for RC4, as it can accept keys of up to 1024 bits.

RC4 doesn’t use an initialization vector, as it’s built into the algorithm. So we no longer need to create the IV, store it to the output file, or read it from the input file. There are only two lines that need alterations in both encrypt() and decrypt(). For encrypt() they are highlighted as shown below:

and for decrypt(), they are:

Now we should be able to encrypt and decrypt files using RC4 instead of Rijndael/AES.

We’ve finished our discussion of cipher streams, and are going to move on to a new class that uses symmetric encryption: SealedObject.

Sealed Objects

The JCE provides a way to encrypt objects one at a time with a given cipher or key. The encrypted objects are then called sealed objects. Sealed objects can be useful for storing or transferring an encrypted version of an object, although the object to be encrypted needs to be serializable.

When constructing the sealed object, we need to use a fully initialized cipher. When decrypting, we can use either a cipher or just the key, and the sealed object will remember the settings for the cipher used to encrypt the object. Let’s try an example of sealing an object with a DESede key. This example doesn’t really do anything interesting externally, but rather simply expresses the syntax of using a SealedObject to seal a String object:

There is a bug, err, feature in the JDK1.2 that prevents extensions from using the class loader to create classes that are neither standard objects nor extensions. This means that if we create a custom object, say, a CreditCard  object, as opposed to a simple String, we won’t be able to decrypt it. Instead, we’ll need to build our own implementation of SealedObject to accomplish that.

We’ve written an EncryptedObject class to do exactly that. It’s functionally equivalent to SealedObject, but can be used with custom classes. We just need to have it in our classpath to use it. It’s called EncryptedObject to avoid namespace collision with SealedObject, but they are otherwise identical, and you can use this EncryptedObject class as a drop-in replacement for SealedObject because its method signatures are exactly the same.

Summary

Symmetric encryption is a valuable tool for security. It provides us with the ability to hide data from prying eyes in various formats. Symmetric-key encryption has one big problem though: key distribution. In order to send encrypted messages to someone, we need to share a key with them. As we demonstrated in Chapter 3 with our Hamlet examples, this can be very difficult to do properly. In essence, we need to have a shared secret at some point in order to share secret messages in the future. What’s really needed is some way to bootstrap the process, some way to create a shared secret by exchanging purely public information. Asymmetric-key encryption, or public-keyencryption does exactly that. In the next chapter, we’ll show how to incorporate public-key encryption into our Java programs.