javascriptc#node.jsencryptionrijndael

Invalid Key Length in NodeJS, but Valid Key Length in C#


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:

  1. All resulting ciphertexts are the same. I am guessing this stems from salts.
  2. 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
  3. 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());

Solution

  • A possible NodeJS implementation based on your sandbox code that is compatible with the C# code is:

    const crypto = require("crypto");
    const Rijndael = require("rijndael-js");
    const pkcs7 = require('pkcs7-padding');
    
    const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
    const SECRET_IV = "HElL!o@wOrld!#@%$"; // 17 chars
    const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16); 
    const rgbSalt = Buffer.from([]);
    
    const keySize = 256;
    const hashAlgorithm = "SHA512";
    
    const minSaltLen = 4;
    const maxSaltLen = 8;
    
    function encrypt(plaintextStr) {
      var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);  
      var cipher = new Rijndael(derivedPasswordBytes, "cbc");
      var plaintext = Buffer.from(plaintextStr, "utf8");
      var salt = generateSalt();
      var saltPlaintext = Buffer.concat([salt, plaintext])
      var saltPlaintextPadded = pkcs7.pad(saltPlaintext, 16)
      var ciphertext = Buffer.from(cipher.encrypt(saltPlaintextPadded, 128, rgbIV));
      return ciphertext.toString("base64");
    }
    
    function decrypt(ciphertextB64) {
      var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);  
      var cipher = new Rijndael(derivedPasswordBytes, "cbc");
      var ciphertext = Buffer.from(ciphertextB64, 'base64');
      var saltPlaintextPadded = Buffer.from(cipher.decrypt(ciphertext, 128, rgbIV));
      var sourceIndex = saltPlaintextPadded[0] & 3 | saltPlaintextPadded[1] & 12 | saltPlaintextPadded[2] & 48 | saltPlaintextPadded[3] & 192
      var plaintextPadded = saltPlaintextPadded.subarray(sourceIndex)
      var plaintext = pkcs7.unpad(plaintextPadded)
      return plaintext;
    }
    
    function generateSalt() {
      var length =  minSaltLen !=  maxSaltLen ?  crypto.randomInt(minSaltLen,  maxSaltLen + 1) :  minSaltLen;
      var data = crypto.randomBytes(length);
      data[0] = data[0] & 252 | length & 3;
      data[1] = data[1] & 243 | length & 12;
      data[2] = data[2] & 207 | length & 48;
      data[3] = data[3] & 63 | length & 192;
      return data;
    }
    
    var plaintext = "1234";
    var ciphertextB64 = encrypt(plaintext);
    var plaintext = decrypt(ciphertextB64);
    console.log(ciphertextB64);
    console.log(plaintext.toString('hex'))
    

    using the key derivation from the linked post.

    Ciphertexts generated with this code can be decrypted with the C# code, and vice versa, ciphertexts generated with the C# code can be decrypted with this code.


    Explanation:


    Security: