Refactor JavaScript decryption into C# – AES 128

I need to take working JavaScript that decrypts a message and convert it into C#. I have the decryption information (the “decrypt” variable below) which looks like: AES-128:<salt>:<iv>:<key>. Here’s the JavaScript:

function decodeString(message, decrypt) {
    var parts = decrypt.split(':', 4);
    var salt = CryptoJS.enc.Hex.parse(parts[1]);
    var iv = CryptoJS.enc.Hex.parse(parts[2]);
    var key = CryptoJS.PBKDF2(parts[3], salt, { keySize: 128/32, iterations: 100 });
    try {
        message = message.replace(/s+/g, '');
        var d = CryptoJS.AES.decrypt(message, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
        message = d.toString(CryptoJS.enc.Utf8);
    } catch (e) {
        console.error("Encountered a problem decrypting and encrypted page!");
        console.log(e);
    }
    
    return(message);       
}

Here’s what I have in C#, but I get an exception on the CreateDecryptor call.

using System.Security.Cryptography;

private string DecodeString(string message, string decrypt)
{
    string[] parts = decrypt.ToString().Split(':');
    byte[] salt = Encoding.UTF8.GetBytes(parts[1]);
    byte[] iv = Encoding.UTF8.GetBytes(parts[2]);
    var pbkdf2 = new Rfc2898DeriveBytes(parts[3], salt, 100);
    int numKeyBytes = 128;  // Not sure this is correct
    byte[] key = pbkdf2.GetBytes(numKeyBytes);

    string plainText = null;
    using (AesManaged aes = new AesManaged())
    {
        aes.KeySize = numKeyBytes;  // Not sure if this is correct
        aes.BlockSize = 128;    // Defaults to 128, but not sure this is correct
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;

        try
        {
            // The below line has the following exception:
            //    The specified key is not a valid size for this algorithm.
            //    Parameter name: key
            using (var decrypter = aes.CreateDecryptor(key, iv))
            using (var plainTextStream = new MemoryStream())
            {
                using (var decrypterStream = new CryptoStream(plainTextStream, decrypter, CryptoStreamMode.Write))
                using (var binaryWriter = new BinaryWriter(decrypterStream))
                {
                    string encryptedText = Regex.Replace(message, @"s+", "");
                    binaryWriter.Write(encryptedText);
                }
                byte[] plainTextBytes = plainTextStream.ToArray();
                plainText = Encoding.UTF8.GetString(plainTextBytes);
            }
        }
        catch (Exception ex)
        {
            log.Error("Unable to decrypt message.", ex);
        }

        return plainText;
    }
}

Any suggestions would be appreciated!

Answer

In the C# code there are the following issues:

  • Salt and IV must be hex decoded (and not UTF8 encoded).
  • numKeyBytes specifies the key size in bytes and is therefore 16 (and not 128) for AES-128.
  • aes.KeySize specifies the key size in bits and is therefore numKeyBytes * 8 (and not numKeyBytes), but can alternatively be omitted.
  • For aes.BlockSize, aes.Mode and aes.Padding the default values are used (128, CBC, PKCS7), so they do not need to be specified explicitly.
  • encryptedText must be Base64 decoded.

A possible implementation is:

private string Decrypt(string message, string decrypt)
{
    string[] parts = decrypt.ToString().Split(':');
    byte[] salt = StringToByteArray(parts[1]);                                                              // Hex decode salt                                            
    byte[] iv = StringToByteArray(parts[2]);                                                                // Hex dedoce IV                                           
    var pbkdf2 = new Rfc2898DeriveBytes(parts[3], salt, 100);
    int numKeyBytes = 16;                                                                                   // AES-128 key size in bytes: 16
    byte[] key = pbkdf2.GetBytes(numKeyBytes);

    string plainText = null;
    using (AesManaged aes = new AesManaged())
    {
        aes.KeySize = 192;
        try
        {
            string encryptedText = Regex.Replace(message, @"s+", "");
            using (var decrypter = aes.CreateDecryptor(key, iv))
            using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(encryptedText)))      // Base64 decode ciphertext
            {
                using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decrypter, CryptoStreamMode.Read))
                {
                    using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                    {
                        plainText = srDecrypt.ReadToEnd();
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Unable to decrypt message.", ex);
        }

        return plainText;
    }
}

where StringToByteArray() is from here.

I changed the Stream part to be analogous to this example, but the original implementation works as well, so this change is optional.

Test: Both codes return for the data

message = "YhyXEjjNAnRUUONwVzlha59tRoWkeEwTkOtSKOicRd/iBKkGgIp+DeWmvEXxAU53";
decrypt = "AES-128:30313233343536373839303132333435:35343332313039383736353433323130:my passphrase";

the plaintext:

The quick brown fox jumps over the lazy dog