A screen shot of the node.js home page

Migrating from Cipher to Cipheriv

Print Friendly and PDF

Posted: December 19, 2023 | | Categories: Node.js

I have a Firebase project that uses a Firebase function to generate a URL containing encrypted data and another function to decrypt the data when someone invokes the URL (opens it in a browser). The functions use the Node.js Crypto library to do the encrypting and decrypting and I noticed recently in the library's docs that the library's createCipher method was deprecated and I needed to migrate my project to use createCipheriv instead. In this post, I demonstrate how I did it.

Background

Apparently createCipher has some issues:

This function is semantically insecure for all supported ciphers and fatally flawed for ciphers in counter mode (such as CTR, GCM, or CCM).

My particular application doesn't get a lot of hacker attention (that I know of anyway), but why keep an insecure algorithm lying around in your code, right?

The createCipheriv method configures an encryption algorithm that uses an Initialization Vector (iv) to increase randomness (or, actually, in my words, the perception of randomness) in the encrypted data generated by the cipher.

I'm not an encryption expert and I'm sure there's a lot of folks out there that can go into great detail about this, but all you really need to know about createCipheriv is this:

  • An iv is a random string or buffer array of hex data of a particular size depending on the encryption algorithm being used to encrypt the data.
  • You should create a separate iv for every encryption event. I want to say must here, but that's not technically true, you could use the same iv every time (for all your data encryption), but then you're defeating the purpose of using a more secure cipher.
  • The app encrypting the data will use a cipher password and the iv to encrypt the data.
  • The app decrypting the data will use a cipher password and the iv to decrypt the data.
  • Do not share the cipher password, that should remain internal to the code that encrypts and decrypts the data.
  • You don't have to hide the iv, you can pass it along with the encrypted data if you want. It's more secure if you protect the iv, but for my use case I don't have any choice but to include the iv with the data.

The Original Code

Note: As you look at the following code examples, assume I have a secret password stored in a variable called cipherPassword and some data I want to encrypt in a JSON Object called theData.

Using the deprecated encryption method, my original code looked like this:

const cipher = crypto.createCipher("aes192", cipherPassword);
let encrypted = cipher.update(
  JSON.stringify(theData),
  "utf8",
  "hex"
);
encrypted += cipher.final("hex");
return URL_ROOT + encrypted;

In this example, the code:

  • Creates a Cipher that uses the aes192 encryption algorithm
  • Encrypts the theData and finalizes it
  • Returns a URL consisting of a URL root plus the encrypted data

In this example, the root URL looks like this: https://mycooldomain.com/?gcec= and the resulting URL looks something like this (using eclipses to indicate missing data):

https://mycooldomain.com/?gcec=36e2eb4ea40299800e47dfaab20...11ef7db68ef07e74e9e411f43316735c46cffaaed

Using the deprecated encryption method, decrypting the data looked something like this:

const decipher = crypto.createDecipher("aes192", cipherPassword);
var decrypted = decipher.update(encryptedData, "hex", "utf8");
decrypted += decipher.final("utf8");
const reqData = JSON.parse(decrypted);

When it's done, the reqData is a JSON object that contains the original data properties.

The Migrated Code

Note: As you look at the following code examples, assume I have a secret password stored in a variable called cipherPassword and some data I want to encrypt in a JSON Object called theData.

Converting the code to use an iv when encrypting the data adds just a couple of extra steps to the encryption and decryption steps.

To encrypt, the code must create an iv consisting of a random string or buffer of hex data of a particular size depending on the encryption algorithm being applied to the data. My particular application uses aes192 which apparently requires an iv that's 16 bytes long. The Cipher library provides a simple method to do this: const iv = crypto.randomBytes(16).

Note: To improve the security of your encrypted data, you should create a separate iv for every request.

With that in place, encrypting the data looks like this:

const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes192", cipherPassword, iv);
let encrypted = cipher.update(
  JSON.stringify(theData), 
  "utf8", 
  "hex"
);
encrypted += cipher.final("hex");
return URL_ROOT + iv.toString('hex') + encrypted;

Notice that the code appends the iv to the beginning of the encrypted data portion of the URL. I have to send the iv along with the encrypted data because the only delivery mechanism I have is the URL. Until now, hackers didn't know the encryption algorithm I used or the length of the iv, so anyone who saw the URL will not know what's iv and what's encrypted data.

Again, this isn't a heavily hacked app, so I'm secure including the iv in the package delivered to the user.

Decrypting the data looks like this:

// http://www.java2s.com/example/nodejs/string/convert-hex-string-to-byte-array.html
function hexStringToByteArray(hexString: String): Uint8Array {
  if (hexString.length % 2 !== 0) {
      throw "Must have an even number of hex digits to convert to bytes";
  }
  var numBytes = hexString.length / 2;
  var byteArray = new Uint8Array(numBytes);
  for (var i=0; i<numBytes; i++) {
      byteArray[i] = parseInt(hexString.substr(i*2, 2), 16);
  }
  return byteArray;
}

// Pull the iv and encrypted data from the URL (first 32 bytes is the iv)
const iv = encryptedData.substr(0, 32);
const ivArray = hexStringToByteArray(iv);
// create a decipher object to decrypt the data
const decipher = crypto.createDecipheriv("aes192", cipherPassword, ivArray);
// capture the rest of the string as our encrypted data
const theData = encryptedData.substr(32);
var decrypted = decipher.update(theData, "hex", "utf8");
decrypted += decipher.final("utf8");
const reqData = JSON.parse(decrypted);

The first thing the code has to do is extract the iv from the URL. Since it's 16 bytes of hex, that means its 32 bytes of string, so the code pulls the first 32 bytes off of the content then converts it to a byte array using hexStringToByteArray I found at java2s.com.

The data to be decrypted is the rest of the data from the URL (starting at byte 32 through the end of the string).

From there, the code calls the decipher method to decrypt the data and then parses the data into the JSON object reqData.

Conclusion

I hope this helps you understand how to do the migration to an iv-based cipher. When I first started this process, I didn't know much about iv and what do to with it in my scenario (getting it to the recipient by passing it in the generated URL).

The biggest issue I encountered was figuring out how to convert the iv into a format I could use in the createDecipheriv method, but a quick search on Stack Overflow quickly pointed me to the hexStringToByteArray function I needed.


Next Post: Adding a GitHub Repository List to an Eleventy Site

Previous Post: Losing My Skills

If this content helps you in some way, please consider buying me a coffee.

Header image: https://nodejs.org/