ICryptoTransform
You'll get to be good friends with this interface as you put encryption to work in your applications. ICryptoTransform
is the standard interface through which you ask the cipher to encrypt and decrypt data. Note that this interface is also used by other cryptographic classes, such as hashes. Let's have a quick look at the members of this interface.
TransformBlock
– Call this method to transform (encrypt or decrypt) 1 or more blocks before the end of the message.
TransformFinalBlock
– Call this method to transform 1 or more blocks at the end of the message. Given our discussion of padding, you should understand why it is necessary to transform blocks at the end of the message differently than those before the end.
CanReuseTransform
– Returns a boolean indicating whether the current transform can be reused. In the case of the FCL ciphers, this will always return true.
CanTransformMultipleBlocks
– Returns a boolean indicating whether multiple blocks can be transformed in a single call to either TransformBlock
or TransformFinalBlock
. In the case of the FCL ciphers, this property also always returns true, indicating that you need not add logic to your code to process individual blocks of data. The one caveat, of course, is that you must call the appropriate transform depending on whether the data is at the end of the message (TransformFinalBlock
) or not (TransformBlock
).
InputBlockSize
/OutputBlockSize
– These properties return an integer indicating the input and output block sizes, respectively. In the case of ciphers, these will always be the same, and their return value will be dependent upon the value you specified in the BlockSize property of the cipher.
Encrypting and Decrypting
We now have enough background to begin encrypting and decrypting data. To perform the actual transformation of Encryption or Decryption, you need to create an instance of what is called a Transform
class. You do this in .NET by calling the cipher's CreateEncryptor
or CreateDecryptor
methods, depending, of course, on whether you're encrypting data or decrypting data. The CreateEncryptor
/Decryptor
methods return an ICryptoTransform
interface to the newly created transform class. Once you have the ICryptoTransform
interface, you simply need to call the TransformBlock
and TransformFinalBlock
methods as appropriate. Let's add two new methods to our CipherWrapper
class: EncryptMessage
and DecryptMessage
.
public byte[] EncryptMessage(byte[] plainText)
{
ICryptoTransform transform = _cipher.CreateEncryptor();
byte[] cipherText = transform.TransformFinalBlock(plainText, 0, plainText.Length);
return cipherText;
}
public byte[] DecryptMessage(byte[] cipherText)
{
ICryptoTransform transform = _cipher.CreateDecryptor();
byte[] plainText = transform.TransformFinalBlock(cipherText, 0, cipherText.Length);
return plainText;
}
Note that the only difference between the two functions is the call to either CreateEncryptor
or CreateDecryptor
. These two methods return an ICryptoTransform
interface to a new transform class which actually performs the correct cryptographic transformations depending on the encryption mode, Encrypt or Decrypt. It is necessary to return a new class instance, because the transformation must maintain state in the case of transformation performed in multiple steps.
The example above assumes that we're using a message-based protocol where we know the entire contents of either the plain or cipher text at encryption/decryption time. Since we also know that the FCL ciphers always return true from CanTransformMultipleBlocks
, we need not call TransformBlock
method at all. We can simply transform the entire message in one go by calling TransformFinalBlock
. If you are implementing a system where you may not know the entire message, you will have to modify this simple example to call TransformBlock
/TransformFinalBlock
as appropriate. It's not terribly difficult to use the TransformBlock
method. Basically, the only difference from a coding standpoint is that it does not return any data. Instead, you must pass in a buffer, into which the cipher will place the transformed bytes, as well as an offset into the buffer where you would like the data to be stored.
Crypto Streams
For stream-based protocols, it would be nice to have built-in functionality to allow us to read and write individual bytes at a time or at least to write chunks not broken into perfect blocks. Fortunately, the FCL provides this functionality in the CryptoStream
class. I don't want to clutter the article with code that is specific to streaming rather than cryptography, so I'll just provide a couple of examples that demonstrate the basics of using the CryptoStream
class.
byte[] EncryptMessageUsingStream(byte[] plainText)
{
ICryptoTransform transform = _cipher.CreateEncryptor();
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Write);
cs.Write(plainText, 0, plainText.Length);
// Remember padding! This instructs the transform to pad and finish.
cs.FlushFinalBlock();
byte[] cipherText = ms.ToArray();
return cipherText;
}
Notes: In order to implement this in a stream-based protocol, you would break this method up into possibly several methods that initialize the stream once, then call CryptoStream.Write
in iterations, then call FlushFinalBlock
and read the data.
byte[] DecryptMessageUsingStream(byte[] cipherText)
{
ICryptoTransform transform = _cipher.CreateDecryptor();
MemoryStream ms = new MemoryStream(cipherText);
CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Read);
byte[] plainTextBuffer = new byte[cipherText.Length];
// Remember padding! The length read may be different than the
// length of the plain text.
int plainTextLength = cs.Read(plainTextBuffer, 0, cipherText.Length);
byte[] plainText = new byte[plainTextLength];
Array.Copy(plainTextBuffer, 0, plainText, 0, plainTextLength);
return plainText;
}
Notes: The notes on EncryptMessageUsingStream
also apply here. The reason that the length of the cipher text and the actual number of bytes that are read from the stream are different is, of course, padding. You don't necessarily have to allocate a temporary buffer, as I have done. You could simply return the entire plainTextBuffer
with an integer indicating the length of the actual plain text, which is returned from the CryptoStream.Read
call. Alternatively, if you know the length of the plain text, you could pass this as a parameter and only allocate and read the appropriate length.
Comments