I'm converting Rijndael decryption from C# to NodeJS.
The Key (or Passphrase) used is 13 characters long. The IV used is 17 characters long.
Note: I have no control over the length choice
Below is the Rijndael decryption in C#
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class Program
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector)
: this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3)
{
}
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Decrypt(string cipherText) {
return this.Decrypt(Convert.FromBase64String(cipherText));
}
public string Decrypt(byte[] cipherTextBytes) {
return Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
}
public byte[] DecryptToBytes(string cipherText) {
return this.DecryptToBytes(Convert.FromBase64String(cipherText));
}
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
}
public static void Main()
{
string Key = "";
string IV = "";
string encryptedUserData = "u7uENpFfpQhMXiTThL/ajA==";
string decryptedUserData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
decryptedUserData = crypto.Decrypt(encryptedUserData.Trim());
Console.WriteLine(decryptedUserData);
}
}
which for some reason, I can decrypt the string in dotnetfiddle, but not in Visual Studio (because it returns an error of 'Specified initialization vector (IV) does not match the block size for this algorithm. (Parameter 'rgbIV')'
Below is my attempt to convert in NodeJS using the rijndael-js library:
const Rijndael = require("rijndael-js");
const key = "";
const iv = "";
const cipher = new Rijndael(key, "cbc");
const ciphertext = "u7uENpFfpQhMXiTThL/ajA==";
const plaintext = Buffer.from(cipher.decrypt(ciphertext, 256, iv));
which returns an error of Unsupported key size: 104 bit
All errors point to the same thing: Invalid Key/IV lengths.
Would there be a work-around where I can force NodeJS to accept the Key and IV as valid lengths? Is there something I am missing, doing incorrectly, or misconfigured?
Edit:
I was able to find a PasswordDeriveBytes
implementation for NodeJS and compared the results from C# and they are equal.
I updated my NodeJS implementation (see sandbox) and noticed a few things:
- All resulting ciphertexts are the same. I am guessing this stems from salts.
- I tried decrypting a ciphertext generated from C#, but there seems to be a few characters to the left of the resulting value.
Example: C# Encrypted String:
zAqv5w/gwT0sFYXZEx+Awg==
, NodeJS Decrypted String:���&��4423
- When I try to decrypt a ciphertext generated in NodeJS in C#, the C# compiler returns an error of
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
Edit:
C# code (executable with .NET Framework 4.7.2):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace ProgramEncrypt
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector) : this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3) { }
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Encrypt(string plainText) => this.Encrypt(Encoding.UTF8.GetBytes(plainText));
public string Encrypt(byte[] plainTextBytes) => Convert.ToBase64String(this.EncryptToBytes(plainTextBytes));
public byte[] EncryptToBytes(string plainText) => this.EncryptToBytes(Encoding.UTF8.GetBytes(plainText));
public byte[] EncryptToBytes(byte[] plainTextBytes)
{
byte[] buffer = this.AddSalt(plainTextBytes);
MemoryStream memoryStream = new MemoryStream();
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.encryptor, CryptoStreamMode.Write);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
byte[] array = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
return array;
}
}
public string Decrypt(string cipherText) => this.Decrypt(Convert.FromBase64String(cipherText));
public string Decrypt(byte[] cipherTextBytes) => Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
public byte[] DecryptToBytes(string cipherText) => this.DecryptToBytes(Convert.FromBase64String(cipherText));
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
private byte[] AddSalt(byte[] plainTextBytes)
{
if (this.maxSaltLen == 0 || this.maxSaltLen < this.minSaltLen)
return plainTextBytes;
byte[] salt = this.GenerateSalt();
byte[] destinationArray = new byte[plainTextBytes.Length + salt.Length];
Array.Copy((Array)salt, (Array)destinationArray, salt.Length);
Array.Copy((Array)plainTextBytes, 0, (Array)destinationArray, salt.Length, plainTextBytes.Length);
return destinationArray;
}
private byte[] GenerateSalt()
{
int length = this.minSaltLen != this.maxSaltLen ? this.GenerateRandomNumber(this.minSaltLen, this.maxSaltLen) : this.minSaltLen;
byte[] data = new byte[length];
new RNGCryptoServiceProvider().GetNonZeroBytes(data);
data[0] = (byte)((int)data[0] & 252 | length & 3);
data[1] = (byte)((int)data[1] & 243 | length & 12);
data[2] = (byte)((int)data[2] & 207 | length & 48);
data[3] = (byte)((int)data[3] & 63 | length & 192);
return data;
}
private int GenerateRandomNumber(int minValue, int maxValue)
{
byte[] data = new byte[4];
new RNGCryptoServiceProvider().GetBytes(data);
return new Random(((int)data[0] & (int)sbyte.MaxValue) << 24 | (int)data[1] << 16 | (int)data[2] << 8 | (int)data[3]).Next(minValue, maxValue + 1);
}
public static void Main()
{
string Key = "HelL!oWoRL3ds";
string IV = "HElL!o@wOrld!#@%$";
string toEncrypt = "1234";
string encryptedData, decryptedData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
encryptedData = crypto.Encrypt(toEncrypt.Trim());
decryptedData = crypto.Decrypt(encryptedData.Trim());
Console.WriteLine("ENCRYPTED: " + encryptedData);
Console.WriteLine("DECRYPTED: " + decryptedData);
}
}
}
NodeJS code (codesandbox.io):
import { deriveBytesFromPassword } from "./deriveBytesFromPassword";
const Rijndael = require("rijndael-js");
const dataToEncrypt = "1234";
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o@wOrld!#@%$"; // 17 chars
const keySize = 256;
const hashAlgorithm = "SHA512";
// Use only the first 16 bytes of the IV
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16); // @ref https://stackoverflow.com/a/57147116/12278028
const rgbSalt = Buffer.from([]);
const derivedPasswordBytes = deriveBytesFromPassword(
SECRET_KEY,
rgbSalt,
3,
hashAlgorithm,
keySize / 8
);
const dataToEncryptInBytes = Buffer.from(dataToEncrypt, "utf8");
const cipher = new Rijndael(derivedPasswordBytes, "cbc");
const encrypted = Buffer.from(cipher.encrypt(dataToEncryptInBytes, 16, rgbIV));
console.log(encrypted.toString("base64"));
// Use this if you only have the Base64 string
// Note: The Base64 string in Line 34 is from C#
// const decrypted = Buffer.from(
// cipher.decrypt(Buffer.from("zAqv5w/gwT0sFYXZEx+Awg==", "base64"), 16, rgbIV)
// );
const decrypted = Buffer.from(cipher.decrypt(encrypted, 16, rgbIV));
console.log(decrypted.toString());