Search code examples
c#node.js.net-4.6

Equivalent encryption with method CryptoJS.AES.encrypt in c#


I want to encrypt in c# exactly like in Node.Js which this method in Node.js does:


let encrypted = CryptoJS.AES.encrypt(JSON.stringify(text), passPhrase).toString()


So I could encrypt in c# and be then decrypt it in Node.Js with this method:


var decrypted = CryptoJS.AES.decrypt(encrypted, passPhrase);

I wrote this method to create key and IV from passPhrase in c#:

        public static byte[] DeriveKeyAndIVFromPassword(string password, int derivedBytesLength)
        {

            using (Rfc2898DeriveBytes rfc2898 = new Rfc2898DeriveBytes(password, derivedBytesLength))
            {
                return rfc2898.GetBytes(derivedBytesLength);
            }
        }

and the main method to encrypt this:

   public static string EncryptAES(string plainText, string passphrase)
        {
            try
            {
                using (AesCryptoServiceProvider aesAlg = new AesCryptoServiceProvider())
                {
                    aesAlg.Mode = CipherMode.CBC;  // Use CBC mode
                    aesAlg.Padding = PaddingMode.PKCS7;  // Use PKCS7 padding
                    aesAlg.KeySize = 256;  // Use AES-256

                    byte[] derivedKeyAndIV = DeriveKeyAndIVFromPassword(passphrase, aesAlg.KeySize / 8 + aesAlg.BlockSize / 8);

       
                    byte[] key = derivedKeyAndIV.Take(aesAlg.KeySize / 8).ToArray();
                    byte[] iv = derivedKeyAndIV.Skip(aesAlg.KeySize / 8).Take(aesAlg.BlockSize / 8).ToArray();

                    aesAlg.Key = key;

                    using (var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, iv))
                    {
                        using (MemoryStream msEncrypt = new MemoryStream())
                        {
      
                            msEncrypt.Write(iv, 0, iv.Length);

                            using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                            {
                                using (var swEncrypt = new StreamWriter(csEncrypt))
                                {
                                    swEncrypt.Write(plainText);
                                }
                            }
                            return Convert.ToBase64String(msEncrypt.ToArray());
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error: " + ex.Message);
                return null;
            }
        }


Based on this answer, I set Mode to CBS, Padding to PaddingMode.PKCS7 and KeySize to 256, which in main document it says:

CryptoJS supports AES-128, AES-192, and AES-256. It will pick the variant by the size of the key you pass in. If you use a passphrase, then it will generate a 256-bit key.

and the code not working, the encryption text which finally is made by c# code cannot be decrypted by Node.Js.

So anyone has any idea what I did wrong?


Solution

  • The reason for the problem is essentially that both codes use two different key derivation functions.

    In CryptoJS.AES.encrypt(), when the key material is passed as a string (as opposed to a WordArray), the key material is interpreted as a passphrase, a random 8 bytes salt is generated, and the key and IV are derived from both using a key derivation function, namely the OpenSSL proprietary EVP_BytesToKey().
    When using toString(), the result is returned in OpenSSL format, which corresponds to the Base64 encoding of the concatenation of the ASCII encoding of Salted__, the 8 bytes salt and the actual ciphertext.

    However, in the C# code a different key derivation function is used, namely PBKDF2 (which is implemented by Rfc2898DeriveBytes) and the OpenSSL format is not applied!


    For the fix, an implementation of EVP_BytesToKey() is required. There are several on the web. I would recommend BouncyCastle, as this library is comparatively trustworthy:

    public static void DeriveKeyAndIVFromPassword(string passphrase, out byte[] salt, out byte[] key, out byte[] iv)
    {
        salt = RandomNumberGenerator.GetBytes(8);
        int iterationCount = 1;
        PbeParametersGenerator pbeParametersGenerator = new OpenSslPbeParametersGenerator(new MD5Digest());
        pbeParametersGenerator.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(passphrase.ToCharArray()), salt, iterationCount);
        ParametersWithIV parametersWithIV = (ParametersWithIV)pbeParametersGenerator.GenerateDerivedParameters("AES", 256, 128); 
        KeyParameter keyParameter = (KeyParameter)parametersWithIV.Parameters;
        key = keyParameter.GetKey();
        iv = parametersWithIV.GetIV();
    }
    

    In addition, the result must be provided in OpenSSL format:

    public static string FormatResult(byte[] salt, byte[] ciphertext)
    {
        byte[] prefix = Encoding.ASCII.GetBytes("Salted__");
        byte[] prefixSaltCiphertext = new byte[prefix.Length + salt.Length + ciphertext.Length];
        Buffer.BlockCopy(prefix, 0, prefixSaltCiphertext, 0, prefix.Length);
        Buffer.BlockCopy(salt, 0, prefixSaltCiphertext, prefix.Length, salt.Length);
        Buffer.BlockCopy(ciphertext, 0, prefixSaltCiphertext, prefix.Length + salt.Length, ciphertext.Length);
        return Convert.ToBase64String(prefixSaltCiphertext);
    }
    

    All together:

    using Org.BouncyCastle.Crypto;
    using Org.BouncyCastle.Crypto.Digests;
    using Org.BouncyCastle.Crypto.Generators;
    using Org.BouncyCastle.Crypto.Parameters;
    using System;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;
    ...
    public static string EncryptAES(string plainText, string passphrase)
    {
        ...
        using (AesCryptoServiceProvider aesAlg = new AesCryptoServiceProvider()) // deprecated! Apply e.g. Aes.Create()
        {
            byte[] salt, key, iv;
            DeriveKeyAndIVFromPassword(passphrase, out salt, out key, out iv);
            aesAlg.Key = key;
            aesAlg.IV = iv;
            using (var encryptor = aesAlg.CreateEncryptor())
            {
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (var swEncrypt = new StreamWriter(csEncrypt))
                        {
                            swEncrypt.Write(plainText);
                        }
                    }
                    return FormatResult(salt, msEncrypt.ToArray());
                }
            }
        }
        ...
    }
    

    Test:

    Console.WriteLine(EncryptAES("The quick brown fox jumps over the lazy dog", "my passphrase"));
    

    returns as a possible output:

    U2FsdGVkX18kXCY/HbXX57fEj6EjwhlzQvRfZ6cpZ+z8EzZb9v2U6/Oy1XE1S6ASMkKglSCu3W0kI61U09X6gw==
    

    Keep in mind that because of the random salt and thus the different key and IV, each encryption gives a different result.

    The ciphertext generated with the C# code can be decrypted with CryptoJS as the following CryptoJS code shows:

    console.log(CryptoJS.AES.decrypt("U2FsdGVkX18kXCY/HbXX57fEj6EjwhlzQvRfZ6cpZ+z8EzZb9v2U6/Oy1XE1S6ASMkKglSCu3W0kI61U09X6gw==", "my passphrase").toString(CryptoJS.enc.Utf8)); 
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>


    Note that EVP_BytesToKey() is nowadays deemed as a vulnerability and is therefore deprecated. Instead, Argon2 or at least PBKDF2 should be used as KDF.
    Also, CryptoJS is discontinued and no longer maintained (latest version is 4.2.0).