Okay, I’m porting some modules from Nodejs to C# and I couldn’t find any built-in modules or libraries to do this so I had to implement it manually, luckily, with the help from Stackoverflow.

I have a message that was encrypted using crypto-js and stored in the database. Here is the Nodejs code that generates the encrypted data

const cryptojs = require('crypto-js');
const encryptedMsg = cryptojs.AES.encrypt('message', 'secret').toString();

The result is a string that looks like this

U2FsdGVkX184KJolbrZkg8w+rX/V9OW7sbUvWPVogdY=

Now, I need to read it back in C# and decrypt it to get the original message. The built-in Aes class in C# requires a Key and an IV to be explicitly passed in but there is no utility to generate the Key and the IV from a specified string. The above encrypt method from crypto-js is a simplified and implicit version of the Key and the IV. It doesn’t play well with C# and actually is not the AES standard (crypto-js still allows you to pass in the Key and IV explicitly).

For AES Cipher Algorithm, we need a Key and an IV (Initialization Vector) to add randomness to the encrypted data.

After playing around with crypto-js code base and with the help from Stackoverflow, I finally figured out how the data is stored and how the Key/IV are generated. In order to derive a key from the passphrase, it uses the OpenSSL-compatible derivation function EVP_BytesToKey. Here are the steps

  • Generate a random 8byte salt.
  • Use it along with the input passphrase to generate the Key and the IV.
  • The Key and the IV are then fed into AES function to produce the ciphertext.
  • The final result is a base64-encoded string containing the Salted__ string at the beginning followed by the 8byte salt and the actual ciphertext.

Simply base64-decode the above string to verify, we get this result Salted__8(�%n�d��>���廱�/X�h��

However, I couldn’t find the equivalent function of EVP_BytesToKey in C#. And yeah, Stackoverflow came to the rescue again. This method produces a 32byte Key and a 16byte IV, using MD5, just like the default behavior of crypto-js.

void DeriveKeyAndIv(byte[] passphrase, byte[] salt, int iterations, out byte[] key, out byte[] iv)
{
    var hashList = new List<byte>();

    var preHashLength = passphrase.Length + (salt?.Length ?? 0);
    var preHash = new byte[preHashLength];

    Buffer.BlockCopy(passphrase, 0, preHash, 0, passphrase.Length);
    if (salt != null)
        Buffer.BlockCopy(salt, 0, preHash, passphrase.Length, salt.Length);

    var hash = MD5.Create();
    var currentHash = hash.ComputeHash(preHash);

    for (var i = 1; i < iterations; i++)
    {
        currentHash = hash.ComputeHash(currentHash);
    }

    hashList.AddRange(currentHash);

    while (hashList.Count < 48) // for 32-byte key and 16-byte iv
    {
        preHashLength = currentHash.Length + passphrase.Length + (salt?.Length ?? 0);
        preHash = new byte[preHashLength];

        Buffer.BlockCopy(currentHash, 0, preHash, 0, currentHash.Length);
        Buffer.BlockCopy(passphrase, 0, preHash, currentHash.Length, passphrase.Length);
        if (salt != null)
            Buffer.BlockCopy(salt, 0, preHash, currentHash.Length + passphrase.Length, salt.Length);

        currentHash = hash.ComputeHash(preHash);

        for (var i = 1; i < iterations; i++)
        {
            currentHash = hash.ComputeHash(currentHash);
        }

        hashList.AddRange(currentHash);
    }

    hash.Clear();
    key = new byte[32];
    iv = new byte[16];
    hashList.CopyTo(0, key, 0, 32);
    hashList.CopyTo(32, iv, 0, 16);
}

The remaining job is to extract the salt from the encrypted string by reading the bytes, use that to calculate the Key and the IV to input to AES (read more at Aes Class).

public static string DecryptAes(string encryptedString, string passphrase)
{
    // encryptedString is a base64-encoded string starting with "Salted__" followed by a 8-byte salt and the
    // actual ciphertext. Split them here to get the salted and the ciphertext
    var base64Bytes = Convert.FromBase64String(encryptedString);
    var saltBytes = base64Bytes[8..16];
    var cipherTextBytes = base64Bytes[16..];

    // get the byte array of the passphrase
    var passphraseBytes = Encoding.UTF8.GetBytes(passphrase);

    // derive the key and the iv from the passphrase and the salt, using 1 iteration
    // (cryptojs uses 1 iteration by default)
    DeriveKeyAndIv(passphraseBytes, saltBytes, 1, out var keyBytes, out var ivBytes);

    // create the AES decryptor
    using var aes = Aes.Create();
    aes.Key = keyBytes;
    aes.IV = ivBytes;
    // here are the config that cryptojs uses by default
    // https://cryptojs.gitbook.io/docs/#ciphers
    aes.KeySize = 256;
    aes.Padding = PaddingMode.PKCS7;
    aes.Mode = CipherMode.CBC;
    var decryptor = aes.CreateDecryptor(keyBytes, ivBytes);

    // example code on MSDN https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=net-5.0
    using var msDecrypt = new MemoryStream(cipherTextBytes);
    using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
    using var srDecrypt = new StreamReader(csDecrypt);

    // read the decrypted bytes from the decrypting stream and place them in a string.
    return srDecrypt.ReadToEnd();
}

See the full working file here.

Final words

  • The above solution contains many little hacks, I don’t feel good about it, but at least, it works.
  • The function EVP_BytesToKey is not secure since it uses MD5 hashing function, which is very fast and crypto-js also uses only 1 iteration to perform the hash.
  • My bad (several years ago) for not researching AES when using it! Next time, I should read carefully about what I plan to apply, don’t just choose the easy and fast solution. 😂