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?
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).