0

I'm trying to adapt an example RinjaelManaged encryption class (see: Encrypting & Decrypting a String in C#) to instead use the AesCryptoServiceProvider so it can run on computers set to use only FIPS compliant algorithms.

However it seems it's not as simple as swapping out the class names as I now get an error about the length of the Initialization Vector. I realise there are several questions about this already but I've been unsuccessful with trying to use the answers from other questions in my particular use case.

What do I need to change to get the IV length to match what's needed?

namespace Encryption
{
    #region Using Statements
    using System;
    using System.IO;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Text;
    #endregion

    public class EncryptionHelper
    {
        #region Private Fields

        // This constant determines the number of iterations for the password bytes generation function.
        private const int DerivationIterations = 1000;

        // This constant is used to determine the keysize of the encryption algorithm in bits.
        // We divide this by 8 within the code below to get the equivalent number of bytes.
        private const int KeySize   = 256;
        private const int BlockSize = 128;

        #endregion Private Fields

        #region Public Methods

        /// <summary>Decrypts the specified cipher text.</summary>
        /// <param name="cipherText">The cipher text.</param>
        /// <param name="passPhrase">The pass phrase.</param>
        /// <returns></returns>
        public static string Decrypt(string cipherText, string passPhrase)
        {
            // Get the complete stream of bytes that represent:
            // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
            byte[] cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
            // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
            byte[] saltStringBytes = cipherTextBytesWithSaltAndIv.Take(KeySize / 8).ToArray();
            // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
            byte[] ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(KeySize / 8).Take(KeySize / 8).ToArray();
            // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
            byte[] cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip(KeySize / 8 * 2).Take(cipherTextBytesWithSaltAndIv.Length - KeySize / 8 * 2).ToArray();

            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                byte[] keyBytes = password.GetBytes(KeySize / 8);

                using (var aes = new AesCryptoServiceProvider())
                {
                    aes.BlockSize = BlockSize;
                    aes.Mode = CipherMode.CBC;
                    aes.Padding = PaddingMode.PKCS7;
                    using (ICryptoTransform decryptor = aes.CreateDecryptor(keyBytes, ivStringBytes))
                    using (var memoryStream = new MemoryStream(cipherTextBytes))
                    using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                    {
                        var plainTextBytes = new byte[cipherTextBytes.Length];
                        int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                        memoryStream.Close();
                        cryptoStream.Close();
                        return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
                    }
                }
            }
        }

        /// <summary>Encrypts the specified plain text.</summary>
        /// <param name="plainText">The plain text.</param>
        /// <param name="passPhrase">The pass phrase.</param>
        /// <returns></returns>
        public static string Encrypt(string plainText, string passPhrase)
        {
            // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
            // so that the same Salt and IV values can be used when decrypting.  
            byte[] saltStringBytes = Generate256BitsOfRandomEntropy();
            byte[] ivStringBytes = Generate256BitsOfRandomEntropy();
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                byte[] keyBytes = password.GetBytes(KeySize / 8);
                using (var symmetricKey = new AesCryptoServiceProvider())
                {
                    symmetricKey.BlockSize = BlockSize;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                            {
                                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                                cryptoStream.FlushFinalBlock();
                                // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
                                byte[] cipherTextBytes = saltStringBytes;
                                cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
                                cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
                                memoryStream.Close();
                                cryptoStream.Close();
                                return Convert.ToBase64String(cipherTextBytes);
                            }
                        }
                    }
                }
            }
        }

        #endregion Public Methods

        #region Private Methods

        /// <summary>Generate256s the bits of random entropy.</summary>
        /// <returns></returns>
        private static byte[] Generate256BitsOfRandomEntropy()
        {
            var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits.

            using (var rngCsp = new RNGCryptoServiceProvider())
            {
                // Fill the array with cryptographically secure random bytes.
                rngCsp.GetBytes(randomBytes);
            }

            return randomBytes;
        }

        #endregion Private Methods
    }
}
jww
  • 97,681
  • 90
  • 411
  • 885
NickG
  • 9,315
  • 16
  • 75
  • 115
  • An "invalid padding" generally means the decryption failed. Because the decryption failed the data is wrong and that means that the padding is going to be wrong. – zaph Mar 23 '18 at 12:35
  • I'm not following the question. The title appears to present an error message or exception. Then the body does not discuss it. I think you should clearly state the error and where it is occurring. – jww Mar 24 '18 at 07:01

2 Answers2

7

OK I eventually got to the bottom of it by clearly separating the IV length from the Salt length:

public class EncryptionHelper
{
    #region Private Fields

    // This constant determines the number of iterations for the password bytes generation function.
    private const int DerivationIterations = 1000;

    // This constant is used to determine the keysize of the encryption algorithm in bits.
    // We divide this by 8 within the code below to get the equivalent number of bytes.
    private const int saltBytes = 32; //  bytes
    private const int ivBytes = 16; // bytes

    #endregion Private Fields

    #region Public Methods

    /// <summary>Decrypts the specified cipher text.</summary>
    /// <param name="cipherText">The cipher text.</param>
    /// <param name="passPhrase">The pass phrase.</param>
    /// <returns></returns>
    public static string Decrypt(string cipherText, string passPhrase)
    {
        // Get the complete stream of bytes that represent:
        // [32 bytes of Salt] + [16 bytes of IV] + [n bytes of CipherText]
        byte[] cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText); 
        // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
        byte[] saltStringBytes = cipherTextBytesWithSaltAndIv.Take(saltBytes).ToArray();
        // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
        byte[] ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(saltBytes).Take(ivBytes).ToArray();
        // Get the actual cipher text bytes by removing the first 48 bytes from the cipherText string.
        byte[] cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip(saltBytes + ivBytes).Take(cipherTextBytesWithSaltAndIv.Length - (saltBytes + ivBytes)).ToArray();

        using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
        {
            byte[] keyBytes = password.GetBytes(saltBytes);

            using (var symmetricKey = new AesCryptoServiceProvider())
            {
                symmetricKey.BlockSize = 128;
                symmetricKey.Mode = CipherMode.CBC;
                symmetricKey.Padding = PaddingMode.PKCS7;
                using (ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
                using (var memoryStream = new MemoryStream(cipherTextBytes))
                using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                {
                    var plainTextBytes = new byte[cipherTextBytes.Length];
                    int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                    memoryStream.Close();
                    cryptoStream.Close();
                    return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
                }
            }
        }
    }

    /// <summary>Encrypts the specified plain text.</summary>
    /// <param name="plainText">The plain text.</param>
    /// <param name="passPhrase">The pass phrase.</param>
    /// <returns></returns>
    public static string Encrypt(string plainText, string passPhrase)
    {
        // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
        // so that the same Salt and IV values can be used when decrypting.  
        byte[] saltStringBytes = GenerateBitsOfRandomEntropy(32);
        byte[] ivStringBytes = GenerateBitsOfRandomEntropy(16);
        byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

        using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
        {
            byte[] keyBytes = password.GetBytes(saltBytes);
            using (var symmetricKey = new AesCryptoServiceProvider())
            {
                symmetricKey.BlockSize = 128;
                symmetricKey.Mode = CipherMode.CBC;
                symmetricKey.Padding = PaddingMode.PKCS7;
                using (ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                        {
                            cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                            cryptoStream.FlushFinalBlock();

                            // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
                            byte[] cipherTextBytes = saltStringBytes;
                            cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
                            cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
                            memoryStream.Close();
                            cryptoStream.Close();
                            return Convert.ToBase64String(cipherTextBytes);
                        }
                    }
                }
            }
        }
    }

    #endregion Public Methods

    #region Private Methods

    /// <summary>Generate bits of random entropy.</summary>
    /// <returns></returns>
    private static byte[] GenerateBitsOfRandomEntropy(int num)
    {
        var randomBytes = new byte[num]; // 32 Bytes will give us 256 bits.

        using (var rngCsp = new RNGCryptoServiceProvider())
        {
            // Fill the array with cryptographically secure random bytes.
            rngCsp.GetBytes(randomBytes);
        }

        return randomBytes;
    }

    #endregion Private Methods
NickG
  • 9,315
  • 16
  • 75
  • 115
  • As MemoryStream and CryptoStream are in using block, memoryStream.Close(); and cryptoStream.Close(); are not needed. – Maulik Modi Jan 20 '22 at 11:31
4

The length of your IV should be the same as your block size. That is, 128 bits. Currently your code assumes a 256 bit IV. Adjust appropriately throughout your code and this should solve the issue.

Luke Joshua Park
  • 9,527
  • 5
  • 27
  • 44
  • That's what I've been trying to do, but I always get the same error. – NickG Mar 23 '18 at 10:47
  • Or, if I fix the error, the decrypt method returns a zero length string. – NickG Mar 23 '18 at 10:53
  • OR I get an error about invalid padding. I just can't seem to figure out the correct combo of length changes, to make it work. – NickG Mar 23 '18 at 12:03