Encrypting Text using AES-CBC in NodeJS

Encryption allows an actor to obscure data in a day that prevents unwanted parties from viewing the raw contents. Using some kind of secret, an actor can encrypt data that can be later decrypted by using the same secret. With regards to the majority of encryption algorithms, this secret is known as the key. Users usually trade a password for a key before the encryption is performed – that way, only someone with the password can decrypt the encrypted payload to retrieve the raw data.

There are many secure encryption techniques available today, but one solid one that we’ll cover a bit of here is AES. AES stands for Advanced Encryption Standard, a block cipher very commonly used for encrypting data. AES will allow us to easily encrypt some text using a key.

Before getting started, we should digress and discuss a best practice before jumping in to the technical side of things. It’s unwise to simply use the user’s password as the key, as this makes attacks on the encrypted payload much more straightforward. Encryption keys should be derived from user passwords – hence the term key derivation. Deriving a key involves hashing or somehow transforming the key/password using a 1-way algorithm, performing this process many times (iterations). Key derivation is usually quite time-consuming (in terms of computational time) and can therefore be used as a deterrent and preventative measure for attacks as guesswork becomes too difficult.

There are a variety of ways to securely derive an encryption key, but we’ll be speaking about PBKDF2 (Password-Based Key Derivation Function 2). This method of derivation is achievable when writing software on NodeJS and will form the first part of our encryption process. For our key derivation we’ll be using a third-party dependency: pbkdf2. This library uses callbacks but we’ll wrap that into a Promise for easier use. We’ll also create a separate method to perform the derivation as we need to generate a HMAC to later provide to the encryption method (A HMAC is an authentication code which can be used to verify encrypted payloads to ensure that they have not been tampered with).

Here’s the start of our derivation process:

The derivation process is a bit meaty, but that’s because we need to generate a few things in the process of deriving a key:

  • A derived key, for encrypting
  • A HMAC code, for authentication
  • (Passed-through) A salt
  • (Passed-through) The rounds used

The key and HMAC are generated in the same process, as the desired key length (32 bytes) is doubled and the resulting value split to provide both values. The HMAC is generated from the password because, like the key, it is secret and cannot be stored alongside the encrypted payload.

The salt is a random string that is generated and used to pad the key before derivation. The salt can be stored alongside the encrypted payload as it is not secret.

The iteration rounds is a number which indicates how many derivation rounds PBKDF2 should perform on the key. In 2016 NIST recommended a minimum of 10k iterations, but we should consider that as time passes computers become exceptionally more powerful and we should always be raising the number of iterations. The amount should be as high as possible while remaining tolerable.

To use the code above, we can write something like the following:

deriveFromPassword("testing", "agd7sd67v", 50000).then(info => {
  // do something with info:
  // {
  //     salt,
  //     key,
  //     rounds,
  //     hmac
  // }
});

Encrypting and Decrypting

There are a variety of ways to safely encrypt text in NodeJS, but few are as tried-and-trusted as AES in CBC mode. The CBC stands for Cipher Block Chaining, which is a mode of operation for AES. CBC works by XORing blocks of plaintext with each other – each block is XORed with the previous block, and in doing so form a chain of dependence.

CBC is a fine mode to use for encrypting a block of text, which is the case for most encryption implementations. Here’s our basic encryption implementation, which relies on the key derivation introduced earlier:

The encryption process can be demonstrated as follows:

encryptText("test text", "myPass")
    .then(out => {
        console.log("Encrypted:", out);
        return decryptText(out, "myPass");
    })
    .then(decrypted => {
        console.log("Decrypted:", decrypted);
    });

Encryption is performed by using the target string and a password, and decryption by using the encrypted string and the same password.

Essentially the implementation above just wraps Node’s crypto library in some helper functions. You’ll notice the use of Promise.resolve() around the place to force everything to work asynchronously (even though under the hood it behaves synchronously). This is to future proof our implementation so that later we might consider a more performance friendly (truly async) version of such functions.

Speaking of functions, we introduced a few new ones since our key derivation example:

  • The constantTimeCompare function is a string comparison method that takes the same time to compare multiple differently-sized strings with some reference string. This is to reduce the possibility of a timing attack against out implementation.
  • The generateIV method generates a random string of a fixed length – an initialisation vector – which is used in the encryption procedure. The purpose of an IV is to ensure that the output from encryption remains different for each invocation if the key (password) were to remain the same. This is to ensure that any potential attacker would not be able to infer any relationship between segments of the encrypted payload. The IV does not need to remain secret and can be output with the encrypted string.
  • The generateSalt method generates a random string for use as a salt. This is required for key derivation, and is then passed in as a HMAC parameter to tie everything together. The salt does not need to remain secret either.
  • The encryptText method encrypts a string using a password, and wraps all the more complex calls to generate various values in one easier-to-use function that is safe to consume by a naive method. It generates the necessary pieces required for encryption (IV, salt, derived key, derived HMAC key), encrypts the string and then creates an authentication string with the HMAC key.
  • The decryptText method takes the output of the encryptText method as the first parameter and the same password as the second. It parses the payload, decrypts the encrypted text, verifies the HMAC authentication and outputs the original, unencrypted text.
  • The packageContents and unpackageContents methods manage the stringification and splitting of the encryption components as they’re handled between encryption and decryption.

The encryption procedure is quite basic: Node’s encryption method wants to know what type of encryption to perform (CBC), what the IV is, and then what the content to encrypt is. We wrap some procedures around the encryption which are intended to further secure the payload (HMAC and key derivation). A HMAC (or MAC) is a method with which to sign an encrypted payload, using secrets a witness would not know, so that the encrypted payload can be verified later. If the HMAC cannot be authenticated, decryption fails as the payload has been tampered with or has become corrupt.

Putting it Together

That’s about the gist of it! Haha. You can check out the full working example here. Just make sure to npm install before running node encryption-example.js so that pbkdf2 is installed first.

Finally, make sure to practice safe crypto – don’t invent too much yourself, and be generous with the amount of time you give your crypto to work. Increase the number of PBKDF iterations so that derivation takes longer – this will hinder any brute force attacks or other guesses as a password. If you switch derivation or hashing methods be sure to keep the time component at a high yet acceptable level.

Happy encryption!