13.5. Encrypting Data
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:
-
public class Cipher implements Cloneable
-
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:
-
ECB
-
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.
-
CBC
-
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.
-
CFB
-
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.
-
OFB
-
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.
-
PCBC
-
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.
PCBC mode requires an initialization vector for
decryption.
The
padding schemes specified by the SunJCE security
provider are:
-
PKCS5Padding
-
This padding scheme ensures that the input data is padded to a
multiple of 8 bytes.
-
NoPadding
-
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.
13.5.1. Using the Cipher Class
In order to obtain an instance of the Cipher
class, we call one of these methods:
-
public static Cipher getInstance(String algorithmName)
-
public static Cipher getInstance(String algorithmName, String provider)
-
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:
-
public final void init(int op, Key k)
-
public final void init(int op, Key k, AlgorithmParameterSpec aps)
-
public final void init(int op, Key k, AlgorithmParameterSpec aps, SecureRandom sr)
-
public final void init(int op, Key k, SecureRandom sr)
-
public final void init(int op, Key k, AlgorithmParameters ap)
-
public final void init(int op, Key k, AlgorithmParameters ap, SecureRandom sr)
-
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:
-
public final byte[] update(byte[] input)
-
public final byte[] update(byte[] input, int offset, int length)
-
public final int update(byte[] input, int offset, int length, byte[] output)
-
public final int update(byte[] input, int offset, int length, byte[] output, int outOffset)
-
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:
-
public final byte[] doFinal()
-
public final int doFinal(byte[] output, int offset)
-
public final byte[] doFinal(byte[] input)
-
public final byte[] doFinal(byte[] input, int offset, int length)
-
public final int doFinal(byte[] input, int offset, int length, byte[] output)
-
public final int doFinal(byte[] input, int offset, int length, byte[] output, int outOffset)
-
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:
-
public final byte[] getIV()
-
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:
-
public final int getOutputSize(int inputLength)
-
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:
-
public final Provider getProvider()
-
Return the provider class that defined this engine.
-
public final int getBlockSize()
-
Get the block size of the mode of the algorithm that this cipher
implements.
Let's put this all together into a simple example:
Class Definition
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:
Class Definition
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.
13.5.2. Cipher Algorithms
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:
Class Definition
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.
13.5.3. Implementing the Cipher Class
As in all 1.2-based engines, the SPI for
the Cipher class is a separate class: the
CipherSpi class
(javax.crypto.CipherSpi):
-
public abstract class 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:
-
public abstract void engineSetMode(String s)
-
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.
-
public abstract void engineSetPadding(String s)
-
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:
-
public abstract int engineGetBlockSize()
-
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).
-
public abstract byte[] engineGetIV()
-
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.
-
public abstract int engineGetOutputSize(int inputSize)
-
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.
-
public void engineInit(int op, Key key, SecureRandom sr)
-
public void engineInit(int op, Key key, AlgorithmParameterSpec aps, SecureRandom sr)
-
public void engineInit(int op, Key key, AlgorithmParameters ap, SecureRandom sr)
-
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.
-
public abstract byte[] engineUpdate(int input[], int offset, int len)
-
public abstract int engineUpdate(int input[], int offset, int len, byte[] output, int outOff)
-
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).
-
public abstract byte[] engineDoFinal(int input[], int offset, int len)
-
public abstract int engineDoFinal(int input[], int offset, int len, byte[] output, int outOff)
-
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:
Class Definition
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":
Class Definition
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:
Class Definition
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.
| | |
13.4. Secret Key Engines | | 13.6. Cipher Streams |
Copyright © 2001 O'Reilly & Associates. All rights reserved.
|