12.5. The Cipher ClassThe javax.crypto.Cipher class is a concrete class that encrypts arrays of bytes. The default implementation performs no encryption, but you'll never see this. You'll only receive subclasses that implement particular algorithms.
The subclasses of Cipher that do real encryption are supplied by providers. Different providers can provide different sets of algorithms. For instance, an authoritarian government might only allow the installation of algorithms it knows how to crack, and create a provider that provided those algorithms and only those algorithms. A corporation might want to install algorithms that allow for key recovery in the event that an employee leaves the company or forgets their password. JDK 1.3 and earlier only include the Sun provider that supplies no encryption schemes, though it does supply several digest algorithms. The JCE (which is bundled with Java 1.4 and later) adds one more provider, SunJCE, Most providers include some unique algorithms. However, providers usually also include some algorithms already supplied by other providers. At compile time, you do not know which providers will be installed at runtime. Indeed, different people running your program are likely to have different providers available, especially if you ship internationally. Therefore, rather than using constructors, the Cipher class relies on two static getInstance( ) factory methods that return Cipher objects initialized to support particular transformations:
The first argument, TRansformation, is a string that names the algorithm, mode, and padding scheme to be used to encrypt or decrypt the data. Examples include "DES", "PBEWithMD5AndDES", and "DES/ECB/PKCS5Padding". The optional second argument to getInstance( ), provider, names the preferred provider for the requested transformation. If more than one installed provider supports the transformation, the one named in the second argument is used. Otherwise, an implementation is selected from any available provider that supports the transformation. If you request a transformation from getInstance( ) that the provider does not support, a NoSuchAlgorithmException or NoSuchPaddingException is thrown. If you request a provider that is not installed, a NoSuchProviderException is thrown. The transformation string always includes the name of a cryptographic algorithm: for example, DES. The standard names for common algorithms are listed in Table 12-2. Not all of these algorithms are guaranteed to be available. Sun's JDK 1.4 only bundles DES, DESede, AES, Blowfish, PBEWithMD5AndDES, and PBEWithMD5AndTripleDES. JDK 1.5 added RC2, ARCFOUR, PBEWithSHA1AndDESede, and PBEWithSHA1AndRC2_40.
When faced with input longer than its block size, a block cipher must divide and possibly reorder that input into blocks of the appropriate size. The algorithm for doing this is called a mode. A mode name may be included in the transformation string separated from the algorithm by a slash. If a mode is not selected, the provider supplies a default. Modes apply to block ciphers in general and DES in particular, though other block ciphers like Blowfish may use some of these modes as well. The named modes in the JCE are listed in Table 12-3. All of these modes are supported by the JCE, but modes are algorithm-specific. If you try to use an unsupported mode or a mode that doesn't match the algorithm, a NoSuchAlgorithmException is thrown.
If the algorithm is a block cipher like DES, the transformation string may include a padding scheme that adds extra bytes to the input to fill out the last block. The named padding schemes
Encrypting data with a Cipher object takes six steps:
Steps 1 and 2 can be reversed, as is done in the flowchart for this process shown in Figure 12-2. Decryption is almost an identical process except that you pass Cipher.DECRYPT_MODE to init( ) instead of Cipher.ENCRYPT_MODE. The same engine can both encrypt and decrypt data with a given transformation. Figure 12-2. Encrypting dataExample 12-4 is a simple program that reads a filename and a password from the command line and encrypts the file with DES. The key is generated from the bytes of the password in a fairly predictable and insecure fashion. The cipher is initialized for encryption with the DES algorithm in CBC mode with PKCS5Padding and a random initialization vector. The initialization vector and its length are written at the start of the encrypted file so they'll be conveniently available for decryption. Data is read from the file in 64-byte blocks. This happens to be an integral multiple of the 8-byte block size used by DES, but that's not necessary. The Cipher object buffers as necessary to handle nonintegral multiples of the block size. Each block of data is fed into the update( ) method to be encrypted. update( ) returns either encrypted data or null if it doesn't have enough data to fill out a block. If it returns the encrypted data, that's written into the output file. When no more input data remains, the cipher's doFinal( ) method is invoked to pad and flush any remaining data. Then both input and output files are closed. Example 12-4. File Encryptor
Many different exceptions must be caught. Except for the usual IOException, they are all subclasses of java.security.GeneralSecurityException. You could save some space simply by catching that. For example:
One exception I'll note in particular (because it threw me more than once while writing this chapter): if you should see a NoSuchAlgorithmException, it probably means you haven't properly installed a provider that supports your algorithm. Decrypting a file is similar, as Example 12-5 shows. The name of the input and output files and the password are read from the command line. A DES key factory converts the password to a DES secret key. Both input and output files are opened in file streams, and a data input stream is chained to the input file. The main reason for this is to read the initialization vector. First, the integer size is read, and then the actual bytes of the vector. The resulting array is used to construct an IvParameterSpec object that is used along with the key to initialize the cipher. Once the cipher is initialized, the data is copied from input to output much as before. Example 12-5. File Decryptor
Let's investigate some of the methods used in Example 12-4 and Example 12-5 in more detail. 12.5.1. init( )Before a Cipher object can encrypt or decrypt data, it needs four things:
The init( ) method prepares the cipher by providing these four quantities or reasonable defaults. There are six overloaded variants:
You can reuse a cipher object by invoking its init( ) method a second time. If you do, all previous information in the object is lost. 12.5.1.1. ModeThe mode determines whether this cipher is used for encryption or decryption. The mode argument has two possible values, which are both mnemonic constants defined by the Cipher class: Cipher.ENCRYPT_MODE and Cipher.DECRYPT_MODE.
12.5.1.2. KeyThe key is an instance of the java.security.Key interface. Symmetric ciphers like DES use the same key for both encryption and decryption. Asymmetric ciphers like RSA use different keys for encryption or decryption. Keys generally depend on the cipher. For instance, an RSA key cannot be used to encrypt a DES file or vice versa. If the key you provide doesn't match the cipher's algorithm, an InvalidKeyException is thrown. To create a key, you first use the bytes of the key to construct a KeySpec for the algorithm you're using. Key specs are instances of the java.security.spec.KeySpec interface. Algorithm-specific implementations in the java.security.spec package include EncodedKeySpec, X509EncodedKeySpec, KCS8EncodedKeySpec, DSAPrivateKeySpec, and DSAPublicKeySpec, RSAPrivateKeySpec, RSAPrivateCrtKeySpec, RSAMultiPrimePrivateCrtKeySpec, RSAPublicKeySpec, and X509EncodedKeySpec. Java 5 added ECPrivateKeySpec and ECPublicKeySpec for public key cryptography based on elliptic curves rather than prime factorization. The javax.crypto spec package provides a few more including DESKeySpec, DESedeKeySpec, DHPrivateKeySpec, DHPublicKeySpec, PBEKeySpec. For example, this code fragment creates a DESKeySpec object that can be used to encrypt or decrypt from a password string using the DES algorithm:
Once you've constructed a key specification from the raw bytes of the key, a key factory generates the actual key. A key factory is normally an instance of an algorithm-specific subclass of java.security.KeyFactory. It's retrieved by passing the name of the algorithm to the factory method javax.crypto.SecretKeyFactory.getInstance( ). For example:
Providers should supply the necessary key factories and spec classes for any algorithms they implement. A few algorithms, most notably Blowfish, use raw bytes as a key without any further manipulations. In these cases there may not be a key factory for the algorithm. Instead, you simply use the key spec as the secret key. For example:
Most of the examples in this book use very basic and not particularly secure passwords as keys. Stronger encryption requires more random keys. The javax.crypto.KeyGenerator class provides methods that generate random keys for any installed algorithm. For example:
Generating random keys opens up the issue of how one stores and transmits the secret keys. To my way of thinking, random key generation makes more sense in public key cryptography, where all keys that need to be transmitted can be transmitted in the clear. 12.5.1.3. Algorithm parametersThe third possible argument to init( ) is a series of instructions for the cipher contained in an instance of the java.security.spec.AlgorithmParameterSpec interface or an instance of the java.security.AlgorithmParameters class. The AlgorithmParameterSpec interface declares no methods or constants. It's simply a marker for more specific subclasses that can provide additional, algorithm-dependent parameters for specific algorithms and modes (for instance, an initialization vector). If the algorithm parameters you provide don't fit the cipher's algorithm, an InvalidAlgorithmParameterException is thrown. The JCE provides several AlgorithmParameterSpec classes in the javax.crypto.spec package, including IVParameterSpec, which can set an initialization vector for modes that need it (CBC, CFB, and OFB), and PBEParameterSpec for password-based encryption. 12.5.1.4. Source of randomnessThe final possible argument to init( ) is a SecureRandom object. This argument is only used when in encryption mode. It is an instance of the java.security.SecureRandom class, a subclass of java.util.Random that uses a pseudo-random number algorithm based on the SHA-1 hash algorithm instead of java.util.Random's linear congruential formula. java.util.Random's random numbers aren't random enough for strong cryptography. In this book, I will simply accept the default source of randomness. 12.5.2. update( )Once the init( ) method has prepared the cipher for use, the update( ) method feeds data into it, encrypting or decrypting as it goes. This method has four overloaded variants. The first two return the encrypted or decrypted bytes:
They may return null if you're using a block cipher and not enough data has been provided to fill a block. The input data to be encrypted or decrypted is passed in as an array of bytes. Optional offsets and lengths may be used to select a particular subarray to be processed. update( ) tHRows an IllegalStateException if the cipher has not been initialized or it has already been finished with doFinal( ). In either case, it's not prepared to accept data until init( ) is called. The second two variants of update( ) store the output in a buffer byte array passed in as the fourth argument and return the number of bytes stored in the buffer:
You can also provide an offset into the output array to specify where in the array data should be stored. An offset is useful when you want to repeatedly encrypt/decrypt data into the same array until the data is exhausted. You cannot, however, specify a length for the output data because it's up to the cipher to determine how many bytes of data it's willing to provide. The trick here is to make sure your output buffer is big enough to hold the processed output. Most of the time, the number of output bytes is close to the number of input bytes. However, block ciphers sometimes return fewer bytes on one call and more on the next. You can use the getOutputSize( ) method to determine an upper bound on the amount of data that will be returned if you were to pass in inputLength bytes of data:
If you don't do this and your output buffer is too small, update( ) throws a ShortBufferException. In this case, the cipher stores the data for the next call to update( ). Java 5 added an update( ) method that reads from a ByteBuffer and writes into an output ByteBuffer:
Once you run out of data to feed to update( ), invoke doFinal( ) 12.5.3. doFinal( )The doFinal( ) method is responsible for reading one final array of data, wrapping that up with any data remaining in the cipher's internal buffer, adding any extra padding that might be necessary, and then returning the last chunk of encrypted or decrypted data. The simplest implementation of doFinal( ) takes no arguments and returns an array of bytes containing the encrypted or decrypted data. This is used to flush out any data that still remains in the cipher's buffer.
An IllegalStateException means that the cipher is not ready to be finished; it has not been initialized; it has been initialized but no data has been fed into it; or it has already been finished and not yet reinitialized. An IllegalBlockSizeException is thrown by encrypting block ciphers if no padding has been requested, and the total number of bytes fed into the cipher is not a multiple of the block size. A BadPaddingException is thrown by a decrypting cipher that does not find the padding it expects to see. There are five overloaded variants of doFinal( ) that allow you to provide additional input data or to place the result in an output buffer you supply. These variants are:
All of the arguments are essentially the same as they are for update( ). output is a buffer where the cipher places the encrypted or decrypted data. outputOffset is the position in the output buffer where this data is placed. input is a byte array that contains the last chunk of data to be encrypted. inputOffset and inputLength select a subarray of input to be encrypted or decrypted. 12.5.4. Accessor MethodsAs well as the methods that actually perform the encryption, the Cipher class has several getter methods that provide various information about the cipher. The getProvider( ) method returns a reference to the Provider that's implementing this algorithm. This is an instance of a subclass of java.security.Provider.
For block ciphers, getBlockSize( )returns the number of bytes in a block. For nonblock methods, it returns 0.
The getOutputSize( ) method tells you how many bytes of output this cipher produces for a given number of bytes of input. You generally use this before calling doFinal( ) or update( ) to make sure you provide a large enough byte array for the output, given inputLength additional bytes of data.
The length returned is the maximum number of bytes that may be needed. In some cases, fewer bytes may actually be returned when doFinal( ) is called. An IllegalStateException is thrown if the cipher is not ready to accept more data. The getIV( ) method returns a new byte array containing this cipher's initialization vector. It's useful when the system picks a random initialization vector and you need to find out what that vector is so you can pass it to the decryption program, perhaps by storing it with the encrypted data.
getIV( ) returns null if the algorithm doesn't use initialization vectors or if the initialization vector isn't yet set. |
Tuesday, January 19, 2010
Section 12.5. The Cipher Class
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment