51. Encrypt and decrypt strings

Before you can use the .NET Framework's cryptography tools to encrypt and decrypt strings (or files, arrays of bytes, or just about anything else), you need to perform some setup. The following method creates an ICryptoTransform object that you can use to encrypt or decrypt data:

// Prepare a cryptographic transformation for this password
// and SymmetricAlgorithm.
private static ICryptoTransform MakeCryptoTransform(
string password, bool doEncryption, SymmetricAlgorithm cryptoProvider)
{
// Find a valid key size for this provider.
int numKeyBits = FindKeySize(cryptoProvider);
Console.WriteLine($"Key size: {numKeyBits} bits");

// Get the block size for this provider.
int blockSizeBits = cryptoProvider.BlockSize;

// Generate the key and IV.
byte[] key = null;
byte[] iv = null;
byte[] salt = { 0x03, 0x07, 0x11, 0x22, 0xAB, 0xCD, 0x1F,
0xF1, 0xF1, 0x00, 0xA4, 0x6B, 0xC4, 0x99 };
MakeKeyAndIV(password, salt, numKeyBits, blockSizeBits,
out key, out iv);

// Make the AES encryptor or decryptor.
ICryptoTransform cryptoTransform;
if (doEncryption)
cryptoTransform = cryptoProvider.CreateEncryptor(key, iv);
else
cryptoTransform = cryptoProvider.CreateDecryptor(key, iv);
return cryptoTransform;
}

This method takes as parameters a password, a flag indicating whether you want to encrypt or decrypt, and a SymmetricAlgorithm provider object. You'll see how the last one works later when I show you how to use this method.

The code first needs to find an appropriate key size for the cryptographic provider that it will use. The key sizes that you can use depend on your version of Windows, and that depends on the country where you bought Windows. For example, the US government may allow Windows to use a 256-bit key in the United States but a shorter, less secure key size in some other countries. The FindKeySize method described shortly returns a key size that the algorithm can use.

Next, the program must create a key and initialization vector (IV) to initialize the algorithm. The key is sort of like the password after it has been processed. The IV is an array of bytes that determines the encryption algorithm's initial internal state. The algorithm uses both of those to initialize itself.

The MakeKeyAndIV method described shortly creates a key and IV. Most of its parameters are straightforward. The most confusing is the salt.

A salt is an array of random values that you pick to make it harder for an attacker to build a dictionary of possible passwords. If different programs use different salts, then the attacker cannot use a single dictionary to try to attack them all. You should pick a different salt for your programs. In particular, do not use the one shown here. (Of course, if one program needs to decrypt messages produced by another program, then they need to use the same salt.)

Next, the code calls the cryptographic service provider's CreateEncryptor or CreateDecryptor method to create the object that will actually encrypt or decrypt messages. Finally, the method returns the resulting transform object.

The following code shows the FindKeySize helper method:

// Find a valid key size for this algorithm.
private static int FindKeySize(SymmetricAlgorithm algorithm)
{
for (int i = 1024; i > 1; i--)
{
if (algorithm.ValidKeySize(i)) return i;
}
throw new InvalidOperationException(
$"Cannot find a valid key size for
{algorithm.GetType().Name}.");
}

This method loops through possible key sizes starting at 1,024 bits and moving through smaller sizes. It uses the cryptographic provider's ValidKeySize method to check each potential key size and returns that size if the method returns true.

If the method cannot find a valid key size, it throws an exception.

The following code shows the MakeKeyandIV method:

// Use a password to make a key and IV.
private static void MakeKeyAndIV(string password, byte[] salt,
int keySizeBits, int blockSizeBits, out byte[] key, out byte[] iv)
{
Rfc2898DeriveBytes deriveBytes =
new Rfc2898DeriveBytes(password, salt, 1000);
key = deriveBytes.GetBytes(keySizeBits / 8);
iv = deriveBytes.GetBytes(blockSizeBits / 8);
}

This method creates an Rfc2898DeriveBytes object. That object is sort of like a CPRNG that initializes itself from a password and salt that you pass into its constructor. The final parameter to the constructor is the number of times that the constructor should run its algorithm (which for this object is the HMACSHA1 algorithm) before generating the key and IV.

Microsoft recommends that you set the iteration count to at least 1,000, at least partly to make the operation take longer. (See the Remarks section at https://docs.microsoft.com/dotnet/api/system.security.cryptography.rfc2898derivebytes.iterationcount.) If an attacker wants to break your code by trying random passwords, this makes each attempt take longer. Most programs only need to create an Rfc2898DeriveBytes  object once, so it doesn't hurt you too much to use a large iteration count.

After creating the Rfc2898DeriveBytes object, the method calls its GetBytes method twice to get the necessary number of bytes for the key and IV.

At this point, you're ready to use the MakeCryptoTransform method to encrypt or decrypt strings. The following extension method encrypts or decrypts an array of bytes:

// Encrypt or decrypt a byte[].
private static byte[] EncryptDecryptBytes(string password,
byte[] inputBytes, bool doEncryption)
{
try
{
// Make the encryptor or decryptor.
ICryptoTransform cryptoTransform = MakeCryptoTransform(
password, doEncryption, new AesCryptoServiceProvider());

// Make the output stream.
using (MemoryStream outputStream = new MemoryStream())
{
// Attach a CryptoStream.
using (CryptoStream cryptoStream = new CryptoStream(
outputStream, cryptoTransform, CryptoStreamMode.Write))
{
// Write the bytes into the CryptoStream.
cryptoStream.Write(inputBytes, 0, inputBytes.Length);
cryptoStream.FlushFinalBlock();
return outputStream.ToArray();
}
}
}
catch (CryptographicException ex)
{
// The password is incorrect.
throw new CryptographicException("Invalid password.", ex);
}
catch
{
// Re-throw.
throw;
}
}

This method calls the MakeCryptoTransform method to create an ICryptoTransform object. That object only works with streams, so the program creates a MemoryStream to hold the encrypted output. It then makes a CryptoStream associated with the output stream and the ICryptoTransform object.

The code then writes the message bytes into the CryptoStream. That stream automatically uses the ICryptoTransform object to encrypt the message and writes the result into the output stream.

The method then returns the resulting output stream converted into an array of bytes.

Note that the whole method is enclosed in a try-catch block. When a cryptographic object fails, it is usually because the program tried to decrypt a message with the wrong password. Unfortunately in that case, the exception usually doesn't tell you that the password was wrong. Instead, it gives you some other cryptic message (no pun intended) that isn't very helpful. This method catches those kinds of exceptions and raises a new one that makes more sense.

Now you can use the EncryptDecryptBytes method to encrypt or decrypt strings. The following method encrypts a string:

// Encrypt a string into a byte[].
public static byte[] Encrypt(this string plaintext, string password)
{
byte[] plainbytes = Encoding.Unicode.GetBytes(plaintext);
return EncryptDecryptBytes(password, plainbytes, true);
}

This code converts a Unicode string into a byte array, calls EncryptDecryptBytes to encrypt the array, and returns the result.

The following method decrypts a string:

// Decrypt a string from a byte[].
public static string Decrypt(this byte[] cipherbytes, string password)
{
byte[] plainbytes = EncryptDecryptBytes(password, cipherbytes,
false);
return Encoding.Unicode.GetString(plainbytes);
}

This method calls EncryptDecryptBytes to decrypt the encrypted array, converts the resulting bytes into a Unicode string, and returns the result.

Those methods encrypt a string into a byte array and decrypt a byte array into a string. The example solution also displays the encrypted bytes as a string with a format similar to 33-33-93-CB-6E-BE-3F-B4-95-27-EB-2B-C4-... The following method converts a byte array into that kind of string:

// Convert a byte[] into hexadecimal values.
public static string BytesToHex(this byte[] bytes)
{
return BitConverter.ToString(bytes, 0);
}

This method simply calls BitConverter.ToString to convert the array into a string representation. The last parameter 0 tells the method to start at byte 0 in the array.

Unfortunately, the BitConverter class does not have a simple method to convert back from a string representation to a byte array. The following method makes that conversion:

// Convert two-digit hexadecimal values into a byte[].
public static byte[] HexToBytes(this string hexString)
{
// Separate the bytes.
char separator = hexString[2];
string[] hexPairs = hexString.Split(separator);

// Allocate the array.
int numBytes = hexPairs.Length;
byte[] bytes = new byte[numBytes];

// Parse the pairs.
for (int i = 0; i < numBytes; i++)
bytes[i] = Convert.ToByte(hexPairs[i], 16);
return bytes;
}

This method assumes that the string's third character is a separator such as or a space. It uses the separator to split the string into pieces, each of which holds two hexadecimal digits that represent a single byte. The code allocates a byte array large enough to hold the bytes and then loops through the values converting the pieces into bytes.

The rest of the example solution uses the methods described earlier to encrypt and decrypt strings. Download the EncryptDecryptStrings example solution to see additional details.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.201.55