In this section, we'll look at the engine that performs encryption within the JCE. This engine is called the Cipher class (javax.crypto.Cipher); it provides an interface to encrypt and decrypt data either in arrays within the program or as that data is read or written through Java's stream interfaces:
Perform encryption and decryption of arbitrary data, using (potentially) a wide array of encryption algorithms.
Like all security engines, the cipher engine implements named algorithms. However, the naming convention for the cipher engine is different, in that cipher algorithms are compound names that can include the name of the algorithm along with the name of a padding scheme and the name of a mode. Padding schemes and modes are specified by names--just like algorithms. In theory, just as you may pick a new name for an algorithm, you may specify new names for a padding scheme or a mode, although the SunJCE security provider specifies several standard ones.
Modes and padding schemes are present in the Cipher class because that class implements what is known as a block cipher; that is, it expects to operate on data one block (e.g., 8 bytes) at a time. Padding schemes are required in order to ensure that the length of the data is an integral number of blocks.
Modes are provided to further alter the encrypted data in an attempt to make it harder to break the encryption. For example, if the data to be encrypted contains a number of similar patterns--repeated names, or header/footer information, for example--any patterns in the resulting data may aid in breaking the encryption. Different modes of encrypting data help prevent these sorts of attacks. Depending upon the mode used by a cipher, it may need to be initialized in a special manner when the cipher is used for decryption. Some modes require initialization via an initialization vector.
Modes also enable a block cipher to behave as a stream cipher; that is, instead of requiring a large, 8-byte chunk of data to operate upon, a mode may allow data to be processed in smaller quantities. So modes are very important in stream-based operations, where data may need to be transmitted one or two characters at a time.
The modes specified by the SunJCE security provider are:
This is the electronic cookbook mode. ECB is the simplest of all modes; it takes a simple block of data (8 bytes in the SunJCE implementation, which is standard) and encrypts the entire block at once. No attempt is made to hide patterns in the data, and the blocks may be rearranged without affecting decryption (though the resulting plaintext will be out of order). Because of these limitations, ECB is recommended only for binary data; text or other data with patterns in it is not well-suited for this mode.
ECB mode can only operate on full blocks of data, so it is generally used with a padding scheme.
ECB mode does not require an initialization vector.
This is the cipher block chaining mode. In this mode, input from one block of data is used to modify the encryption of the next block of data; this helps to hide patterns (although data that contains identical initial text--such as mail messages--will still show an initial pattern). As a result, this mode is suitable for text data.
CBC mode can only operate on full blocks of data (8-byte blocks in the SunJCE implementation), so it is generally used with a padding scheme.
CBC mode requires an initialization vector for decryption.
This is thecipher-feedback mode. This mode is very similar to CBC, but its internal implementation is slightly different. CBC requires a full block (8 bytes) of data to begin its encryption, while CFB can begin encryption with a smaller amount of data. So this mode is suitable for encrypting text, especially when that text may need to be processed a character at a time. By default, CFB mode operates on 8-byte (64-bit) blocks, but you may append a number of bits after CFB (e.g., CFB8) to specify a different number of bits on which the mode should operate. This number must be a multiple of 8.
CFB requires that the data be padded so that it fills a complete block. Since that size may vary, the padding scheme that is used with it must vary as well. For CFB8, no padding is required, since data is always fed in an integral number of bytes.
CFB mode requires an initialization vector for decryption.
This is the output-feedback mode. This mode is also suitable for text; it is used most often when there is a possibility that bits of the encrypted data may be altered in transit (e.g., over a noisy modem). While a 1-bit error would cause an entire block of data to be lost in the other modes, it only causes a loss of 1 bit in this mode. By default, OFB mode operates on 8-byte (64-bit) blocks, but you may append a number of bits after OFB (e.g., OFB8) to specify a different number of bits on which the mode should operate. This number must be a multiple of 8.
OFB requires that the data be padded so that it fills a complete block. Since that size may vary, the padding scheme that is used with it must vary as well. For OFB8, no padding is required, since data is always fed in an integral number of bytes.
OFB mode requires an initialization vector for decryption.
This is the propagating cipher block chaining mode. This mode is popular in a particular system known as Kerberos; if you need to speak to a Kerberos version 4 system, this is the mode to use. However, this mode has some known methods of attack, and Kerberos version 5 has switched to using CBC mode. Hence, PCBC mode is no longer recommended.
PCBC mode requires that the input be padded to a multiple of 8 bytes.
The padding schemes specified by the SunJCE security provider are:
This padding scheme ensures that the input data is padded to a multiple of 8 bytes.
When this scheme is specified, no padding of input is done. In this case, the number of input bytes presented to the encryption cipher must be a multiple of the block size of the cipher; otherwise, when the cipher attempts to encrypt or decrypt the data, it generates an error.
Remember that these uses of mode and padding are specific to the SunJCE security provider. The modes and padding schemes are based upon accepted standards and are thus likely to be implemented in this manner by third-party security providers as well, but you should check your third-party provider documentation to be sure.
The mode and padding scheme specified for decryption must match the mode and padding scheme specified for encryption, or the decryption will fail.
In order to obtain an instance of the Cipher class, we call one of these methods:
Obtain a cipher engine that can perform encryption and decryption by implementing the named algorithm. The engine is provided by the given security provider, or the list of installed security providers is searched for an appropriate engine.
If an implementation of the given algorithm cannot be found, a NoSuchAlgorithmException is thrown. If the named provider cannot be found, a NoSuchProviderException is thrown.
The algorithm name passed to the getInstance() method may either be a simple algorithm name (e.g., DES), or it may be an algorithm name that specifies a mode and padding in this format: algorithm/mode/padding (e.g., DES/ECB/PKCS5Padding). If the mode and padding are not specified, they default to an implementation-specific value; in the SunJCE security provider, the mode defaults to ECB and padding defaults to PKCS5.
Once you've obtained a cipher object, you must initialize it. An object can be initialized for encryption or decryption, but in either case, you must provide a key. If the algorithm is a symmetric cipher, you should provide a secret key; otherwise, you should provide a public key to encrypt data and a private key to decrypt data (in fact, the key must match the algorithm type: a DES cipher must use a DES key, and so on). Initialization is achieved with one of these methods:
Initialize the cipher to encrypt or decrypt data. If op is Cipher.ENCRYPT_MODE, the cipher is initialized to encrypt data; if op is Cipher.DECRYPT_MODE, the cipher is initialized to decrypt data. (In practice, other values will initialize the cipher for encryption rather than generating an exception; this is arguably a bug in the early-access implementation of the JCE.)
These calls reset the engine to an initial state, discarding any previous data that may have been fed to the engine. Hence, a single cipher object can be used to encrypt data and then later to decrypt data.
Many algorithm modes we discussed earlier require an initialization vector to be specified when the cipher is initialized for decrypting. In these cases, the initialization vector must be passed to the init() method within the algorithm parameter specification or algorithm parameters; the IvParameterSpec class is typically used to do this for DES encryption.
In the SunJCE security provider, specifying an initialization vector for a mode that does not support it will eventually lead to a NullPointerException. Failure to specify an initialization vector for a mode that requires one will generate incorrect decrypted data.
After an engine has been initialized, it must be fed data. There are two sets of methods to accomplish this. The first set can be used any number of times:
Encrypt or decrypt the data in the input array (starting at the given offset for the given length, if applicable). The resulting data is either placed in the given output array (in which case the size of the output data is returned) or returned in a new array. If the cipher has not been initialized, an IllegalStateException is thrown.
If the length of the data passed to this method is not an integral number of blocks, any extra data is buffered internally within the cipher engine; the next call to an update() or doFinal() method processes that buffered data as well as any new data that is just being provided.
If the given output buffer is too small to hold the data, a ShortBufferException is thrown. The required size of the output buffer can be obtained from the getOutputSize() method. A ShortBufferException does not clear the state of the cipher: any buffered data is still held, and the call can be repeated (with a correctly sized buffer) with no ill effects.
This second set of methods should only be called once:
Encrypt or decrypt the data in the input array as well as any data that has been previously buffered in the cipher engine. This method behaves exactly the same as the update() method, except that this method signals that all data has been fed to the engine. If the engine is performing padding, the padding scheme will be used to process the pad bytes (i.e., add padding bytes for encryption and remove padding bytes for decryption). If the cipher engine is not performing padding and the total of all processed data is not a multiple of the mode's block size, an IllegalBlockSizeException is thrown.
These methods throw an IllegalStateException or a ShortBufferException in the same circumstances as the update() methods.
In order to initialize some ciphers for decryption, you need to specify an initialization vector; this initialization vector must be the same vector that was used when the cipher was initialized for encryption. For encryption, you may specify the initialization vector, or you may use a system-provided initialization vector. In order to retrieve this vector for later use (e.g., to send it to someone who will eventually need to decrypt the data), you may use this method:
Return the initialization vector that was used to initialize this cipher. If a system-provided initialization vector is used, that vector is not available until after the first call to an update() or doFinal() method.
In order to preallocate an output buffer for use in the update() and doFinal() methods, you must know its size, which is returned from this method:
Return the output size for the next call to the update() or doFinal() methods, assuming that one of those methods is called with the specified amount of data. Note that the size returned from this call includes any possible padding that the doFinal() method might add. A call to the update() method may actually generate less data than this method would indicate, because it will not create any padding.
Finally, there are two miscellaneous methods of this class:
Return the provider class that defined this engine.
Get the block size of the mode of the algorithm that this cipher implements.
Let's put this all together into a simple example:
public class CipherTest { public static void main(String args[]) { try { KeyGenerator kg = KeyGenerator.getInstance("DES"); Cipher c = Cipher.getInstance("DES/CBC/PKCS5Padding"); Key key = kg.generateKey(); c.init(Cipher.ENCRYPT_MODE, key); byte input[] = "Stand and unfold yourself".getBytes(); byte encrypted[] = c.doFinal(input); byte iv[] = c.getIV(); IvParameterSpec dps = new IvParameterSpec(iv); c.init(Cipher.DECRYPT_MODE, key, dps); byte output[] = c.doFinal(encrypted); System.out.println("The string was "); System.out.println(new String(output)); } catch (Exception e) { e.printStackTrace(); } } }
We've reused the single engine object to perform both the encryption and the decryption. Since DES is a symmetric encryption algorithm, we generated a single key that is used for both operations. Within the try block, the second block of code performs the encryption:
We initialize the cipher engine for encrypting.
We pass the bytes we want to encrypt to the doFinal() method. Of course, we might have had any number of calls to the update() method preceding this call, with data in any arbitrary amounts. Since we've specified a padding scheme, we don't have to worry about the size of the data we pass to the doFinal() method.
Finally, we save the initialization vector the system provided to perform the encryption. Note that this step would not be needed for ECB mode.
Performing the decryption is similar:
Performing the decryption is similar:
First, we initialize the cipher engine for decrypting. In this case, however, we must provide an initialization vector to initialize the engine in order to get the correct results (again, this would be unnecessary for ECB mode).
Next, we pass the encrypted data to the doFinal() method. Again, we might have had multiple calls to the update() method first.
In typical usage, of course, encryption is done in one program and decryption is done in another program. In the example above, this entails that the initialization vector and the encrypted data must be transmitted to a receiver; this may be done via a socket or a file or any other convenient means. There is no security risk in transmitting the initialization vector, as it has the same properties as the rest of the encrypted data.
In this example, we used the PKCS5 padding scheme to provide the necessary padding. This is by far the simplest way. If you want to do your own padding--if, for example, you're using a CFB32 mode for some reason--you need to do something like this:
Cipher c = Cipher.getInstance("DES/CFB32/NoPadding"); c.init(Cipher.ENCRYPT_MODE, desKey); int blockSize = c.getBlockSize(); byte b[] = "This string has an odd length".getBytes(); byte padded[] = new byte[b.length + blockSize -(b.length % blockSize)]; System.arraycopy(b, 0, padded, 0, b.length); for (int i = 0; i < blockSize - (b.length % blockSize); i++) padded[b.length + i] = 0; byte output[] = c.doFinal(padded);
The problem with this code is that when the data is decrypted, there is no indication of how many bytes should be discarded as padding. PKCS5 and other padding schemes solve this problem by encoding that information into the padding itself.
The SunJCEsecurity provider supports three cipher algorithms:
DES, the Data Encryption Standard algorithm, a standard that has been adopted by various organizations, including the U.S. government. There are known ways to attack this encryption, though they require a lot of computing power to do so; despite widespread predictions about the demise of DES, it continues to be used in many applications and is generally considered secure. The examples in this chapter are mostly based on DES encryption.
DESede, also known as triple-DES or multiple-DES. This algorithm uses multiple DES keys to perform three rounds of DES encryption or decryption; the added complexity greatly increases the amount of time required to break the encryption. It also greatly increases the amount of time required to encrypt and to decrypt the data.
From a developer's perspective, DESede is equivalent to DES; only the algorithm name passed to the key generator and cipher engines is different. Although DESede requires multiple keys, these keys are encoded into a single secret key. Hence, the programming steps required to use DESede are identical to the steps required to use DES.
PBEWithMD5AndDES, the password-based encryption defined in PKCS#5. This algorithm entails using a password, a byte array known as salt, and an iteration count along with an MD5 message digest to produce a DES secret key; this key is then used to perform DES encryption or decryption. PKCS#5 was developed by RSA Data Security, Inc., primarily to encrypt private keys, although it may be used to encrypt any arbitrary data.
From a developer's perspective, this algorithm requires some special programming to obtain the key. A password-based cipher cannot be initialized without special data that is passed via the algorithm specification. This data is known as the salt and iteration count. Hence, a password-based cipher is initialized as follows:
String password = "Come you spirits that tend on mortal thoughts"; byte[] salt = { (byte) 0xc9, (byte) 0x36, (byte) 0x78, (byte) 0x99, (byte) 0x52, (byte) 0x3e, (byte) 0xea, (byte) 0xf2 }; PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 20); PBEKeySpec keySpec = new PBEKeySpec(password); SecretKeyFactory kf = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); SecretKey key = kf.generateSecret(keySpec); Cipher c = Cipher.getInstance("PBEWithMD5AndDES"); c.init(Cipher.ENCRYPT_MODE, key, paramSpec);
The rationale behind this system is that it allows the password to be shared verbally (or otherwise) between participants in the cipher; rather than coding the password as we've done above, the user would presumably enter the password. Since these types of passwords are often easy to guess (a string comparison of the above password against the collected works of Shakespeare would guess the password quite easily, despite its length), the iteration and salt provide a means to massage the password into something more secure. The salt itself should be random, and the higher the iteration count, the more expensive a brute-force attack against the key becomes (though it also takes longer to generate the key itself).
Of course, despite the presence of the salt and iteration, the password chosen in the method should not be easy to guess in the first place: it should contain special characters, not be known quotes from literature, and follow all the other usual rules that apply to selecting a passwor d.
As in all 1.2-based engines, the SPI for the Cipher class is a separate class: the CipherSpi class (javax.crypto.CipherSpi):
The SPI for the Cipher class. This class is responsible for performing the encryption or decryption according to its internal algorithm. Support for various modes or padding schemes must be handled by this class as well.
There is very little intelligence in the Cipher class itself; virtually all of its methods are simply passthough calls to corresponding methods in the SPI. The one exception to this is the getInstance() method, which is responsible for parsing the algorithm string and removing the mode and padding strings if present. If it finds a mode and padding specification, it calls these methods of the SPI:
Set the mode of the cipher engine according to the specified string. If the given mode is not supported by this cipher, a NoSuchAlgorithmException should be thrown.
Set the padding scheme of the cipher engine according to the specified string. If the given padding scheme is not supported by this cipher, a NoSuchPaddingException should be thrown.
Remember that the mode and padding strings we looked at earlier are specific to the implementation of the SunJCE security provider. Hence, while ECB is a common mode specification, it is completely at the discretion of your implementation whether that string should be recognized or not. If you choose to implement a common mode, it is recommended that you use the standard strings, but you may use any naming convention that you find attractive. The same is true of padding schemes.
Complicating this matter is the fact that there are no classes in the JCE that assist you with implementing any mode or padding scheme. So if you need to support a mode or padding scheme, you must write the required code from scratch.
The remaining methods of the SPI are all called directly from the corresponding methods of the Cipher class:
Return the number of bytes that comprise a block for this engine. Unless the cipher is capable of performing padding, input data for this engine must total a multiple of this block size (though individual calls to the update() method do not necessarily have to provide data in block-sized chunks).
Return the initialization vector that was used to initialize the cipher. If the cipher was in a mode where no initialization vector was required, this method should return null.
Return the number of bytes that the cipher will produce if the given amount of data is fed to the cipher. This method should take into account any data that is presently being buffered by the cipher as well as any padding that may need to be added if the cipher is performing padding.
Initialize the cipher based on the op, which will be either Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE. This method should ensure that the key is of the correct type and throw an InvalidKeyException if it is not (or if it is otherwise invalid), and use the given random number generator (and algorithm parameters, if applicable) to initialize its internal state. If algorithm parameters are provided but not supported or are otherwise invalid, this method should throw an InvalidAlgorithmParameterException.
Encrypt or decrypt the input data. The data that is passed to these methods will is not necessarily an integral number of blocks. It is the responsibility of these methods to process as much of the input data as possible and to buffer the remaining data internally. Upon the next call to an engineUpdate() or engineDoFinal() method, this buffered data must be processed first, followed by the input data of that method (and again leaving any leftover data in an internal buffer).
Encrypt or decrypt the input data. Like the update() method, this method must consume any buffered data before processing the input data. However, since this is the final set of data to be processed, this method must make sure that the total amount of data has been an integral number of blocks; it should not leave any data in its internal buffers.
If the cipher supports padding (and padding was requested through the engineSetPadding() method), this method should perform the required padding; an error in padding should cause a BadPaddingException to be thrown. Otherwise, if padding is not being performed and the total amount of data has not been an integral number of blocks, this method should throw an IllegalBlockSizeException.
Using our typical XOR strategy of encryption, here's a simple implementation of a cipher engine:
public class XORCipher extends CipherSpi { byte xorByte; public void engineInit(int i, Key k, SecureRandom sr) throws InvalidKeyException { if (!(k instanceof XORKey)) throw new InvalidKeyException("XOR requires an XOR key"); xorByte = k.getEncoded()[0]; } public void engineInit(int i, Key k, AlgorithmParameterSpec aps, SecureRandom sr) throws InvalidKeyException, InvalidAlgorithmParameterException { throw new InvalidAlgorithmParameterException( "Algorithm parameters not supported in this class"); } public void engineInit(int i, Key k, AlgorithmParameters ap, SecureRandom sr) throws InvalidKeyException, InvalidAlgorithmParameterException { throw new InvalidAlgorithmParameterException( "Algorithm parameters not supported in this class"); } public byte[] engineUpdate(byte in[], int off, int len) { return engineDoFinal(in, off, len); } public int engineUpdate(byte in[], int inoff, int length, byte out[], int outoff) { for (int i = 0; i < length; i++) out[outoff + i] = (byte) (in[inoff + i] ^ xorByte); return length; } public byte[] engineDoFinal(byte in[], int off, int len) { byte out[] = new byte[len - off]; engineUpdate(in, off, len, out, 0); return out; } public int engineDoFinal(byte in[], int inoff, int len, byte out[], int outoff) { return engineUpdate(in, inoff, len, out, outoff); } public int engineGetBlockSize() { return 1; } public byte[] engineGetIV() { return null; } public int engineGetOutputSize(int sz) { return sz; } public void engineSetMode(String s) throws NoSuchAlgorithmException { throw new NoSuchAlgorithmException("Unsupported mode " + s); } public void engineSetPadding(String s) throws NoSuchPaddingException { throw new NoSuchPaddingException("Unsupported padding " + s); } }
The bulk of the work of any cipher engine will be in the engineUpdate() method, which is responsible for actually providing the ciphertext or plaintext. In this case, we've simply XORed the key value with every byte, a process that works both for encryption as well as decryption. Because the work done by the engineUpdate() method is so symmetric, we don't need to keep track internally of whether we're encrypting or decrypting; for us, the work is always the same. For some algorithms, you may need to keep track of the state of the cipher by setting an internal variable when the engineInit() method is called.
Similarly, because we can operate on individual bytes at a time, we didn't have to worry about padding and buffering internal data. Such an extension is easy, using the code we showed earlier that uses the modulus operator to group the input arrays into blocks.
To use this class, we would need to add these two lines to the XYZProvider class we developed in Chapter 8, "Security Providers":
put("Cipher.XOR", "XORCipher"); put("KeyGenerator.XOR", "XORKeyGenerator");
Then it is a simple matter of installing the XOR security provider and getting an instance of this cipher engine:
Security.addProvider(new XYZProvider()); KeyGenerator kg = KeyGenerator.getInstance("XOR"); Cipher c = Cipher.getInstance("XOR");
Note that "XOR" is the only valid algorithm name for this implementation since we do not support any modes or padding schemes.
Copyright © 2001 O'Reilly & Associates. All rights reserved.
This HTML Help has been published using the chm2web software. |