Stronger Encryption and Decryption in Node.js

If your encryption method produces the same encrypted result given the same original text input, your encryption is broken. Yet this is what I see in most other examples around the web on how to do encryption in Node.js. Strong encryption should always produce a different output, even given the same exact input.

A Simple Example

Weak encryption produces the same output result given the same input:

Input Output
encrypt(‘test’) 67d38dc3110369fa0c8f154d994df436a…
encrypt(‘test’) 67d38dc3110369fa0c8f154d994df436a…

 

Strong encryption should always produce different output, even given the same exact input:

Input Output
encrypt(‘test’) c22c2dd341873996d5acc90f2b740ef54…
encrypt(‘test’) 83f30af9798ac6e64f630814539373d16…

 

Why Does It Matter?

But what does it matter if the content is encrypted anyways, you ask? It matters because if attackers ever gain access to your encrypted data, one of the first steps is to analyze it for similarities and patterns. If multiple records have the same output – even if the text is encrypted – that lets the attacker know that the input for both those records was the same. That might not sound like you’re giving up any valuable information, but it could be enough information for the attacker to infer the content of other encrypted records.

For instance, if the attacker knows the original content of a single encrypted record (perhaps even by using your service themselves), they can scan the database for the same output result in other records, and thus learn the contents of them as well. You can imagine scenarios in which attackers will continue using your service to encrypt things, then keep checking the database for the same results to learn the contents of other encrypted records by brute-force. Adding some randomness to ensure encrypted output is always different prevents this attack vector.

Adding Randomness With an IV

To ensure the encrypted content never produces the same output, we will use an Initialization Vector (IV) to add some randomness to the encryption algorithm. For this to be strong, we need to generate a unique random IV per encryption run – not a single fixed pre-defined IV. This is similar to a salt for password hashing, and will be stored with our encrypted data so we can decrypt it later along with the key.

In order to keep things simple and still use a single database field and value for our encrypted data, we will generate our IV before encryption, and prepend it to the encrypted result. Then before decryption, will read the IV we prepended to the encrypted result and use it along with our key for decryption. This is very similar to how bcrypt works.

A Node.js module for encryption with a random IV looks like this:

'use strict';

const crypto = require('crypto');

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // Must be 256 bytes (32 characters)
const IV_LENGTH = 16; // For AES, this is always 16

function encrypt(text) {
 let iv = crypto.randomBytes(IV_LENGTH);
 let cipher = crypto.createCipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
 let encrypted = cipher.update(text);

 encrypted = Buffer.concat([encrypted, cipher.final()]);

 return iv.toString('hex') + ':' + encrypted.toString('hex');
}

function decrypt(text) {
 let textParts = text.split(':');
 let iv = new Buffer(textParts.shift(), 'hex');
 let encryptedText = new Buffer(textParts.join(':'), 'hex');
 let decipher = crypto.createDecipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
 let decrypted = decipher.update(encryptedText);

 decrypted = Buffer.concat([decrypted, decipher.final()]);

 return decrypted.toString();
}

module.exports = { decrypt, encrypt };

I created a GitHub Gist of the code if you want to star it, fork it, or add comments.

Usage Notes

With AES encryption (this example uses aes-256-cbc), the IV length is always 16. Since we chose aes-256-cbc with an IV,  our key needs to be 256 bits (32 ASCII characters).

Encryption Is Easy To Get Wrong

Be careful with the encryption methods you find from a simple web search. If they don’t use an IV (or any form of randomness), they may leave your data a lot more insecure than you think, even with a strong key, and even in encrypted form.