node.js.netencryptionrijndaelrijndaelmanaged

Rijndael implementation in NodeJs not working the same as C# or Java


I'm trying to encrypt something in NodeJs with Rjndael-js and I'm not getting the same result that I'm getting with Java and C#. I think I must be doing something wrong with how I'm encoding strings going into the JS cipher, but I can't figure it out. The java code and the c# below both output the same result which is not the same as the JS. Please help. Thnx

Here's the JS:

const toHexString = (bytes) => {
  return Array.from(bytes, (byte) => {
    return ('0' + (byte & 0xff).toString(16)).slice(-2);
  }).join('');
};

const hexToBytes = (hex) => {
  var bytes = [];
  for (var c = 0; c < hex.length; c += 2) {
    bytes.push(parseInt(hex.substr(c, 2), 16));
  }
  return bytes;
};


const stringToEncrypt = 'test';
const keyAsHex = '12341234123412341234123412341234';
const ivAsHex = '43214321432143214321432143214321';

const keyAsBytes = hexToBytes(keyAsHex);
const ivAsBytes = hexToBytes(ivAsHex);
const Rijndael = require('rijndael-js');
const cipher = new Rijndael(keyAsBytes, 'cbc');
const cipherAsBytes = cipher.encrypt(stringToEncrypt, 128, ivAsBytes);

console.log("cipherAsHex",toHexString(cipherAsBytes)); //outputs: 172e3a9bec36e96f8f7f1da65e6e876c

Here's the Java:

import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class cryptoTesting {
    public static void main(String[] args) {
        try{

            String keyAsHex = "12341234123412341234123412341234";
            String ivAsHex = "43214321432143214321432143214321";
            String message = "test";

            byte[] messageAsBytes = message.getBytes();
            byte[] keyAsBytes = HexFormat.of().parseHex(keyAsHex);
            byte[] ivAsBytes = HexFormat.of().parseHex(ivAsHex);

            System.out.println(Arrays.toString(messageAsBytes));
            System.out.println(Arrays.toString(keyAsBytes));
            System.out.println(Arrays.toString(ivAsBytes));
            
            String plaintext = "test";
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            
            //You can use ENCRYPT_MODE or DECRYPT_MODE
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyAsBytes, "AES"), new IvParameterSpec(ivAsBytes));
            byte[] cipherAsBytes = cipher.doFinal(messageAsBytes);
            String cipherAsHex = HexFormat.of().formatHex(cipherAsBytes);
            System.out.println (cipherAsHex); //outputs: 647768a8c969dc195e34c7968514494f
        
        } catch (Exception exc){
            exc.printStackTrace(System.out);
            //System.out.write(exc);
        }
    }
}

And here's the C#

using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

namespace RijndaelManaged_Example
{
    class RijndaelExample
    {
        public static void Main()
        {
            string original = "test";

            // Create a new instance of the RijndaelManaged
            // class.  This generates a new key and initialization
            // vector (IV).
            using (RijndaelManaged myRijndael = new RijndaelManaged())
            {

                myRijndael.Key = Convert.FromHexString("12341234123412341234123412341234");
                myRijndael.IV = Convert.FromHexString("43214321432143214321432143214321");
                myRijndael.BlockSize = 128;

                // Encrypt the string to an array of bytes.
                byte[] encrypted = EncryptStringToBytes(original, myRijndael.Key, myRijndael.IV);
                PrintByteArray(encrypted);
                Console.WriteLine(Convert.ToHexString(encrypted)); //647768A8C969DC195E34C7968514494F

            }
        }

        public static void PrintByteArray(byte[] bytes)
        {
            var sb = new StringBuilder("new byte[] { ");
            foreach (var b in bytes)
            {
                sb.Append(b + ", ");
            }
            sb.Append("}");
            Console.WriteLine(sb.ToString());
        }

        static byte[] EncryptStringToBytes(string plainText, byte[] Key, byte[] IV)
        {
      
            byte[] encrypted;
            // Create an RijndaelManaged object
            // with the specified key and IV.
            using (RijndaelManaged rijAlg = new RijndaelManaged())
            {
                rijAlg.Key = Key;
                rijAlg.IV = IV;
                rijAlg.BlockSize = 128;

                
                Console.WriteLine(rijAlg.IV);

                // Create an encryptor to perform the stream transform.
                ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);

                // Create the streams used for encryption.
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {

                            //Write all data to the stream.
                            swEncrypt.Write(plainText);
                        }
                    }

                    encrypted = msEncrypt.ToArray();
                }
            }

            PrintByteArray(Key);
            PrintByteArray(IV);

            // Return the encrypted bytes from the memory stream.
            return encrypted;
        }
    }
}

Solution

  • rijndael.js applies hard-coded zero-padding, s. here, while in the Java and C# code PKCS#7 padding is used. Zero-padding is unreliable in contrast to PKCS#7 padding. You should consider switching to a different NodeJS library.
    Because of the block and key size you are using (both 128 bits), you don't need a Rijndael library, but an AES library will suffice. NodeJS has its own crypto module (that supports AES, CBC and PKCS#7, among others), which is a more secure and convenient alternative.


    If you absolutely want to stick to rijndael.js, you can implement PKCS#7 padding yourself. Since the zero-padding variant used here does not pad if the plaintext length already equals a multiple of the block size, a plaintext padded with PKCS#7 is not padded again with zero-padding (i.e. the PKCS#7 padding automatically disables zero-padding).

    The functions for padding and unpadding are:

    function pad (text, bs) {
        var padLen = bs - text.length % bs;
        return text.padEnd(text.length + padLen, String.fromCharCode(padLen))
    }
    
    function unpad(text){
        var padLen = text.charCodeAt(text.length - 1);
        return text.substring(0, text.length - padLen);
    }
    

    Example of use:

    const Rijndael = require('rijndael-js');
    const stringToEncrypt = 'test';
    const keyAsHex = '12341234123412341234123412341234';
    const ivAsHex = '43214321432143214321432143214321';
    // Encryption
    const keyAsBytes = hexToBytes(keyAsHex);
    const ivAsBytes = hexToBytes(ivAsHex);
    const cipher = new Rijndael(keyAsBytes, 'cbc');
    const cipherAsBytes = cipher.encrypt(pad(stringToEncrypt, 16), 128, ivAsBytes);     // Fix: pad with PKCS#7
    console.log("cipherAsHex", toHexString(cipherAsBytes)); //outputs: cipherAsHex 647768a8c969dc195e34c7968514494f
    // Decryption
    const decryptedAsBytes = cipher.decrypt(cipherAsBytes, 128, ivAsBytes);
    const decryptedPadded = Buffer.from(cipher.decrypt(cipherAsBytes, 128, ivAsBytes)).toString();
    const decrypted = unpad(decryptedPadded);    // Fix: unpad with PKCS#7
    console.log("decryptedPadded", decryptedPadded, "decrypted", decrypted); //outputs: decryptedPadded test             decrypted test